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)