diff --git a/addons/epr/models/__pycache__/epr_rfq.cpython-312.pyc b/addons/epr/models/__pycache__/epr_rfq.cpython-312.pyc index 543598a..47c7231 100644 Binary files a/addons/epr/models/__pycache__/epr_rfq.cpython-312.pyc and b/addons/epr/models/__pycache__/epr_rfq.cpython-312.pyc differ 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 fc34cdc..15f0877 100644 Binary files a/addons/epr/wizards/__pycache__/epr_create_rfq.cpython-312.pyc and b/addons/epr/wizards/__pycache__/epr_create_rfq.cpython-312.pyc differ 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)