Thêm PO
Thêm wizard gom nhiều RFQs để tạo POs (những RFQs cùng vendor)
This commit is contained in:
parent
7c3605508d
commit
0570680d58
@ -26,15 +26,15 @@
|
||||
'security/epr_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'security/epr_record_rules.xml',
|
||||
'data/epr_sequence_data.xml',
|
||||
# 'data/epr_approval_default_data.xml',
|
||||
# 'data/mail_template_data.xml',
|
||||
'data/epr_pr_sequence_data.xml',
|
||||
'data/epr_rfq_sequence_data.xml',
|
||||
'views/epr_purchase_request_views.xml',
|
||||
# 'views/epr_request_line_views.xml',
|
||||
# 'views/epr_approval_matrix_views.xml',
|
||||
# 'views/res_config_settings_views.xml',
|
||||
'views/epr_rfq_views.xml',
|
||||
'views/epr_approval_views.xml',
|
||||
'views/epr_po_views.xml',
|
||||
'views/epr_menus.xml',
|
||||
'wizards/epr_reject_wizard_views.xml',
|
||||
'wizards/epr_create_po_views.xml',
|
||||
],
|
||||
'assets': {
|
||||
# 'web.assets_backend': [
|
||||
|
||||
39
addons/epr/data/epr_rfq_sequence_data.xml
Normal file
39
addons/epr/data/epr_rfq_sequence_data.xml
Normal file
@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!--
|
||||
noupdate="1": BẮT BUỘC.
|
||||
- Nếu để 0, mỗi lần bạn upgrade module, số thứ tự (Next Number)
|
||||
sẽ bị reset về 1, gây trùng lặp mã phiếu.
|
||||
- Nếu muốn cập nhật lại (ví dụ đổi prefix): Bạn phải vào giao diện Odoo:
|
||||
Settings > Technical > Sequences, tìm mã epr.rfq và sửa trực tiếp trên giao diện.
|
||||
-->
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="seq_epr_rfq" model="ir.sequence">
|
||||
<field name="name">ePR Request for Quotation</field>
|
||||
<!-- CODE này phải khớp chính xác với hàm next_by_code trong Python -->
|
||||
<field name="code">epr.rfq</field>
|
||||
|
||||
<!-- Định dạng: RFQ/2024/00001 -->
|
||||
<field name="prefix">RFQ/%(year)s/</field>
|
||||
|
||||
<!-- Padding: Độ dài phần số. Nên để 5 (đến 99,999) để dùng lâu dài -->
|
||||
<field name="padding">5</field>
|
||||
|
||||
<!-- Cấu hình bước nhảy -->
|
||||
<field name="number_next">1</field>
|
||||
<field name="number_increment">1</field>
|
||||
|
||||
<!--
|
||||
company_id = False: Sequence này dùng chung cho toàn hệ thống.
|
||||
Nếu bạn muốn mỗi công ty có chuỗi số riêng (VD: Cty A số 1, Cty B số 1),
|
||||
bạn cần để trường này, nhưng thường cấu hình mặc định là False để tránh lỗi không tìm thấy.
|
||||
-->
|
||||
<field name="company_id" eval="False"/>
|
||||
|
||||
<!-- implementation: 'standard' (nhanh hơn) hoặc 'no_gap' (chậm hơn nhưng không mất số) -->
|
||||
<field name="implementation">standard</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@ -1 +1,4 @@
|
||||
from . import epr_purchase_request
|
||||
from . import epr_rfq
|
||||
from . import epr_approval
|
||||
from . import epr_po
|
||||
Binary file not shown.
BIN
addons/epr/models/__pycache__/epr_approval.cpython-312.pyc
Normal file
BIN
addons/epr/models/__pycache__/epr_approval.cpython-312.pyc
Normal file
Binary file not shown.
BIN
addons/epr/models/__pycache__/epr_po.cpython-312.pyc
Normal file
BIN
addons/epr/models/__pycache__/epr_po.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
addons/epr/models/__pycache__/epr_rfq.cpython-312.pyc
Normal file
BIN
addons/epr/models/__pycache__/epr_rfq.cpython-312.pyc
Normal file
Binary file not shown.
220
addons/epr/models/epr_approval.py
Normal file
220
addons/epr/models/epr_approval.py
Normal file
@ -0,0 +1,220 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# MODEL CẤU HÌNH (TEMPLATE)
|
||||
# Admin thiết lập: Tại bước (Sequence) này, ai cần duyệt và duyệt theo kiểu gì?
|
||||
# ==============================================================================
|
||||
class EprApprovalConfig(models.Model):
|
||||
_name = 'epr.approval.config'
|
||||
_description = 'EPR Approval Configuration'
|
||||
_order = 'sequence, min_amount'
|
||||
|
||||
name = fields.Char(
|
||||
string='Rule Name',
|
||||
required=True,
|
||||
translate=True
|
||||
)
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
company_id = fields.Many2one(
|
||||
comodel_name='res.company',
|
||||
string='Company',
|
||||
default=lambda self: self.env.company
|
||||
)
|
||||
|
||||
# 1. Điều kiện kích hoạt
|
||||
sequence = fields.Integer(
|
||||
string='Sequence',
|
||||
default=10,
|
||||
required=True,
|
||||
help="Thứ tự thực hiện. Các Rule cùng Sequence sẽ chạy song song."
|
||||
)
|
||||
|
||||
min_amount = fields.Monetary(
|
||||
string='Minimum Amount',
|
||||
currency_field='currency_id',
|
||||
default=0.0,
|
||||
help="Rule này chỉ áp dụng nếu tổng tiền RFQ lớn hơn số này."
|
||||
)
|
||||
|
||||
currency_id = fields.Many2one(
|
||||
comodel_name='res.currency',
|
||||
string='Currency',
|
||||
default=lambda self: self.env.company.currency_id
|
||||
)
|
||||
|
||||
# 2. Ai duyệt?
|
||||
user_ids = fields.Many2many(
|
||||
comodel_name='res.users',
|
||||
string='Approvers',
|
||||
required=True,
|
||||
domain="[('share', '=', False)]", # Chỉ lấy user nội bộ
|
||||
help="Danh sách những người có quyền duyệt rule này."
|
||||
)
|
||||
|
||||
# 3. Logic duyệt: 1 người hay tất cả?
|
||||
approval_type = fields.Selection(
|
||||
selection=[
|
||||
('any', 'Any Approver (One pass)'),
|
||||
('all', 'All Approvers (Everyone must sign)')
|
||||
],
|
||||
string='Approval Type',
|
||||
default='any',
|
||||
required=True
|
||||
)
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# MODEL VẬN HÀNH (INSTANCE)
|
||||
# Sinh ra khi RFQ -> 'to_approve'.
|
||||
# Đây là snapshot của Config tại thời điểm submit.
|
||||
# ==============================================================================
|
||||
class EprApprovalEntry(models.Model):
|
||||
_name = 'epr.approval.entry'
|
||||
_description = 'RFQ Approval Entry'
|
||||
_order = 'sequence, id'
|
||||
|
||||
# Link về RFQ
|
||||
rfq_id = fields.Many2one(
|
||||
comodel_name='epr.rfq',
|
||||
string='RFQ Reference',
|
||||
required=True,
|
||||
ondelete='cascade'
|
||||
)
|
||||
|
||||
config_id = fields.Many2one(
|
||||
comodel_name='epr.approval.config',
|
||||
string='Source Rule',
|
||||
readonly=True,
|
||||
ondelete='set null' # Giữ entry dù config bị xóa
|
||||
)
|
||||
|
||||
# Thông tin snapshot từ Config
|
||||
name = fields.Char(
|
||||
string='Summary',
|
||||
required=True
|
||||
)
|
||||
|
||||
sequence = fields.Integer(
|
||||
string='Sequence',
|
||||
required=True
|
||||
)
|
||||
|
||||
approval_type = fields.Selection(
|
||||
selection=[
|
||||
('any', 'Any'),
|
||||
('all', 'All')
|
||||
],
|
||||
required=True
|
||||
)
|
||||
|
||||
# Trạng thái dòng duyệt này
|
||||
status = fields.Selection(
|
||||
selection=[
|
||||
('new', 'To Process'), # Chưa đến lượt (do sequence cao hơn)
|
||||
('pending', 'Pending'), # Đang chờ duyệt
|
||||
('approved', 'Approved'), # Đã xong
|
||||
('rejected', 'Rejected') # Bị từ chối
|
||||
],
|
||||
string='Status',
|
||||
default='new',
|
||||
index=True,
|
||||
readonly=True
|
||||
)
|
||||
|
||||
# Danh sách người CẦN duyệt
|
||||
required_user_ids = fields.Many2many(
|
||||
comodel_name='res.users',
|
||||
relation='epr_approval_req_users_rel',
|
||||
string='Required Approvers',
|
||||
readonly=True
|
||||
)
|
||||
|
||||
# Danh sách người ĐÃ duyệt (quan trọng cho logic 'All')
|
||||
actual_user_ids = fields.Many2many(
|
||||
comodel_name='res.users',
|
||||
relation='epr_approval_act_users_rel',
|
||||
string='Approved By',
|
||||
readonly=True
|
||||
)
|
||||
|
||||
approval_date = fields.Datetime(
|
||||
string='Last Action Date',
|
||||
readonly=True
|
||||
)
|
||||
|
||||
# Field hỗ trợ UI: User hiện tại có được nút Approve không?
|
||||
can_approve = fields.Boolean(compute='_compute_can_approve')
|
||||
|
||||
@api.depends('status', 'required_user_ids', 'actual_user_ids', 'approval_type')
|
||||
@api.depends_context('uid')
|
||||
def _compute_can_approve(self):
|
||||
current_user = self.env.user
|
||||
for record in self:
|
||||
# 1. Phải đang ở trạng thái Pending
|
||||
if record.status != 'pending':
|
||||
record.can_approve = False
|
||||
continue
|
||||
|
||||
# 2. User phải nằm trong danh sách được phép
|
||||
if current_user not in record.required_user_ids:
|
||||
record.can_approve = False
|
||||
continue
|
||||
|
||||
# 3. Nếu là 'All', user này chưa được duyệt trước đó
|
||||
if record.approval_type == 'all' and current_user in record.actual_user_ids:
|
||||
record.can_approve = False
|
||||
else:
|
||||
record.can_approve = True
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# ACTION METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
def action_approve_line(self):
|
||||
"""User bấm nút Approve trên dòng này"""
|
||||
self.ensure_one()
|
||||
if not self.can_approve:
|
||||
raise UserError(_("You either don't have permission to approve or you've already approved it."))
|
||||
|
||||
# Ghi nhận người duyệt
|
||||
self.write({
|
||||
'actual_user_ids': [(4, self.env.user.id)],
|
||||
'approval_date': fields.Datetime.now()
|
||||
})
|
||||
|
||||
# Kiểm tra điều kiện hoàn thành dòng này
|
||||
is_done = False
|
||||
if self.approval_type == 'any':
|
||||
# Chỉ cần 1 người -> Done
|
||||
is_done = True
|
||||
elif self.approval_type == 'all':
|
||||
# Phải đủ tất cả required users
|
||||
# Dùng set để so sánh ID cho nhanh
|
||||
required_set = set(self.required_user_ids.ids)
|
||||
actual_set = set(self.actual_user_ids.ids)
|
||||
if required_set.issubset(actual_set):
|
||||
is_done = True
|
||||
|
||||
if is_done:
|
||||
self.status = 'approved'
|
||||
# Gọi về RFQ cha để check xem có mở tiếp tầng sau (Sequence kế) không?
|
||||
self.rfq_id._check_approval_progression()
|
||||
|
||||
def action_refuse_line(self):
|
||||
"""User từ chối -> Từ chối TOÀN BỘ RFQ"""
|
||||
self.ensure_one()
|
||||
if not self.can_approve:
|
||||
raise UserError(_("You don't have permission to reject this RFQ."))
|
||||
|
||||
self.write({
|
||||
'status': 'rejected',
|
||||
'approval_date': fields.Datetime.now()
|
||||
})
|
||||
|
||||
# Đẩy RFQ về trạng thái 'rejected' hoặc 'cancel'
|
||||
# Gọi hàm xử lý từ chối bên RFQ
|
||||
self.rfq_id.action_reject_approval()
|
||||
68
addons/epr/models/epr_po.py
Normal file
68
addons/epr/models/epr_po.py
Normal file
@ -0,0 +1,68 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
|
||||
class PurchaseOrder(models.Model):
|
||||
_inherit = 'purchase.order'
|
||||
|
||||
# === FIELD LIÊN KẾT 1-1 TỚI EPR RFQ ===
|
||||
# Dùng cho action_create_po() trong epr.rfq
|
||||
epr_rfq_id = fields.Many2one(
|
||||
comodel_name='epr.rfq',
|
||||
string='Original ePR RFQ',
|
||||
readonly=True,
|
||||
copy=False,
|
||||
ondelete='set null',
|
||||
help="Tham chiếu đến phiếu báo giá nội bộ (ePR) đã tạo ra PO này."
|
||||
)
|
||||
|
||||
# === COMPUTED FIELDS CHO SMART BUTTON (Line-Level Linking) ===
|
||||
# Tìm tất cả các RFQ gốc dựa trên các dòng PO lines
|
||||
epr_rfq_ids = fields.Many2many(
|
||||
comodel_name='epr.rfq',
|
||||
string='Source RFQs',
|
||||
compute='_compute_epr_rfq_data',
|
||||
help="Các phiếu yêu cầu báo giá (EPR RFQ) liên quan đến đơn mua hàng này."
|
||||
)
|
||||
|
||||
epr_rfq_count = fields.Integer(
|
||||
string='RFQ Count',
|
||||
compute='_compute_epr_rfq_data'
|
||||
)
|
||||
|
||||
@api.depends('order_line.epr_rfq_line_id')
|
||||
def _compute_epr_rfq_data(self):
|
||||
for po in self:
|
||||
# Logic: Quét qua tất cả line của PO -> lấy epr_rfq_line -> lấy rfq_id -> unique
|
||||
# Mapped tự động loại bỏ các giá trị trùng lặp (set)
|
||||
rfqs = po.order_line.mapped('epr_rfq_line_id.rfq_id')
|
||||
po.epr_rfq_ids = rfqs
|
||||
po.epr_rfq_count = len(rfqs)
|
||||
|
||||
# === ACTION SMART BUTTON ===
|
||||
def action_view_epr_rfqs(self):
|
||||
"""Mở danh sách các EPR RFQ nguồn"""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _('Source RFQs'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'epr.rfq',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('id', 'in', self.epr_rfq_ids.ids)],
|
||||
'context': {'create': False}, # Không cho tạo mới RFQ từ đây để tránh mất quy trình
|
||||
}
|
||||
|
||||
|
||||
class PurchaseOrderLine(models.Model):
|
||||
_inherit = 'purchase.order.line'
|
||||
|
||||
# === FIELD LIÊN KẾT QUAN TRỌNG NHẤT ===
|
||||
# Field này sẽ được Wizard 'epr.create.po.wizard' ghi dữ liệu vào
|
||||
epr_rfq_line_id = fields.Many2one(
|
||||
comodel_name='epr.rfq.line',
|
||||
string='Source RFQ Line',
|
||||
readonly=True,
|
||||
copy=False,
|
||||
index=True,
|
||||
help="Dòng yêu cầu báo giá gốc đã tạo ra dòng đơn mua hàng này."
|
||||
)
|
||||
@ -43,7 +43,7 @@ class EprPurchaseRequest(models.Model):
|
||||
compute='_compute_is_owner',
|
||||
store=False
|
||||
)
|
||||
|
||||
|
||||
date_required = fields.Date(
|
||||
string='Date Required',
|
||||
required=True,
|
||||
@ -74,13 +74,6 @@ class EprPurchaseRequest(models.Model):
|
||||
group_expand='_expand_groups'
|
||||
)
|
||||
|
||||
# approver_ids = fields.Many2many(
|
||||
# 'res.users',
|
||||
# string='Approvers',
|
||||
# compute='_compute_approvers',
|
||||
# store=True
|
||||
# )
|
||||
|
||||
# Approvers: Nên là field thường (không compute) để lưu cố định người duyệt lúc Submit
|
||||
approver_ids = fields.Many2many(
|
||||
comodel_name='res.users',
|
||||
@ -93,17 +86,20 @@ class EprPurchaseRequest(models.Model):
|
||||
readonly=True,
|
||||
tracking=True
|
||||
)
|
||||
|
||||
currency_id = fields.Many2one(
|
||||
'res.currency',
|
||||
string='Currency',
|
||||
default=lambda self: self.env.company.currency_id,
|
||||
required=True
|
||||
)
|
||||
|
||||
line_ids = fields.One2many(
|
||||
'epr.purchase.request.line',
|
||||
'request_id',
|
||||
string='Products'
|
||||
)
|
||||
|
||||
estimated_total = fields.Monetary(
|
||||
string='Estimated Total',
|
||||
compute='_compute_estimated_total',
|
||||
@ -111,6 +107,22 @@ class EprPurchaseRequest(models.Model):
|
||||
currency_field='currency_id'
|
||||
)
|
||||
|
||||
# Link sang RFQs
|
||||
rfq_ids = fields.Many2many(
|
||||
comodel_name='epr.rfq',
|
||||
relation='epr_rfq_purchase_request_rel', # Tên bảng trung gian
|
||||
column1='request_id',
|
||||
column2='rfq_id',
|
||||
string='RFQs',
|
||||
readonly=True
|
||||
)
|
||||
|
||||
# Số lượng RFQ
|
||||
rfq_count = fields.Integer(
|
||||
compute='_compute_rfq_count',
|
||||
string='RFQ Count'
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# LOG FIELDS
|
||||
# ==========================================================================
|
||||
@ -196,6 +208,12 @@ class EprPurchaseRequest(models.Model):
|
||||
# else:
|
||||
# request.approver_ids = False
|
||||
|
||||
# Compute RFQ count
|
||||
@api.depends('rfq_ids')
|
||||
def _compute_rfq_count(self):
|
||||
for record in self:
|
||||
record.rfq_count = len(record.rfq_ids)
|
||||
|
||||
# ==========================================================================
|
||||
# HELPER METHODS (Tách logic tìm người duyệt ra riêng)
|
||||
# ==========================================================================
|
||||
@ -371,6 +389,26 @@ class EprPurchaseRequest(models.Model):
|
||||
# NOTE: Commented out for testing without mail server
|
||||
# self.activity_ids.unlink()
|
||||
|
||||
# === ACTION SMART BUTTON ===
|
||||
def action_view_rfqs(self):
|
||||
"""Mở danh sách các RFQ liên quan đến PR này"""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _('Request for Quotations'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'epr.rfq',
|
||||
'view_mode': 'list,form', # Odoo 18 ưu tiên dùng 'list' thay vì 'tree'
|
||||
'domain': [('id', 'in', self.rfq_ids.ids)],
|
||||
'context': {
|
||||
'default_request_ids': [(6, 0, [self.id])], # Tự động link ngược lại PR này nếu tạo mới RFQ
|
||||
'create': True,
|
||||
},
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# CLASS CON: epr.purchase.request.line (Chi tiết hàng hóa trong PR)
|
||||
# ==============================================================================
|
||||
|
||||
|
||||
class EprPurchaseRequestLine(models.Model):
|
||||
"""
|
||||
|
||||
409
addons/epr/models/epr_rfq.py
Normal file
409
addons/epr/models/epr_rfq.py
Normal file
@ -0,0 +1,409 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
|
||||
class EprRfq(models.Model):
|
||||
_name = 'epr.rfq'
|
||||
_description = 'EPR Request for Quotation'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'id desc'
|
||||
|
||||
# === 1. IDENTIFICATION ===
|
||||
name = fields.Char(
|
||||
string='Reference',
|
||||
required=True,
|
||||
copy=False,
|
||||
readonly=True,
|
||||
default=lambda self: _('New')
|
||||
)
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
state = fields.Selection(
|
||||
selection=[
|
||||
('draft', 'Draft'),
|
||||
('sent', 'Sent'),
|
||||
('received', 'Received'), # Nhà cung cấp đã báo giá
|
||||
('to_approve', 'To Approve'), # Trình sếp duyệt giá
|
||||
('confirmed', 'Confirmed'), # Đã duyệt -> Sẵn sàng tạo PO
|
||||
('cancel', 'Cancelled')
|
||||
],
|
||||
string='Status',
|
||||
readonly=True,
|
||||
index=True,
|
||||
copy=False,
|
||||
default='draft',
|
||||
tracking=True
|
||||
)
|
||||
|
||||
approval_ids = fields.One2many(
|
||||
comodel_name='epr.approval.entry',
|
||||
inverse_name='rfq_id',
|
||||
string='Approval Steps'
|
||||
)
|
||||
|
||||
# Tính tổng tiền trên RFQ để so sánh trong approval process
|
||||
amount_total = fields.Monetary(
|
||||
compute='_compute_amount_total',
|
||||
string='Total',
|
||||
currency_field='currency_id'
|
||||
)
|
||||
|
||||
# === 2. RELATIONS ===
|
||||
# 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
|
||||
column1='rfq_id',
|
||||
column2='request_id',
|
||||
string='Source Requests',
|
||||
# Chỉ lấy các PR đã duyệt để tạo RFQ
|
||||
domain="[('state', '=', 'approved')]",
|
||||
readonly=True
|
||||
)
|
||||
|
||||
partner_id = fields.Many2one(
|
||||
comodel_name='res.partner',
|
||||
string='Vendor',
|
||||
required=True,
|
||||
tracking=True,
|
||||
# domain="[('supplier_rank', '>', 0)]", # Chỉ chọn đã từng được chọn qua ít nhất 01 lần
|
||||
readonly=True
|
||||
)
|
||||
|
||||
company_id = fields.Many2one(
|
||||
comodel_name='res.company',
|
||||
string='Company',
|
||||
required=True,
|
||||
default=lambda self: self.env.company
|
||||
)
|
||||
|
||||
currency_id = fields.Many2one(
|
||||
comodel_name='res.currency',
|
||||
string='Currency',
|
||||
required=True,
|
||||
default=lambda self: self.env.company.currency_id,
|
||||
readonly=True
|
||||
)
|
||||
|
||||
# === 3. DATES ===
|
||||
date_order = fields.Datetime(
|
||||
string='Order Date',
|
||||
default=fields.Datetime.now,
|
||||
readonly=True
|
||||
)
|
||||
|
||||
date_deadline = fields.Date(
|
||||
string='Bid Deadline',
|
||||
help="Ngày hạn chót Vendor phải gửi báo giá",
|
||||
readonly=True
|
||||
)
|
||||
|
||||
# === 4. LINES & PURCHASES ===
|
||||
line_ids = fields.One2many(
|
||||
comodel_name='epr.rfq.line',
|
||||
inverse_name='rfq_id',
|
||||
string='Products',
|
||||
copy=True,
|
||||
readonly=True
|
||||
)
|
||||
|
||||
# Link sang Purchase Order gốc của Odoo
|
||||
purchase_ids = fields.One2many(
|
||||
comodel_name='purchase.order',
|
||||
inverse_name='epr_rfq_id',
|
||||
string='Purchase Orders'
|
||||
)
|
||||
|
||||
purchase_count = fields.Integer(
|
||||
compute='_compute_purchase_count',
|
||||
string='PO Count'
|
||||
)
|
||||
|
||||
# === 5. COMPUTE METHODS ===
|
||||
@api.depends('purchase_ids')
|
||||
def _compute_purchase_count(self):
|
||||
for rfq in self:
|
||||
rfq.purchase_count = len(rfq.purchase_ids)
|
||||
|
||||
# === 6. CRUD OVERRIDES ===
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get('name', _('New')) == _('New'):
|
||||
vals['name'] = self.env['ir.sequence'].next_by_code('epr.rfq') or _('New')
|
||||
return super().create(vals_list)
|
||||
|
||||
@api.depends('line_ids.subtotal')
|
||||
def _compute_amount_total(self):
|
||||
for rfq in self:
|
||||
rfq.amount_total = sum(rfq.line_ids.mapped('subtotal'))
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 7. ACTIONS
|
||||
# -------------------------------------------------------------------------
|
||||
def action_send_email(self):
|
||||
"""Gửi email RFQ cho Vendor"""
|
||||
self.ensure_one()
|
||||
if self.state != 'draft':
|
||||
raise UserError(_("Chỉ có thể gửi RFQ khi ở trạng thái Draft."))
|
||||
self.write({'state': 'sent'})
|
||||
return True
|
||||
|
||||
def action_mark_received(self):
|
||||
"""Chuyển trạng thái sang Received khi nhận được phản hồi từ NCC"""
|
||||
for rfq in self:
|
||||
if rfq.state != 'sent':
|
||||
# Tư vấn: Nên chặn nếu nhảy cóc trạng thái để đảm bảo quy trình
|
||||
raise UserError(_("Chỉ có thể đánh dấu 'Đã nhận' khi RFQ đang ở trạng thái 'Đã gửi'."))
|
||||
rfq.write({'state': 'received'})
|
||||
|
||||
def action_confirm_rfq(self):
|
||||
"""Chốt RFQ, chuyển sang Confirmed"""
|
||||
for rfq in self:
|
||||
if rfq.state != 'received':
|
||||
raise UserError(_("Vui lòng chuyển sang trạng thái 'Đã nhận' trước khi xác nhận."))
|
||||
rfq.write({'state': 'confirmed'})
|
||||
|
||||
def action_cancel_rfq(self):
|
||||
"""Hủy RFQ ở bất kỳ trạng thái nào (trừ khi đã hủy rồi)"""
|
||||
for rfq in self:
|
||||
if rfq.state == 'cancel':
|
||||
continue
|
||||
|
||||
# Nếu đã có PO liên kết, nên cảnh báo hoặc hủy luôn PO con
|
||||
if rfq.purchase_ids and any(po.state not in ['cancel'] for po in rfq.purchase_ids):
|
||||
raise UserError(_("Không thể hủy RFQ này vì đã có Đơn mua hàng (PO) được tạo. Vui lòng hủy PO trước."))
|
||||
|
||||
rfq.write({'state': 'cancel'})
|
||||
|
||||
def action_create_po(self):
|
||||
"""Chuyển đổi RFQ này thành Purchase Order"""
|
||||
self.ensure_one()
|
||||
if not self.line_ids:
|
||||
raise ValidationError(_("Vui lòng thêm sản phẩm trước khi tạo PO."))
|
||||
|
||||
# Logic tạo PO
|
||||
po_vals = {
|
||||
'partner_id': self.partner_id.id,
|
||||
'date_order': fields.Datetime.now(),
|
||||
'epr_rfq_id': self.id, # Link ngược lại RFQ này
|
||||
'origin': self.name,
|
||||
'company_id': self.company_id.id,
|
||||
'currency_id': self.currency_id.id,
|
||||
'order_line': []
|
||||
}
|
||||
|
||||
for line in self.line_ids:
|
||||
po_vals['order_line'].append((0, 0, {
|
||||
'product_id': line.product_id.id,
|
||||
'name': line.description or line.product_id.name,
|
||||
'product_qty': line.quantity,
|
||||
'price_unit': line.price_unit,
|
||||
'product_uom': line.uom_id.id,
|
||||
'date_planned': fields.Datetime.now(),
|
||||
}))
|
||||
|
||||
new_po = self.env['purchase.order'].create(po_vals)
|
||||
|
||||
self.write({'state': 'confirmed'})
|
||||
|
||||
# Mở view PO vừa tạo
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'purchase.order',
|
||||
'res_id': new_po.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
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."))
|
||||
rfq.write({'state': 'draft'})
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# RFQ APPROVAL PROCESS
|
||||
# -------------------------------------------------------------------------
|
||||
def action_submit_approval(self):
|
||||
"""Nút bấm Submit for Approval"""
|
||||
self.ensure_one()
|
||||
if not self.line_ids:
|
||||
raise UserError(_("Vui lòng nhập chi tiết sản phẩm trước khi trình duyệt."))
|
||||
|
||||
# 1. Tìm các Rules phù hợp
|
||||
configs = self.env['epr.approval.config'].search([
|
||||
('active', '=', True),
|
||||
('company_id', '=', self.company_id.id),
|
||||
('min_amount', '<=', self.amount_total)
|
||||
], order='sequence asc')
|
||||
|
||||
if not configs:
|
||||
# Nếu không có rule nào -> Auto Approve
|
||||
self.write({'state': 'received'}) # Hoặc trạng thái tiếp theo bạn muốn
|
||||
return
|
||||
|
||||
# 2. Xóa dữ liệu duyệt cũ (nếu submit lại)
|
||||
self.approval_ids.unlink()
|
||||
|
||||
# 3. Tạo Approval Entries (Snapshot)
|
||||
approval_vals = []
|
||||
for conf in configs:
|
||||
approval_vals.append({
|
||||
'rfq_id': self.id,
|
||||
'config_id': conf.id, # Nếu bạn muốn link lại
|
||||
'name': conf.name,
|
||||
'sequence': conf.sequence,
|
||||
'approval_type': conf.approval_type,
|
||||
'required_user_ids': [(6, 0, conf.user_ids.ids)],
|
||||
'status': 'new', # Mặc định là new
|
||||
})
|
||||
|
||||
self.env['epr.approval.entry'].create(approval_vals)
|
||||
|
||||
# 4. Chuyển trạng thái và Kích hoạt tầng đầu tiên
|
||||
self.write({'state': 'to_approve'})
|
||||
self._check_approval_progression()
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# APPROVAL LOGIC: LINEARIZATION
|
||||
# -------------------------------------------------------------------------
|
||||
def _check_approval_progression(self):
|
||||
"""Hàm này được gọi mỗi khi có 1 dòng được Approved hoặc khi mới Submit"""
|
||||
self.ensure_one()
|
||||
|
||||
# Lấy tất cả entries
|
||||
all_entries = self.approval_ids
|
||||
|
||||
# 1. Tìm các Sequence đang 'pending' (Đang chạy)
|
||||
current_pending = all_entries.filtered(lambda x: x.status == 'pending')
|
||||
|
||||
if current_pending:
|
||||
# Nếu còn dòng đang pending -> Chưa làm gì cả, đợi user khác duyệt tiếp
|
||||
return
|
||||
|
||||
# 2. Nếu không còn pending, tìm Sequence nhỏ nhất đang là 'new' (Chưa chạy)
|
||||
next_new_entries = all_entries.filtered(lambda x: x.status == 'new')
|
||||
|
||||
if next_new_entries:
|
||||
# Tìm số sequence nhỏ nhất tiếp theo
|
||||
min_seq = min(next_new_entries.mapped('sequence'))
|
||||
|
||||
# Kích hoạt TOÀN BỘ các rule có cùng sequence đó (Parallel Approval)
|
||||
to_activate = next_new_entries.filtered(lambda x: x.sequence == min_seq)
|
||||
to_activate.write({'status': 'pending'})
|
||||
|
||||
# Gửi thông báo/Email cho những người vừa được kích hoạt (Optional)
|
||||
# self._notify_approvers(to_activate)
|
||||
else:
|
||||
# 3. Không còn 'new', cũng không còn 'pending' -> Tất cả đã Approved
|
||||
# Chuyển RFQ sang bước tiếp theo
|
||||
self.action_mark_approved()
|
||||
|
||||
def action_mark_approved(self):
|
||||
"""Hoàn tất quy trình duyệt"""
|
||||
# Chuyển sang trạng thái 'Confirmed'
|
||||
self.write({'state': 'confirmed'})
|
||||
self.message_post(body=_("Tất cả các cấp phê duyệt đã hoàn tất."))
|
||||
|
||||
def action_reject_approval(self):
|
||||
"""Xử lý khi bị từ chối"""
|
||||
self.write({'state': 'draft'}) # Quay về nháp để sửa
|
||||
self.message_post(body=_("Yêu cầu phê duyệt đã bị từ chối."))
|
||||
|
||||
# ==============================================================================
|
||||
# CLASS CON: epr.rfq.line (Chi tiết hàng hóa trong RFQ)
|
||||
# ==============================================================================
|
||||
|
||||
|
||||
class EprRfqLine(models.Model):
|
||||
_name = 'epr.rfq.line'
|
||||
_description = 'EPR RFQ Line'
|
||||
|
||||
rfq_id = fields.Many2one(
|
||||
comodel_name='epr.rfq',
|
||||
string='RFQ Reference',
|
||||
required=True,
|
||||
ondelete='cascade'
|
||||
)
|
||||
|
||||
# Sản phẩm (Odoo Product)
|
||||
product_id = fields.Many2one(
|
||||
comodel_name='product.product',
|
||||
string='Product',
|
||||
required=True
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
uom_id = fields.Many2one(
|
||||
comodel_name='uom.uom',
|
||||
string='UoM'
|
||||
)
|
||||
|
||||
price_unit = fields.Float(
|
||||
string='Unit Price',
|
||||
digits='Product Price'
|
||||
)
|
||||
|
||||
taxes_id = fields.Many2many(
|
||||
comodel_name='account.tax',
|
||||
relation='epr_rfq_line_taxes_rel',
|
||||
column1='line_id',
|
||||
column2='tax_id',
|
||||
string='Taxes'
|
||||
)
|
||||
|
||||
# Tổng tiền trên RFQ line
|
||||
subtotal = fields.Monetary(
|
||||
compute='_compute_subtotal',
|
||||
string='Subtotal',
|
||||
store=True
|
||||
)
|
||||
|
||||
currency_id = fields.Many2one(
|
||||
related='rfq_id.currency_id'
|
||||
)
|
||||
|
||||
@api.depends('quantity', 'price_unit')
|
||||
def _compute_subtotal(self):
|
||||
for line in self:
|
||||
line.subtotal = line.quantity * line.price_unit
|
||||
|
||||
@api.onchange('product_id')
|
||||
def _onchange_product_id(self):
|
||||
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
|
||||
|
||||
# ==============================================================================
|
||||
# LINE-LEVEL LINKING LOGIC (From RFQs to POs)
|
||||
# ==============================================================================
|
||||
|
||||
# Link tới dòng của Purchase Order chuẩn Odoo
|
||||
purchase_line_id = fields.Many2one(
|
||||
'purchase.order.line',
|
||||
string='Purchase Order Line',
|
||||
readonly=True,
|
||||
copy=False
|
||||
)
|
||||
|
||||
# Tiện ích để xem nhanh trạng thái
|
||||
po_id = fields.Many2one(
|
||||
related='purchase_line_id.order_id',
|
||||
string='Purchase Order',
|
||||
store=True,
|
||||
readonly=True
|
||||
)
|
||||
@ -1,12 +1,27 @@
|
||||
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 Line User,model_epr_purchase_request_line,group_epr_user,1,1,1,0
|
||||
access_epr_reject_wizard_user,ePR Reject Wizard User,model_epr_reject_wizard,group_epr_user,1,0,0,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 Line Manager,model_epr_purchase_request_line,group_epr_manager,1,1,1,0
|
||||
access_epr_reject_wizard_manager,ePR Reject Wizard Manager,model_epr_reject_wizard,group_epr_manager,1,1,1,1
|
||||
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 Line Officer,model_epr_purchase_request_line,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 Line Admin,model_epr_purchase_request_line,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_approval_config_officer,ePR Approval Config Officer (read only),model_epr_approval_config,group_epr_purchasing_officer,1,0,0,0
|
||||
access_epr_approval_config_admin,ePR Approval Config Admin,model_epr_approval_config,group_epr_admin,1,1,1,1
|
||||
access_epr_approval_entry_user,ePR Approval Entry User (read only),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,0,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 (read only),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
|
||||
|
||||
|
138
addons/epr/views/epr_approval_views.xml
Normal file
138
addons/epr/views/epr_approval_views.xml
Normal file
@ -0,0 +1,138 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- 1. APPROVAL CONFIGURATION -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<!-- List View -->
|
||||
<record id="view_epr_approval_config_list" model="ir.ui.view">
|
||||
<field name="name">epr.approval.config.list</field>
|
||||
<field name="model">epr.approval.config</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Approval Rules" multi_edit="1" editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="min_amount" widget="monetary" options="{'currency_field': 'currency_id'}"/>
|
||||
<field name="currency_id" column_invisible="True"/>
|
||||
<field name="approval_type"/>
|
||||
<field name="user_ids" widget="many2many_avatar_user"/>
|
||||
<field name="company_id" groups="base.group_multi_company" optional="hide"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Form View -->
|
||||
<record id="view_epr_approval_config_form" model="ir.ui.view">
|
||||
<field name="name">epr.approval.config.form</field>
|
||||
<field name="model">epr.approval.config</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Approval Rule">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
<h1><field name="name" placeholder="e.g. Manager Approval (> 50M)"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="min_amount" widget="monetary"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="approval_type" widget="radio"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="user_ids" widget="many2many_tags_avatar"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<field name="active" invisible="1"/>
|
||||
<field name="sequence" invisible="1"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Search View -->
|
||||
<record id="view_epr_approval_config_search" model="ir.ui.view">
|
||||
<field name="name">epr.approval.config.search</field>
|
||||
<field name="model">epr.approval.config</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name"/>
|
||||
<field name="user_ids"/>
|
||||
<filter string="Archived" name="archived" domain="[('active', '=', False)]"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action -->
|
||||
<record id="action_epr_approval_config" model="ir.actions.act_window">
|
||||
<field name="name">Approval Rules</field>
|
||||
<field name="res_model">epr.approval.config</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Define your Purchase Request approval rules here.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- 2. APPROVAL ENTRY (Nhật ký duyệt - Nhúng trong RFQ) -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<!-- List View (Dùng để hiện trong Tab 'Approvals' của RFQ) -->
|
||||
<record id="view_epr_approval_entry_list" model="ir.ui.view">
|
||||
<field name="name">epr.approval.entry.list</field>
|
||||
<field name="model">epr.approval.entry</field>
|
||||
<field name="arch" type="xml">
|
||||
<!--
|
||||
decoration-muted: Làm mờ nếu chưa đến lượt (new)
|
||||
decoration-danger: Màu đỏ nếu bị từ chối
|
||||
decoration-success: Màu xanh nếu đã duyệt
|
||||
-->
|
||||
<list string="Approvals"
|
||||
create="0"
|
||||
delete="0"
|
||||
edit="0"
|
||||
decoration-muted="status == 'new'"
|
||||
decoration-danger="status == 'rejected'"
|
||||
decoration-success="status == 'approved'">
|
||||
|
||||
<field name="sequence"/>
|
||||
<field name="name" string="Step"/>
|
||||
<field name="required_user_ids" widget="many2many_avatar_user"/>
|
||||
<field name="approval_type"/>
|
||||
|
||||
<!-- Status Badge -->
|
||||
<field name="status"
|
||||
widget="badge"
|
||||
decoration-info="status == 'pending'"
|
||||
decoration-success="status == 'approved'"
|
||||
decoration-danger="status == 'rejected'"/>
|
||||
|
||||
<field name="actual_user_ids" widget="many2many_tags_avatar" optional="show"/>
|
||||
<field name="approval_date" optional="show"/>
|
||||
<field name="can_approve" column_invisible="True"/>
|
||||
|
||||
<!-- Inline Actions Buttons -->
|
||||
<button name="action_approve_line"
|
||||
string="Approve"
|
||||
type="object"
|
||||
icon="fa-check text-success"
|
||||
invisible="not can_approve"
|
||||
title="Approve this step"/>
|
||||
|
||||
<button name="action_refuse_line"
|
||||
string="Refuse"
|
||||
type="object"
|
||||
icon="fa-times text-danger"
|
||||
invisible="not can_approve"
|
||||
title="Refuse this step"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@ -27,6 +27,18 @@
|
||||
parent="menu_epr_root"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem
|
||||
id="menu_epr_rfq_category"
|
||||
name="RFQs"
|
||||
parent="menu_epr_root"
|
||||
sequence="20"/>
|
||||
|
||||
<menuitem
|
||||
id="menu_epr_po_category"
|
||||
name="POs"
|
||||
parent="menu_epr_root"
|
||||
sequence="30"/>
|
||||
|
||||
<!--
|
||||
===================================================================
|
||||
ACTION MENUS
|
||||
@ -42,9 +54,7 @@
|
||||
action="action_epr_purchase_request"
|
||||
sequence="10"/>
|
||||
|
||||
<!-- Gợi ý: Menu To Approve (Dành cho Manager) -->
|
||||
<!-- Bạn có thể thêm action riêng cho Manager ở đây sau này -->
|
||||
|
||||
<!-- Menu To Approve (Dành cho Manager) -->
|
||||
<menuitem
|
||||
id="menu_epr_request_to_approve"
|
||||
name="To Approve"
|
||||
@ -53,19 +63,80 @@
|
||||
sequence="20"
|
||||
groups="group_epr_manager"/>
|
||||
|
||||
<!-- Menu: My RFQs (Action đã định nghĩa ở file view) -->
|
||||
<menuitem
|
||||
id="menu_epr_rfq_act"
|
||||
name="My RFQs"
|
||||
parent="menu_epr_rfq_category"
|
||||
action="action_epr_rfq"
|
||||
sequence="10"/>
|
||||
|
||||
<!-- Menu: My POs -->
|
||||
<menuitem
|
||||
id="menu_epr_po_act"
|
||||
name="My POs"
|
||||
parent="menu_epr_po_category"
|
||||
action="action_epr_po"
|
||||
sequence="10"
|
||||
groups="epr.group_epr_admin,epr.group_epr_purchasing_officer"/>
|
||||
|
||||
<!--
|
||||
===================================================================
|
||||
CONFIGURATION MENU
|
||||
Thường đặt sequence cao để nằm cuối
|
||||
===================================================================
|
||||
-->
|
||||
<menuitem
|
||||
id="menu_epr_config"
|
||||
name="Configuration"
|
||||
parent="menu_epr_root"
|
||||
sequence="100"
|
||||
groups="epr.group_epr_manager"/>
|
||||
<menuitem id="menu_epr_config"
|
||||
name="Configuration"
|
||||
parent="menu_epr_root"
|
||||
sequence="100"
|
||||
groups="epr.group_epr_admin,epr.group_epr_purchasing_officer"/>
|
||||
|
||||
<!--
|
||||
1. PRODUCTS MENU
|
||||
Action: purchase.product_normal_action_puchased
|
||||
Lý do: Chỉ hiện sản phẩm có 'Purchase OK' = True.
|
||||
Dependency: Cần module 'purchase' trong manifest.
|
||||
-->
|
||||
<menuitem id="menu_epr_config_products"
|
||||
name="Products"
|
||||
parent="menu_epr_config"
|
||||
action="purchase.product_normal_action_puchased"
|
||||
sequence="10"
|
||||
groups="epr.group_epr_admin,epr.group_epr_purchasing_officer"/>
|
||||
|
||||
<!--
|
||||
2. VENDORS MENU
|
||||
Action: account.res_partner_action_supplier
|
||||
Lý do: Đây là action chuẩn của Odoo 18 cho Vendors (hiển thị cột nợ phải trả, bills...)
|
||||
Dependency: Cần module 'account' trong manifest.
|
||||
-->
|
||||
<menuitem id="menu_epr_config_vendors"
|
||||
name="Vendors"
|
||||
parent="menu_epr_config"
|
||||
action="account.res_partner_action_supplier"
|
||||
sequence="20"
|
||||
groups="epr.group_epr_admin,epr.group_epr_purchasing_officer"/>
|
||||
|
||||
<!--
|
||||
3. PRODUCT CATEGORIES
|
||||
Để phân loại sản phẩm tốt hơn
|
||||
-->
|
||||
<menuitem id="menu_epr_config_product_categ"
|
||||
name="Product Categories"
|
||||
parent="menu_epr_config"
|
||||
action="product.product_category_action_form"
|
||||
sequence="30"
|
||||
groups="epr.group_epr_admin,epr.group_epr_purchasing_officer"/>
|
||||
|
||||
<!--
|
||||
4. APPROVAL RULES
|
||||
-->
|
||||
<menuitem id="menu_epr_config_approval_rules"
|
||||
name="Approval Rules"
|
||||
parent="menu_epr_config"
|
||||
action="action_epr_approval_config"
|
||||
sequence="40"
|
||||
groups="epr.group_epr_admin,epr.group_epr_purchasing_officer"/>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
63
addons/epr/views/epr_po_views.xml
Normal file
63
addons/epr/views/epr_po_views.xml
Normal file
@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<!-- Kế thừa Form View chuẩn của Purchase Order -->
|
||||
<record id="view_purchase_order_form_inherit_epr" model="ir.ui.view">
|
||||
<field name="name">purchase.order.form.inherit.epr</field>
|
||||
<field name="model">purchase.order</field>
|
||||
<field name="inherit_id" ref="purchase.purchase_order_form"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<!-- 1. Thêm Smart Button liên kết ngược về RFQs -->
|
||||
<div name="button_box" position="inside">
|
||||
<button name="action_view_epr_rfqs"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-files-o"
|
||||
invisible="epr_rfq_count == 0">
|
||||
<field name="epr_rfq_count" widget="statinfo" string="EPR RFQs"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 2. Thêm cột tham chiếu RFQ vào danh sách sản phẩm (Mặc định ẩn) -->
|
||||
<!-- Tìm xpath tới field order_line/list -->
|
||||
<xpath expr="//field[@name='order_line']/list//field[@name='name']" position="after">
|
||||
<field name="epr_rfq_line_id"
|
||||
string="Ref. RFQ"
|
||||
optional="hide"
|
||||
readonly="1"/>
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Kế thừa Search View để cho phép tìm kiếm PO theo RFQ gốc -->
|
||||
<record id="view_purchase_order_filter_inherit_epr" model="ir.ui.view">
|
||||
<field name="name">purchase.order.search.inherit.epr</field>
|
||||
<field name="model">purchase.order</field>
|
||||
<field name="inherit_id" ref="purchase.view_purchase_order_filter"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="origin" position="after">
|
||||
<!-- Cho phép search: "RFQ/2024/001" sẽ ra PO tương ứng -->
|
||||
<field name="epr_rfq_ids" string="EPR RFQ" filter_domain="[('epr_rfq_ids.name', 'ilike', self)]"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action cho menu My POs -->
|
||||
<record id="action_epr_po" model="ir.actions.act_window">
|
||||
<field name="name">My Purchase Orders</field>
|
||||
<field name="res_model">purchase.order</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="domain">[('epr_rfq_count', '>', 0)]</field>
|
||||
<field name="context">{'create': False}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No Purchase Orders created from EPR RFQs yet.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@ -173,6 +173,7 @@
|
||||
<field name="model">epr.purchase.request</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Purchase Request">
|
||||
|
||||
<header>
|
||||
<!-- Nút Submit: Chỉ hiện khi Draft -->
|
||||
<button name="action_submit"
|
||||
@ -210,6 +211,20 @@
|
||||
</header>
|
||||
|
||||
<sheet>
|
||||
<!--
|
||||
BUTTON BOX: Khu vực chứa các nút thống kê/liên kết
|
||||
Thường đặt ở góc trên bên phải của sheet
|
||||
-->
|
||||
<div name="button_box" class="oe_button_box">
|
||||
<button name="action_view_rfqs"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-list-alt"
|
||||
invisible="rfq_count == 0">
|
||||
<field name="rfq_count" widget="statinfo" string="RFQs"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Ribbons trạng thái -->
|
||||
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" invisible="active"/>
|
||||
<widget name="web_ribbon" title="Rejected" bg_color="bg-danger" invisible="state != 'rejected'"/>
|
||||
|
||||
221
addons/epr/views/epr_rfq_views.xml
Normal file
221
addons/epr/views/epr_rfq_views.xml
Normal file
@ -0,0 +1,221 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- LIST VIEW -->
|
||||
<!-- ============================================================ -->
|
||||
<record id="view_epr_rfq_list" model="ir.ui.view">
|
||||
<field name="name">epr.rfq.list</field>
|
||||
<field name="model">epr.rfq</field>
|
||||
<field name="arch" type="xml">
|
||||
<!-- decoration-muted: Làm mờ dòng nếu trạng thái là 'cancel' -->
|
||||
<list string="RFQs" decoration-muted="state == 'cancel'" sample="1">
|
||||
<field name="name" decoration-bf="1"/>
|
||||
<field name="partner_id" widget="many2one_avatar"/>
|
||||
<field name="date_order" widget="date"/>
|
||||
<field name="date_deadline" widget="remaining_days" optional="show"/>
|
||||
<field name="company_id" groups="base.group_multi_company" optional="hide"/>
|
||||
<field name="currency_id" column_invisible="True"/>
|
||||
|
||||
<!-- Badge hiển thị trạng thái với màu sắc -->
|
||||
<field name="state"
|
||||
widget="badge"
|
||||
decoration-info="state == 'draft'"
|
||||
decoration-warning="state in ('sent', 'received')"
|
||||
decoration-success="state == 'confirmed'"
|
||||
decoration-danger="state == 'cancel'"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- FORM VIEW -->
|
||||
<!-- ============================================================ -->
|
||||
<record id="view_epr_rfq_form" model="ir.ui.view">
|
||||
<field name="name">epr.rfq.form</field>
|
||||
<field name="model">epr.rfq</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Request for Quotation">
|
||||
<!-- HEADER: Thanh trạng thái và các nút hành động -->
|
||||
<header>
|
||||
<!-- Các nút bấm (Placeholder) - Bạn cần định nghĩa hàm python tương ứng -->
|
||||
<button name="action_send_email"
|
||||
string="Send to Vendor"
|
||||
type="object"
|
||||
class="oe_highlight"
|
||||
invisible="state != 'draft'"/>
|
||||
|
||||
<button name="action_mark_received"
|
||||
string="Mark as Received"
|
||||
type="object"
|
||||
class="oe_highlight"
|
||||
invisible="state != 'sent'"/>
|
||||
|
||||
<button name="action_confirm_rfq"
|
||||
string="Confirm RFQ"
|
||||
type="object"
|
||||
class="oe_highlight"
|
||||
invisible="state != 'received'"/>
|
||||
|
||||
<button name="action_cancel_rfq"
|
||||
string="Cancel RFQ"
|
||||
type="object"
|
||||
class="oe_highlight"
|
||||
invisible="state == 'cancel'"/>
|
||||
|
||||
<button name="action_create_po"
|
||||
string="Create Purchase Order"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
invisible="state != 'confirmed'"/>
|
||||
|
||||
<button name="action_reset_draft"
|
||||
string="Reset to Draft"
|
||||
type="object"
|
||||
class="btn-warning"
|
||||
invisible="state not in ('sent', 'to_approve', 'cancel')"/>
|
||||
|
||||
<!-- Widget Statusbar: Hiển thị quy trình -->
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,sent,received,to_approve,confirmed"/>
|
||||
</header>
|
||||
|
||||
<sheet>
|
||||
<!-- SMART BUTTON AREA -->
|
||||
<!-- <div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_purchase_orders"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-shopping-cart"
|
||||
invisible="purchase_count == 0">
|
||||
<field name="purchase_count" widget="statinfo" string="Purchase Orders"/>
|
||||
</button>
|
||||
</div> -->
|
||||
|
||||
<!-- TITLE: Số phiếu RFQ -->
|
||||
<div class="oe_title">
|
||||
<span class="o_form_label">Request for Quotation</span>
|
||||
<h1>
|
||||
<field name="name" readonly="1"/>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- GROUP: Thông tin chính -->
|
||||
<group>
|
||||
<group>
|
||||
<!-- Readonly logic: Chỉ cho sửa khi nháp -->
|
||||
<field name="partner_id" widget="res_partner_many2one" context="{'show_address': 1}" readonly="state != 'draft'"/>
|
||||
<field name="request_ids" widget="many2many_tags" options="{'color_field': 'color'}" readonly="state != 'draft'"/>
|
||||
<field name="currency_id" groups="base.group_multi_currency" readonly="state != 'draft'"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="date_order" readonly="state != 'draft'"/>
|
||||
<field name="date_deadline" readonly="state != 'draft'"/>
|
||||
<field name="company_id" groups="base.group_multi_company" readonly="1"/>
|
||||
<field name="active" invisible="1"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<!-- NOTEBOOK: Tab chi tiết -->
|
||||
<notebook>
|
||||
<page string="Products" name="products">
|
||||
<field name="line_ids" readonly="state != 'draft'">
|
||||
<!--
|
||||
LIST EDITABLE: Cho phép sửa trực tiếp trên dòng
|
||||
Lưu ý: Dùng thẻ <list> thay vì <tree>
|
||||
-->
|
||||
<list editable="bottom">
|
||||
<field name="product_id"/>
|
||||
<field name="description" optional="show"/>
|
||||
<field name="quantity"/>
|
||||
<field name="uom_id" groups="uom.group_uom"/>
|
||||
<field name="price_unit"/>
|
||||
<field name="taxes_id" widget="many2many_tags" optional="show"/>
|
||||
|
||||
<!-- Monetary: Tự động hiển thị ký hiệu tiền tệ dựa trên currency_id -->
|
||||
<field name="subtotal" widget="monetary"/>
|
||||
<field name="currency_id" column_invisible="True"/>
|
||||
</list>
|
||||
</field>
|
||||
|
||||
<!-- Khu vực tổng tiền bên phải -->
|
||||
<group name="note_group" col="6" class="mt-2 mt-md-0">
|
||||
<group class="oe_subtotal_footer oe_right" colspan="2" name="sale_total">
|
||||
<!-- Lưu ý: Cần thêm field amount_total compute trong python nếu muốn hiện tổng ở đây -->
|
||||
</group>
|
||||
<div class="oe_clear"/>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<page string="Other Info" name="other_info">
|
||||
<group>
|
||||
<field name="purchase_ids" widget="many2many_tags" readonly="1"/>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<page string="Approvals" name="approvals" invisible="state == 'draft'">
|
||||
<field name="approval_ids"
|
||||
nolabel="1"
|
||||
readonly="1"
|
||||
context="{'default_rfq_id': id}"/>
|
||||
</page>
|
||||
|
||||
</notebook>
|
||||
</sheet>
|
||||
|
||||
<!-- CHATTER: Khu vực lịch sử và tin nhắn -->
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- SEARCH VIEW -->
|
||||
<!-- ============================================================ -->
|
||||
<record id="view_epr_rfq_search" model="ir.ui.view">
|
||||
<field name="name">epr.rfq.search</field>
|
||||
<field name="model">epr.rfq</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search RFQ">
|
||||
<field name="name"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="request_ids"/>
|
||||
|
||||
<filter string="Draft" name="draft" domain="[('state', '=', 'draft')]"/>
|
||||
<filter string="Sent" name="sent" domain="[('state', '=', 'sent')]"/>
|
||||
<filter string="Late" name="late" domain="[('date_deadline', '<', current_date), ('state', 'in', ('draft', 'sent'))]"/>
|
||||
|
||||
<separator/>
|
||||
<filter string="Archived" name="archived" domain="[('active', '=', False)]"/>
|
||||
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Vendor" name="vendor" context="{'group_by': 'partner_id'}"/>
|
||||
<filter string="Status" name="status" context="{'group_by': 'state'}"/>
|
||||
<filter string="Order Date" name="order_date" context="{'group_by': 'date_order'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- ACTION -->
|
||||
<!-- ============================================================ -->
|
||||
<record id="action_epr_rfq" model="ir.actions.act_window">
|
||||
<field name="name">Requests for Quotation</field>
|
||||
<field name="res_model">epr.rfq</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_epr_rfq_search"/>
|
||||
<field name="help" type="html">
|
||||
<field name="view_ids" eval="[(5, 0, 0),
|
||||
(0, 0, {'view_mode': 'list', 'view_id': ref('view_epr_rfq_list')}),
|
||||
(0, 0, {'view_mode': 'form', 'view_id': ref('view_epr_rfq_form')})]"/>
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your first Request for Quotation (RFQ)
|
||||
</p>
|
||||
<p>
|
||||
Manage your vendor negotiations and create Purchase Orders directly from RFQs.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@ -1 +1,2 @@
|
||||
from . import epr_reject_wizard
|
||||
from . import epr_create_po
|
||||
|
||||
Binary file not shown.
BIN
addons/epr/wizards/__pycache__/epr_create_po.cpython-312.pyc
Normal file
BIN
addons/epr/wizards/__pycache__/epr_create_po.cpython-312.pyc
Normal file
Binary file not shown.
181
addons/epr/wizards/epr_create_po.py
Normal file
181
addons/epr/wizards/epr_create_po.py
Normal file
@ -0,0 +1,181 @@
|
||||
from odoo import models, fields, api, Command, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class EprCreatePoWizard(models.TransientModel):
|
||||
_name = 'epr.create.po.wizard'
|
||||
_description = 'Merge RFQs to Purchase Order'
|
||||
|
||||
# Hiển thị Vendor chung để user confirm
|
||||
partner_id = fields.Many2one(
|
||||
comodel_name='res.partner',
|
||||
string='Vendor',
|
||||
required=True,
|
||||
readonly=True
|
||||
)
|
||||
|
||||
currency_id = fields.Many2one(
|
||||
comodel_name='res.currency',
|
||||
string='Currency',
|
||||
required=True,
|
||||
readonly=True
|
||||
)
|
||||
|
||||
# Danh sách các dòng sẽ được đẩy vào PO (Cho phép user bỏ tick để xé nhỏ RFQ)
|
||||
line_ids = fields.One2many(
|
||||
comodel_name='epr.create.po.line.wizard',
|
||||
inverse_name='wizard_id',
|
||||
string='Products to Order'
|
||||
)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
res = super().default_get(fields_list)
|
||||
active_ids = self.env.context.get('active_ids', [])
|
||||
if not active_ids:
|
||||
return res
|
||||
|
||||
# 1. Lấy danh sách RFQ được chọn
|
||||
rfqs = self.env['epr.rfq'].browse(active_ids)
|
||||
|
||||
# 2. Validate: Cùng Vendor và Currency
|
||||
first_partner = rfqs[0].partner_id
|
||||
first_currency = rfqs[0].currency_id
|
||||
|
||||
if any(r.partner_id != first_partner for r in rfqs):
|
||||
raise UserError(_("Tất cả các RFQ được chọn phải cùng một Nhà cung cấp."))
|
||||
|
||||
if any(r.currency_id != first_currency for r in rfqs):
|
||||
raise UserError(_("Tất cả các RFQ được chọn phải cùng loại Tiền tệ."))
|
||||
|
||||
if any(r.state != 'confirmed' for r in rfqs): # Giả sử trạng thái 'confirmed' là đã chốt
|
||||
raise UserError(_("Chỉ có thể tạo PO từ các RFQ đã xác nhận (Confirmed)."))
|
||||
|
||||
# 3. Loop qua từng dòng RFQ để prepare dữ liệu cho Wizard
|
||||
lines_list = []
|
||||
for rfq in rfqs:
|
||||
for line in rfq.line_ids:
|
||||
# Chỉ load những dòng chưa tạo PO
|
||||
if not line.purchase_line_id:
|
||||
lines_list.append(Command.create({
|
||||
'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
|
||||
'price_unit': line.price_unit,
|
||||
'uom_id': line.uom_id.id,
|
||||
'taxes_id': [Command.set(line.taxes_id.ids)],
|
||||
}))
|
||||
|
||||
if not lines_list:
|
||||
raise UserError(_("Không tìm thấy dòng sản phẩm nào khả dụng để tạo PO (có thể đã được tạo trước đó)."))
|
||||
|
||||
res.update({
|
||||
'partner_id': first_partner.id,
|
||||
'currency_id': first_currency.id,
|
||||
'line_ids': lines_list
|
||||
})
|
||||
return res
|
||||
|
||||
def action_create_po(self):
|
||||
self.ensure_one()
|
||||
if not self.line_ids:
|
||||
raise UserError(_("Vui lòng chọn ít nhất một dòng sản phẩm."))
|
||||
|
||||
# 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
|
||||
'order_line': [],
|
||||
}
|
||||
|
||||
# 2. Prepare PO Lines
|
||||
for w_line in self.line_ids:
|
||||
po_line_vals = {
|
||||
'product_id': w_line.product_id.id,
|
||||
'name': w_line.description or w_line.product_id.name,
|
||||
'product_qty': w_line.quantity,
|
||||
'price_unit': w_line.price_unit,
|
||||
'product_uom': w_line.uom_id.id,
|
||||
'taxes_id': [Command.set(w_line.taxes_id.ids)],
|
||||
# inherit purchase.order.line để link 2 chiều chặt chẽ
|
||||
'epr_rfq_line_id': w_line.rfq_line_id.id
|
||||
}
|
||||
po_vals['order_line'].append(Command.create(po_line_vals))
|
||||
|
||||
# 3. Tạo PO
|
||||
purchase_order = self.env['purchase.order'].create(po_vals)
|
||||
|
||||
# 4. Update ngược lại RFQ Line (Line-Level Linking)
|
||||
# Vì PO Line được tạo qua Command.create, ta cần map lại ID
|
||||
# Cách đơn giản nhất: Loop lại PO Lines vừa tạo
|
||||
|
||||
# Lưu ý: Logic này giả định thứ tự tạo không đổi, để chính xác tuyệt đối
|
||||
# nên thêm field 'epr_rfq_line_id' vào 'purchase.order.line'
|
||||
for i, po_line in enumerate(purchase_order.order_line):
|
||||
# Lấy dòng wizard tương ứng theo index (nếu thứ tự được bảo toàn)
|
||||
# Hoặc tốt hơn: Thêm field epr_rfq_line_id vào purchase.order.line (Xem mục 3)
|
||||
wizard_line = self.line_ids[i]
|
||||
wizard_line.rfq_line_id.write({'purchase_line_id': po_line.id})
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'purchase.order',
|
||||
'res_id': purchase_order.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
|
||||
class EprCreatePoLineWizard(models.TransientModel):
|
||||
_name = 'epr.create.po.line.wizard'
|
||||
_description = 'Line details for PO creation'
|
||||
|
||||
wizard_id = fields.Many2one(
|
||||
comodel_name='epr.create.po.wizard',
|
||||
string='Wizard',
|
||||
required=True,
|
||||
readonly=True
|
||||
)
|
||||
|
||||
rfq_line_id = fields.Many2one(
|
||||
comodel_name='epr.rfq.line',
|
||||
string='RFQ Line',
|
||||
required=True,
|
||||
readonly=True
|
||||
)
|
||||
|
||||
product_id = fields.Many2one(
|
||||
comodel_name='product.product',
|
||||
string='Product',
|
||||
readonly=True
|
||||
)
|
||||
|
||||
description = fields.Text(
|
||||
string='Description',
|
||||
readonly=True
|
||||
)
|
||||
|
||||
quantity = fields.Float(
|
||||
string='Quantity',
|
||||
required=True
|
||||
)
|
||||
|
||||
uom_id = fields.Many2one(
|
||||
comodel_name='uom.uom',
|
||||
string='UoM',
|
||||
readonly=True
|
||||
)
|
||||
|
||||
price_unit = fields.Float(
|
||||
string='Price',
|
||||
readonly=True
|
||||
)
|
||||
|
||||
taxes_id = fields.Many2many(
|
||||
comodel_name='account.tax',
|
||||
string='Taxes',
|
||||
readonly=True
|
||||
)
|
||||
53
addons/epr/wizards/epr_create_po_views.xml
Normal file
53
addons/epr/wizards/epr_create_po_views.xml
Normal file
@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_epr_create_po_wizard_form" model="ir.ui.view">
|
||||
<field name="name">epr.create.po.wizard.form</field>
|
||||
<field name="model">epr.create.po.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Create Purchase Order">
|
||||
<group>
|
||||
<group>
|
||||
<field name="partner_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="currency_id"/>
|
||||
</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="description" optional="hide"/>
|
||||
<field name="quantity"/>
|
||||
<field name="uom_id"/>
|
||||
<field name="price_unit"/>
|
||||
<field name="taxes_id" widget="many2many_tags"/>
|
||||
<field name="rfq_line_id" column_invisible="True"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
<footer>
|
||||
<button name="action_create_po"
|
||||
string="Create PO"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
data-hotkey="q"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Server Action để gọi Wizard từ List View của RFQ -->
|
||||
<record id="action_epr_rfq_create_po" model="ir.actions.act_window">
|
||||
<field name="name">Create Purchase Order</field>
|
||||
<field name="res_model">epr.create.po.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="binding_model_id" ref="model_epr_rfq"/>
|
||||
<field name="binding_view_types">list</field>
|
||||
<field name="groups_id" eval="[(4, ref('epr.group_epr_purchasing_officer')), (4, ref('epr.group_epr_admin'))]"/>
|
||||
</record>
|
||||
</odoo>
|
||||
Loading…
Reference in New Issue
Block a user