Nút "Nhân bản" (Duplicate) trong menu của Odoo gọi thẳng phương thức copy(). Nghe đơn giản, nhưng nhân bản cẩn thận hơn bạn tưởng: có những trường không được sao chép (số đơn, mã tự tăng), và bạn thường muốn chỉnh vài giá trị khi nhân. Nối tiếp bộ CRUD (đọc–tạo–sửa–xóa), bài này bàn thao tác họ hàng: copy().
copy() cơ bản: nhân một bản y hệt
Gọi copy() trên một bản ghi (tập một phần tử) để tạo bản sao. Odoo tự thêm hậu tố "(copy)" vào trường tên:

Hình 1: copy() không tham số cho bản sao y hệt (tên + "(copy)"). copy(default={...}) ghi đè giá trị khi nhân. Trường khai copy=False thì không được sao chép — quan trọng cho số tự tăng. Và copy_data là móc để can thiệp trước khi tạo bản sao.
Chạy thật — ba tình huống
Đây là output thật trên db blog19:

Hình 2: Output thật. (1) copy() bản ghi id 15 → bản sao id 3546, tên có "(copy)". (2) copy({'name':'Chi nhánh MỚI', 'city':'Cần Thơ'}) → bản sao dùng đúng giá trị ghi đè. (3) Nhân bản đơn hàng: S00015 → S00016 — số đơn không trùng vì trường name của sale.order khai copy=False, buộc Odoo cấp số mới qua sequence.
copy=False — vì sao cực quan trọng
Nếu số đơn được sao chép nguyên, bạn sẽ có hai đơn cùng số S00015 — loạn ngay. Vì vậy các trường "định danh duy nhất" (số đơn, mã hóa đơn, số serial) đều khai copy=False:
class SaleOrder(models.Model):
_inherit = 'sale.order'
name = fields.Char(copy=False) # số đơn, mỗi bản ghi một số qua sequence
Khi tự viết model có trường mã tự tăng hay giá trị phải duy nhất, nhớ khai copy=False — nếu không, nhân bản sẽ tạo trùng lặp.
default — điều chỉnh khi nhân
Tham số default là một dict ghi đè giá trị cho bản sao. Rất hữu ích khi bạn muốn nhân bản làm khuôn:
# Nhân một mẫu hợp đồng, đổi tên và đặt ngày hôm nay
mau.copy({
'name': 'Hợp đồng tháng 10',
'date_start': fields.Date.today(),
'state': 'draft', # bản sao luôn về nháp
})
default cũng là cách ghi đè khi trường mặc định được copy nhưng bạn muốn khác đi.
copy_data — can thiệp sâu
Muốn xử lý phức tạp (ví dụ đổi toàn bộ dòng con, tính lại giá trị) thì override copy_data:
def copy_data(self, default=None):
vals_list = super().copy_data(default)
for vals in vals_list:
vals['note'] = 'Bản sao tạo ngày %s' % fields.Date.today()
return vals_list
copy_data trả về dữ liệu sẽ dùng để tạo bản sao — bạn chỉnh nó trước khi Odoo create. (Ở Odoo 19 copy_data làm việc theo lô, trả về list.)
Trường quan hệ khi copy
- One2many (dòng con): mặc định được sao chép — copy một đơn thì copy cả các dòng của nó.
- Many2one (tham chiếu): sao chép id tham chiếu (trỏ tới cùng bản ghi đích, không nhân đôi đích).
- Many2many: sao chép danh sách liên kết (cùng trỏ tới các bản ghi đích).
Nếu không muốn dòng con theo, đặt copy=False trên trường One2many đó.
Nhớ ba ý
copy()nhân bản một bản ghi (tự thêm "(copy)" vào tên);copy(default={...})ghi đè giá trị khi nhân — hữu ích để nhân bản làm khuôn.- Trường
copy=Falsekhông được sao chép — bắt buộc cho số đơn/mã duy nhất (đo thật:S00015→S00016); tự viết model có mã tự tăng nhớ khai. - Override
copy_datađể can thiệp sâu trước khi tạo bản sao; One2many mặc định copy theo, Many2one/Many2many copy tham chiếu.
Xong toàn bộ thao tác dữ liệu của ORM. Chương tiếp đi vào phần "thông minh" của model: trường tính toán. Phần sau mở màn bằng trường computed và @api.depends — cách một trường tự tính giá trị từ các trường khác và cập nhật khi chúng đổi.