Compare commits

...

2 Commits

Author SHA1 Message Date
Vincent Phan
d113554d0f
Merge d993893bc3 into cb68d1a71a 2026-01-06 20:29:05 -05:00
mtpc4s9
d993893bc3 Viết xong Wizard Reject RFQs
Điều chỉnh Wizard tạo PO
2026-01-03 14:07:59 +07:00
24 changed files with 338 additions and 89 deletions

View File

@ -34,6 +34,7 @@
'views/epr_po_views.xml',
'views/epr_menus.xml',
'wizards/epr_reject_wizard_views.xml',
'wizards/epr_reject_rfq_wizard_views.xml',
'wizards/epr_create_rfq_views.xml',
'wizards/epr_create_po_views.xml',
],

View File

@ -115,15 +115,17 @@ class EprApprovalEntry(models.Model):
# Trigger kiểm tra xem RFQ đã được duyệt hoàn toàn chưa
self.rfq_id._check_approval_completion()
def action_refuse_line(self):
"""User bấm nút Refuse"""
# return {
# 'type': 'ir.actions.act_window',
# 'name': _('Refuse Reason'),
# 'res_model': 'epr.approval.refuse.wizard', # Viết sau
# 'target': 'new',
# 'context': {'default_entry_id': self.id}
# }
self.write({
'status': 'refused'
})
def action_reject_line(self):
"""User bấm nút Refuse - Mở wizard để nhập lý do"""
self.ensure_one()
return {
'name': _('Reject RFQ'),
'type': 'ir.actions.act_window',
'res_model': 'epr.reject.rfq.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'active_id': self.rfq_id.id,
'active_model': 'epr.rfq'
}
}

View File

@ -17,17 +17,32 @@ class PurchaseOrder(models.Model):
help="Danh sách các phiếu yêu cầu báo giá (EPR RFQ) nguồn tạo nên PO này."
)
epr_source_pr_ids = fields.Many2many(
comodel_name='epr.purchase.request',
relation='epr_pr_purchase_order_rel',
column1='purchase_id',
column2='epr_pr_id',
string='Source ePR Requests',
copy=False,
readonly=True,
)
# === COMPUTED FIELDS CHO SMART BUTTON (Line-Level Linking) ===
epr_rfq_count = fields.Integer(
string='RFQ Count',
compute='_compute_epr_rfq_count'
compute='_compute_epr_counts'
)
@api.depends('epr_source_rfq_ids')
def _compute_epr_rfq_count(self):
epr_pr_count = fields.Integer(
string='PR Count',
compute='_compute_epr_counts'
)
@api.depends('epr_source_rfq_ids', 'epr_source_pr_ids')
def _compute_epr_counts(self):
for po in self:
# Đếm trực tiếp từ field Many2many đã lưu trữ
po.epr_rfq_count = len(po.epr_source_rfq_ids)
po.epr_pr_count = len(po.epr_source_pr_ids)
# === ACTION SMART BUTTON ===
def action_view_epr_rfqs(self):
@ -55,6 +70,19 @@ class PurchaseOrder(models.Model):
'context': {'create': False},
}
def action_view_epr_prs(self):
"""Mở danh sách các PR nguồn"""
self.ensure_one()
pr_ids = self.epr_source_pr_ids.ids
return {
'name': _('Source PRs'),
'type': 'ir.actions.act_window',
'res_model': 'epr.purchase.request',
'view_mode': 'list,form',
'domain': [('id', 'in', pr_ids)],
'context': {'create': False},
}
class PurchaseOrderLine(models.Model):
_inherit = 'purchase.order.line'

View File

@ -28,6 +28,7 @@ class EprRfq(models.Model):
('to_approve', 'To Approve'), # Trình sếp duyệt giá
('approved', 'Approved'), # Đã duyệt xong, chờ PO
('confirmed', 'Confirmed'), # Đã chốt -> Đang tạo/Có PO
('rejected', 'Rejected'), # Đã từ chối
('cancel', 'Cancelled')
],
string='Status',
@ -64,6 +65,14 @@ class EprRfq(models.Model):
currency_field='currency_id'
)
# Stores the reason directly on the RFQ for easy visibility
rejection_reason = fields.Text(
string='Rejection Reason',
readonly=True,
tracking=True,
help="The reason why this RFQ was rejected."
)
department_id = fields.Many2one(
comodel_name='hr.department',
compute='_compute_department_id',
@ -278,11 +287,50 @@ class EprRfq(models.Model):
'target': 'current',
}
def action_reject(self):
"""
Opens the specific RFQ Rejection Wizard.
"""
self.ensure_one()
return {
'name': _('Reject RFQ'),
'type': 'ir.actions.act_window',
'res_model': 'epr.reject.rfq.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'default_reason': self.rejection_reason or '', # Optional: Pre-fill if needed
'active_id': self.id,
'active_model': 'epr.rfq'
}
}
# === CALLBACK: HANDLE REJECTION ===
def action_handle_rejection(self, reason):
"""
Callback method called by the Wizard after confirmation.
1. Updates the state to 'cancel' (or keeps it in a specific rejected state).
2. Stores the reason.
"""
for rfq in self:
rfq.write({
'state': 'rejected', # Move RFQ to Cancelled state
'approval_state': 'refused', # Update Approval Matrix status
'rejection_reason': reason # Persist the reason on the main record
})
# Log a chatter message for visibility
# rfq.message_post(
# body=_("RFQ has been rejected. Reason: %s") % reason,
# message_type='comment',
# subtype_xmlid='mail.mt_note'
# )
def action_reset_draft(self):
"""Cho phép quay lại Draft nếu cần sửa"""
for rfq in self:
if rfq.state not in ['sent', 'to_approve', 'cancel']:
raise UserError(_("Chỉ có thể reset khi ở trạng thái Sent, To Approve hoặc Cancel."))
if rfq.state not in ['sent', 'to_approve', 'cancel', 'rejected']:
raise UserError(_("Chỉ có thể reset khi ở trạng thái Sent, To Approve, Cancel hoặc Rejected."))
rfq.write({'state': 'draft'})
# Mở danh sách các PO được tạo từ RFQ này
@ -360,7 +408,7 @@ class EprRfq(models.Model):
return
# 4. Hỗ trợ Duyệt song song cùng tầng (Sequence)
self.approval_entry_ids.unlink()
self.sudo().approval_entry_ids.unlink()
vals_list = []
min_seq = applicable_lines[0].sequence
for line in applicable_lines:
@ -397,7 +445,7 @@ class EprRfq(models.Model):
# A. Nếu có bất kỳ dòng nào bị từ chối -> Hủy toàn bộ quy trình
if any(e.status == 'refused' for e in self.approval_entry_ids):
self.write({
'state': 'cancel',
'state': 'rejected',
'approval_state': 'refused'
})

View File

@ -75,5 +75,14 @@
eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
</record>
<!-- === QUAN TRỌNG: TỰ ĐỘNG GÁN QUYỀN REQUESTER CHO ALL USERS === -->
<!--
Logic: Can thiệp vào nhóm gốc 'base.group_user' (Internal User)
và bảo nó rằng: "Bất kỳ ai là Internal User thì mặc định cũng là ePR User".
-->
<record id="base.group_user" model="res.groups">
<field name="implied_ids" eval="[(4, ref('epr.group_epr_user'))]"/>
</record>
</data>
</odoo>

View File

@ -1,37 +1,41 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_epr_purchase_request_user,ePR Request User,model_epr_purchase_request,group_epr_user,1,1,1,0
access_epr_purchase_request_line_user,ePR Request Line User,model_epr_purchase_request_line,group_epr_user,1,1,1,0
access_epr_purchase_request_manager,ePR Request Manager,model_epr_purchase_request,group_epr_manager,1,1,1,0
access_epr_purchase_request_line_manager,ePR Request Line Manager,model_epr_purchase_request_line,group_epr_manager,1,1,1,0
access_epr_purchase_request_officer,ePR Request Officer,model_epr_purchase_request,group_epr_purchasing_officer,1,1,1,0
access_epr_purchase_request_line_officer,ePR Request Line Officer,model_epr_purchase_request_line,group_epr_purchasing_officer,1,1,1,0
access_epr_purchase_request_admin,ePR Request Admin,model_epr_purchase_request,group_epr_admin,1,1,1,1
access_epr_purchase_request_line_admin,ePR Request Line Admin,model_epr_purchase_request_line,group_epr_admin,1,1,1,1
access_epr_rfq_officer,ePR RFQ Officer,model_epr_rfq,group_epr_purchasing_officer,1,1,1,0
access_epr_rfq_line_officer,ePR RFQ Line Officer,model_epr_rfq_line,group_epr_purchasing_officer,1,1,1,0
access_epr_rfq_admin,ePR RFQ Admin,model_epr_rfq,group_epr_admin,1,1,1,1
access_epr_rfq_line_admin,ePR RFQ Line Admin,model_epr_rfq_line,group_epr_admin,1,1,1,1
access_epr_rfq_manager,ePR RFQ Manager,model_epr_rfq,group_epr_manager,1,1,0,0
access_epr_rfq_line_manager,ePR RFQ Line Manager,model_epr_rfq_line,group_epr_manager,1,1,0,0
access_epr_approval_rule_officer,ePR Approval Rule Officer,model_epr_approval_rule,group_epr_purchasing_officer,1,1,1,1
access_epr_approval_rule_line_officer,ePR Approval Rule Line Officer,model_epr_approval_rule_line,group_epr_purchasing_officer,1,1,1,1
access_epr_approval_rule_admin,ePR Approval Rule Admin,model_epr_approval_rule,group_epr_admin,1,1,1,1
access_epr_approval_rule_line_admin,ePR Approval Rule Line Admin,model_epr_approval_rule_line,group_epr_admin,1,1,1,1
access_epr_approval_rule_manager,ePR Approval Rule Manager,model_epr_approval_rule,group_epr_manager,1,0,0,0
access_epr_approval_rule_line_manager,ePR Approval Rule Line Manager,model_epr_approval_rule_line,group_epr_manager,1,0,0,0
access_epr_approval_entry_user,ePR Approval Entry User,model_epr_approval_entry,group_epr_user,1,0,0,0
access_epr_approval_entry_manager,ePR Approval Entry Manager,model_epr_approval_entry,group_epr_manager,1,1,0,0
access_epr_approval_entry_officer,ePR Approval Entry Officer,model_epr_approval_entry,group_epr_purchasing_officer,1,1,1,0
access_epr_approval_entry_admin,ePR Approval Entry Admin,model_epr_approval_entry,group_epr_admin,1,1,1,1
access_epr_reject_wizard_user,ePR Reject Wizard User,model_epr_reject_wizard,group_epr_user,1,0,0,0
access_epr_reject_wizard_manager,ePR Reject Wizard Manager,model_epr_reject_wizard,group_epr_manager,1,1,1,1
access_epr_reject_wizard_officer,ePR Reject Wizard Officer,model_epr_reject_wizard,group_epr_purchasing_officer,1,1,1,1
access_epr_reject_wizard_admin,ePR Reject Wizard Admin,model_epr_reject_wizard,group_epr_admin,1,1,1,1
access_epr_create_po_wizard_officer,ePR Create PO Wizard Officer,model_epr_create_po_wizard,group_epr_purchasing_officer,1,1,1,1
access_epr_create_po_wizard_admin,ePR Create PO Wizard Admin,model_epr_create_po_wizard,group_epr_admin,1,1,1,1
access_epr_create_po_line_wizard_officer,ePR Create PO Line Wizard Officer,model_epr_create_po_line_wizard,group_epr_purchasing_officer,1,1,1,1
access_epr_create_po_line_wizard_admin,ePR Create PO Line Wizard Admin,model_epr_create_po_line_wizard,group_epr_admin,1,1,1,1
access_epr_create_rfq_wizard_officer,ePR Create RFQ Wizard Officer,model_epr_create_rfq_wizard,group_epr_purchasing_officer,1,1,1,1
access_epr_create_rfq_wizard_admin,ePR Create RFQ Wizard Admin,model_epr_create_rfq_wizard,group_epr_admin,1,1,1,1
access_epr_create_rfq_line_officer,ePR Create RFQ Line Officer,model_epr_create_rfq_line,group_epr_purchasing_officer,1,1,1,1
access_epr_create_rfq_line_admin,ePR Create RFQ Line Admin,model_epr_create_rfq_line,group_epr_admin,1,1,1,1
access_epr_purchase_request_user,ePR Request User,model_epr_purchase_request,epr.group_epr_user,1,1,1,0
access_epr_purchase_request_line_user,ePR Request Line User,model_epr_purchase_request_line,epr.group_epr_user,1,1,1,0
access_epr_purchase_request_manager,ePR Request Manager,model_epr_purchase_request,epr.group_epr_manager,1,1,1,0
access_epr_purchase_request_line_manager,ePR Request Line Manager,model_epr_purchase_request_line,epr.group_epr_manager,1,1,1,0
access_epr_purchase_request_officer,ePR Request Officer,model_epr_purchase_request,epr.group_epr_purchasing_officer,1,1,1,0
access_epr_purchase_request_line_officer,ePR Request Line Officer,model_epr_purchase_request_line,epr.group_epr_purchasing_officer,1,1,1,0
access_epr_purchase_request_admin,ePR Request Admin,model_epr_purchase_request,epr.group_epr_admin,1,1,1,1
access_epr_purchase_request_line_admin,ePR Request Line Admin,model_epr_purchase_request_line,epr.group_epr_admin,1,1,1,1
access_epr_rfq_officer,ePR RFQ Officer,model_epr_rfq,epr.group_epr_purchasing_officer,1,1,1,0
access_epr_rfq_line_officer,ePR RFQ Line Officer,model_epr_rfq_line,epr.group_epr_purchasing_officer,1,1,1,0
access_epr_rfq_admin,ePR RFQ Admin,model_epr_rfq,epr.group_epr_admin,1,1,1,1
access_epr_rfq_line_admin,ePR RFQ Line Admin,model_epr_rfq_line,epr.group_epr_admin,1,1,1,1
access_epr_rfq_manager,ePR RFQ Manager,model_epr_rfq,epr.group_epr_manager,1,1,0,0
access_epr_rfq_line_manager,ePR RFQ Line Manager,model_epr_rfq_line,epr.group_epr_manager,1,1,0,0
access_epr_approval_rule_officer,ePR Approval Rule Officer,model_epr_approval_rule,epr.group_epr_purchasing_officer,1,1,1,1
access_epr_approval_rule_line_officer,ePR Approval Rule Line Officer,model_epr_approval_rule_line,epr.group_epr_purchasing_officer,1,1,1,1
access_epr_approval_rule_admin,ePR Approval Rule Admin,model_epr_approval_rule,epr.group_epr_admin,1,1,1,1
access_epr_approval_rule_line_admin,ePR Approval Rule Line Admin,model_epr_approval_rule_line,epr.group_epr_admin,1,1,1,1
access_epr_approval_rule_manager,ePR Approval Rule Manager,model_epr_approval_rule,epr.group_epr_manager,1,0,0,0
access_epr_approval_rule_line_manager,ePR Approval Rule Line Manager,model_epr_approval_rule_line,epr.group_epr_manager,1,0,0,0
access_epr_approval_entry_user,ePR Approval Entry User Read,model_epr_approval_entry,epr.group_epr_user,1,0,0,0
access_epr_approval_entry_user,ePR Approval Entry User,model_epr_approval_entry,epr.group_epr_user,1,0,0,0
access_epr_approval_entry_manager,ePR Approval Entry Manager,model_epr_approval_entry,epr.group_epr_manager,1,1,1,0
access_epr_approval_entry_officer,ePR Approval Entry Officer,model_epr_approval_entry,epr.group_epr_purchasing_officer,1,1,1,0
access_epr_approval_entry_admin,ePR Approval Entry Admin,model_epr_approval_entry,epr.group_epr_admin,1,1,1,1
access_epr_reject_wizard_user,ePR Reject Wizard User,model_epr_reject_wizard,epr.group_epr_user,1,0,0,0
access_epr_reject_wizard_manager,ePR Reject Wizard Manager,model_epr_reject_wizard,epr.group_epr_manager,1,1,1,1
access_epr_reject_wizard_officer,ePR Reject Wizard Officer,model_epr_reject_wizard,epr.group_epr_purchasing_officer,1,1,1,1
access_epr_reject_wizard_admin,ePR Reject Wizard Admin,model_epr_reject_wizard,epr.group_epr_admin,1,1,1,1
access_epr_create_po_wizard_officer,ePR Create PO Wizard Officer,model_epr_create_po_wizard,epr.group_epr_purchasing_officer,1,1,1,1
access_epr_create_po_wizard_admin,ePR Create PO Wizard Admin,model_epr_create_po_wizard,epr.group_epr_admin,1,1,1,1
access_epr_create_po_line_wizard_officer,ePR Create PO Line Wizard Officer,model_epr_create_po_line_wizard,epr.group_epr_purchasing_officer,1,1,1,1
access_epr_create_po_line_wizard_admin,ePR Create PO Line Wizard Admin,model_epr_create_po_line_wizard,epr.group_epr_admin,1,1,1,1
access_epr_create_rfq_wizard_officer,ePR Create RFQ Wizard Officer,model_epr_create_rfq_wizard,epr.group_epr_purchasing_officer,1,1,1,1
access_epr_create_rfq_wizard_admin,ePR Create RFQ Wizard Admin,model_epr_create_rfq_wizard,epr.group_epr_admin,1,1,1,1
access_epr_create_rfq_line_officer,ePR Create RFQ Line Officer,model_epr_create_rfq_line,epr.group_epr_purchasing_officer,1,1,1,1
access_epr_create_rfq_line_admin,ePR Create RFQ Line Admin,model_epr_create_rfq_line,epr.group_epr_admin,1,1,1,1
access_epr_reject_rfq_wizard_manager,ePR Reject RFQ Wizard Manager,model_epr_reject_rfq_wizard,epr.group_epr_manager,1,1,1,1
access_epr_reject_rfq_wizard_officer,ePR Reject RFQ Wizard Officer,model_epr_reject_rfq_wizard,epr.group_epr_purchasing_officer,1,1,1,1
access_epr_reject_rfq_wizard_admin,ePR Reject RFQ Wizard Admin,model_epr_reject_rfq_wizard,epr.group_epr_admin,1,1,1,1

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_epr_purchase_request_user ePR Request User model_epr_purchase_request group_epr_user epr.group_epr_user 1 1 1 0
3 access_epr_purchase_request_line_user ePR Request Line User model_epr_purchase_request_line group_epr_user epr.group_epr_user 1 1 1 0
4 access_epr_purchase_request_manager ePR Request Manager model_epr_purchase_request group_epr_manager epr.group_epr_manager 1 1 1 0
5 access_epr_purchase_request_line_manager ePR Request Line Manager model_epr_purchase_request_line group_epr_manager epr.group_epr_manager 1 1 1 0
6 access_epr_purchase_request_officer ePR Request Officer model_epr_purchase_request group_epr_purchasing_officer epr.group_epr_purchasing_officer 1 1 1 0
7 access_epr_purchase_request_line_officer ePR Request Line Officer model_epr_purchase_request_line group_epr_purchasing_officer epr.group_epr_purchasing_officer 1 1 1 0
8 access_epr_purchase_request_admin ePR Request Admin model_epr_purchase_request group_epr_admin epr.group_epr_admin 1 1 1 1
9 access_epr_purchase_request_line_admin ePR Request Line Admin model_epr_purchase_request_line group_epr_admin epr.group_epr_admin 1 1 1 1
10 access_epr_rfq_officer ePR RFQ Officer model_epr_rfq group_epr_purchasing_officer epr.group_epr_purchasing_officer 1 1 1 0
11 access_epr_rfq_line_officer ePR RFQ Line Officer model_epr_rfq_line group_epr_purchasing_officer epr.group_epr_purchasing_officer 1 1 1 0
12 access_epr_rfq_admin ePR RFQ Admin model_epr_rfq group_epr_admin epr.group_epr_admin 1 1 1 1
13 access_epr_rfq_line_admin ePR RFQ Line Admin model_epr_rfq_line group_epr_admin epr.group_epr_admin 1 1 1 1
14 access_epr_rfq_manager ePR RFQ Manager model_epr_rfq group_epr_manager epr.group_epr_manager 1 1 0 0
15 access_epr_rfq_line_manager ePR RFQ Line Manager model_epr_rfq_line group_epr_manager epr.group_epr_manager 1 1 0 0
16 access_epr_approval_rule_officer ePR Approval Rule Officer model_epr_approval_rule group_epr_purchasing_officer epr.group_epr_purchasing_officer 1 1 1 1
17 access_epr_approval_rule_line_officer ePR Approval Rule Line Officer model_epr_approval_rule_line group_epr_purchasing_officer epr.group_epr_purchasing_officer 1 1 1 1
18 access_epr_approval_rule_admin ePR Approval Rule Admin model_epr_approval_rule group_epr_admin epr.group_epr_admin 1 1 1 1
19 access_epr_approval_rule_line_admin ePR Approval Rule Line Admin model_epr_approval_rule_line group_epr_admin epr.group_epr_admin 1 1 1 1
20 access_epr_approval_rule_manager ePR Approval Rule Manager model_epr_approval_rule group_epr_manager epr.group_epr_manager 1 0 0 0
21 access_epr_approval_rule_line_manager ePR Approval Rule Line Manager model_epr_approval_rule_line group_epr_manager epr.group_epr_manager 1 0 0 0
22 access_epr_approval_entry_user ePR Approval Entry User ePR Approval Entry User Read model_epr_approval_entry group_epr_user epr.group_epr_user 1 0 0 0
23 access_epr_approval_entry_manager access_epr_approval_entry_user ePR Approval Entry Manager ePR Approval Entry User model_epr_approval_entry group_epr_manager epr.group_epr_user 1 1 0 0 0
24 access_epr_approval_entry_officer access_epr_approval_entry_manager ePR Approval Entry Officer ePR Approval Entry Manager model_epr_approval_entry group_epr_purchasing_officer epr.group_epr_manager 1 1 1 0
25 access_epr_approval_entry_admin access_epr_approval_entry_officer ePR Approval Entry Admin ePR Approval Entry Officer model_epr_approval_entry group_epr_admin epr.group_epr_purchasing_officer 1 1 1 1 0
26 access_epr_reject_wizard_user access_epr_approval_entry_admin ePR Reject Wizard User ePR Approval Entry Admin model_epr_reject_wizard model_epr_approval_entry group_epr_user epr.group_epr_admin 1 0 1 0 1 0 1
27 access_epr_reject_wizard_manager access_epr_reject_wizard_user ePR Reject Wizard Manager ePR Reject Wizard User model_epr_reject_wizard group_epr_manager epr.group_epr_user 1 1 0 1 0 1 0
28 access_epr_reject_wizard_officer access_epr_reject_wizard_manager ePR Reject Wizard Officer ePR Reject Wizard Manager model_epr_reject_wizard group_epr_purchasing_officer epr.group_epr_manager 1 1 1 1
29 access_epr_reject_wizard_admin access_epr_reject_wizard_officer ePR Reject Wizard Admin ePR Reject Wizard Officer model_epr_reject_wizard group_epr_admin epr.group_epr_purchasing_officer 1 1 1 1
30 access_epr_create_po_wizard_officer access_epr_reject_wizard_admin ePR Create PO Wizard Officer ePR Reject Wizard Admin model_epr_create_po_wizard model_epr_reject_wizard group_epr_purchasing_officer epr.group_epr_admin 1 1 1 1
31 access_epr_create_po_wizard_admin access_epr_create_po_wizard_officer ePR Create PO Wizard Admin ePR Create PO Wizard Officer model_epr_create_po_wizard group_epr_admin epr.group_epr_purchasing_officer 1 1 1 1
32 access_epr_create_po_line_wizard_officer access_epr_create_po_wizard_admin ePR Create PO Line Wizard Officer ePR Create PO Wizard Admin model_epr_create_po_line_wizard model_epr_create_po_wizard group_epr_purchasing_officer epr.group_epr_admin 1 1 1 1
33 access_epr_create_po_line_wizard_admin access_epr_create_po_line_wizard_officer ePR Create PO Line Wizard Admin ePR Create PO Line Wizard Officer model_epr_create_po_line_wizard group_epr_admin epr.group_epr_purchasing_officer 1 1 1 1
34 access_epr_create_rfq_wizard_officer access_epr_create_po_line_wizard_admin ePR Create RFQ Wizard Officer ePR Create PO Line Wizard Admin model_epr_create_rfq_wizard model_epr_create_po_line_wizard group_epr_purchasing_officer epr.group_epr_admin 1 1 1 1
35 access_epr_create_rfq_wizard_admin access_epr_create_rfq_wizard_officer ePR Create RFQ Wizard Admin ePR Create RFQ Wizard Officer model_epr_create_rfq_wizard group_epr_admin epr.group_epr_purchasing_officer 1 1 1 1
36 access_epr_create_rfq_line_officer access_epr_create_rfq_wizard_admin ePR Create RFQ Line Officer ePR Create RFQ Wizard Admin model_epr_create_rfq_line model_epr_create_rfq_wizard group_epr_purchasing_officer epr.group_epr_admin 1 1 1 1
37 access_epr_create_rfq_line_admin access_epr_create_rfq_line_officer ePR Create RFQ Line Admin ePR Create RFQ Line Officer model_epr_create_rfq_line group_epr_admin epr.group_epr_purchasing_officer 1 1 1 1
38 access_epr_create_rfq_line_admin ePR Create RFQ Line Admin model_epr_create_rfq_line epr.group_epr_admin 1 1 1 1
39 access_epr_reject_rfq_wizard_manager ePR Reject RFQ Wizard Manager model_epr_reject_rfq_wizard epr.group_epr_manager 1 1 1 1
40 access_epr_reject_rfq_wizard_officer ePR Reject RFQ Wizard Officer model_epr_reject_rfq_wizard epr.group_epr_purchasing_officer 1 1 1 1
41 access_epr_reject_rfq_wizard_admin ePR Reject RFQ Wizard Admin model_epr_reject_rfq_wizard epr.group_epr_admin 1 1 1 1

View File

@ -113,10 +113,11 @@
decoration-muted="status == 'pending'"/>
<field name="actual_user_id"/>
<field name="approval_date"/>
<field name="rejection_reason" optional="show"/>
<button name="action_approve_line" string="Approve" type="object"
icon="fa-check text-success"
invisible="not can_approve"/>
<button name="action_refuse_line" string="Refuse" type="object"
<button name="action_reject_line" string="Refuse" type="object"
icon="fa-times text-danger"
invisible="not can_approve"/>
<field name="can_approve" column_invisible="True"/>
@ -134,7 +135,7 @@
<button name="action_approve_line" string="Approve" type="object"
class="oe_highlight" invisible="not can_approve"
confirm="Are you sure you want to approve this request?"/>
<button name="action_refuse_line" string="Refuse" type="object"
<button name="action_reject_line" string="Refuse" type="object"
invisible="not can_approve"/>
<field name="status" widget="statusbar"/>
</header>

View File

@ -11,9 +11,7 @@
id="menu_epr_root"
name="eProcurement"
sequence="10"
web_icon="epr,static/description/icon.png"
groups="epr.group_epr_user"/>
<!-- Chỉ hiện cho nhóm User (và Manager vì Manager kế thừa User) -->
web_icon="epr,static/description/icon.png"/>
<!--
===================================================================
@ -77,7 +75,8 @@
name="Pending Approvals"
parent="menu_epr_rfq_category"
action="action_epr_my_approvals"
sequence="20"/>
sequence="20"
groups="epr.group_epr_manager,epr.group_epr_admin"/>
<!-- Menu: My POs -->
<menuitem

View File

@ -18,6 +18,10 @@
invisible="epr_rfq_count == 0">
<field name="epr_rfq_count" widget="statinfo" string="EPR RFQs"/>
</button>
<button name="action_view_epr_prs" type="object" class="oe_stat_button" icon="fa-shopping-cart" invisible="epr_pr_count == 0">
<field name="epr_pr_count" widget="statinfo" string="Source PRs"/>
</button>
</div>
<!-- 2. Tab Other Information: Hiển thị Link Many2many -->

View File

@ -58,6 +58,12 @@
class="oe_highlight"
invisible="state != 'received'"/>
<button name="action_reject"
string="Reject"
type="object"
class="btn-danger"
invisible="state != 'to_approve'"/>
<button name="action_confirm"
string="Confirm RFQ"
type="object"
@ -80,10 +86,10 @@
string="Reset to Draft"
type="object"
class="btn-warning"
invisible="state not in ('sent', 'to_approve', 'approved', 'cancel')"/>
invisible="state not in ('sent', 'to_approve', 'approved', 'rejected', 'cancel')"/>
<!-- Widget Statusbar: Hiển thị quy trình -->
<field name="state" widget="statusbar" statusbar_visible="draft,sent,received,to_approve,approved,confirmed"/>
<field name="state" widget="statusbar" statusbar_visible="draft,sent,received,to_approve,approved,confirmed,rejected"/>
</header>
<sheet>
@ -167,7 +173,7 @@
<page string="Approval Matrix" name="approval_matrix">
<field name="approval_entry_ids" readonly="1">
<list editable="bottom" create="0" delete="0" decoration-success="status=='approved'" decoration-danger="status=='refused'">
<list editable="bottom" create="0" delete="0" decoration-success="status=='approved'" decoration-danger="status=='rejected'">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="required_user_ids" widget="many2many_tags"/>
@ -177,13 +183,14 @@
decoration-muted="status == 'pending'"/>
<field name="actual_user_id"/>
<field name="approval_date"/>
<field name="rejection_reason"/>
<field name="can_approve" column_invisible="True"/>
<!-- Buttons -->
<button name="action_approve_line" string="Approve" type="object"
icon="fa-check" class="text-success"
invisible="not can_approve"/>
<button name="action_refuse_line" string="Refuse" type="object"
<button name="action_reject_line" string="Reject" type="object"
icon="fa-times" class="text-danger"
invisible="not can_approve"/>
</list>

View File

@ -1,3 +1,4 @@
from . import epr_reject_wizard
from . import epr_create_rfq
from . import epr_create_po
from . import epr_reject_rfq_wizard

View File

@ -10,15 +10,13 @@ class EprCreatePoWizard(models.TransientModel):
partner_id = fields.Many2one(
comodel_name='res.partner',
string='Vendor',
required=True,
readonly=True
required=True
)
currency_id = fields.Many2one(
comodel_name='res.currency',
string='Currency',
required=True,
readonly=True
required=True
)
# Danh sách các dòng sẽ được đẩy vào PO (Cho phép user bỏ tick để xé nhỏ RFQ)
@ -61,7 +59,7 @@ class EprCreatePoWizard(models.TransientModel):
'rfq_line_id': line.id,
'product_id': line.product_id.id,
'description': line.description,
'quantity': line.quantity, # User có thể sửa số lượng tại wizard nếu muốn partial
'quantity': line.quantity, # User có thể sửa số lượng tại wizard nếu muốn partial
'price_unit': line.price_unit,
'uom_id': line.uom_id.id,
'taxes_id': [Command.set(line.taxes_id.ids)],
@ -82,12 +80,17 @@ class EprCreatePoWizard(models.TransientModel):
if not self.line_ids:
raise UserError(_("Vui lòng chọn ít nhất một dòng sản phẩm."))
rfq_ids = self.line_ids.mapped('rfq_line_id.rfq_id').ids
pr_ids = self.line_ids.mapped('rfq_line_id.pr_line_id.request_id').ids
# 1. Prepare Header PO (Chuẩn Odoo)
po_vals = {
'partner_id': self.partner_id.id,
'currency_id': self.currency_id.id,
'date_order': fields.Datetime.now(),
'origin': ', '.join(self.line_ids.mapped('rfq_line_id.rfq_id.name')), # Source Document
'origin': ', '.join(self.line_ids.mapped('rfq_line_id.rfq_id.name')),
'epr_source_rfq_ids': [Command.set(rfq_ids)], # Gán Link RFQ
'epr_source_pr_ids': [Command.set(pr_ids)], # Gán Link PR
'order_line': [],
}
@ -143,19 +146,16 @@ class EprCreatePoLineWizard(models.TransientModel):
rfq_line_id = fields.Many2one(
comodel_name='epr.rfq.line',
string='RFQ Line',
required=True,
readonly=True
required=True
)
product_id = fields.Many2one(
comodel_name='product.product',
string='Product',
readonly=True
string='Product'
)
description = fields.Text(
string='Description',
readonly=True
string='Description'
)
quantity = fields.Float(
@ -165,13 +165,11 @@ class EprCreatePoLineWizard(models.TransientModel):
uom_id = fields.Many2one(
comodel_name='uom.uom',
string='UoM',
readonly=True
string='UoM'
)
price_unit = fields.Float(
string='Price',
readonly=True
string='Price'
)
taxes_id = fields.Many2many(

View File

@ -7,23 +7,23 @@
<form string="Create Purchase Order">
<group>
<group>
<field name="partner_id"/>
<field name="partner_id" readonly="1" force_save="1"/>
</group>
<group>
<field name="currency_id"/>
<field name="currency_id" readonly="1" force_save="1"/>
</group>
</group>
<notebook>
<page string="Lines to Order">
<field name="line_ids" nolabel="1">
<list editable="bottom" create="0">
<field name="product_id"/>
<field name="product_id" readonly="1" force_save="1"/>
<field name="description" optional="hide"/>
<field name="quantity"/>
<field name="uom_id"/>
<field name="price_unit"/>
<field name="quantity" readonly="1" force_save="1"/>
<field name="uom_id" readonly="1"/>
<field name="price_unit" readonly="1" force_save="1"/>
<field name="taxes_id" widget="many2many_tags"/>
<field name="rfq_line_id" column_invisible="True"/>
<field name="rfq_line_id" column_invisible="True" force_save="1"/>
</list>
</field>
</page>

View File

@ -54,6 +54,7 @@ class EprCreateRfqWizard(models.TransientModel):
'final_product_id': pr_line.product_id.id,
'product_description': pr_line.name or pr_line.product_id.name,
'quantity': pr_line.quantity,
'price_unit': pr_line.estimated_price,
'uom_id': (
pr_line.product_id.uom_po_id.id or
pr_line.product_id.uom_id.id
@ -105,6 +106,7 @@ class EprCreateRfqWizard(models.TransientModel):
'product_id': wiz_line.final_product_id.id,
'description': wiz_line.product_description,
'quantity': wiz_line.quantity,
'price_unit': wiz_line.price_unit,
'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
@ -183,6 +185,11 @@ class EprCreateRfqLine(models.TransientModel):
digits='Product Unit of Measure'
)
price_unit = fields.Float(
string='Price',
digits='Product Price'
)
uom_id = fields.Many2one(
'uom.uom',
string='UoM'

View File

@ -39,6 +39,7 @@
<!-- Quantity -->
<field name="quantity"/>
<field name="price_unit" string="Est. Price"/>
<!-- UOM -->
<field name="uom_name" optional="show" string="PR UoM"/>

View File

@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
from odoo.exceptions import UserError
class EprRejectRfqWizard(models.TransientModel):
"""
Wizard specifically for rejecting EPR RFQs.
It updates both the Approval Entry (history) and the RFQ record (status & reason).
"""
_name = 'epr.reject.rfq.wizard'
_description = 'EPR RFQ Reject Wizard'
# ==========================================================================
# FIELDS
# ==========================================================================
reason = fields.Text(
string="Rejection Reason",
required=True,
help="Please explain why you are rejecting this RFQ."
)
# ==========================================================================
# METHODS
# ==========================================================================
def action_confirm_reject(self):
"""
Process the rejection:
1. Find and update the user's pending approval entry.
2. Call the callback on the RFQ to update its state and store the reason.
"""
self.ensure_one()
# 1. Get Context Data
active_id = self.env.context.get('active_id')
active_model = self.env.context.get('active_model')
if not active_id or active_model != 'epr.rfq':
raise UserError(_("This wizard can only be used for EPR RFQs."))
rfq = self.env['epr.rfq'].browse(active_id)
# 2. Update Approval Entry (The Log)
# Search for a pending approval entry for this user and this RFQ
approval_entry = self.env['epr.approval.entry'].search([
('rfq_id', '=', rfq.id),
('required_user_ids', 'in', self.env.uid),
('status', '=', 'new')
], limit=1)
if approval_entry:
approval_entry.write({
'status': 'refused',
'rejection_reason': self.reason,
'actual_user_id': self.env.uid,
'approval_date': fields.Datetime.now()
})
# Note: Depending on your strictness, you might allow Admins to reject without an entry.
# Here we strictly enforce that an entry must exist.
if approval_entry:
approval_entry.write({
'status': 'refused',
'rejection_reason': self.reason,
'actual_user_id': self.env.uid,
'approval_date': fields.Datetime.now()
})
# 3. Update the RFQ Record (The Main Document)
# We pass the reason back to the RFQ model to store it permanently
rfq.action_handle_rejection(self.reason)
# 4. Feedback to User
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('RFQ Rejected'),
'message': _('The RFQ has been rejected and the reason has been recorded.'),
'type': 'warning',
'sticky': False,
'next': {'type': 'ir.actions.act_window_close'},
}
}

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_epr_reject_rfq_wizard_form" model="ir.ui.view">
<field name="name">epr.reject.rfq.wizard.form</field>
<field name="model">epr.reject.rfq.wizard</field>
<field name="arch" type="xml">
<form string="Reject RFQ">
<sheet>
<group>
<div class="d-flex align-items-center">
<div class="me-2">
<i class="fa fa-exclamation-triangle fa-2x"/>
</div>
<div class="alert alert-warning" role="alert">
<strong>Attention:</strong> You are about to reject this Request for Quotation.
This action will set the RFQ to Rejected and notify the creator.
</div>
</div>
</group>
<group>
<field name="reason"
widget="text"
placeholder="e.g. Price too high, Vendor not qualified, Specification mismatch..."
required="1"/>
</group>
</sheet>
<footer>
<button string="Confirm Reject"
name="action_confirm_reject"
type="object"
class="btn-danger"
data-hotkey="q"/>
<button string="Cancel"
class="btn-secondary"
special="cancel"
data-hotkey="z"/>
</footer>
</form>
</field>
</record>
<record id="action_epr_reject_rfq_wizard" model="ir.actions.act_window">
<field name="name">Reject RFQ</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">epr.reject.rfq.wizard</field>
<field name="view_mode">form</field>
<field name="view_id" ref="view_epr_reject_rfq_wizard_form"/>
<field name="target">new</field>
</record>
</data>
</odoo>