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
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
**/__pycache__/
|
||||||
|
*.pyc
|
||||||
@@ -45,9 +45,8 @@ class ResPartner(models.Model):
|
|||||||
# ==========================================================================
|
# ==========================================================================
|
||||||
x_fc_odsp_member_id = fields.Char(
|
x_fc_odsp_member_id = fields.Char(
|
||||||
string='ODSP Member ID',
|
string='ODSP Member ID',
|
||||||
size=9,
|
|
||||||
tracking=True,
|
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(
|
x_fc_case_worker_id = fields.Many2one(
|
||||||
'res.partner',
|
'res.partner',
|
||||||
@@ -69,6 +68,17 @@ class ResPartner(models.Model):
|
|||||||
store=True,
|
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')
|
@api.depends('x_fc_contact_type')
|
||||||
def _compute_is_odsp_office(self):
|
def _compute_is_odsp_office(self):
|
||||||
for partner in self:
|
for partner in self:
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ class FusionXmlParser(models.AbstractModel):
|
|||||||
# Step 3: Create/update profile
|
# Step 3: Create/update profile
|
||||||
profile = self._find_or_create_profile(model_vals, sale_order)
|
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
|
# Step 4: Create application data record
|
||||||
model_vals['profile_id'] = profile.id
|
model_vals['profile_id'] = profile.id
|
||||||
model_vals['sale_order_id'] = sale_order.id if sale_order else False
|
model_vals['sale_order_id'] = sale_order.id if sale_order else False
|
||||||
@@ -637,6 +641,39 @@ class FusionXmlParser(models.AbstractModel):
|
|||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# PROFILE MANAGEMENT
|
# 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):
|
def _find_or_create_profile(self, vals, sale_order=None):
|
||||||
"""Find or create a client profile from parsed application data."""
|
"""Find or create a client profile from parsed application data."""
|
||||||
Profile = self.env['fusion.client.profile']
|
Profile = self.env['fusion.client.profile']
|
||||||
|
|||||||
@@ -16,6 +16,13 @@
|
|||||||
<field name="x_fc_contact_type" placeholder="Select contact type..."/>
|
<field name="x_fc_contact_type" placeholder="Select contact type..."/>
|
||||||
</xpath>
|
</xpath>
|
||||||
|
|
||||||
|
<!-- Authorizer number for OTs/PTs/authorizer contacts -->
|
||||||
|
<xpath expr="//field[@name='x_fc_contact_type']" position="after">
|
||||||
|
<field name="x_fc_authorizer_number" string="ADP Reg. Number"
|
||||||
|
invisible="x_fc_contact_type not in ('occupational_therapist', 'physiotherapist', 'adp_agent')"
|
||||||
|
placeholder="e.g. 3000001234"/>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
<!-- ODSP section in notebook -->
|
<!-- ODSP section in notebook -->
|
||||||
<xpath expr="//notebook" position="inside">
|
<xpath expr="//notebook" position="inside">
|
||||||
<page string="ODSP" name="odsp_info"
|
<page string="ODSP" name="odsp_info"
|
||||||
|
|||||||
BIN
fusion_so_to_po/.DS_Store
vendored
Normal file
BIN
fusion_so_to_po/.DS_Store
vendored
Normal file
Binary file not shown.
4
fusion_so_to_po/__init__.py
Normal file
4
fusion_so_to_po/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
from . import wizard
|
||||||
26
fusion_so_to_po/__manifest__.py
Normal file
26
fusion_so_to_po/__manifest__.py
Normal file
@@ -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',
|
||||||
|
}
|
||||||
3
fusion_so_to_po/models/__init__.py
Normal file
3
fusion_so_to_po/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import fusion_so_to_po
|
||||||
234
fusion_so_to_po/models/fusion_so_to_po.py
Normal file
234
fusion_so_to_po/models/fusion_so_to_po.py
Normal file
@@ -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',
|
||||||
|
)
|
||||||
4
fusion_so_to_po/security/ir.model.access.csv
Normal file
4
fusion_so_to_po/security/ir.model.access.csv
Normal file
@@ -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
|
||||||
|
BIN
fusion_so_to_po/static/.DS_Store
vendored
Normal file
BIN
fusion_so_to_po/static/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
fusion_so_to_po/static/description/icon.png
Normal file
BIN
fusion_so_to_po/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
55
fusion_so_to_po/views/fusion_so_to_po_views.xml
Normal file
55
fusion_so_to_po/views/fusion_so_to_po_views.xml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<!-- Sale Order form: add "Create PO" button and purchase count stat -->
|
||||||
|
<record id="fusion_sale_order_form_inherit" model="ir.ui.view">
|
||||||
|
<field name="name">fusion.sale.order.form.inherit</field>
|
||||||
|
<field name="model">sale.order</field>
|
||||||
|
<field name="inherit_id" ref="sale.view_order_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//form//header//button[@name='action_quotation_send']" position="after">
|
||||||
|
<button name="%(fusion_purchase_order_wizard_action)d" class="btn-primary" type="action" string="Create PO"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//button[@name='action_view_invoice']" position="before">
|
||||||
|
<button type="object" name="action_view_fusion_purchases" class="oe_stat_button" invisible="fusion_purchase_count == 0" icon="fa-file">
|
||||||
|
<field name="fusion_purchase_count" widget="statinfo" string="Purchase"/>
|
||||||
|
</button>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Purchase Order form: add linked SOs, marked for, match button, stat -->
|
||||||
|
<record id="fusion_purchase_order_form_inherit" model="ir.ui.view">
|
||||||
|
<field name="name">fusion.purchase.order.form.inherit</field>
|
||||||
|
<field name="model">purchase.order</field>
|
||||||
|
<field name="inherit_id" ref="purchase.purchase_order_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<!-- Match SO button in header -->
|
||||||
|
<xpath expr="//header" position="inside">
|
||||||
|
<button name="action_fusion_match_sale_order" type="object" string="Match Sale Order"
|
||||||
|
class="btn-secondary"
|
||||||
|
confirm="This will try to match this PO to a Sale Order based on the 'Marked For' field. Continue?"/>
|
||||||
|
</xpath>
|
||||||
|
<!-- Stat button for linked Sale Orders -->
|
||||||
|
<xpath expr="//div[@name='button_box']" position="inside">
|
||||||
|
<button type="object" name="action_view_fusion_sale_order" class="oe_stat_button"
|
||||||
|
invisible="fusion_sale_count == 0" icon="fa-shopping-cart">
|
||||||
|
<field name="fusion_sale_count" widget="statinfo" string="Sale Orders"/>
|
||||||
|
</button>
|
||||||
|
</xpath>
|
||||||
|
<!-- Marked For (contacts) below Vendor Reference -->
|
||||||
|
<xpath expr="//field[@name='partner_ref']" position="after">
|
||||||
|
<field name="fusion_marked_for_ids" widget="many2many_tags" string="Marked For"
|
||||||
|
placeholder="Select customers..." options="{'no_create': False, 'no_quick_create': False}"/>
|
||||||
|
</xpath>
|
||||||
|
<!-- Sale Orders (many2many) before order date -->
|
||||||
|
<xpath expr="//field[@name='date_order']" position="before">
|
||||||
|
<field name="fusion_sale_ids" widget="many2many_tags" string="Sale Orders"
|
||||||
|
placeholder="Link sale orders..." options="{'no_create': True}"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
4
fusion_so_to_po/wizard/__init__.py
Normal file
4
fusion_so_to_po/wizard/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import fusion_purchase_order_wiz
|
||||||
|
from . import fusion_match_sale_order_wiz
|
||||||
54
fusion_so_to_po/wizard/fusion_match_sale_order_wiz.py
Normal file
54
fusion_so_to_po/wizard/fusion_match_sale_order_wiz.py
Normal file
@@ -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'},
|
||||||
|
},
|
||||||
|
}
|
||||||
43
fusion_so_to_po/wizard/fusion_match_sale_order_wiz.xml
Normal file
43
fusion_so_to_po/wizard/fusion_match_sale_order_wiz.xml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<record id="fusion_view_match_sale_order_wizard" model="ir.ui.view">
|
||||||
|
<field name="name">fusion.match.so.wiz.form</field>
|
||||||
|
<field name="model">fusion.match.so.wiz</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Match Sale Order">
|
||||||
|
<field name="fusion_po_id" invisible="1"/>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<p class="text-muted" colspan="2">
|
||||||
|
Search and select the Sale Order to link to this Purchase Order.
|
||||||
|
</p>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<field name="fusion_search_hint" string="Suggested Search" invisible="not fusion_search_hint"/>
|
||||||
|
<field name="fusion_sale_order_id"
|
||||||
|
string="Sale Order"
|
||||||
|
options="{'no_create': True, 'no_quick_create': True}"
|
||||||
|
context="{'search_default_name': fusion_search_hint}"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<button name="action_fusion_confirm" string="Link Sale Order" type="object" class="btn-primary"/>
|
||||||
|
<button special="cancel" string="Cancel" class="btn-secondary"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="fusion_action_match_sale_order_wizard" model="ir.actions.act_window">
|
||||||
|
<field name="name">Match Sale Order</field>
|
||||||
|
<field name="res_model">fusion.match.so.wiz</field>
|
||||||
|
<field name="type">ir.actions.act_window</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="target">new</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
191
fusion_so_to_po/wizard/fusion_purchase_order_wiz.py
Normal file
191
fusion_so_to_po/wizard/fusion_purchase_order_wiz.py
Normal file
@@ -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,
|
||||||
|
}
|
||||||
57
fusion_so_to_po/wizard/fusion_purchase_order_wiz.xml
Normal file
57
fusion_so_to_po/wizard/fusion_purchase_order_wiz.xml
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<record id="fusion_view_purchase_order_wizard" model="ir.ui.view">
|
||||||
|
<field name="name">fusion.so.po.wiz.form</field>
|
||||||
|
<field name="model">fusion.so.po.wiz</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Create Purchase Orders">
|
||||||
|
<field name="fusion_so_id" invisible="1"/>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<group string="Assign Vendor to Selected">
|
||||||
|
<field name="fusion_batch_vendor_id" options="{'no_create': True}" placeholder="Select vendor to assign..."/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<p class="text-muted mt-4">
|
||||||
|
<b>Tip:</b> 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.
|
||||||
|
</p>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<separator/>
|
||||||
|
|
||||||
|
<field name="fusion_line_ids" nolabel="1">
|
||||||
|
<list editable="bottom" create="false">
|
||||||
|
<field name="selected" string="Select"/>
|
||||||
|
<field name="vendor_id" string="Vendor" placeholder="Select vendor..." widget="many2one" options="{'no_create': True, 'no_quick_create': True}"/>
|
||||||
|
<field name="product_id" string="Product" readonly="1" force_save="1"/>
|
||||||
|
<field name="description" string="Description" readonly="1" force_save="1" column_invisible="1"/>
|
||||||
|
<field name="product_uom_qty" string="Qty" readonly="1" force_save="1"/>
|
||||||
|
<field name="product_uom" string="UoM" readonly="1" force_save="1" groups="uom.group_uom" optional="hide"/>
|
||||||
|
<field name="price_unit" string="Unit Price" force_save="1"/>
|
||||||
|
<field name="price_subtotal" string="Subtotal" readonly="1" force_save="1"/>
|
||||||
|
<field name="fusion_so_line_id" column_invisible="1" force_save="1"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<button name="fusion_create_po" string="Create Purchase Orders" type="object" class="btn-primary"/>
|
||||||
|
<button special="cancel" string="Cancel" class="btn-secondary"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="fusion_purchase_order_wizard_action" model="ir.actions.act_window">
|
||||||
|
<field name="name">Create Purchase Order</field>
|
||||||
|
<field name="res_model">fusion.so.po.wiz</field>
|
||||||
|
<field name="type">ir.actions.act_window</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="target">new</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
Reference in New Issue
Block a user