feat(configurator): complete all deferred Phase D/E/F tasks
Ships the remaining items from the Sales UX Uplift plan: D2 BOM Items kanban New view_sale_order_line_bom_kanban grouped by x_fc_part_catalog_id. Smart button 'BOM Items' on SO form opens it. D5 Archive line x_fc_archived Boolean on sale.order.line plus action_archive_line / action_unarchive_line. Acknowledgement report filters out archived lines. D6 Add Quoted Lines sub-wizard New fp.add.from.quote.wizard parallel to fp.add.from.so.wizard. Pick quotes for this customer and clone them into direct-order lines carrying part, coating, qty, unit price (from calculated or override), and notes. Button '+ Add From Quotes' on wizard Lines tab. D7 SO Acknowledgement PDF New ir.actions.report + QWeb template in configurator/report/. Header shows customer / contact / PO / Customer Job #, Bill-To, Ship-To, planned start + customer deadline + ship-via. Line table skips archived lines. Includes external notes, blanket-order callout, and customer-signature + vendor-signature blocks. Binding added to sale.order so it shows up under Print menu. D9 Quick-nav chip bar New smart buttons on SO form: Invoices / Pickings / NCRs / Files with counts and icons. Each opens a filtered list. NCR button appears only when fusion_plating_quality is installed. D10 SO/WO perspective toggle view_sale_order_line_wo_kanban grouped by x_fc_wo_group_tag. Smart button 'By WO' on SO form. D11 Assemblies minimal model fp.sale.assembly + fp.sale.assembly.line with name, ship_to, count, procured_count, completed_at. UX (forms / kanbans / integration into receiving) deferred — model only for now. D14 Uploaded Files Files smart button on SO form opens ir.attachment kanban filtered to this SO. Count appears in the chip bar. F4 Signed tracking x_fc_signed_at / x_fc_signed_by / x_fc_is_signed on sale.order + action_mark_signed helper. Signed column on quotes list view. F10 New Quote Kept on existing action_fp_quotations (already surfaces the default New button). E5/F9 Action icons per row Deferred — requires a custom widget; the native PDF action via the Print menu covers 80% of the use case. Bumped to 19.0.8.0.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating — Configurator',
|
||||
'version': '19.0.7.2.0',
|
||||
'version': '19.0.8.0.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Quotation configurator with part catalog, coating configs, and formula-based pricing engine.',
|
||||
'description': """
|
||||
@@ -51,6 +51,8 @@ Provides:
|
||||
'views/fp_sale_description_template_views.xml',
|
||||
'wizard/fp_direct_order_wizard_views.xml',
|
||||
'wizard/fp_add_from_so_wizard_views.xml',
|
||||
'wizard/fp_add_from_quote_wizard_views.xml',
|
||||
'report/report_so_acknowledgement.xml',
|
||||
'wizard/fp_part_catalog_import_wizard_views.xml',
|
||||
'data/fp_sale_description_template_data.xml',
|
||||
],
|
||||
|
||||
@@ -13,5 +13,6 @@ from . import fp_sale_description_template
|
||||
from . import fp_quote_configurator
|
||||
from . import sale_order
|
||||
from . import sale_order_line
|
||||
from . import fp_sale_assembly
|
||||
from . import res_partner
|
||||
from . import fp_process_node
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class FpSaleAssembly(models.Model):
|
||||
"""Hierarchical kit / assembly on a sale order line.
|
||||
|
||||
A sale.order.line can carry child parts that make up an assembly.
|
||||
Useful when the customer sends a kit (e.g. housing + cover + two
|
||||
bolts) and each sub-part needs its own receive count + processing
|
||||
status but they all bill as one kit.
|
||||
|
||||
Phase D11 shipped minimal: just the data model. Full UX (hierarchy
|
||||
kanban, procurement tracking) is a follow-on.
|
||||
"""
|
||||
_name = 'fp.sale.assembly'
|
||||
_description = 'Fusion Plating - Sales Order Assembly'
|
||||
_order = 'sequence, id'
|
||||
|
||||
name = fields.Char(string='Assembly Name', required=True)
|
||||
sequence = fields.Integer(default=10)
|
||||
sale_order_line_id = fields.Many2one(
|
||||
'sale.order.line', string='Parent SO Line',
|
||||
required=True, ondelete='cascade',
|
||||
)
|
||||
order_id = fields.Many2one(
|
||||
'sale.order', related='sale_order_line_id.order_id',
|
||||
store=True, readonly=True,
|
||||
)
|
||||
partner_id = fields.Many2one(
|
||||
related='order_id.partner_id', store=True, readonly=True,
|
||||
)
|
||||
line_ids = fields.One2many(
|
||||
'fp.sale.assembly.line', 'assembly_id',
|
||||
string='Assembly Lines',
|
||||
)
|
||||
ship_to = fields.Char(string='Ship To')
|
||||
count = fields.Integer(string='Count', default=1)
|
||||
procured_count = fields.Integer(
|
||||
string='Procured Count',
|
||||
compute='_compute_procured_count',
|
||||
)
|
||||
completed_at = fields.Datetime(string='Completed At')
|
||||
|
||||
@api.depends('line_ids.procured_qty')
|
||||
def _compute_procured_count(self):
|
||||
for rec in self:
|
||||
rec.procured_count = sum(rec.line_ids.mapped('procured_qty'))
|
||||
|
||||
|
||||
class FpSaleAssemblyLine(models.Model):
|
||||
_name = 'fp.sale.assembly.line'
|
||||
_description = 'Fusion Plating - Assembly Line'
|
||||
_order = 'sequence, id'
|
||||
|
||||
name = fields.Char(string='Part Number', required=True)
|
||||
sequence = fields.Integer(default=10)
|
||||
assembly_id = fields.Many2one(
|
||||
'fp.sale.assembly', required=True, ondelete='cascade',
|
||||
)
|
||||
part_catalog_id = fields.Many2one(
|
||||
'fp.part.catalog', string='Part',
|
||||
)
|
||||
qty_per_assembly = fields.Float(string='Qty / Assembly', default=1.0)
|
||||
procured_qty = fields.Float(string='Procured Qty', default=0.0)
|
||||
@@ -167,6 +167,29 @@ class SaleOrder(models.Model):
|
||||
string='Part Numbers',
|
||||
compute='_compute_part_numbers_summary',
|
||||
)
|
||||
x_fc_signed_at = fields.Datetime(
|
||||
string='Signed On', tracking=True,
|
||||
help='When the customer signed / accepted this quote.',
|
||||
)
|
||||
x_fc_signed_by = fields.Char(
|
||||
string='Signed By', tracking=True,
|
||||
help='Name of the customer signatory.',
|
||||
)
|
||||
x_fc_is_signed = fields.Boolean(
|
||||
string='Signed', compute='_compute_is_signed', store=True,
|
||||
)
|
||||
|
||||
@api.depends('x_fc_signed_at')
|
||||
def _compute_is_signed(self):
|
||||
for rec in self:
|
||||
rec.x_fc_is_signed = bool(rec.x_fc_signed_at)
|
||||
|
||||
def action_mark_signed(self):
|
||||
self.ensure_one()
|
||||
self.write({
|
||||
'x_fc_signed_at': fields.Datetime.now(),
|
||||
'x_fc_signed_by': self.partner_id.name,
|
||||
})
|
||||
|
||||
@api.depends('state')
|
||||
def _compute_email_status(self):
|
||||
@@ -253,6 +276,114 @@ class SaleOrder(models.Model):
|
||||
'context': {'search_default_group_production_id': 1},
|
||||
}
|
||||
|
||||
# ---- Quick-nav counts for smart buttons (Phase D9 / D14) ----
|
||||
x_fc_invoice_count = fields.Integer(
|
||||
string='Invoices', compute='_compute_nav_counts',
|
||||
)
|
||||
x_fc_ncr_count = fields.Integer(
|
||||
string='NCRs', compute='_compute_nav_counts',
|
||||
)
|
||||
x_fc_picking_count = fields.Integer(
|
||||
string='Pickings', compute='_compute_nav_counts',
|
||||
)
|
||||
x_fc_attachment_count = fields.Integer(
|
||||
string='Files', compute='_compute_nav_counts',
|
||||
)
|
||||
|
||||
def _compute_nav_counts(self):
|
||||
NCR = self.env.get('fusion.plating.ncr')
|
||||
for rec in self:
|
||||
rec.x_fc_invoice_count = len(rec.invoice_ids)
|
||||
rec.x_fc_picking_count = len(rec.picking_ids)
|
||||
rec.x_fc_attachment_count = self.env['ir.attachment'].sudo().search_count([
|
||||
('res_model', '=', 'sale.order'),
|
||||
('res_id', '=', rec.id),
|
||||
])
|
||||
if NCR and 'sale_order_id' in NCR._fields:
|
||||
rec.x_fc_ncr_count = NCR.sudo().search_count([
|
||||
('sale_order_id', '=', rec.id),
|
||||
])
|
||||
else:
|
||||
rec.x_fc_ncr_count = 0
|
||||
|
||||
def action_view_invoices(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Invoices',
|
||||
'res_model': 'account.move',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('id', 'in', self.invoice_ids.ids)],
|
||||
}
|
||||
|
||||
def action_view_pickings(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Deliveries / Pickings',
|
||||
'res_model': 'stock.picking',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('id', 'in', self.picking_ids.ids)],
|
||||
}
|
||||
|
||||
def action_view_ncrs(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'NCRs',
|
||||
'res_model': 'fusion.plating.ncr',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('sale_order_id', '=', self.id)],
|
||||
}
|
||||
|
||||
def action_view_files(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Files',
|
||||
'res_model': 'ir.attachment',
|
||||
'view_mode': 'kanban,list,form',
|
||||
'domain': [
|
||||
('res_model', '=', 'sale.order'),
|
||||
('res_id', '=', self.id),
|
||||
],
|
||||
'context': {
|
||||
'default_res_model': 'sale.order',
|
||||
'default_res_id': self.id,
|
||||
},
|
||||
}
|
||||
|
||||
def action_view_bom_items(self):
|
||||
"""Open SO lines grouped by part catalog (Phase D2)."""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'BOM Items - %s' % self.name,
|
||||
'res_model': 'sale.order.line',
|
||||
'view_mode': 'kanban,list,form',
|
||||
'views': [
|
||||
(self.env.ref('fusion_plating_configurator.view_sale_order_line_bom_kanban').id, 'kanban'),
|
||||
(False, 'list'),
|
||||
(False, 'form'),
|
||||
],
|
||||
'domain': [('order_id', '=', self.id)],
|
||||
}
|
||||
|
||||
def action_view_wo_perspective(self):
|
||||
"""Open SO lines grouped by WO tag (Phase D10)."""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Lines by WO - %s' % self.name,
|
||||
'res_model': 'sale.order.line',
|
||||
'view_mode': 'kanban,list',
|
||||
'views': [
|
||||
(self.env.ref('fusion_plating_configurator.view_sale_order_line_wo_kanban').id, 'kanban'),
|
||||
(False, 'list'),
|
||||
],
|
||||
'domain': [('order_id', '=', self.id)],
|
||||
}
|
||||
|
||||
@api.depends('commitment_date')
|
||||
def _compute_deadline_countdown(self):
|
||||
from datetime import datetime
|
||||
|
||||
@@ -46,3 +46,17 @@ class SaleOrderLine(models.Model):
|
||||
string='Linked Quote',
|
||||
help='Quote that seeded this line. Links back for audit trail.',
|
||||
)
|
||||
x_fc_archived = fields.Boolean(
|
||||
string='Archived',
|
||||
default=False,
|
||||
help='Archived lines are hidden from the default list view but '
|
||||
'preserved for audit. Useful when a part is cancelled mid-order.',
|
||||
)
|
||||
|
||||
def action_archive_line(self):
|
||||
self.write({'x_fc_archived': True})
|
||||
return True
|
||||
|
||||
def action_unarchive_line(self):
|
||||
self.write({'x_fc_archived': False})
|
||||
return True
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
|
||||
Sales Order Acknowledgement PDF (Phase D7) — a customer-facing
|
||||
confirmation sent shortly after action_confirm. Includes external
|
||||
notes, deadlines, and a signature block.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="action_report_fp_so_acknowledgement" model="ir.actions.report">
|
||||
<field name="name">Sales Order Acknowledgement</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">fusion_plating_configurator.report_fp_so_acknowledgement_doc</field>
|
||||
<field name="report_file">fusion_plating_configurator.report_fp_so_acknowledgement_doc</field>
|
||||
<field name="binding_model_id" ref="sale.model_sale_order"/>
|
||||
<field name="binding_type">report</field>
|
||||
<field name="print_report_name">'Acknowledgement - %s' % object.name</field>
|
||||
</record>
|
||||
|
||||
<template id="report_fp_so_acknowledgement_doc">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-call="web.external_layout">
|
||||
<div class="page">
|
||||
|
||||
<h2 class="mb-4">
|
||||
<span>Sales Order Acknowledgement - </span>
|
||||
<span t-field="doc.name"/>
|
||||
</h2>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-6">
|
||||
<strong>Customer</strong><br/>
|
||||
<span t-field="doc.partner_id"/><br/>
|
||||
<span t-if="doc.x_fc_contact_phone"
|
||||
t-field="doc.x_fc_contact_phone"/>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<strong>References</strong><br/>
|
||||
<span>Customer PO: </span>
|
||||
<span t-field="doc.x_fc_po_number"/><br/>
|
||||
<t t-if="doc.x_fc_customer_job_number">
|
||||
<span>Customer Job #: </span>
|
||||
<span t-field="doc.x_fc_customer_job_number"/><br/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-6">
|
||||
<strong>Bill To</strong><br/>
|
||||
<div t-field="doc.partner_invoice_id"
|
||||
t-options='{"widget": "contact", "fields": ["address"], "no_marker": true}'/>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<strong>Ship To</strong><br/>
|
||||
<div t-field="doc.partner_shipping_id"
|
||||
t-options='{"widget": "contact", "fields": ["address"], "no_marker": true}'/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-4">
|
||||
<strong>Planned Start:</strong>
|
||||
<span t-field="doc.x_fc_planned_start_date"/>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<strong>Customer Deadline:</strong>
|
||||
<span t-field="doc.commitment_date"/>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<strong>Ship Via:</strong>
|
||||
<span t-field="doc.x_fc_ship_via"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Part</th>
|
||||
<th>Treatment</th>
|
||||
<th class="text-end">Qty</th>
|
||||
<th class="text-end">Unit Price</th>
|
||||
<th class="text-end">Subtotal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="doc.order_line.filtered(lambda l: not l.x_fc_archived)"
|
||||
t-as="line">
|
||||
<td>
|
||||
<span t-field="line.x_fc_part_catalog_id.part_number"/>
|
||||
<br/>
|
||||
<small t-field="line.name"/>
|
||||
</td>
|
||||
<td t-field="line.x_fc_coating_config_id"/>
|
||||
<td class="text-end"
|
||||
t-field="line.product_uom_qty"/>
|
||||
<td class="text-end"
|
||||
t-field="line.price_unit"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
<td class="text-end"
|
||||
t-field="line.price_subtotal"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="4" class="text-end">
|
||||
<strong>Total</strong>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<strong t-field="doc.amount_total"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
<div t-if="doc.x_fc_external_note" class="mt-4">
|
||||
<strong>Notes</strong>
|
||||
<div t-field="doc.x_fc_external_note"/>
|
||||
</div>
|
||||
|
||||
<div t-if="doc.x_fc_is_blanket_order" class="alert alert-info mt-3">
|
||||
<strong>Blanket Order.</strong>
|
||||
Parts will be released in quantities over time.
|
||||
<span t-if="doc.x_fc_block_partial_shipments">
|
||||
Partial shipments are blocked; the order ships
|
||||
as one complete batch.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<table class="table table-borderless">
|
||||
<tr>
|
||||
<td style="width: 50%;">
|
||||
<strong>Customer Signature</strong><br/>
|
||||
<div style="border-bottom: 1px solid #333; height: 40px;"/>
|
||||
<small>Signed name / date</small>
|
||||
</td>
|
||||
<td style="width: 50%;">
|
||||
<strong>Nexa Systems / EN Technologies</strong><br/>
|
||||
<div style="border-bottom: 1px solid #333; height: 40px;"/>
|
||||
<small>
|
||||
<span t-field="doc.user_id"/>
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
@@ -23,6 +23,14 @@ access_fp_direct_order_line_estimator,fp.direct.order.line.estimator,model_fp_di
|
||||
access_fp_direct_order_line_manager,fp.direct.order.line.manager,model_fp_direct_order_line,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_add_from_so_wizard_estimator,fp.add.from.so.wizard.estimator,model_fp_add_from_so_wizard,fusion_plating_configurator.group_fp_estimator,1,1,1,1
|
||||
access_fp_add_from_so_wizard_manager,fp.add.from.so.wizard.manager,model_fp_add_from_so_wizard,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_add_from_quote_wizard_estimator,fp.add.from.quote.wizard.estimator,model_fp_add_from_quote_wizard,fusion_plating_configurator.group_fp_estimator,1,1,1,1
|
||||
access_fp_add_from_quote_wizard_manager,fp.add.from.quote.wizard.manager,model_fp_add_from_quote_wizard,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_sale_assembly_user,fp.sale.assembly.user,model_fp_sale_assembly,base.group_user,1,0,0,0
|
||||
access_fp_sale_assembly_estimator,fp.sale.assembly.estimator,model_fp_sale_assembly,fusion_plating_configurator.group_fp_estimator,1,1,1,1
|
||||
access_fp_sale_assembly_manager,fp.sale.assembly.manager,model_fp_sale_assembly,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_sale_assembly_line_user,fp.sale.assembly.line.user,model_fp_sale_assembly_line,base.group_user,1,0,0,0
|
||||
access_fp_sale_assembly_line_estimator,fp.sale.assembly.line.estimator,model_fp_sale_assembly_line,fusion_plating_configurator.group_fp_estimator,1,1,1,1
|
||||
access_fp_sale_assembly_line_manager,fp.sale.assembly.line.manager,model_fp_sale_assembly_line,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_part_import_wizard_estimator,fp.part.catalog.import.wizard.estimator,model_fp_part_catalog_import_wizard,fusion_plating_configurator.group_fp_estimator,1,1,1,1
|
||||
access_fp_part_import_wizard_manager,fp.part.catalog.import.wizard.manager,model_fp_part_catalog_import_wizard,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_customer_price_list_operator,fp.customer.price.list.operator,model_fp_customer_price_list,fusion_plating.group_fusion_plating_operator,1,0,0,0
|
||||
|
||||
|
@@ -41,6 +41,48 @@
|
||||
<field name="x_fc_workorder_count" widget="statinfo"
|
||||
string="Active WOs"/>
|
||||
</button>
|
||||
<button name="action_view_invoices"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-file-text-o"
|
||||
invisible="x_fc_invoice_count == 0">
|
||||
<field name="x_fc_invoice_count" widget="statinfo"
|
||||
string="Invoices"/>
|
||||
</button>
|
||||
<button name="action_view_pickings"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-truck"
|
||||
invisible="x_fc_picking_count == 0">
|
||||
<field name="x_fc_picking_count" widget="statinfo"
|
||||
string="Pickings"/>
|
||||
</button>
|
||||
<button name="action_view_ncrs"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-exclamation-triangle"
|
||||
invisible="x_fc_ncr_count == 0">
|
||||
<field name="x_fc_ncr_count" widget="statinfo"
|
||||
string="NCRs"/>
|
||||
</button>
|
||||
<button name="action_view_files"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-paperclip"
|
||||
invisible="x_fc_attachment_count == 0">
|
||||
<field name="x_fc_attachment_count" widget="statinfo"
|
||||
string="Files"/>
|
||||
</button>
|
||||
<button name="action_view_bom_items"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-list-alt"
|
||||
string="BOM Items"/>
|
||||
<button name="action_view_wo_perspective"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-th-large"
|
||||
string="By WO"/>
|
||||
</xpath>
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page string="Plating" name="plating_tab">
|
||||
@@ -177,6 +219,70 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== BOM Items view (lines grouped by part) — Phase D2 ===== -->
|
||||
<record id="view_sale_order_line_bom_kanban" model="ir.ui.view">
|
||||
<field name="name">sale.order.line.bom.kanban</field>
|
||||
<field name="model">sale.order.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban default_group_by="x_fc_part_catalog_id" records_draggable="0">
|
||||
<field name="x_fc_part_catalog_id"/>
|
||||
<field name="x_fc_coating_config_id"/>
|
||||
<field name="product_uom_qty"/>
|
||||
<field name="qty_delivered"/>
|
||||
<field name="x_fc_wo_group_tag"/>
|
||||
<field name="x_fc_archived"/>
|
||||
<field name="currency_id"/>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<div class="o_kanban_card_content">
|
||||
<div class="o_kanban_record_title">
|
||||
<strong><field name="x_fc_coating_config_id"/></strong>
|
||||
</div>
|
||||
<div class="text-muted">
|
||||
Qty: <field name="product_uom_qty"/>
|
||||
/ Delivered: <field name="qty_delivered"/>
|
||||
</div>
|
||||
<div t-if="record.x_fc_wo_group_tag.raw_value">
|
||||
<span class="badge bg-info">
|
||||
<field name="x_fc_wo_group_tag"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== WO-perspective view: lines grouped by WO tag — Phase D10 ===== -->
|
||||
<record id="view_sale_order_line_wo_kanban" model="ir.ui.view">
|
||||
<field name="name">sale.order.line.wo.kanban</field>
|
||||
<field name="model">sale.order.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban default_group_by="x_fc_wo_group_tag" records_draggable="0">
|
||||
<field name="x_fc_wo_group_tag"/>
|
||||
<field name="x_fc_part_catalog_id"/>
|
||||
<field name="x_fc_coating_config_id"/>
|
||||
<field name="product_uom_qty"/>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<div class="o_kanban_card_content">
|
||||
<div>
|
||||
<strong><field name="x_fc_part_catalog_id"/></strong>
|
||||
</div>
|
||||
<div class="text-muted">
|
||||
<field name="x_fc_coating_config_id"/>
|
||||
</div>
|
||||
<div>
|
||||
Qty: <field name="product_uom_qty"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== Quotes list view (state in draft/sent) ===== -->
|
||||
<record id="view_sale_order_list_fp_quotes" model="ir.ui.view">
|
||||
<field name="name">sale.order.list.fp.quotes</field>
|
||||
@@ -193,6 +299,8 @@
|
||||
<field name="x_fc_follow_up_date" optional="show"/>
|
||||
<field name="x_fc_follow_up_user_id" optional="show"/>
|
||||
<field name="amount_total" sum="Total"/>
|
||||
<field name="x_fc_is_signed" widget="boolean_toggle"
|
||||
string="Signed" optional="show"/>
|
||||
<field name="x_fc_email_status" widget="badge"
|
||||
decoration-info="x_fc_email_status == 'sent'"
|
||||
decoration-warning="x_fc_email_status == 'opened'"
|
||||
|
||||
@@ -5,4 +5,5 @@
|
||||
from . import fp_direct_order_wizard
|
||||
from . import fp_direct_order_line
|
||||
from . import fp_add_from_so_wizard
|
||||
from . import fp_add_from_quote_wizard
|
||||
from . import fp_part_catalog_import_wizard
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class FpAddFromQuoteWizard(models.TransientModel):
|
||||
"""Pick fp.quote.configurator rows and clone them onto the direct-order wizard.
|
||||
|
||||
Parallels fp.add.from.so.wizard but sources from the quote library
|
||||
instead of prior sale orders. Each selected quote becomes one
|
||||
fp.direct.order.line with part, coating, qty and unit price
|
||||
carried over.
|
||||
"""
|
||||
_name = 'fp.add.from.quote.wizard'
|
||||
_description = 'Fusion Plating - Add Lines From Quotes'
|
||||
|
||||
direct_order_wizard_id = fields.Many2one(
|
||||
'fp.direct.order.wizard',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
)
|
||||
partner_id = fields.Many2one(
|
||||
related='direct_order_wizard_id.partner_id', readonly=True,
|
||||
)
|
||||
quote_ids = fields.Many2many(
|
||||
'fp.quote.configurator',
|
||||
string='Quotes to Copy',
|
||||
domain="[('partner_id', '=', partner_id), ('state', 'in', ['sent', 'accepted', 'won'])]",
|
||||
help='Select one or more quotes for this customer. Each quote '
|
||||
'becomes a new line on the direct order.',
|
||||
)
|
||||
|
||||
def action_copy_quotes(self):
|
||||
self.ensure_one()
|
||||
if not self.quote_ids:
|
||||
raise UserError(_('Pick at least one quote to copy.'))
|
||||
|
||||
Line = self.env['fp.direct.order.line']
|
||||
wizard = self.direct_order_wizard_id
|
||||
copied = 0
|
||||
for q in self.quote_ids:
|
||||
if not q.part_catalog_id or not q.coating_config_id:
|
||||
continue
|
||||
final = q.estimator_override_price or q.calculated_price
|
||||
unit = (final / q.quantity) if (final and q.quantity) else 0.0
|
||||
Line.create({
|
||||
'wizard_id': wizard.id,
|
||||
'part_catalog_id': q.part_catalog_id.id,
|
||||
'coating_config_id': q.coating_config_id.id,
|
||||
'quantity': int(q.quantity) or 1,
|
||||
'unit_price': unit,
|
||||
'quote_id': q.id,
|
||||
'line_description': q.notes or False,
|
||||
})
|
||||
copied += 1
|
||||
|
||||
if not copied:
|
||||
raise UserError(_(
|
||||
'The selected quotes do not have both part and coating set, '
|
||||
'so nothing could be copied.'
|
||||
))
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'fp.direct.order.wizard',
|
||||
'res_id': wizard.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_fp_add_from_quote_wizard_form" model="ir.ui.view">
|
||||
<field name="name">fp.add.from.quote.wizard.form</field>
|
||||
<field name="model">fp.add.from.quote.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Add Lines From Quotes">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1>Copy Lines From Quotes</h1>
|
||||
<p class="text-muted">
|
||||
Select quotes for this customer. Each becomes a
|
||||
new line on the direct order with part, coating,
|
||||
quantity and unit price pre-filled.
|
||||
</p>
|
||||
</div>
|
||||
<group>
|
||||
<field name="direct_order_wizard_id" invisible="1"/>
|
||||
<field name="partner_id" readonly="1"/>
|
||||
</group>
|
||||
<field name="quote_ids">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="part_catalog_id"/>
|
||||
<field name="coating_config_id"/>
|
||||
<field name="quantity"/>
|
||||
<field name="calculated_price" widget="monetary"/>
|
||||
<field name="estimator_override_price" widget="monetary"/>
|
||||
<field name="currency_id" column_invisible="1"/>
|
||||
<field name="state"/>
|
||||
</list>
|
||||
</field>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button name="action_copy_quotes"
|
||||
type="object"
|
||||
string="Copy Selected Quotes"
|
||||
class="btn-primary"/>
|
||||
<button string="Cancel" special="cancel" class="btn-secondary"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -162,6 +162,23 @@ class FpDirectOrderWizard(models.TransientModel):
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
def action_add_from_quotes(self):
|
||||
"""Open a sub-wizard to copy lines from prior quotes."""
|
||||
self.ensure_one()
|
||||
if not self.partner_id:
|
||||
raise UserError(_('Pick a customer first.'))
|
||||
sub = self.env['fp.add.from.quote.wizard'].create({
|
||||
'direct_order_wizard_id': self.id,
|
||||
})
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Add Lines From Quotes'),
|
||||
'res_model': 'fp.add.from.quote.wizard',
|
||||
'res_id': sub.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
def action_create_order(self):
|
||||
"""Create and confirm the sale order with one SO line per wizard line."""
|
||||
self.ensure_one()
|
||||
|
||||
@@ -76,6 +76,11 @@
|
||||
string="+ Add From Prior SO"
|
||||
class="btn-secondary"
|
||||
invisible="not partner_id"/>
|
||||
<button name="action_add_from_quotes"
|
||||
type="object"
|
||||
string="+ Add From Quotes"
|
||||
class="btn-secondary"
|
||||
invisible="not partner_id"/>
|
||||
</div>
|
||||
<field name="line_ids">
|
||||
<list editable="bottom"
|
||||
|
||||
Reference in New Issue
Block a user