From f3766c2898f5de181a881a891f91f728b7620825 Mon Sep 17 00:00:00 2001 From: Nexa Admin Date: Wed, 11 Mar 2026 17:22:02 +0000 Subject: [PATCH] feat: add x_fc_authorizer_number, x_fc_account_number, x_marked_for fields; auto-link authorizer from XML - fusion_claims: added x_fc_authorizer_number to res.partner for ADP authorizer registration numbers - fusion_claims: XML parser auto-links authorizer contact to sale order by ADP number - fusion_claims: removed size=9 constraint from x_fc_odsp_member_id - fusion_claims: authorizer number shown on OT/PT contact form - fusion_so_to_po: added x_marked_for (Many2one) field definition on purchase.order - fusion_so_to_po: added x_fc_account_number on res.partner for vendor account numbers --- .gitignore | 2 + fusion_claims/models/res_partner.py | 14 +- fusion_claims/models/xml_parser.py | 37 +++ fusion_claims/views/res_partner_views.xml | 7 + fusion_so_to_po/.DS_Store | Bin 0 -> 8196 bytes fusion_so_to_po/__init__.py | 4 + fusion_so_to_po/__manifest__.py | 26 ++ fusion_so_to_po/models/__init__.py | 3 + fusion_so_to_po/models/fusion_so_to_po.py | 234 ++++++++++++++++++ fusion_so_to_po/security/ir.model.access.csv | 4 + fusion_so_to_po/static/.DS_Store | Bin 0 -> 6148 bytes fusion_so_to_po/static/description/icon.png | Bin 0 -> 39783 bytes .../views/fusion_so_to_po_views.xml | 55 ++++ fusion_so_to_po/wizard/__init__.py | 4 + .../wizard/fusion_match_sale_order_wiz.py | 54 ++++ .../wizard/fusion_match_sale_order_wiz.xml | 43 ++++ .../wizard/fusion_purchase_order_wiz.py | 191 ++++++++++++++ .../wizard/fusion_purchase_order_wiz.xml | 57 +++++ 18 files changed, 733 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 fusion_so_to_po/.DS_Store create mode 100644 fusion_so_to_po/__init__.py create mode 100644 fusion_so_to_po/__manifest__.py create mode 100644 fusion_so_to_po/models/__init__.py create mode 100644 fusion_so_to_po/models/fusion_so_to_po.py create mode 100644 fusion_so_to_po/security/ir.model.access.csv create mode 100644 fusion_so_to_po/static/.DS_Store create mode 100644 fusion_so_to_po/static/description/icon.png create mode 100644 fusion_so_to_po/views/fusion_so_to_po_views.xml create mode 100644 fusion_so_to_po/wizard/__init__.py create mode 100644 fusion_so_to_po/wizard/fusion_match_sale_order_wiz.py create mode 100644 fusion_so_to_po/wizard/fusion_match_sale_order_wiz.xml create mode 100644 fusion_so_to_po/wizard/fusion_purchase_order_wiz.py create mode 100644 fusion_so_to_po/wizard/fusion_purchase_order_wiz.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93fcd87 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +**/__pycache__/ +*.pyc diff --git a/fusion_claims/models/res_partner.py b/fusion_claims/models/res_partner.py index 9c60e4a..5803494 100644 --- a/fusion_claims/models/res_partner.py +++ b/fusion_claims/models/res_partner.py @@ -45,9 +45,8 @@ class ResPartner(models.Model): # ========================================================================== x_fc_odsp_member_id = fields.Char( string='ODSP Member ID', - size=9, tracking=True, - help='9-digit Ontario Disability Support Program Member ID', + help='Ontario Disability Support Program Member ID', ) x_fc_case_worker_id = fields.Many2one( 'res.partner', @@ -69,6 +68,17 @@ class ResPartner(models.Model): store=True, ) + # ========================================================================== + # AUTHORIZER FIELDS + # ========================================================================== + x_fc_authorizer_number = fields.Char( + string='ADP Authorizer Number', + tracking=True, + index=True, + help='ADP Registration Number for this authorizer (e.g. OT). ' + 'Used to auto-link authorizers when processing ADP XML files.', + ) + @api.depends('x_fc_contact_type') def _compute_is_odsp_office(self): for partner in self: diff --git a/fusion_claims/models/xml_parser.py b/fusion_claims/models/xml_parser.py index 9c3fc3c..1a879de 100644 --- a/fusion_claims/models/xml_parser.py +++ b/fusion_claims/models/xml_parser.py @@ -66,6 +66,10 @@ class FusionXmlParser(models.AbstractModel): # Step 3: Create/update profile profile = self._find_or_create_profile(model_vals, sale_order) + # Step 3b: Auto-link authorizer on sale order by ADP number + if sale_order: + self._link_authorizer_by_adp_number(model_vals, sale_order) + # Step 4: Create application data record model_vals['profile_id'] = profile.id model_vals['sale_order_id'] = sale_order.id if sale_order else False @@ -637,6 +641,39 @@ class FusionXmlParser(models.AbstractModel): # ------------------------------------------------------------------ # PROFILE MANAGEMENT # ------------------------------------------------------------------ + def _link_authorizer_by_adp_number(self, vals, sale_order): + """Auto-link the authorizer contact on the sale order using the ADP number from XML.""" + adp_number = (vals.get('authorizer_adp_number') or '').strip() + if not adp_number or adp_number.upper() in ('NA', 'N/A', ''): + return + + if sale_order.x_fc_authorizer_id: + return + + Partner = self.env['res.partner'] + authorizer = Partner.search([ + ('x_fc_authorizer_number', '=', adp_number), + ], limit=1) + + if not authorizer: + first = (vals.get('authorizer_first_name') or '').strip() + last = (vals.get('authorizer_last_name') or '').strip() + if first and last: + authorizer = Partner.search([ + '|', + ('name', 'ilike', f'{first} {last}'), + ('name', 'ilike', f'{last}, {first}'), + ], limit=1) + if authorizer and not authorizer.x_fc_authorizer_number: + authorizer.write({'x_fc_authorizer_number': adp_number}) + + if authorizer: + sale_order.write({'x_fc_authorizer_id': authorizer.id}) + _logger.info( + 'Auto-linked authorizer %s (ADP# %s) to SO %s', + authorizer.name, adp_number, sale_order.name, + ) + def _find_or_create_profile(self, vals, sale_order=None): """Find or create a client profile from parsed application data.""" Profile = self.env['fusion.client.profile'] diff --git a/fusion_claims/views/res_partner_views.xml b/fusion_claims/views/res_partner_views.xml index 824238d..1a0dfc4 100644 --- a/fusion_claims/views/res_partner_views.xml +++ b/fusion_claims/views/res_partner_views.xml @@ -16,6 +16,13 @@ + + + + + Y*&wx)2FjyiK^j1)V$rIpN~^Y{PBIOq&P=eKhPJAd z4}c9Dc5L_nB$lk;17OJ(Bz}MmYq;|W$}+nk5)4Fz)j8~ z*JCu01TRm~W#yj7^1PDbWtI;mH?0g-%!*~~lRG=-6BE}Gqt0X^u{-MQOkNxxbtW!d z+1-s%GZ+9;Ey-&DcF5u6jDh*=#^JakYyPeH^8| zZ8Y^Y5$0(A{;#3=trGeS{2Guay%SK%mJ5NGpIKiIX|!0^UlOzB0`B{>JoF-uv!xX| zdqj)VY@Oqb;tFO;!W1shAVh58EH1f0;IaZuZLRulmijX+#iNK97d>2sxNJ%_x^FN18-HmPvn@-ro z0H9E5es_>(EkRI0&ut8u0P>9i(A240cP6_ P|H!u4y#Hl{{GR3qrY3g* literal 0 HcmV?d00001 diff --git a/fusion_so_to_po/__init__.py b/fusion_so_to_po/__init__.py new file mode 100644 index 0000000..35e7c96 --- /dev/null +++ b/fusion_so_to_po/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import models +from . import wizard diff --git a/fusion_so_to_po/__manifest__.py b/fusion_so_to_po/__manifest__.py new file mode 100644 index 0000000..479dc3f --- /dev/null +++ b/fusion_so_to_po/__manifest__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +{ + 'name': 'Fusion - Sale Order to Purchase Order', + 'author': 'Fusion Products', + 'version': '19.0.1.0.0', + 'images': ['static/description/icon.png'], + 'summary': 'Create Purchase Orders directly from Sale Orders with vendor assignment and cost price mapping.', + 'description': """ +Fusion - Sale Order to Purchase Order +====================================== +Quickly create and manage Purchase Orders from Sale Orders. +Assign vendors per product line, auto-map cost prices, and track +the link between SO and PO in both directions. + """, + 'depends': ['base', 'sale_management', 'purchase'], + 'license': 'OPL-1', + 'data': [ + 'security/ir.model.access.csv', + 'wizard/fusion_purchase_order_wiz.xml', + 'wizard/fusion_match_sale_order_wiz.xml', + 'views/fusion_so_to_po_views.xml', + ], + 'installable': True, + 'auto_install': False, + 'category': 'Sales', +} diff --git a/fusion_so_to_po/models/__init__.py b/fusion_so_to_po/models/__init__.py new file mode 100644 index 0000000..d3e8f7b --- /dev/null +++ b/fusion_so_to_po/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import fusion_so_to_po diff --git a/fusion_so_to_po/models/fusion_so_to_po.py b/fusion_so_to_po/models/fusion_so_to_po.py new file mode 100644 index 0000000..8a65cc6 --- /dev/null +++ b/fusion_so_to_po/models/fusion_so_to_po.py @@ -0,0 +1,234 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api, _ +from odoo.exceptions import UserError + +import re + + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + fusion_purchase_count = fields.Integer( + string='Purchase', + compute='_compute_fusion_purchase_count', + ) + + def _compute_fusion_purchase_count(self): + for so in self: + so.fusion_purchase_count = self.env['purchase.order'].search_count([ + ('fusion_sale_ids', 'in', so.id), + ]) + + def action_view_fusion_purchases(self): + purchases = self.env['purchase.order'].search([ + ('fusion_sale_ids', 'in', self.id), + ]) + return { + 'name': _('Purchase Orders'), + 'view_mode': 'list,form', + 'res_model': 'purchase.order', + 'domain': [('id', 'in', purchases.ids)], + 'type': 'ir.actions.act_window', + } + + +class PurchaseOrder(models.Model): + _inherit = 'purchase.order' + + fusion_sale_ids = fields.Many2many( + 'sale.order', + 'fusion_po_so_rel', + 'purchase_order_id', + 'sale_order_id', + string='Sale Orders', + help='Sale orders linked to this purchase order', + ) + fusion_marked_for_ids = fields.Many2many( + 'res.partner', + 'fusion_po_marked_for_rel', + 'purchase_order_id', + 'partner_id', + string='Marked For', + help='Customers this purchase order is marked for', + ) + x_marked_for = fields.Many2one( + 'res.partner', + string='Marked For (Legacy)', + help='Legacy single-customer marked-for field (migrated from Studio)', + index=True, + ) + fusion_sale_count = fields.Integer( + string='Sales', + compute='_compute_fusion_sale_count', + ) + + def _compute_fusion_sale_count(self): + for po in self: + po.fusion_sale_count = len(po.fusion_sale_ids) + + def action_view_fusion_sale_order(self): + self.ensure_one() + if not self.fusion_sale_ids: + raise UserError(_("No Sale Orders are linked to this Purchase Order.")) + if len(self.fusion_sale_ids) == 1: + return { + 'name': _('Sale Order'), + 'view_mode': 'form', + 'res_model': 'sale.order', + 'res_id': self.fusion_sale_ids.id, + 'type': 'ir.actions.act_window', + } + return { + 'name': _('Sale Orders'), + 'view_mode': 'list,form', + 'res_model': 'sale.order', + 'domain': [('id', 'in', self.fusion_sale_ids.ids)], + 'type': 'ir.actions.act_window', + } + + def _open_fusion_match_wizard(self, search_hint=''): + wizard = self.env['fusion.match.so.wiz'].create({ + 'fusion_po_id': self.id, + 'fusion_search_hint': search_hint, + }) + return { + 'name': _('Match Sale Order'), + 'type': 'ir.actions.act_window', + 'res_model': 'fusion.match.so.wiz', + 'res_id': wizard.id, + 'view_mode': 'form', + 'target': 'new', + } + + def action_fusion_match_sale_order(self): + """Match this PO to a Sale Order based on x_marked_for field.""" + self.ensure_one() + + marked_for_value = getattr(self, 'x_marked_for', None) + if not marked_for_value: + return self._open_fusion_match_wizard('') + + marked_for_str = str(marked_for_value) + search_hint = marked_for_str + matching_partners = None + + partner_id_match = re.search(r'res\.partner\((\d+)', marked_for_str) + if partner_id_match: + partner_id = int(partner_id_match.group(1)) + partner = self.env['res.partner'].browse(partner_id).exists() + if partner: + matching_partners = partner + search_hint = partner.name + + if not matching_partners: + matching_partners = self.env['res.partner'].search([ + '|', + ('name', 'ilike', marked_for_str), + ('display_name', 'ilike', marked_for_str), + ]) + + if not matching_partners: + return self._open_fusion_match_wizard(search_hint) + + matching_sales = self.env['sale.order'].search([ + ('partner_id', 'in', matching_partners.ids), + ]) + + if not matching_sales or len(matching_sales) > 1: + hint = matching_partners[0].name if matching_partners else search_hint + return self._open_fusion_match_wizard(hint) + + self.write({ + 'fusion_sale_ids': [(4, matching_sales.id)], + 'fusion_marked_for_ids': [(4, matching_partners[0].id)], + }) + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Success'), + 'message': _('Linked to Sale Order: %s') % matching_sales.name, + 'type': 'success', + 'sticky': False, + }, + } + + def action_fusion_batch_match(self): + """Batch match multiple POs to Sale Orders based on x_marked_for field.""" + matched = 0 + skipped = 0 + errors = [] + + for po in self: + marked_for_value = getattr(po, 'x_marked_for', None) + if not marked_for_value: + skipped += 1 + continue + + marked_for_str = str(marked_for_value) + matching_partners = None + + partner_id_match = re.search(r'res\.partner\((\d+)', marked_for_str) + if partner_id_match: + partner_id = int(partner_id_match.group(1)) + matching_partners = self.env['res.partner'].browse(partner_id).exists() + + if not matching_partners: + matching_partners = self.env['res.partner'].search([ + '|', + ('name', 'ilike', marked_for_str), + ('display_name', 'ilike', marked_for_str), + ]) + + if not matching_partners: + errors.append(_("PO %s: No customer found for '%s'") % (po.name, marked_for_str)) + continue + + matching_sales = self.env['sale.order'].search([ + ('partner_id', 'in', matching_partners.ids), + ]) + + if not matching_sales: + errors.append(_("PO %s: No SO found for '%s'") % (po.name, matching_partners[0].name)) + continue + + if len(matching_sales) > 1: + errors.append( + _("PO %s: Multiple SOs (%d) for '%s'") % (po.name, len(matching_sales), matching_partners[0].name) + ) + continue + + po.write({ + 'fusion_sale_ids': [(4, matching_sales.id)], + 'fusion_marked_for_ids': [(4, matching_partners[0].id)], + }) + matched += 1 + + message = _("Matched: %d, Skipped: %d") % (matched, skipped) + if errors: + message += "\n" + "\n".join(errors[:5]) + if len(errors) > 5: + message += _("\n... and %d more errors") % (len(errors) - 5) + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Batch Match Complete'), + 'message': message, + 'type': 'info' if matched > 0 else 'warning', + 'sticky': True, + }, + } + + +class ResPartner(models.Model): + _inherit = 'res.partner' + + x_fc_account_number = fields.Char( + string='Account Number', + tracking=True, + help='Vendor/supplier account number', + ) diff --git a/fusion_so_to_po/security/ir.model.access.csv b/fusion_so_to_po/security/ir.model.access.csv new file mode 100644 index 0000000..405c3c7 --- /dev/null +++ b/fusion_so_to_po/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_fusion_so_po_wiz,access_fusion_so_po_wiz,model_fusion_so_po_wiz,base.group_user,1,1,1,1 +access_fusion_so_po_line_wiz,access_fusion_so_po_line_wiz,model_fusion_so_po_line_wiz,base.group_user,1,1,1,1 +access_fusion_match_so_wiz,access_fusion_match_so_wiz,model_fusion_match_so_wiz,base.group_user,1,1,1,1 diff --git a/fusion_so_to_po/static/.DS_Store b/fusion_so_to_po/static/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..7f42bcfffd8e63b46f50908042f2a2fd976cd98e GIT binary patch literal 6148 zcmeHK-A)rh6g~r0wp3(+7V$6H*c%h7QpF$Qg-gL0;st~ySOQpf+oi57Gj(@MMMBaC z&}$#S2k_o2eE_}mRuf;q8?W)q&jzq^sS&d$nfcDnoNs2%x7nE;03fAitq7n4fQE%* zsu!!@81Lt$!@5*U3XzC00`Q;#5!S3DI%!B0NEG z7{RxP5|*ob!q7XC<+Q?vV!d-3z86MgW1mS^ck0B+9<4{qXsgx}*0LfyYKA2{SmsX) zjQLLN+s>lrwkmpdiqXjSs2d1^S939B<&j5gtW{!7S_=i&k{w!FOIP%BJ3Cj3<9CV$ zW1?8xEf_l!*T)LR_>Eh;yJ;<#zjk-Nvf*!1_EKa31a~cAeF|fEh0YPGc-1YHANd&P z_eIy^4AZ@tQ+=oV^?|{mf$Tsomm5Aaa(3|ig^E7p*wu}Cz*j!Sd=`}ard!ebL(G4s z%II9+ZeiZ#$bBAJ-V5v+{Dm;@)*kOSEo?V1CcEDt27M5?E?uq%UTt=LJ#?e;0e^|l zRt=c%&#};pJVuvR#O#r~IYZYO&d66V+u&W{q8o&W9h~x#9RxP5yQS?_-*#MomX=r) zvCXoFS@4yMDXoTG^=LhR@4M7Aoi{H_PltRgvzCoLg(Uwz{3`qf%BYK^IoXppAF2=y zMqv^xSb=BIhS%^0-ogj?1mEBX?2$e)OfHd8QXn_UZDNv1GEbJt8nMY!$?u9^`9(FJ z0urKg#4F#j0_rnb$B3AHn0!p1R5DuTn65urcA`L{z@Jlq?+*qRj;6x4M6q>XC9VL7 z{5L`v`ut~sa}0&1!nQ>0K_iliD5*qUF^HsNyP@Jtg>8wF4n)k1-H7wkC pTw9``V5N>@;qaq)1dA~2Gq{6jDr`%{7PRC?K**3(i2{FBfuBln--iGI literal 0 HcmV?d00001 diff --git a/fusion_so_to_po/static/description/icon.png b/fusion_so_to_po/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a38e2dab26c5a0b20d86eaa4c886530eac23a1a GIT binary patch literal 39783 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 br1k#+Ih(hE$ROyI00000NkvXXu0mjfo-y$} literal 0 HcmV?d00001 diff --git a/fusion_so_to_po/views/fusion_so_to_po_views.xml b/fusion_so_to_po/views/fusion_so_to_po_views.xml new file mode 100644 index 0000000..71f689c --- /dev/null +++ b/fusion_so_to_po/views/fusion_so_to_po_views.xml @@ -0,0 +1,55 @@ + + + + + + + fusion.sale.order.form.inherit + sale.order + + + + + + + + + + + fusion.purchase.order.form.inherit + purchase.order + + + + + + + + + + + + + + + + + + + diff --git a/fusion_so_to_po/wizard/__init__.py b/fusion_so_to_po/wizard/__init__.py new file mode 100644 index 0000000..27b09ad --- /dev/null +++ b/fusion_so_to_po/wizard/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import fusion_purchase_order_wiz +from . import fusion_match_sale_order_wiz diff --git a/fusion_so_to_po/wizard/fusion_match_sale_order_wiz.py b/fusion_so_to_po/wizard/fusion_match_sale_order_wiz.py new file mode 100644 index 0000000..3cbe0f4 --- /dev/null +++ b/fusion_so_to_po/wizard/fusion_match_sale_order_wiz.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api, _ +from odoo.exceptions import UserError + + +class FusionMatchSaleOrderWiz(models.TransientModel): + _name = 'fusion.match.so.wiz' + _description = 'Fusion - Match Sale Order Wizard' + + fusion_po_id = fields.Many2one( + 'purchase.order', + string='Purchase Order', + required=True, + readonly=True, + ) + fusion_sale_order_id = fields.Many2one( + 'sale.order', + string='Sale Order', + help='Select the Sale Order to link to this Purchase Order', + ) + fusion_search_hint = fields.Char( + string='Search Hint', + readonly=True, + help='Suggested search term based on Marked For field', + ) + + def action_fusion_confirm(self): + self.ensure_one() + + if not self.fusion_sale_order_id: + raise UserError(_("Please select a Sale Order.")) + + customer = self.fusion_sale_order_id.partner_id + + self.fusion_po_id.write({ + 'fusion_sale_ids': [(4, self.fusion_sale_order_id.id)], + 'fusion_marked_for_ids': [(4, customer.id)], + }) + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Success'), + 'message': _('Linked Purchase Order %s to Sale Order %s') % ( + self.fusion_po_id.name, + self.fusion_sale_order_id.name, + ), + 'type': 'success', + 'sticky': False, + 'next': {'type': 'ir.actions.act_window_close'}, + }, + } diff --git a/fusion_so_to_po/wizard/fusion_match_sale_order_wiz.xml b/fusion_so_to_po/wizard/fusion_match_sale_order_wiz.xml new file mode 100644 index 0000000..ed5bb28 --- /dev/null +++ b/fusion_so_to_po/wizard/fusion_match_sale_order_wiz.xml @@ -0,0 +1,43 @@ + + + + + + fusion.match.so.wiz.form + fusion.match.so.wiz + +
+ + + +

+ Search and select the Sale Order to link to this Purchase Order. +

+
+ + + + + + +
+
+ +
+
+ + + Match Sale Order + fusion.match.so.wiz + ir.actions.act_window + form + new + + +
+
diff --git a/fusion_so_to_po/wizard/fusion_purchase_order_wiz.py b/fusion_so_to_po/wizard/fusion_purchase_order_wiz.py new file mode 100644 index 0000000..5c6450e --- /dev/null +++ b/fusion_so_to_po/wizard/fusion_purchase_order_wiz.py @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api, _ +from odoo.exceptions import UserError +from collections import defaultdict + + +class FusionPurchaseOrderWiz(models.TransientModel): + _name = 'fusion.so.po.wiz' + _description = 'Fusion - Create Purchase Order Wizard' + + fusion_so_id = fields.Many2one('sale.order', string='Sale Order') + fusion_line_ids = fields.One2many( + 'fusion.so.po.line.wiz', + 'fusion_wizard_id', + string='Products', + ) + fusion_batch_vendor_id = fields.Many2one( + 'res.partner', + string='Assign Vendor to All', + domain="[('is_company', '=', True)]", + help='Select a vendor here to assign it to ALL products at once', + ) + + @api.onchange('fusion_batch_vendor_id') + def _onchange_fusion_batch_vendor_id(self): + if self.fusion_batch_vendor_id: + selected_lines = self.fusion_line_ids.filtered(lambda l: l.selected) + lines_to_update = selected_lines if selected_lines else self.fusion_line_ids + + for line in lines_to_update: + line.vendor_id = self.fusion_batch_vendor_id + if line.product_id: + seller = line.product_id.seller_ids.filtered( + lambda s: s.partner_id.id == self.fusion_batch_vendor_id.id + )[:1] + if seller and seller.price: + line.price_unit = seller.price + else: + line.price_unit = line.product_id.standard_price + line.price_subtotal = line.price_unit * line.product_uom_qty + + for line in selected_lines: + line.selected = False + + @api.model + def default_get(self, fields_list): + res = super().default_get(fields_list) + active_ids = self._context.get('active_ids') + if not active_ids: + return res + + sale_order = self.env['sale.order'].browse(active_ids[0]) + if not sale_order.order_line: + raise UserError(_("Please add some valid sale order lines...!")) + + res['fusion_so_id'] = sale_order.id + + product_lines = [] + for line in sale_order.order_line: + if line.display_type in ('line_section', 'line_note'): + continue + + description = line.name or line.product_id.display_name or line.product_id.name or 'Product' + + cost_price = line.product_id.standard_price + product_lines.append((0, 0, { + 'product_id': line.product_id.id, + 'description': description, + 'product_uom_qty': line.product_uom_qty, + 'price_unit': cost_price, + 'product_uom': line.product_uom_id.id, + 'price_subtotal': cost_price * line.product_uom_qty, + 'fusion_so_line_id': line.id, + 'vendor_id': False, + })) + + res['fusion_line_ids'] = product_lines + return res + + def fusion_create_po(self): + self.ensure_one() + + vendor_products = defaultdict(list) + for product_line in self.fusion_line_ids: + if product_line.vendor_id: + vendor_products[product_line.vendor_id.id].append(product_line) + + if not vendor_products: + raise UserError(_("Please assign at least one product to a vendor before creating Purchase Orders.")) + + now = fields.Datetime.now() + created_pos = self.env['purchase.order'] + customer = self.fusion_so_id.partner_id + + for vendor_id, product_lines in vendor_products.items(): + partner = self.env['res.partner'].browse(vendor_id) + fpos = self.env['account.fiscal.position'].sudo()._get_fiscal_position(partner) + + date_planned = self.fusion_so_id.commitment_date or now + + purchase_order = self.env['purchase.order'].create({ + 'partner_id': partner.id, + 'partner_ref': partner.ref, + 'company_id': self.fusion_so_id.company_id.id, + 'currency_id': self.env.company.currency_id.id, + 'dest_address_id': False, + 'origin': self.fusion_so_id.name, + 'payment_term_id': partner.property_supplier_payment_term_id.id, + 'date_order': now, + 'fiscal_position_id': fpos.id, + 'fusion_sale_ids': [(4, self.fusion_so_id.id)], + 'fusion_marked_for_ids': [(4, customer.id)], + }) + + for product_line in product_lines: + values = product_line._prepare_fusion_po_line(purchase_order, date_planned) + self.env['purchase.order.line'].create(values) + + created_pos |= purchase_order + + if len(created_pos) == 1: + return { + 'name': _('Purchase Order'), + 'view_mode': 'form', + 'res_model': 'purchase.order', + 'view_id': self.env.ref('purchase.purchase_order_form').id, + 'res_id': created_pos.id, + 'type': 'ir.actions.act_window', + 'target': 'current', + } + return { + 'name': _('Purchase Orders'), + 'view_mode': 'list,form', + 'res_model': 'purchase.order', + 'domain': [('id', 'in', created_pos.ids)], + 'type': 'ir.actions.act_window', + 'target': 'current', + } + + +class FusionPurchaseProductWiz(models.TransientModel): + _name = 'fusion.so.po.line.wiz' + _description = 'Fusion - Purchase Product Wizard Line' + + fusion_wizard_id = fields.Many2one('fusion.so.po.wiz', string='Wizard', ondelete='cascade') + selected = fields.Boolean(string='Select', default=False) + fusion_so_line_id = fields.Many2one('sale.order.line', string='SO Line') + product_id = fields.Many2one('product.product', string='Product') + product_uom = fields.Many2one('uom.uom', string='Unit of Measure') + description = fields.Char(string='Description') + product_uom_qty = fields.Float(string='Quantity') + price_unit = fields.Float(string='Unit Price') + price_subtotal = fields.Float(string='Subtotal') + vendor_id = fields.Many2one( + 'res.partner', + string='Vendor', + domain="[('is_company', '=', True)]", + ) + + @api.onchange('vendor_id') + def _onchange_vendor_id(self): + if self.vendor_id and self.product_id: + seller = self.product_id.seller_ids.filtered( + lambda s: s.partner_id.id == self.vendor_id.id + )[:1] + if seller and seller.price: + self.price_unit = seller.price + else: + self.price_unit = self.product_id.standard_price + self.price_subtotal = self.price_unit * self.product_uom_qty + + def _prepare_fusion_po_line(self, purchase_order, date_planned): + self.ensure_one() + product_uom = self.product_uom or self.product_id.uom_id + + name = self.description + if not name: + name = self.product_id.display_name or self.product_id.name or 'Product' + if self.product_id.default_code and self.product_id.default_code not in name: + name = '[%s] %s' % (self.product_id.default_code, name) + + return { + 'name': name, + 'product_qty': self.product_uom_qty, + 'product_id': self.product_id.id, + 'product_uom_id': product_uom.id, + 'price_unit': self.price_unit, + 'date_planned': date_planned, + 'order_id': purchase_order.id, + } diff --git a/fusion_so_to_po/wizard/fusion_purchase_order_wiz.xml b/fusion_so_to_po/wizard/fusion_purchase_order_wiz.xml new file mode 100644 index 0000000..79d8eec --- /dev/null +++ b/fusion_so_to_po/wizard/fusion_purchase_order_wiz.xml @@ -0,0 +1,57 @@ + + + + + + fusion.so.po.wiz.form + fusion.so.po.wiz + +
+ + + + + + + +

+ Tip: Check the boxes below to select products, then pick a vendor above to assign to selected only. + If nothing is selected, vendor applies to all. +

+
+
+ + + + + + + + + + + + + + + + + +
+
+ +
+
+ + + Create Purchase Order + fusion.so.po.wiz + ir.actions.act_window + form + new + + +
+