Thêm 03 trường nhập liệu Vendors, chuẩn bị cho

việc gộp nhiều PR vào RFQs sau này

Tôi ưu UX

Thêm group Rejection

Điều chỉnh lại login nhận dạng Line Manager
This commit is contained in:
mtpc4s9 2025-12-06 12:09:51 +07:00
parent 25c864ef28
commit 7c3605508d
4 changed files with 231 additions and 69 deletions

View File

@ -38,6 +38,12 @@ class EprPurchaseRequest(models.Model):
readonly=True
)
# Xác định người tạo PR
is_owner = fields.Boolean(
compute='_compute_is_owner',
store=False
)
date_required = fields.Date(
string='Date Required',
required=True,
@ -64,7 +70,8 @@ class EprPurchaseRequest(models.Model):
default='draft',
tracking=True,
index=True,
copy=False
copy=False,
group_expand='_expand_groups'
)
# approver_ids = fields.Many2many(
@ -122,6 +129,21 @@ class EprPurchaseRequest(models.Model):
help="Người đã phê duyệt."
)
date_rejected = fields.Datetime(
string='Rejected Date',
readonly=True,
copy=False,
help="Ngày bị từ chối."
)
rejected_by_id = fields.Many2one(
comodel_name='res.users',
string='Rejected By',
readonly=True,
copy=False,
help="Người đã từ chối."
)
date_submitted = fields.Datetime(
string='Submitted Date',
readonly=True,
@ -131,7 +153,7 @@ class EprPurchaseRequest(models.Model):
# ==========================================================================
# MODEL METHODS
# ==========================================================================
# Hàm tạo sequence cho Request Reference
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
@ -139,6 +161,12 @@ class EprPurchaseRequest(models.Model):
vals['name'] = self.env['ir.sequence'].next_by_code('epr.purchase.request') or _('New')
return super().create(vals_list)
# --- Kanban Grouping (Để Kanban hiển thị đủ cột Draft/Done dù không có data) ---
@api.model
def _expand_groups(self, states, domain, order=None):
"""Force display all state columns in Kanban, even if empty"""
return ['draft', 'to_approve', 'approved', 'rejected', 'in_progress', 'done', 'cancel']
# Compute estimated total
@api.depends('line_ids.subtotal_estimated', 'currency_id')
def _compute_estimated_total(self):
@ -149,6 +177,12 @@ class EprPurchaseRequest(models.Model):
total = sum(line.subtotal_estimated for line in request.line_ids)
request.estimated_total = total
# Xác định người tạo PR
@api.depends_context('uid')
def _compute_is_owner(self):
for record in self:
record.is_owner = record.employee_id.user_id.id == self.env.uid
# Compute approvers
# @api.depends('employee_id', 'department_id', 'estimated_total')
# def _compute_approvers(self):
@ -254,19 +288,6 @@ class EprPurchaseRequest(models.Model):
'approved_by_id': self.env.user.id
})
# Mark activities as done
# NOTE: Commented out for testing without mail server
# self.activity_ids.filtered(
# lambda a: a.user_id == self.env.user
# ).action_feedback(feedback='Approved')
# Post message to chatter
# self.message_post(
# body=_('Purchase Request approved by %s') % (
# self.env.user.name
# )
# )
# Hàm mở Wizard
def action_reject_wizard(self):
"""Open rejection wizard"""
@ -298,25 +319,14 @@ class EprPurchaseRequest(models.Model):
# Thực hiện ghi dữ liệu
self.write({
'state': 'draft',
'state': 'rejected',
'rejection_reason': reason,
'approver_ids': [(5, 0, 0)] # QUAN TRỌNG: Xóa sạch người duyệt để clear danh sách chờ
'approver_ids': [(5, 0, 0)], # QUAN TRỌNG: Xóa sạch người duyệt để clear danh sách chờ
# LOG: Ghi nhận thông tin từ chối
'date_rejected': fields.Datetime.now(),
'rejected_by_id': self.env.user.id,
})
# Mark activities as done
# NOTE: Commented out for testing without mail server
# self.activity_ids.filtered(
# lambda a: a.user_id == self.env.user
# ).action_feedback(feedback='Rejected')
# Post message to chatter
# self.message_post(
# body=_('Purchase Request rejected by %s\nReason: %s') % (
# self.env.user.name,
# reason
# )
# )
# User action: Reset to draft when accidentally submitted
def action_reset_to_draft(self):
"""
@ -435,16 +445,38 @@ class EprPurchaseRequestLine(models.Model):
help="Mô tả chi tiết, có thể dán link, hình ảnh minh họa, thông số kỹ thuật..."
)
# Dùng Char hoặc Text cho tên nhà cung cấp gợi ý (Staff không cần chọn trong danh bạ đối tác)
vendor_name = fields.Char(
string='Vendor Name',
# === 1. USER INPUT FIELDS ===
# User chọn từ danh bạ (Không cho tạo mới ở View)
user_vendor_id = fields.Many2one(
comodel_name='res.partner',
string='Approved Vendor',
domain=lambda self: [
('supplier_rank', '>', 0),
'|', ('company_id', '=', False),
('company_id', '=', self.env.company.id)
],
help="Chọn nhà cung cấp có sẵn trong hệ thống."
)
# User nhập text tự do (Dùng khi không tìm thấy hoặc đề xuất mới)
suggested_vendor_name = fields.Char(
string='Suggested Vendor Name',
help="Tên nhà cung cấp được đề xuất bởi người yêu cầu (tham khảo)."
)
# === 2. PURCHASING ONLY FIELDS ===
# Purchasing chốt Vendor cuối cùng để làm RFQ
final_vendor_id = fields.Many2one(
comodel_name='res.partner',
string='Final Vendor',
domain="[('supplier_rank', '>', 0)]",
help="Nhà cung cấp chính thức được bộ phận Mua hàng chốt."
)
quantity = fields.Float(
string='Quantity',
default=1.0,
digits='Product Unit of Measure', # Sử dụng độ chính xác cấu hình trong hệ thống
digits='Product Unit of Measure', # Sử dụng độ chính xác cấu hình trong hệ thống
required=True,
help="Số lượng cần mua."
)
@ -475,26 +507,30 @@ class EprPurchaseRequestLine(models.Model):
price = line.estimated_price or 0.0
line.subtotal_estimated = qty * price
# === logic Onchange ===
# Chỉ chạy khi Purchasing Staff chọn product_id (lúc xử lý phiếu)
# ==========================================================================
# ONCHANGE FIELDS
# ==========================================================================
@api.onchange('user_vendor_id')
def _onchange_user_vendor_id(self):
"""
UX Logic:
1. Nếu User chọn Vendor ID -> Tự động điền text & set Final Vendor.
2. Nếu User bỏ chọn Vendor ID -> Xóa Final Vendor để Purchasing xử lại.
"""
if self.user_vendor_id:
# User đã chọn vendor từ danh bạ
self.suggested_vendor_name = self.user_vendor_id.name
self.final_vendor_id = self.user_vendor_id
else:
# User bỏ chọn vendor
self.final_vendor_id = False
# suggested_vendor_name giữ nguyên để User có thể nhập thủ công
@api.onchange('product_id')
def _onchange_product_id(self):
if not self.product_id:
return
# Đơn vị tính (UoM): NÊN ghi đè theo chuẩn hệ thống
# Lý do: Staff có thể nhập "Cái", nhưng hệ thống kho quản lý là "Unit(s)".
# Để tạo PO chính xác sau này, ta cần lấy UoM chuẩn của sản phẩm.
self.uom_name = self.product_id.uom_po_id.name or self.product_id.uom_id.name
# Giá dự kiến: Chỉ điền nếu Staff để bằng 0
if self.estimated_price == 0.0:
self.estimated_price = self.product_id.standard_price
# Tên sản phẩm: KHÔNG ghi đè (Giữ nguyên mô tả của Staff)
# Vì Staff mô tả nhu cầu thực tế (VD: "Máy tính Dell cho kế toán"),
# còn tên Product hệ thống có thể chung chung (VD: "Laptop Dell Latitude").
# Ta chỉ điền nếu dòng này do Purchasing tạo mới hoàn toàn (name đang rỗng).
if not self.name:
self.name = self.product_id.display_name
@api.constrains('user_vendor_id', 'suggested_vendor_name')
def _check_vendor_presence(self):
"""
Data Integrity: Bắt buộc phải ít nhất 1 thông tin về nhà cung cấp.
"""
for line in self:
if not line.user_vendor_id and not line.suggested_vendor_name:
raise ValidationError(_("Dòng sản phẩm '%s': Vui lòng chọn Nhà cung cấp hoặc nhập tên đề xuất.", line.name))

View File

@ -23,10 +23,11 @@
<record id="rule_epr_manager_approver" model="ir.rule">
<field name="name">ePR: Manager sees department requests</field>
<field name="model_id" ref="model_epr_purchase_request"/>
<field name="domain_force">['|', '|',
<field name="domain_force">['|', '|', '|',
('employee_id.user_id','=',user.id),
('approver_ids', 'in', user.id),
('department_id.manager_id.user_id', '=', user.id)
('department_id.manager_id.user_id', '=', user.id),
('employee_id.parent_id.user_id', '=', user.id)
]</field>
<field name="groups" eval="[(4, ref('epr.group_epr_manager'))]"/>
</record>

View File

@ -36,6 +36,101 @@
</field>
</record>
<!--
===================================================================
2. KANBAN VIEW
Hiển thị dạng thẻ theo quy trình (Pipeline)
===================================================================
-->
<record id="view_epr_purchase_request_kanban" model="ir.ui.view">
<field name="name">epr.purchase.request.kanban</field>
<field name="model">epr.purchase.request</field>
<field name="arch" type="xml">
<!--
default_group_by="state": Kích hoạt chế độ cột theo trạng thái.
records_draggable="false": Chặn kéo thả nếu bạn muốn quy trình chặt chẽ (chỉ đổi trạng thái bằng nút bấm).
Nếu muốn cho phép kéo thả để duyệt nhanh, hãy bỏ attribute này.
-->
<kanban default_group_by="state"
class="o_kanban_small_column"
quick_create="false"
sample="1">
<!-- Các field cần dùng trong logic hiển thị -->
<field name="state"/>
<field name="currency_id"/>
<field name="activity_state"/>
<field name="id"/>
<field name="employee_id"/>
<field name="department_id"/>
<field name="date_required"/>
<field name="priority"/>
<field name="estimated_total"/>
<field name="activity_ids"/>
<!-- Thanh tiến độ (Progress Bar) trên đầu mỗi cột -->
<progressbar field="state"
colors='{"draft": "secondary", "to_approve": "warning", "approved": "success", "rejected": "danger"}'/>
<templates>
<t t-name="kanban-box">
<!-- Khung thẻ: oe_kanban_global_click giúp click vào đâu cũng mở form -->
<div class="oe_kanban_global_click oe_kanban_card d-flex flex-column">
<!-- HEADER: Tên + Menu 3 chấm -->
<div class="o_kanban_record_top mb-2">
<div class="o_kanban_record_headings">
<strong class="o_kanban_record_title">
<field name="name"/>
</strong>
<span class="float-end badge"
t-attf-class="badge-{{record.state.raw_value == 'approved' ? 'success' : (record.state.raw_value == 'rejected' ? 'danger' : 'info')}}">
<field name="state"/>
</span>
</div>
<!-- Dropdown Menu (Odoo 18 dùng Bootstrap 5) -->
<div class="o_dropdown_kanban dropdown">
<a class="dropdown-toggle o-no-caret btn" role="button" data-bs-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<span class="fa fa-ellipsis-v"/>
</a>
<div class="dropdown-menu" role="menu">
<a role="menuitem" type="edit" class="dropdown-item">Edit</a>
<a role="menuitem" type="delete" class="dropdown-item">Delete</a>
</div>
</div>
</div>
<!-- BODY: Thông tin chính -->
<div class="o_kanban_record_body tags-section mb-2">
<div class="d-flex align-items-center mb-1">
<field name="employee_id" widget="many2one_avatar_user"/>
<span class="ms-2 text-muted small">
<field name="department_id"/>
</span>
</div>
<!-- Hiển thị ngày cần hàng nếu có -->
<div t-if="record.date_required.raw_value" class="text-muted small">
<i class="fa fa-clock-o me-1"/> <field name="date_required"/>
</div>
</div>
<!-- FOOTER: Priority + Activity + Total -->
<div class="o_kanban_record_bottom mt-auto d-flex justify-content-between align-items-center">
<div class="oe_kanban_bottom_left d-flex align-items-center">
<field name="priority" widget="priority"/>
<field name="activity_ids" widget="kanban_activity" class="ms-2"/>
</div>
<div class="oe_kanban_bottom_right">
<field name="estimated_total" widget="monetary" options="{'currency_field': 'currency_id'}" class="fw-bold"/>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<!--
===================================================================
2. LIST VIEW
@ -102,12 +197,14 @@
invisible="state != 'to_approve'"
groups="epr.group_epr_manager"/>
<!-- Nút Reset: Cho User sửa lại khi đã submit nhầm -->
<field name="is_owner" invisible="1"/>
<!-- Nút Reset: Cho phép PR's owner sửa lại khi đã submit nhầm -->
<button name="action_reset_to_draft"
string="Reset to Draft"
type="object"
class="btn-warning"
invisible="state not in ['to_approve']"/>
invisible="state not in ['to_approve', 'rejected'] or not is_owner"/>
<field name="state" widget="statusbar" statusbar_visible="draft,to_approve,approved,done"/>
</header>
@ -133,11 +230,6 @@
<group>
<field name="date_required"/>
<field name="priority" widget="priority"/>
<!-- Lý do từ chối chỉ hiện khi bị từ chối -->
<field name="rejection_reason"
invisible="state != 'rejected'"
readonly="1"
class="text-danger"/>
</group>
</group>
@ -158,7 +250,30 @@
<field name="subtotal_estimated" sum="Total"/>
<field name="currency_id" invisible="1"/>
<field name="vendor_name" optional="hide"/>
<!-- ================= VENDOR SECTION ================= -->
<!-- 1. VENDOR LIST (User Search) -->
<!-- options="{'no_create': True}": Ngăn User tạo rác dữ liệu -->
<field name="user_vendor_id"
placeholder="Select existing vendor..."
options="{'no_create': True, 'no_create_edit': True}"
widget="many2one"/>
<!-- 2. VENDOR TEXT (User Type)
"user_vendor_id": Tự động điền vào trường text nếu đã chọn được vendor trong danh bạ
"required="not user_vendor_id": Bắt buộc nhập nếu chưa chọn ID.
-->
<field name="suggested_vendor_name"
placeholder="...or type new vendor name"
required="not user_vendor_id"/>
<!-- 3. FINAL VENDOR (Purchasing Only)
"groups="...": Chỉ hiện cột này cho Purchasing Officer.
User thường sẽ hoàn toàn không thấy cột này.
"optional="show": Cho phép ẩn/hiện trong menu 3 chấm nếu cần.
-->
<field name="final_vendor_id"
groups="epr.group_epr_purchasing_officer"
widget="many2one"
optional="show"/>
<!-- Product ID ẩn, dành cho Purchasing map sau này -->
<field name="product_id" optional="hide" groups="epr.group_epr_purchasing_officer"/>
@ -179,6 +294,16 @@
<field name="approver_ids" widget="many2many_tags" readonly="1"/>
</group>
</group>
<group>
<group string="Rejection" invisible="not date_rejected">
<field name="date_rejected" readonly="1"/>
<field name="rejected_by_id" readonly="1"/>
<field name="rejection_reason"
readonly="1"
class="text-danger"/>
</group>
</group>
</page>
</notebook>
</sheet>
@ -199,7 +324,7 @@
<field name="name">Purchase Requests</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">epr.purchase.request</field>
<field name="view_mode">list,form,search</field>
<field name="view_mode">kanban,list,form,search</field>
<field name="search_view_id" ref="view_epr_purchase_request_search"/>
<field name="context">{'search_default_my_requests': 1}</field>
<!-- <field name="context">{'search_default_my_requests': 1, 'search_default_to_approve_by_me': 1}</field> -->
@ -215,7 +340,7 @@
<field name="name">To Approve</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">epr.purchase.request</field>
<field name="view_mode">list,form,search</field>
<field name="view_mode">kanban,list,form,search</field>
<field name="search_view_id" ref="view_epr_purchase_request_search"/>
<field name="context">{'search_default_to_approve_by_me': 1, 'search_default_to_approve': 1}</field>
<field name="domain">[('state', '=', 'to_approve')]</field>