From fc8141542a1223f4dbd45e6f55881c09b5301ea9 Mon Sep 17 00:00:00 2001 From: mtpc4s9 Date: Mon, 22 Dec 2025 12:32:25 +0700 Subject: [PATCH] =?UTF-8?q?=C4=90=C3=A3=20th=C3=A0nh=20c=C3=B4ng=20trong?= =?UTF-8?q?=20vi=E1=BB=87c=20gom=20PRs=20th=C3=A0nh=20RFQ:=20=20=20=20+=20?= =?UTF-8?q?L=C6=B0u=20=C3=BD=20c=E1=BB=B1c=20k=E1=BB=B3=20quan=20trong:=20?= =?UTF-8?q?L=C3=BD=20do=20tr=C6=B0=E1=BB=9Dng=20"Source=20Requests"=20(req?= =?UTF-8?q?uest=5Fids)=20b=E1=BB=8B=20NULL=20ch=C3=ADnh=20l=C3=A0=20do=20v?= =?UTF-8?q?=E1=BA=A5n=20=C4=91=E1=BB=81=20=C4=91=E1=BB=8Bnh=20ngh=C4=A9a?= =?UTF-8?q?=20trong=20View=20XML=20c=E1=BB=A7a=20Wizard.=20=20=20=20=20?= =?UTF-8?q?=C4=90=C3=A2y=20l=C3=A0=20m=E1=BB=99t=20"b=E1=BA=ABy"=20kinh=20?= =?UTF-8?q?=C4=91i=E1=BB=83n=20(Common=20Pitfall)=20trong=20Odoo=20khi=20l?= =?UTF-8?q?=C3=A0m=20vi=E1=BB=87c=20v=E1=BB=9Bi=20Wizard=20(TransientModel?= =?UTF-8?q?):=20=20=20=20=20Nguy=C3=AAn=20t=E1=BA=AFc:=20Trong=20Wizard,?= =?UTF-8?q?=20n=E1=BA=BFu=20m=E1=BB=99t=20tr=C6=B0=E1=BB=9Dng=20c=C3=B3=20?= =?UTF-8?q?d=E1=BB=AF=20li=E1=BB=87u=20(=C4=91=C6=B0=E1=BB=A3c=20set=20t?= =?UTF-8?q?=E1=BB=AB=20default=5Fget)=20nh=C6=B0ng:=20=20=20=20=20=20=20?= =?UTF-8?q?=20=201.=20Kh=C3=B4ng=20xu=E1=BA=A5t=20hi=E1=BB=87n=20trong=20f?= =?UTF-8?q?ile=20XML.=20=20=20=20=20=20=20=20=202.=20Ho=E1=BA=B7c=20c?= =?UTF-8?q?=C3=B3=20xu=E1=BA=A5t=20hi=E1=BB=87n=20nh=C6=B0ng=20l=C3=A0=20r?= =?UTF-8?q?eadonly=3D"1"=20v=C3=A0=20thi=E1=BA=BFu=20force=5Fsave=3D"1".?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=3D>=20Th=C3=AC=20khi=20b=E1=BA=A1n?= =?UTF-8?q?=20b=E1=BA=A5m=20n=C3=BAt=20"Create=20RFQ",=20Odoo=20Web=20Clie?= =?UTF-8?q?nt=20s=E1=BA=BD=20kh=C3=B4ng=20g=E1=BB=ADi=20gi=C3=A1=20tr?= =?UTF-8?q?=E1=BB=8B=20c=E1=BB=A7a=20tr=C6=B0=E1=BB=9Dng=20=C4=91=C3=B3=20?= =?UTF-8?q?v=E1=BB=81=20server.=20K=E1=BA=BFt=20qu=E1=BA=A3=20l=C3=A0=20co?= =?UTF-8?q?de=20Python=20nh=E1=BA=ADn=20=C4=91=C6=B0=E1=BB=A3c=20gi=C3=A1?= =?UTF-8?q?=20tr=E1=BB=8B=20False=20ho=E1=BA=B7c=20Null.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__pycache__/epr_rfq.cpython-312.pyc | Bin 16938 -> 17658 bytes addons/epr/models/epr_rfq.py | 67 ++++-- addons/epr/views/epr_rfq_views.xml | 2 +- .../epr_create_rfq.cpython-312.pyc | Bin 7931 -> 7474 bytes addons/epr/wizards/epr_create_rfq.py | 207 +++++++++--------- addons/epr/wizards/epr_create_rfq_views.xml | 13 +- addons/epr/wizards/new_epr_create_rfq.py | 138 ++++++++++++ 7 files changed, 295 insertions(+), 132 deletions(-) create mode 100644 addons/epr/wizards/new_epr_create_rfq.py diff --git a/addons/epr/models/__pycache__/epr_rfq.cpython-312.pyc b/addons/epr/models/__pycache__/epr_rfq.cpython-312.pyc index 543598a56d6dd0ab7373b8bef8428d3e13ab5a50..47c723134746d2801c3ec3a1a18fdb7a206d994a 100644 GIT binary patch delta 1784 zcmZ`(T})F~9KQwbrC{lIDU?D_l>r6U3JOyD0WQXvIgt4Epe{}Axua#Jt*5taB_^$j z$%>gmcQndg#4Oo|?Uw1w5}LS&nkCD!M538D&D?CZC*zhn-HQ*)&bckf!*(D3`G5ZY zzw^8I+?(fRFBfFWA6ihk)WtX5RiF+4kCsz#eF zG!U3pXw0{504eE$!FFh>V$~2-|BKK8&H2t;9X{{0>bjr>0%(O>;WpSElI35^ari5z z)!0L}^b)+jjV(HghqHBtlnwTi>Og+`M$JHKtuyw`JgYu@9onHIS6_>EoM&>irW0+_ z%+P<#46dsHhc@+_r1K~k2Ddsjx}9(r;n1BQ-r&$dRWOdg-6X$RW^;1X=ENWG@upbR zPqraFH3niz@@NF|VP4=-4+-bn`53at5-1S#$9X&A&m{P`U=N2P{Jio=l;n9Zbr`%w zV|knxKqL_kC%j~GU-5Qi8YGK16ChAt{VV}y6MhgZu8`z(xsVNjcqkGCDdY!G>O5V% z5e3ts;tdcft|aZYhAO8=lo(}##)+9X#E_3J_l4j(8RE{Xr)mtr;VMI0L#Yw*VZXpb z38%!yg(%{Yk%FjVC<+q+!5%|q$tg8XRWr8w&2nIatAA+TSgV0!k&pnQ?|=#3A5S2D zogq~XwGmU|U}!od#PO7C2aA3q8Jux>?4>RKfDoGFeF9N#zm2PI*hQy?sTrY0(*Ft_ zQM>hRhzdyXf1#ycH|j?G$ORmTep009*vLyAJ7~B-O_G?BT=2io$B8!B>7E{z+3FJ8 zN&0BvRhFMER5+*b8B z^yMDAv*OpZk-z+7N_OVg$s?tDi(b zELdLsVs_Hjh~6UrrKUu5))$MC6QT<=R=`j^7WOB75&tZYGBl-$Ct@*GIK(60x*F&y zKC@Ggz1|W19eSoCq)$4NLcKV4VGLjMcJ+REP_%5jo?1>lw7AwRuFSp%3iD$#xPEB) z&_lCl&FsnSyEsv4!mHjP{D;?FKWXd4Z;bU>OUl8qN>)nm z5Uo+~BsKK6h#pe&gcy1U$$g7V^XM=9{n&2Vk61R|of4JW7h58|Gg|9X>s3qE zBFeXkDvPM;AXTwtf;4Ek^qUne)6sve=elFrA*y>tm6cXCh8In22Y0W*SW&;4H}r_A zHdOgj2Ej`7`6dBga8I?CqNtsO_~(wO1iZ9DF%`@i>hVmrcLXii9}gHFQZCAYDhb# zN>HiNpxqRr_mEaS9 zeh;hkxA$r1PY#ET!f!yooMq)_&cu|hi{$vdnt_j}&Rg^@%>uQl;fQY2OzqA!tBF#W zo1S)d98r&M*Ju!?Lv9CRkW=S0b7#g5PcQg57jhrdaey@K<2{Jig*Vzxq7CIy(IKK< z)Q7ytr~5SqzMl?t_ag~0`haGJzo$c&0(wxRJL){15^hG0duhjL5QUDk(TV$%)}dVM zB@^($1+j1FNCgca>wAe`{v;Z~|1qkc(kz`#K2q-sqX@1udh|`*UpyUt3+(YEZGnZj z4`jLDYEp?KRRAPM++njSkW3`t#Q;CbW7ay0vMbp_9$w1{Zj^;&)AcS@S)3 zt@pC)%9-?y+}g%wK}qK~KPlu2nblo<%KS-8D^tG-Uf560us`5zhk~=IUX%Jh{zTwm zY66y01GE*sO9jS?RI$AR`@Vy@bvw%DH&!#n^m=AZQ7fd>y_s7}Z{*Thq&|RK;{rUH znVA2E?3%%CysL-XN-8p+LjEi-{1lreYk!}Dm8o9)~tte1+i?YS~6Ti zMA_;2UzR_CZN#=?hHLo9hLvL7<&VBxy0=vGKU&^rA}>uQMt)_c*l2@94NUN0!t%Z4 z+Ub2J_|jx%uF#}E*l2|ff%1;lfSY^h|Cii}C*a0#XqDG;XmeFbs)z7=?yPu~rrEar K*A$6c)c*jASte@$ diff --git a/addons/epr/models/epr_rfq.py b/addons/epr/models/epr_rfq.py index 7ab7abd..2f22a65 100644 --- a/addons/epr/models/epr_rfq.py +++ b/addons/epr/models/epr_rfq.py @@ -54,7 +54,7 @@ class EprRfq(models.Model): # Link ngược lại PR gốc (01 RFQ có thể gom nhiều PR) request_ids = fields.Many2many( comodel_name='epr.purchase.request', - relation='epr_rfq_purchase_request_rel', # Tên bảng trung gian + relation='epr_rfq_purchase_request_rel', # Tên bảng trung gian column1='rfq_id', column2='request_id', string='Source Requests', @@ -363,29 +363,48 @@ class EprRfqLine(models.Model): comodel_name='epr.rfq', string='RFQ Reference', required=True, - ondelete='cascade' + ondelete='cascade', + index=True ) - # Sản phẩm (Odoo Product) + # === LIÊN KẾT VỚI PR (QUAN TRỌNG) === + # Link về dòng chi tiết của PR gốc + pr_line_id = fields.Many2one( + 'epr.purchase.request.line', + string='Source PR Line', + ondelete='set null', + index=True, + help="Dòng yêu cầu mua hàng gốc sinh ra dòng báo giá này." + ) + + # Link về PR Header (Tiện ích để group/filter) + purchase_request_id = fields.Many2one( + related='pr_line_id.request_id', + string='Purchase Request', + store=True, + readonly=True + ) + + # === SẢN PHẨM & CHI TIẾT === product_id = fields.Many2one( comodel_name='product.product', - string='Product' + string='Product', + required=True ) - description = fields.Text( - string='Description' - ) + description = fields.Text(string='Description') - # Số lượng & Đơn giá (Có thể khác với PR ban đầu do đàm phán) quantity = fields.Float( string='Quantity', required=True, - default=1.0 + default=1.0, + digits='Product Unit of Measure' ) uom_id = fields.Many2one( comodel_name='uom.uom', - string='UoM' + string='UoM', + required=True ) price_unit = fields.Float( @@ -398,30 +417,40 @@ class EprRfqLine(models.Model): relation='epr_rfq_line_taxes_rel', column1='line_id', column2='tax_id', - string='Taxes' + string='Taxes', + context={'active_test': False} + ) + + # === TÍNH TOÁN TIỀN TỆ === + currency_id = fields.Many2one( + related='rfq_id.currency_id', + store=True, + string='Currency', + readonly=True ) - # Tổng tiền trên RFQ line subtotal = fields.Monetary( compute='_compute_subtotal', string='Subtotal', - store=True + store=True, + currency_field='currency_id' ) - currency_id = fields.Many2one( - related='rfq_id.currency_id' - ) - - @api.depends('quantity', 'price_unit') + @api.depends('quantity', 'price_unit', 'taxes_id') def _compute_subtotal(self): + """Tính tổng tiền (chưa bao gồm thuế)""" for line in self: line.subtotal = line.quantity * line.price_unit + # === ONCHANGE PRODUCT (GỢI Ý) === @api.onchange('product_id') def _onchange_product_id(self): + """Tự động điền UoM và tên khi chọn sản phẩm""" if self.product_id: - self.description = self.product_id.display_name self.uom_id = self.product_id.uom_po_id or self.product_id.uom_id + self.description = self.product_id.display_name + # Tự động lấy thuế mua hàng mặc định của sản phẩm + self.taxes_id = self.product_id.supplier_taxes_id # ============================================================================== # LINE-LEVEL LINKING LOGIC (From RFQs to POs) diff --git a/addons/epr/views/epr_rfq_views.xml b/addons/epr/views/epr_rfq_views.xml index 0cc2713..0f10297 100644 --- a/addons/epr/views/epr_rfq_views.xml +++ b/addons/epr/views/epr_rfq_views.xml @@ -151,7 +151,7 @@ - +
diff --git a/addons/epr/wizards/__pycache__/epr_create_rfq.cpython-312.pyc b/addons/epr/wizards/__pycache__/epr_create_rfq.cpython-312.pyc index fc34cdccd107243894e53725a69f98f870872082..15f08771a87361ea639881dfb3e45f2d1be7f9fb 100644 GIT binary patch literal 7474 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 K6CynY@&5t5T}ul9 diff --git a/addons/epr/wizards/epr_create_rfq.py b/addons/epr/wizards/epr_create_rfq.py index c628d71..01186c3 100644 --- a/addons/epr/wizards/epr_create_rfq.py +++ b/addons/epr/wizards/epr_create_rfq.py @@ -14,65 +14,78 @@ class EprCreateRfqWizard(models.TransientModel): string='PR Lines to Process' ) + # ------------------------------------------------------------------------- + # 1. LOAD DATA (BẮT BUỘC CÓ) + # ------------------------------------------------------------------------- @api.model def default_get(self, fields_list): """ - Khi mở Wizard, tự động load các PR đã chọn từ màn hình danh sách. + Lấy dữ liệu từ các PR được chọn (active_ids) và điền vào dòng Wizard. """ res = super().default_get(fields_list) + + # Lấy ID của các PR đang được chọn ở màn hình danh sách active_ids = self.env.context.get('active_ids', []) if not active_ids: return res - # Lấy danh sách PR gốc + # Đọc dữ liệu PR requests = self.env['epr.purchase.request'].browse(active_ids) - # Prepare dữ liệu cho dòng Wizard + # Kiểm tra trạng thái (Optional: chỉ cho phép gộp PR đã duyệt) + if any(pr.state != 'approved' for pr in requests): + raise UserError(_("Bạn chỉ có thể tạo RFQ từ các PR đã được phê duyệt.")) + lines_vals = [] for pr in requests: - # Chỉ xử lý các PR đã được duyệt (Ví dụ trạng thái 'approved') - # Bạn có thể bỏ comment dòng dưới nếu có field state - if pr.state != 'approved': - raise UserError(_("PR %s chưa được duyệt.", pr.name)) - - for line in pr.line_ids: + # Loop qua từng dòng sản phẩm của PR để đưa vào Wizard + # Giả sử PR Line có model là 'epr.purchase.request.line' + for pr_line in pr.line_ids: lines_vals.append(Command.create({ - 'pr_line_id': line.id, # Link tới PR Line + # Link dữ liệu để truy vết sau này 'request_id': pr.id, - 'final_vendor_id': line.final_vendor_id.id if line.final_vendor_id else False, - 'suggested_vendor_name': line.suggested_vendor_name, + 'pr_line_id': pr_line.id, + + # Dữ liệu hiển thị/chỉnh sửa trên wizard + 'suggested_vendor_name': pr_line.suggested_vendor_name, + 'final_vendor_id': pr_line.final_vendor_id.id, + + 'final_product_id': pr_line.product_id.id, + 'product_description': pr_line.name or pr_line.product_id.name, + 'quantity': pr_line.quantity, + 'uom_id': ( + pr_line.product_id.uom_po_id.id or + pr_line.product_id.uom_id.id + ), })) + # Gán danh sách lệnh tạo dòng vào field line_ids res['line_ids'] = lines_vals return res def action_create_rfqs(self): """ - Logic hardened with .sudo(): - 1. Access PR data using sudo to bypass security rules. - 2. Group by Vendor. - 3. Create RFQ Header and Lines using sudo() to ensure data persistence. - 4. Redirect to the newly created RFQ(s). + Gộp PR thành RFQ: + 1. Validate: Chọn đầy đủ Vendor & Product. + 2. Gom nhóm theo Vendor. + 3. Tạo RFQ Header & Lines (Dùng sudo để bypass quyền truy cập PR). """ self.ensure_one() - # 1. Validation - missing_vendor_lines = self.line_ids.filtered(lambda l: not l.final_vendor_id) - if missing_vendor_lines: - raise UserError(_("Please select a Final Vendor for all lines.")) + # 1. Validate & Sync Vendor + for line in self.line_ids: + if not line.final_vendor_id: + raise UserError(_("Vui lòng chọn Vendor cho sản phẩm: %s", line.product_description)) - missing_product_lines = self.line_ids.filtered(lambda l: not l.final_product_id) - if missing_product_lines: - raise UserError(_("Please select a Final Product for all lines.")) + if line.pr_line_id.final_vendor_id != line.final_vendor_id: + line.pr_line_id.sudo().write({ + 'final_vendor_id': line.final_vendor_id.id + }) # 2. Grouping grouped_lines = {} for wiz_line in self.line_ids: - # Sync back to PR line (Sudo to ensure write permission if needed) - if wiz_line.pr_line_id.final_vendor_id != wiz_line.final_vendor_id: - wiz_line.pr_line_id.sudo().write({'final_vendor_id': wiz_line.final_vendor_id.id}) - vendor = wiz_line.final_vendor_id if vendor not in grouped_lines: grouped_lines[vendor] = self.env['epr.create.rfq.line'] @@ -82,64 +95,52 @@ class EprCreateRfqWizard(models.TransientModel): # 3. RFQ Creation for vendor, wiz_lines in grouped_lines.items(): + # A. Lấy danh sách PR unique cho field Many2many + source_requests = wiz_lines.mapped('request_id') + + # B. Chuẩn bị dữ liệu lines (One2many) rfq_line_commands = [] - source_pr_ids = [] - for wiz_line in wiz_lines: - # DÙNG SUDO: Để chắc chắn lấy được dữ liệu PR line - pr_line_sudo = wiz_line.pr_line_id.sudo() - request_sudo = wiz_line.request_id.sudo() - - # Collect unique source PR ids - if request_sudo and request_sudo.id not in source_pr_ids: - source_pr_ids.append(request_sudo.id) - - # Determine UoM (Directly from Product or PR Line) - uom_id = False - if wiz_line.final_product_id: - uom_id = wiz_line.final_product_id.uom_po_id.id or wiz_line.final_product_id.uom_id.id - - if not uom_id and pr_line_sudo.product_id: - uom_id = pr_line_sudo.product_id.uom_po_id.id or pr_line_sudo.product_id.uom_id.id - - # Create line command - DÙNG GIÁ TRỊ TỪ SUDO RECORD rfq_line_commands.append(Command.create({ 'product_id': wiz_line.final_product_id.id, - 'description': pr_line_sudo.name or '', - 'quantity': pr_line_sudo.quantity or 1.0, - 'uom_id': uom_id, - 'price_unit': 0.0, + 'description': wiz_line.product_description, + 'quantity': wiz_line.quantity, + 'uom_id': wiz_line.uom_id.id, + # Link ngược lại dòng PR gốc để truy vết + 'pr_line_id': wiz_line.pr_line_id.id })) - # Create RFQ header - SỬ DỤNG SUDO ĐỂ TẠO - rfq = self.env['epr.rfq'].sudo().create({ + # Tạo RFQ Header + rfq_vals = { 'partner_id': vendor.id, 'state': 'draft', 'date_order': fields.Datetime.now(), - 'request_ids': [Command.set(source_pr_ids)], + 'request_ids': [Command.set(source_requests.ids)], 'line_ids': rfq_line_commands, - }) + } + + rfq = self.env['epr.rfq'].create(rfq_vals) created_rfqs |= rfq - # 4. Result Action + # 4. Redirect + if not created_rfqs: + return {'type': 'ir.actions.act_window_close'} + + action = { + 'name': _('Generated RFQs'), + 'type': 'ir.actions.act_window', + 'res_model': 'epr.rfq', + 'context': {'create': False}, + } + if len(created_rfqs) == 1: - return { - 'name': _('Request for Quotation'), - 'type': 'ir.actions.act_window', - 'res_model': 'epr.rfq', - 'view_mode': 'form', - 'res_id': created_rfqs.id, - 'target': 'current', - } + action['view_mode'] = 'form' + action['res_id'] = created_rfqs.id else: - return { - 'name': _('Requests for Quotation'), - 'type': 'ir.actions.act_window', - 'res_model': 'epr.rfq', - 'view_mode': 'list,form', - 'domain': [('id', 'in', created_rfqs.ids)], - 'target': 'current', - } + action['view_mode'] = 'list,form' # Odoo 18 dùng 'list' + action['domain'] = [('id', 'in', created_rfqs.ids)] + + return action class EprCreateRfqLine(models.TransientModel): @@ -148,62 +149,52 @@ class EprCreateRfqLine(models.TransientModel): wizard_id = fields.Many2one('epr.create.rfq.wizard', string='Wizard') - # Link tới PR gốc (Readonly) + # Dữ liệu PR gốc request_id = fields.Many2one( - comodel_name='epr.purchase.request', - string='Purchase Request', + 'epr.purchase.request', + string='PR', readonly=True ) - # Link tới PR Line gốc pr_line_id = fields.Many2one( - comodel_name='epr.purchase.request.line', + 'epr.purchase.request.line', string='PR Line', readonly=True ) - # Cột hiển thị text gợi ý (Readonly) -> Giúp Officer tham chiếu - suggested_vendor_name = fields.Char( - string='Suggested Vendor (Text)', - readonly=True, - help="Tên nhà cung cấp do người yêu cầu nhập tay (tham khảo)." - ) + suggested_vendor_name = fields.Char(string='Suggested Vendor', readonly=True) - # Cột Final Vendor (Editable) -> Đây là nơi Officer thao tác chính + # Cho phép User chọn/sửa trong Wizard final_vendor_id = fields.Many2one( - comodel_name='res.partner', + 'res.partner', string='Final Vendor', - required=True, - # domain="[('supplier_rank', '>', 0)]", # Chỉ lấy nhà cung cấp - help="Chọn nhà cung cấp chính thức trong hệ thống để tạo RFQ." + required=True ) - # Lấy thông tin sản phẩm cần mua - product_description = fields.Char( - related='pr_line_id.name', - string='Product / Description', - readonly=True - ) - - # Purchasing Officer chọn sản phẩm tương ứng với mô tả của User final_product_id = fields.Many2one( - comodel_name='product.product', + 'product.product', string='Final Product', - required=True, - # domain="[('purchase_ok', '=', True)]", - help="Chọn sản phẩm tương ứng với mô tả của người yêu cầu." + required=True ) - # Lấy số lượng + product_description = fields.Char(string='Description') quantity = fields.Float( - related='pr_line_id.quantity', string='Qty', + digits='Product Unit of Measure' + ) + + uom_id = fields.Many2one( + 'uom.uom', + string='UoM' + ) + + uom_name = fields.Char( + string='PR UoM', readonly=True ) - # Lấy đơn vị tính - uom_name = fields.Char( - related='pr_line_id.uom_name', - string='UoM', - readonly=True - ) + @api.onchange('final_product_id') + def _onchange_final_product_id(self): + if self.final_product_id: + product = self.final_product_id + self.uom_id = product.uom_po_id or product.uom_id diff --git a/addons/epr/wizards/epr_create_rfq_views.xml b/addons/epr/wizards/epr_create_rfq_views.xml index 32dc1aa..11a0ecf 100644 --- a/addons/epr/wizards/epr_create_rfq_views.xml +++ b/addons/epr/wizards/epr_create_rfq_views.xml @@ -21,10 +21,14 @@ - + - - + + - + + RFQ + request_id = fields.Many2one('epr.purchase.request', readonly=True) + pr_line_id = fields.Many2one('epr.purchase.request.line', readonly=True) # Lưu tham chiếu dòng gốc + + suggested_vendor_name = fields.Char(readonly=True) + final_vendor_id = fields.Many2one('res.partner', string='Final Vendor', required=True) + + final_product_id = fields.Many2one('product.product', string='Product', required=True) + product_description = fields.Char(string='Description') + + # Field Quantity cần editable trong wizard để user có thể điều chỉnh nếu muốn + quantity = fields.Float(string='Quantity', digits='Product Unit of Measure') + uom_name = fields.Char(readonly=True)