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:

copy trong Odoo 19 nhân bản một bản ghi và chỉnh khi nhân: copy không tham số cho bản sao y hệt tên tự thêm copy, ban_sao bằng goc.copy làm name Cà Phê Việt Chi nhánh Hà Nội thành có đuôi copy; copy với default là dict ghi đè giá trị khi nhân bản, goc.copy với name Chi nhánh MỚI và city Cần Thơ thì bản sao dùng đúng name city này thay vì của gốc; copy bằng False trên field thì không được sao chép, class SaleOrder có name fields.Char copy False là số đơn mỗi bản một số riêng nên copy đơn S00015 cho ra S00016 số mới không trùng; móc copy_data để xử lý phức tạp trước khi nhân, def copy_data self default gọi super copy_data rồi chỉnh vals trước khi tạo bản sao và trả về vals

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:

Output copy chạy thật trên database blog19: bản ghi gốc goc bằng P.search is_company True limit 1 ra id 15 name Cà Phê Việt Chi nhánh Hà Nội; một copy không tham số cho bản sao mới id 3546 name Cà Phê Việt Chi nhánh Hà Nội kèm đuôi copy; hai copy với default dict name Chi nhánh MỚI city Cần Thơ cho id 3547 name Chi nhánh MỚI city Cần Thơ; ba sale.order.name có copy False nên số đơn không trùng, so.copy cho goc name S00015 thành copy name S00016 số mới; copy trả về recordset của bản sao sau đó dọn sạch bằng unlink

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 ý

  1. 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.
  2. Trường copy=False khô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.
  3. 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.