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:
parent
25c864ef28
commit
7c3605508d
Binary file not shown.
@ -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ý 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 có í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))
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user