From 7555d2ca6592e17b5ec347dbd6b5b21a5be09138 Mon Sep 17 00:00:00 2001 From: mtpc4s9 Date: Mon, 22 Dec 2025 10:21:39 +0700 Subject: [PATCH] =?UTF-8?q?Ch=C6=B0a=20th=C3=A0nh=20c=C3=B4ng=20trong=20vi?= =?UTF-8?q?=E1=BB=87c=20g=E1=BB=99p=20nhi=E1=BB=81u=20PRs=20=C4=91?= =?UTF-8?q?=E1=BB=83=20t=E1=BA=A1o=20th=C3=A0nh=20RFQs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/epr/__manifest__.py | 1 + addons/epr/data/import_partners.csv | 11 + .../data/import_purchase_request_lines.csv | 21 ++ addons/epr/data/import_purchase_requests.csv | 21 ++ .../epr_purchase_request.cpython-312.pyc | Bin 17262 -> 17281 bytes .../__pycache__/epr_rfq.cpython-312.pyc | Bin 15557 -> 16938 bytes addons/epr/models/epr_purchase_request.py | 12 +- addons/epr/models/epr_rfq.py | 51 ++++- addons/epr/security/ir.model.access.csv | 4 + addons/epr/views/epr_rfq_views.xml | 29 ++- addons/epr/wizards/__init__.py | 1 + .../__pycache__/__init__.cpython-312.pyc | Bin 220 -> 258 bytes .../__pycache__/epr_create_po.cpython-312.pyc | Bin 7350 -> 7350 bytes .../epr_create_rfq.cpython-312.pyc | Bin 0 -> 7931 bytes addons/epr/wizards/epr_create_rfq.py | 209 ++++++++++++++++++ addons/epr/wizards/epr_create_rfq_views.xml | 91 ++++++++ docker-compose.yml | 4 +- 17 files changed, 435 insertions(+), 20 deletions(-) create mode 100644 addons/epr/data/import_partners.csv create mode 100644 addons/epr/data/import_purchase_request_lines.csv create mode 100644 addons/epr/data/import_purchase_requests.csv create mode 100644 addons/epr/wizards/__pycache__/epr_create_rfq.cpython-312.pyc create mode 100644 addons/epr/wizards/epr_create_rfq.py create mode 100644 addons/epr/wizards/epr_create_rfq_views.xml diff --git a/addons/epr/__manifest__.py b/addons/epr/__manifest__.py index ea7d2bd..58be4a8 100644 --- a/addons/epr/__manifest__.py +++ b/addons/epr/__manifest__.py @@ -34,6 +34,7 @@ 'views/epr_po_views.xml', 'views/epr_menus.xml', 'wizards/epr_reject_wizard_views.xml', + 'wizards/epr_create_rfq_views.xml', 'wizards/epr_create_po_views.xml', ], 'assets': { diff --git a/addons/epr/data/import_partners.csv b/addons/epr/data/import_partners.csv new file mode 100644 index 0000000..b240352 --- /dev/null +++ b/addons/epr/data/import_partners.csv @@ -0,0 +1,11 @@ +id,name,email,phone,is_company,supplier_rank +partner_vendor_1,CÔNG TY TNHH THIẾT BỊ VĂN PHÒNG ABC,contact@abc-office.vn,028 1234 5678,True,1 +partner_vendor_2,CÔNG TY CP CÔNG NGHỆ DELTA,sales@deltatech.vn,024 9876 5432,True,1 +partner_vendor_3,NHÀ PHÂN PHỐI PHẦN MỀM SIGMA,info@sigmasoft.vn,028 5555 6666,True,1 +partner_vendor_4,CÔNG TY VẬT TƯ XÂY DỰNG OMEGA,order@omega-building.vn,028 7777 8888,True,1 +partner_vendor_5,ĐẠI LÝ VĂN PHÒNG PHẨM BETA,sales@beta-stationery.vn,028 2222 3333,True,1 +partner_vendor_6,CÔNG TY TNHH ĐIỆN TỬ GAMMA,contact@gamma-electronics.vn,024 4444 5555,True,1 +partner_vendor_7,NHÀ CUNG CẤP NỘI THẤT EPSILON,info@epsilon-furniture.vn,028 6666 7777,True,1 +partner_vendor_8,CÔNG TY DỊCH VỤ IT ZETA,support@zeta-it.vn,1900 1234,True,1 +partner_vendor_9,ĐẠI LÝ PHÂN PHỐI LAMBDA,order@lambda-dist.vn,028 8888 9999,True,1 +partner_vendor_10,CÔNG TY THƯƠNG MẠI THETA,sales@theta-trading.vn,024 1111 2222,True,1 diff --git a/addons/epr/data/import_purchase_request_lines.csv b/addons/epr/data/import_purchase_request_lines.csv new file mode 100644 index 0000000..79c6b45 --- /dev/null +++ b/addons/epr/data/import_purchase_request_lines.csv @@ -0,0 +1,21 @@ +id,request_id/id,name,product_description,quantity,estimated_price,uom_name,suggested_vendor_name +pr_line_1,pr_demo_1,Máy in HP LaserJet Pro M404dn,Máy in laser đen trắng - in 2 mặt tự động - kết nối mạng LAN,2,8500000,Cái,CÔNG TY TNHH THIẾT BỊ VĂN PHÒNG ABC +pr_line_2,pr_demo_2,Laptop Dell Latitude 5540,Laptop văn phòng - Intel Core i5 - RAM 16GB - SSD 512GB,5,25000000,Bộ,CÔNG TY CP CÔNG NGHỆ DELTA +pr_line_3,pr_demo_3,Bàn làm việc 1m4,Bàn làm việc chữ L - kích thước 1m4 x 0.6m - màu gỗ sồi,10,3500000,Cái,NHÀ CUNG CẤP NỘI THẤT EPSILON +pr_line_4,pr_demo_4,Ghế công thái học Ergohuman,Ghế văn phòng cao cấp - hỗ trợ lưng - tay vịn điều chỉnh,5,12000000,Cái,NHÀ CUNG CẤP NỘI THẤT EPSILON +pr_line_5,pr_demo_5,Máy chiếu Epson EB-X51,Máy chiếu 3800 lumens - độ phân giải XGA - kết nối HDMI,2,15000000,Cái,CÔNG TY TNHH ĐIỆN TỬ GAMMA +pr_line_6,pr_demo_6,Microsoft 365 Business Premium (1 năm),Bản quyền phần mềm Office 365 - bao gồm Teams - OneDrive 1TB,50,3000000,License,NHÀ PHÂN PHỐI PHẦN MỀM SIGMA +pr_line_7,pr_demo_7,Ổ cứng SSD Samsung 870 EVO 1TB,Ổ cứng SSD SATA 2.5 inch - tốc độ đọc 560MB/s,10,2500000,Cái,CÔNG TY CP CÔNG NGHỆ DELTA +pr_line_8,pr_demo_8,Dịch vụ bảo trì Server (6 tháng),Gói bảo trì hệ thống server - bao gồm backup và monitoring 24/7,1,50000000,Gói,CÔNG TY DỊCH VỤ IT ZETA +pr_line_9,pr_demo_9,Router Wifi TP-Link Archer AX73,Router Wifi 6 - băng tần kép - tốc độ 5400Mbps,5,3200000,Cái,CÔNG TY TNHH ĐIỆN TỬ GAMMA +pr_line_10,pr_demo_10,Webcam Logitech C920 HD Pro,Webcam Full HD 1080p - tự động lấy nét - micro kép,10,2800000,Cái,CÔNG TY TNHH ĐIỆN TỬ GAMMA +pr_line_11,pr_demo_11,Giấy A4 Double A 80gsm,Giấy in A4 chất lượng cao - độ trắng 96%,100,85000,Ream,ĐẠI LÝ VĂN PHÒNG PHẨM BETA +pr_line_12,pr_demo_12,Bút bi Thiên Long TL-027,Bút bi mực xanh - nét thanh đẹp,200,5000,Cây,ĐẠI LÝ VĂN PHÒNG PHẨM BETA +pr_line_13,pr_demo_13,Kẹp giấy loại lớn,Kẹp giấy kim loại 51mm - hộp 12 cái,50,25000,Hộp,ĐẠI LÝ VĂN PHÒNG PHẨM BETA +pr_line_14,pr_demo_14,Sổ tay bìa cứng A5,Sổ tay bìa da cao cấp - 200 trang giấy kẻ ngang,30,65000,Quyển,ĐẠI LÝ VĂN PHÒNG PHẨM BETA +pr_line_15,pr_demo_15,Mực in HP 76A Black,Hộp mực chính hãng cho máy in HP LaserJet Pro M404,10,2200000,Hộp,CÔNG TY TNHH THIẾT BỊ VĂN PHÒNG ABC +pr_line_16,pr_demo_16,Điều hòa Daikin Inverter 1HP,Điều hòa tiết kiệm điện - công suất 9000BTU,3,12000000,Bộ,CÔNG TY TNHH ĐIỆN TỬ GAMMA +pr_line_17,pr_demo_17,Bình nước nóng Ariston 30L,Bình nước nóng điện - dung tích 30 lít - có chống giật,2,4500000,Cái,CÔNG TY VẬT TƯ XÂY DỰNG OMEGA +pr_line_18,pr_demo_18,Camera IP Hikvision DS-2CD1047G2,Camera IP 4MP - hồng ngoại 30m - chuẩn IP67,8,1800000,Cái,CÔNG TY TNHH ĐIỆN TỬ GAMMA +pr_line_19,pr_demo_19,Tủ hồ sơ sắt 3 ngăn,Tủ hồ sơ kim loại - có khóa - kích thước 1.2m x 0.5m x 0.4m,5,2800000,Cái,NHÀ CUNG CẤP NỘI THẤT EPSILON +pr_line_20,pr_demo_20,Nước uống đóng chai Aquafina 500ml,Nước tinh khiết chai 500ml - thùng 24 chai,50,95000,Thùng,CÔNG TY THƯƠNG MẠI THETA diff --git a/addons/epr/data/import_purchase_requests.csv b/addons/epr/data/import_purchase_requests.csv new file mode 100644 index 0000000..4f52d9a --- /dev/null +++ b/addons/epr/data/import_purchase_requests.csv @@ -0,0 +1,21 @@ +id,date_required,priority,state +pr_demo_1,2024-12-23,2,draft +pr_demo_2,2024-12-21,3,draft +pr_demo_3,2024-12-26,1,draft +pr_demo_4,2024-12-19,4,draft +pr_demo_5,2024-12-30,2,draft +pr_demo_6,2025-01-15,2,draft +pr_demo_7,2024-12-23,3,draft +pr_demo_8,2024-12-21,4,draft +pr_demo_9,2025-01-06,1,draft +pr_demo_10,2024-12-26,2,draft +pr_demo_11,2024-12-19,1,draft +pr_demo_12,2024-12-21,1,draft +pr_demo_13,2024-12-23,2,draft +pr_demo_14,2024-12-26,1,draft +pr_demo_15,2024-12-21,2,draft +pr_demo_16,2024-12-30,3,draft +pr_demo_17,2025-01-06,2,draft +pr_demo_18,2024-12-23,4,draft +pr_demo_19,2024-12-26,2,draft +pr_demo_20,2025-01-15,1,draft 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 a3d9732122405ac6f54c1df0092078a81ec1e8a7..156411f7f30c803043268b069010a497ba74b3c8 100644 GIT binary patch delta 171 zcmaFY#@N`-$a|WXmx}=i%GP;g25#gnL7vj4qq2jdwBf zX)+fH1I;TE2N5Eh%}sAHP1dvU6a;a@!_4&WPHK5187xok zm(u!i)l|Hx>a(wCrdE~(57)!pM!1! zmeS3@GTIC*r!By>v=!*2ZNPPO3vfMc2UgInz)HFeSVgx3UB^|2NniCYm7^niN_$pK z7I~2&Nc8-N24~3;3lk-0$iu<`k;(dAF+hXNp=G7;`&c?615wOBGn^xx{EYFc^%)R2 zG^_#T8+ohANp|zF>AlJ&qcW5$mYL96ObkVs_kc$dok2!n3bPnuZZ>ex?W2oaGB>Ts zbbEtCp-5PCGx1m>Z1J1s;L@bB#xDj~J9k>jNgMCC6p~RsYN;^E z@GBHs{tL4>y0pM;xR5S@Z2g>P)&VjJBP<9k1(mh{j^wq5SkJ&g=hK(-BH9!H##Hyy zY@euzsrq>HUxmWS<}2M7yJrR0@#YT_^5!)lk4{-i2x;K&C-kKEqG#)pXD<^yVewS7 z-r7J&2OmzXBrip;Cwd9-@WkYuHtYm6a)}=v4QyDU(y=M^=L-=N9gyz89T|(`OeI9#i-cj(8oQtLr2zl0we58zEf=t z5@k&wILvxbErM|3sNNF_!9irh{Egh4tPw>N2@c_;%BjO$ zWnTV|+&=wV(3H0DeMMPOn)jSKz8%>hT;7hm`HkdnJX7ei;9@lJ|I?!?e&1%#4`WC9 zC|S-xHPuip)lvOXE|@Y51DZiX4N7~|N2rmS&Kn?;%#k)Q?3}xGL7<+%0YQ5EqW6H{ z1qwTs&b!c!=Uoy?c62+0-%Pyw)45xhy+U~I&5J?cttp|WZKn`;dvb)ef-80uft$H# z^82K4`9P2ju@>x;jdCg#2Rx$@f7rc}J?vXB=rF|hZWOJ@5Z^)60%U_X7zm4p!|Vm< zLBc9gQDQNUI!OSD0OCm?=XktfUohZ4k@UVX`@{5%natwZ^paS5SuEQT%eLRovQK)ix^C27tDUh`&1AY_S>;#hjlpY! zGq##oX8wGNDcSr`Wipvr8aVUf5|u9zh^JKv;i3OKgnV;d`e;0i)OcPG!81k5!Ohcy z*CPzh-J0+U-nm;>Jj)?ElC>)cVHH<`+Y_%hQao-I;E%n8z^#R29*9RrzZ9LPgeM*q z?GsT^N|nY(rLPW+q^#s|v5bf2i;?+)&VQ7O*8)rXQfSuCrdKGTSqGtsW#zs?CtL2? zU3cuRyY{*}_PQC{hMCNI42#u(VKJD@(fz_6jg@h5w>zG+oZl`igJSb9U=LqqZ>daY=vrBo5z1L;>-W&GSo}NGreK$Rn5dTaKnAF z7gXYBOIMe@ik7dT+z^k+6==)G0iQoCGLg!fAqeVzRPyp`rTa5qho*$tB;eN+zeuk0 zys~0a6KyTqgvIw*c_;Dm56X+RZ-OG^84= zZXt}6KCB!rqtXWz#=zN1!DV>6vF{=zoJEV(Sst}g_~WLchx&bBqZD-I;xKH5 z!ZHY6wxh`ogbwWtRK&V1F)WVCttzJu1*zzF$FDy35R@<?TJ zP{DqL&2xyKAg&{DC)qmy+3I$C{T@m3ihjS_?I3IquUnr-Ug5p#XNwn-jA{C@?GMvc zscX(;zMMaiAIm6+CD~#r8Srnxq)*k4ZF`u^dn&FX%KG`ms)4Tv?ctGPdhj|F`?kr3VHu58Wf>3qJ&RcICHn#b=_TEI`ZiY$f8x0ap1;VLo}(XYYHRXkn7 zAGivO9JCaAy6Ljz*Y2v3a>rWP9Dj~1Z z6R1xL`8^}eF&Kb%Z2;2XWA3SUB;CUi!~on_660s<8%%I^lyhc|&ehita*t;i*5h_ delta 2240 zcmZvddu)?c6u`gRu8*zj#-1JQ!MZZK?$M16_AFx?V=^8?94`l>)ZTAW_||T>t=Xoy z!B9a>1dfD&F(fDn+Wn!{w?+-bM56qG{Nec2_=pLR_y>gXhy3L^-$#Z)5L#&yeYwS4R1ayB_0?DT(WcD+JAeJbVM3ryydWU-mlysq38d_ZJm$X zrpMht0S1L47PxEinuG(xgVUPtFo{YWid;e-!A)>Luk{Su1b+K>ZRtodvke3SYOGr{q++%hfX{fv>g2<{C;Wp6}qS*U69i13S{QGbX}z{Ru< zSYiv?^@yrbk;7sQoV8W6ZurjTVh7;1ZCNtM?qaMBnlc{CeH53Qu&)^rj}47_gF+6( zSY>_UOizymodjIhJ>e;y&Tn%-(_b9cSF+*yl0p5!axg8Ux{R@t@f7 z0v~%T`CdV13R?|xMIJT+CB+W50UC>&GrMSs`6VadK=HP`U8LJiuoAR96}VYkR6sQ# zA_Ol&H3vpUDzn_A@{<;OQ zq;j9(hLk*3*<)lU;fI<&wg{GbcV--;FmDjvM8GdKJ&T?AFr%3$O-Ua+zpVJA;w$-T z>-s548G|Lrj#xotiS1oqVRQlM3jIWgFQbq}RJy0+!d8 zvU=EBKZ|;AgU|9V`uB+>zNU4o89r}lM(e$X?UQs;s--IuQRF~ayhKuJzpTi?SSS+K z%15O(W04En~+E2ax|u$v{loit57iL!_pGZk~m2i z#^b!S+%c&Yq*msZ-f&EQQWk9&3zw-9qk>wQnmruhvf|gDr++xE#6sG2ifWp$ zn-Cxb3Ht~y5RMYg66o9nHA(RyLbdz-!3-r3jRs{!@%ueY^uyCDirF4Gz2aKUY*yY} zR%(uUYW?keDcgB0|J35+i)Zu7W-}dV!r!mxV7FwOr`Fxh255XWoo3j~3(Qk%QBl`a z4--vg@LAIimJ21#jjRi{HV>M)X(laUgb$nJEE}p@T&5hJJCg@nT3oh#?%)M{Azw68 z2!~o+mLgt^-X$>8;xaCtDTVJ_Ty`ffOBgVD{J0AYt>um-^BukXAO92ukkPU*bt!jq z6R()@z@AoDc_ptx$uv`)NYNt@5rFt=6hpRp(dj{_M z8rUefSI$;x^G#wDoz!$5ijF9Oaep{4EQ^DrN~K;j5}|f9jU}B6?d^^1AjI20n*0gv z`ajH%G;$#NJcZ&T5`m9MaPToNi^2N{ta>0K41U< diff --git a/addons/epr/models/epr_purchase_request.py b/addons/epr/models/epr_purchase_request.py index 3bcca60..40490d2 100644 --- a/addons/epr/models/epr_purchase_request.py +++ b/addons/epr/models/epr_purchase_request.py @@ -88,15 +88,15 @@ class EprPurchaseRequest(models.Model): ) currency_id = fields.Many2one( - 'res.currency', + comodel_name='res.currency', string='Currency', default=lambda self: self.env.company.currency_id, required=True ) line_ids = fields.One2many( - 'epr.purchase.request.line', - 'request_id', + comodel_name='epr.purchase.request.line', + inverse_name='request_id', string='Products' ) @@ -397,10 +397,10 @@ class EprPurchaseRequest(models.Model): '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' + 'view_mode': 'list,form', '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 + '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, }, } @@ -507,7 +507,7 @@ class EprPurchaseRequestLine(models.Model): final_vendor_id = fields.Many2one( comodel_name='res.partner', string='Final Vendor', - domain="[('supplier_rank', '>', 0)]", + # domain="[('supplier_rank', '>', 0)]", help="Nhà cung cấp chính thức được bộ phận Mua hàng chốt." ) diff --git a/addons/epr/models/epr_rfq.py b/addons/epr/models/epr_rfq.py index ddf3436..7ab7abd 100644 --- a/addons/epr/models/epr_rfq.py +++ b/addons/epr/models/epr_rfq.py @@ -59,17 +59,15 @@ class EprRfq(models.Model): column2='request_id', string='Source Requests', # Chỉ lấy các PR đã duyệt để tạo RFQ - domain="[('state', '=', 'approved')]", - readonly=True + domain="[('state', '=', 'approved')]" ) partner_id = fields.Many2one( comodel_name='res.partner', string='Vendor', required=True, - tracking=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( @@ -105,8 +103,7 @@ class EprRfq(models.Model): comodel_name='epr.rfq.line', inverse_name='rfq_id', string='Products', - copy=True, - readonly=True + copy=True ) # Link sang Purchase Order gốc của Odoo @@ -121,12 +118,23 @@ class EprRfq(models.Model): string='PO Count' ) + request_count = fields.Integer( + compute='_compute_request_count', + string='PR Count' + ) + # === 5. COMPUTE METHODS === @api.depends('purchase_ids') def _compute_purchase_count(self): for rfq in self: rfq.purchase_count = len(rfq.purchase_ids) + # Link ngược về PR gốc + @api.depends('request_ids') + def _compute_request_count(self): + for rfq in self: + rfq.request_count = len(rfq.request_ids) + # === 6. CRUD OVERRIDES === @api.model_create_multi def create(self, vals_list): @@ -225,9 +233,37 @@ class EprRfq(models.Model): raise UserError(_("Chỉ có thể reset khi ở trạng thái Sent, To Approve hoặc Cancel.")) rfq.write({'state': 'draft'}) + # Mở danh sách các PO được tạo từ RFQ này + def action_view_purchase_orders(self): + """Mở danh sách các Purchase Orders (PO) được tạo từ RFQ này""" + self.ensure_one() + return { + 'name': _('Purchase Orders'), + 'type': 'ir.actions.act_window', + 'res_model': 'purchase.order', + 'view_mode': 'list,form', + # Filter các PO có field epr_rfq_id khớp với ID hiện tại + 'domain': [('epr_rfq_id', '=', self.id)], + 'context': {'default_epr_rfq_id': self.id}, + 'target': 'current', + } + + # Mở danh sách các PR gốc của RFQ này + def action_view_source_requests(self): + """Mở danh sách các PR gốc của RFQ này""" + self.ensure_one() + return { + 'name': _('Source Purchase Requests'), + 'type': 'ir.actions.act_window', + 'res_model': 'epr.purchase.request', + 'view_mode': 'list,form', + 'domain': [('id', 'in', self.request_ids.ids)], + 'target': 'current', + } # ------------------------------------------------------------------------- # RFQ APPROVAL PROCESS # ------------------------------------------------------------------------- + def action_submit_approval(self): """Nút bấm Submit for Approval""" self.ensure_one() @@ -333,8 +369,7 @@ class EprRfqLine(models.Model): # Sản phẩm (Odoo Product) product_id = fields.Many2one( comodel_name='product.product', - string='Product', - required=True + string='Product' ) description = fields.Text( diff --git a/addons/epr/security/ir.model.access.csv b/addons/epr/security/ir.model.access.csv index ee07cf3..d2023f0 100644 --- a/addons/epr/security/ir.model.access.csv +++ b/addons/epr/security/ir.model.access.csv @@ -25,3 +25,7 @@ access_epr_create_po_wizard_officer,ePR Create PO Wizard Officer,model_epr_creat access_epr_create_po_wizard_admin,ePR Create PO Wizard Admin,model_epr_create_po_wizard,group_epr_admin,1,1,1,1 access_epr_create_po_line_wizard_officer,ePR Create PO Line Wizard Officer,model_epr_create_po_line_wizard,group_epr_purchasing_officer,1,1,1,1 access_epr_create_po_line_wizard_admin,ePR Create PO Line Wizard Admin,model_epr_create_po_line_wizard,group_epr_admin,1,1,1,1 +access_epr_create_rfq_wizard_officer,ePR Create RFQ Wizard Officer,model_epr_create_rfq_wizard,group_epr_purchasing_officer,1,1,1,1 +access_epr_create_rfq_wizard_admin,ePR Create RFQ Wizard Admin,model_epr_create_rfq_wizard,group_epr_admin,1,1,1,1 +access_epr_create_rfq_line_officer,ePR Create RFQ Line Officer,model_epr_create_rfq_line,group_epr_purchasing_officer,1,1,1,1 +access_epr_create_rfq_line_admin,ePR Create RFQ Line Admin,model_epr_create_rfq_line,group_epr_admin,1,1,1,1 diff --git a/addons/epr/views/epr_rfq_views.xml b/addons/epr/views/epr_rfq_views.xml index b82f9ba..0cc2713 100644 --- a/addons/epr/views/epr_rfq_views.xml +++ b/addons/epr/views/epr_rfq_views.xml @@ -82,15 +82,25 @@ - +
-
--> + + + +
@@ -105,7 +115,7 @@ - + @@ -160,6 +170,17 @@ context="{'default_rfq_id': id}"/> + + + + + + + + + + + diff --git a/addons/epr/wizards/__init__.py b/addons/epr/wizards/__init__.py index 0f3b969..cc0fe26 100644 --- a/addons/epr/wizards/__init__.py +++ b/addons/epr/wizards/__init__.py @@ -1,2 +1,3 @@ from . import epr_reject_wizard +from . import epr_create_rfq from . import epr_create_po diff --git a/addons/epr/wizards/__pycache__/__init__.cpython-312.pyc b/addons/epr/wizards/__pycache__/__init__.cpython-312.pyc index e198c2d00a0ea5231eebc1db2cc42e170dfe6a4b..f72fdf01abae599d99e1220ce692dfc87f7ae42f 100644 GIT binary patch delta 132 zcmcb^*u=zpnwOW00SF%DJ7z{syY85tSx RGRWU$kh#YoUBm$t0syvlAD93D delta 119 zcmZo-y2HqOnwOW00SKDH>@sa8@=7wwOjMV(u4L9^dC3SA)?~aTm|9R2UzD1anp_fJ zo>`SxlmZmw1qmh>r6!i7#uw!KX)@npC}IYxFJhUv$U&A9$O7pu<^~cUm>C%v?=r~W KV~{Rl2l4@J8XepK diff --git a/addons/epr/wizards/__pycache__/epr_create_po.cpython-312.pyc b/addons/epr/wizards/__pycache__/epr_create_po.cpython-312.pyc index 1f6f8fc2e48307c550775c4597a75d4f533ed8c2..7b7c078037e1781ff7d468aad79a446324b46bb1 100644 GIT binary patch delta 19 ZcmdmHxy_R6G%qg~0}#kXZRA=b0{}H_1k(Tj delta 19 ZcmdmHxy_R6G%qg~0}zBnZsb}c0{}Lw1qc8D diff --git a/addons/epr/wizards/__pycache__/epr_create_rfq.cpython-312.pyc b/addons/epr/wizards/__pycache__/epr_create_rfq.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fc34cdccd107243894e53725a69f98f870872082 GIT binary patch literal 7931 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 K6CynY@&5t5T}ul9 literal 0 HcmV?d00001 diff --git a/addons/epr/wizards/epr_create_rfq.py b/addons/epr/wizards/epr_create_rfq.py new file mode 100644 index 0000000..c628d71 --- /dev/null +++ b/addons/epr/wizards/epr_create_rfq.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, Command, _ +from odoo.exceptions import UserError + + +class EprCreateRfqWizard(models.TransientModel): + _name = 'epr.create.rfq.wizard' + _description = 'Wizard: Merge PRs to RFQ' + + # Danh sách các dòng PR đang được xử lý trong Wizard + line_ids = fields.One2many( + comodel_name='epr.create.rfq.line', + inverse_name='wizard_id', + string='PR Lines to Process' + ) + + @api.model + def default_get(self, fields_list): + """ + Khi mở Wizard, tự động load các PR đã chọn từ màn hình danh sách. + """ + res = super().default_get(fields_list) + active_ids = self.env.context.get('active_ids', []) + + if not active_ids: + return res + + # Lấy danh sách PR gốc + requests = self.env['epr.purchase.request'].browse(active_ids) + + # Prepare dữ liệu cho dòng Wizard + lines_vals = [] + for pr in requests: + # Chỉ xử lý các PR đã được duyệt (Ví dụ trạng thái 'approved') + # Bạn có thể bỏ comment dòng dưới nếu có field state + if pr.state != 'approved': + raise UserError(_("PR %s chưa được duyệt.", pr.name)) + + for line in pr.line_ids: + lines_vals.append(Command.create({ + 'pr_line_id': line.id, # Link tới PR Line + 'request_id': pr.id, + 'final_vendor_id': line.final_vendor_id.id if line.final_vendor_id else False, + 'suggested_vendor_name': line.suggested_vendor_name, + })) + + res['line_ids'] = lines_vals + return res + + def action_create_rfqs(self): + """ + Logic hardened with .sudo(): + 1. Access PR data using sudo to bypass security rules. + 2. Group by Vendor. + 3. Create RFQ Header and Lines using sudo() to ensure data persistence. + 4. Redirect to the newly created RFQ(s). + """ + self.ensure_one() + + # 1. Validation + missing_vendor_lines = self.line_ids.filtered(lambda l: not l.final_vendor_id) + if missing_vendor_lines: + raise UserError(_("Please select a Final Vendor for all lines.")) + + missing_product_lines = self.line_ids.filtered(lambda l: not l.final_product_id) + if missing_product_lines: + raise UserError(_("Please select a Final Product for all lines.")) + + # 2. Grouping + grouped_lines = {} + for wiz_line in self.line_ids: + # Sync back to PR line (Sudo to ensure write permission if needed) + if wiz_line.pr_line_id.final_vendor_id != wiz_line.final_vendor_id: + wiz_line.pr_line_id.sudo().write({'final_vendor_id': wiz_line.final_vendor_id.id}) + + vendor = wiz_line.final_vendor_id + if vendor not in grouped_lines: + grouped_lines[vendor] = self.env['epr.create.rfq.line'] + grouped_lines[vendor] |= wiz_line + + created_rfqs = self.env['epr.rfq'].sudo() + + # 3. RFQ Creation + for vendor, wiz_lines in grouped_lines.items(): + rfq_line_commands = [] + source_pr_ids = [] + + for wiz_line in wiz_lines: + # DÙNG SUDO: Để chắc chắn lấy được dữ liệu PR line + pr_line_sudo = wiz_line.pr_line_id.sudo() + request_sudo = wiz_line.request_id.sudo() + + # Collect unique source PR ids + if request_sudo and request_sudo.id not in source_pr_ids: + source_pr_ids.append(request_sudo.id) + + # Determine UoM (Directly from Product or PR Line) + uom_id = False + if wiz_line.final_product_id: + uom_id = wiz_line.final_product_id.uom_po_id.id or wiz_line.final_product_id.uom_id.id + + if not uom_id and pr_line_sudo.product_id: + uom_id = pr_line_sudo.product_id.uom_po_id.id or pr_line_sudo.product_id.uom_id.id + + # Create line command - DÙNG GIÁ TRỊ TỪ SUDO RECORD + rfq_line_commands.append(Command.create({ + 'product_id': wiz_line.final_product_id.id, + 'description': pr_line_sudo.name or '', + 'quantity': pr_line_sudo.quantity or 1.0, + 'uom_id': uom_id, + 'price_unit': 0.0, + })) + + # Create RFQ header - SỬ DỤNG SUDO ĐỂ TẠO + rfq = self.env['epr.rfq'].sudo().create({ + 'partner_id': vendor.id, + 'state': 'draft', + 'date_order': fields.Datetime.now(), + 'request_ids': [Command.set(source_pr_ids)], + 'line_ids': rfq_line_commands, + }) + created_rfqs |= rfq + + # 4. Result Action + if len(created_rfqs) == 1: + return { + 'name': _('Request for Quotation'), + 'type': 'ir.actions.act_window', + 'res_model': 'epr.rfq', + 'view_mode': 'form', + 'res_id': created_rfqs.id, + 'target': 'current', + } + else: + return { + 'name': _('Requests for Quotation'), + 'type': 'ir.actions.act_window', + 'res_model': 'epr.rfq', + 'view_mode': 'list,form', + 'domain': [('id', 'in', created_rfqs.ids)], + 'target': 'current', + } + + +class EprCreateRfqLine(models.TransientModel): + _name = 'epr.create.rfq.line' + _description = 'Wizard Line: PR Details' + + wizard_id = fields.Many2one('epr.create.rfq.wizard', string='Wizard') + + # Link tới PR gốc (Readonly) + request_id = fields.Many2one( + comodel_name='epr.purchase.request', + string='Purchase Request', + readonly=True + ) + + # Link tới PR Line gốc + pr_line_id = fields.Many2one( + comodel_name='epr.purchase.request.line', + string='PR Line', + readonly=True + ) + + # Cột hiển thị text gợi ý (Readonly) -> Giúp Officer tham chiếu + suggested_vendor_name = fields.Char( + string='Suggested Vendor (Text)', + readonly=True, + help="Tên nhà cung cấp do người yêu cầu nhập tay (tham khảo)." + ) + + # Cột Final Vendor (Editable) -> Đây là nơi Officer thao tác chính + final_vendor_id = fields.Many2one( + comodel_name='res.partner', + string='Final Vendor', + required=True, + # domain="[('supplier_rank', '>', 0)]", # Chỉ lấy nhà cung cấp + help="Chọn nhà cung cấp chính thức trong hệ thống để tạo RFQ." + ) + + # Lấy thông tin sản phẩm cần mua + product_description = fields.Char( + related='pr_line_id.name', + string='Product / Description', + readonly=True + ) + + # Purchasing Officer chọn sản phẩm tương ứng với mô tả của User + final_product_id = fields.Many2one( + comodel_name='product.product', + string='Final Product', + required=True, + # domain="[('purchase_ok', '=', True)]", + help="Chọn sản phẩm tương ứng với mô tả của người yêu cầu." + ) + + # Lấy số lượng + quantity = fields.Float( + related='pr_line_id.quantity', + string='Qty', + readonly=True + ) + + # Lấy đơn vị tính + uom_name = fields.Char( + related='pr_line_id.uom_name', + string='UoM', + readonly=True + ) diff --git a/addons/epr/wizards/epr_create_rfq_views.xml b/addons/epr/wizards/epr_create_rfq_views.xml new file mode 100644 index 0000000..32dc1aa --- /dev/null +++ b/addons/epr/wizards/epr_create_rfq_views.xml @@ -0,0 +1,91 @@ + + + + + + + epr.create.rfq.wizard.form + epr.create.rfq.wizard + +
+ +
+

Batch Create RFQs

+

+ Review lines below. Assign a Final Vendor to group them. + Lines with the same Final Vendor will be merged into one RFQ. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+
+ + + + + + Create RFQ + epr.create.rfq.wizard + form + new + + + list + + + + +
diff --git a/docker-compose.yml b/docker-compose.yml index c9f2097..0af8e3f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: '3.1' +# version: '3.1' services: db: image: postgres:17 @@ -32,4 +32,4 @@ services: - ./addons:/mnt/extra-addons - ./etc:/etc/odoo restart: always # run as a service - \ No newline at end of file +