diff --git a/addons/epr/models/__pycache__/epr_purchase_request.cpython-312.pyc b/addons/epr/models/__pycache__/epr_purchase_request.cpython-312.pyc
index d35b642..c596ccf 100644
Binary files a/addons/epr/models/__pycache__/epr_purchase_request.cpython-312.pyc and b/addons/epr/models/__pycache__/epr_purchase_request.cpython-312.pyc differ
diff --git a/addons/epr/models/epr_purchase_request.py b/addons/epr/models/epr_purchase_request.py
index 67ca755..3d93f25 100644
--- a/addons/epr/models/epr_purchase_request.py
+++ b/addons/epr/models/epr_purchase_request.py
@@ -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))
diff --git a/addons/epr/security/epr_record_rules.xml b/addons/epr/security/epr_record_rules.xml
index 4dbe231..1684470 100644
--- a/addons/epr/security/epr_record_rules.xml
+++ b/addons/epr/security/epr_record_rules.xml
@@ -23,10 +23,11 @@
ePR: Manager sees department requests
- ['|', '|',
+ ['|', '|', '|',
('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)
]
diff --git a/addons/epr/views/epr_purchase_request_views.xml b/addons/epr/views/epr_purchase_request_views.xml
index 25058f2..67f3a4a 100644
--- a/addons/epr/views/epr_purchase_request_views.xml
+++ b/addons/epr/views/epr_purchase_request_views.xml
@@ -36,6 +36,101 @@
+
+
+ epr.purchase.request.kanban
+ epr.purchase.request
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+