Compare commits
2 Commits
4f552469b8
...
d113554d0f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d113554d0f | ||
|
|
d993893bc3 |
@ -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',
|
||||
],
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
})
|
||||
|
||||
|
||||
@ -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>
|
||||
@ -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
|
||||
|
||||
|
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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(
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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"/>
|
||||
|
||||
85
addons/epr/wizards/epr_reject_rfq_wizard.py
Normal file
85
addons/epr/wizards/epr_reject_rfq_wizard.py
Normal 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'},
|
||||
}
|
||||
}
|
||||
54
addons/epr/wizards/epr_reject_rfq_wizard_views.xml
Normal file
54
addons/epr/wizards/epr_reject_rfq_wizard_views.xml
Normal 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>
|
||||
Loading…
Reference in New Issue
Block a user