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:
gsinghpal
2026-04-19 21:45:17 -04:00
parent b85e208856
commit b834ae3117
13 changed files with 636 additions and 1 deletions

View File

@@ -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',
],

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
23 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
24 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
25 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
26 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
27 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
28 access_fp_sale_assembly_user fp.sale.assembly.user model_fp_sale_assembly base.group_user 1 0 0 0
29 access_fp_sale_assembly_estimator fp.sale.assembly.estimator model_fp_sale_assembly fusion_plating_configurator.group_fp_estimator 1 1 1 1
30 access_fp_sale_assembly_manager fp.sale.assembly.manager model_fp_sale_assembly fusion_plating.group_fusion_plating_manager 1 1 1 1
31 access_fp_sale_assembly_line_user fp.sale.assembly.line.user model_fp_sale_assembly_line base.group_user 1 0 0 0
32 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
33 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
34 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
35 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
36 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

View File

@@ -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'"

View File

@@ -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

View File

@@ -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',
}

View File

@@ -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>

View File

@@ -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()

View File

@@ -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"