This commit is contained in:
gsinghpal
2026-05-22 18:01:31 -04:00
parent d127e19b45
commit f661724c72
34 changed files with 1011 additions and 59 deletions

View File

@@ -5,7 +5,7 @@
{
'name': 'Fusion Plating — Configurator',
'version': '19.0.21.5.6',
'version': '19.0.21.7.2',
'category': 'Manufacturing/Plating',
'summary': 'Quotation configurator with part catalog, coating configs, and formula-based pricing engine.',
'description': """
@@ -56,10 +56,12 @@ Provides:
'wizard/fp_part_catalog_import_wizard_views.xml',
'wizard/fp_serial_bulk_add_wizard_views.xml',
'views/fp_configurator_menu.xml',
'views/fp_so_job_sort_views.xml',
'data/fp_sale_description_template_data.xml',
],
'assets': {
'web.assets_backend': [
'fusion_plating_configurator/static/src/scss/fp_job_status_pill.scss',
'fusion_plating_configurator/static/src/scss/fp_3d_viewer.scss',
'fusion_plating_configurator/static/src/xml/fp_3d_viewer.xml',
'fusion_plating_configurator/static/src/js/fp_3d_viewer.js',
@@ -72,6 +74,13 @@ Provides:
'fusion_plating_configurator/static/src/xml/fp_part_process_composer.xml',
'fusion_plating_configurator/static/src/js/fp_part_process_composer.js',
],
# Register the Job Status pill SCSS in both bundles so the
# `@if $o-webclient-color-scheme == dark` branch compiles for
# the dark variant (see CLAUDE.md "Dark Mode" — Odoo 19 has no
# runtime DOM toggle, two pre-built bundles).
'web.assets_web_dark': [
'fusion_plating_configurator/static/src/scss/fp_job_status_pill.scss',
],
},
'installable': True,
'application': False,

View File

@@ -8,6 +8,7 @@ from . import fp_part_catalog
from . import fp_pricing_complexity_surcharge
from . import fp_pricing_rule
from . import fp_sale_description_template
from . import fp_so_job_sort
from . import fp_quote_configurator
from . import fp_serial
from . import sale_order

View File

@@ -0,0 +1,58 @@
# -*- 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 FpSoJobSort(models.Model):
"""A user-defined grouping bucket for sale orders ("Job Sorting").
Same pattern as `fusion.plating.tank.section` — every shop slices its
SO backlog differently (by customer programme, by priority, by
fabricator group, by week, etc.). Sections are free-form, renameable,
quick-creatable from the M2O dropdown, and let users group the SO
list with fold/expand sections.
"""
_name = 'fp.so.job.sort'
_description = 'Fusion Plating — Sale Order Job Sort'
_order = 'sequence, name'
name = fields.Char(
string='Job Sorting',
required=True,
translate=True,
)
sequence = fields.Integer(string='Sequence', default=10)
color = fields.Integer(string='Color', default=0)
fold = fields.Boolean(
string='Folded by Default',
help='When set, this section appears collapsed in the grouped '
'SO list so the body rows are hidden until expanded.',
)
description = fields.Text(string='Description', translate=True)
active = fields.Boolean(default=True)
sale_order_ids = fields.One2many(
'sale.order', 'x_fc_job_sort_id', string='Sale Orders',
)
sale_order_count = fields.Integer(
compute='_compute_sale_order_count',
)
@api.depends('sale_order_ids')
def _compute_sale_order_count(self):
for rec in self:
rec.sale_order_count = len(rec.sale_order_ids)
def action_view_sale_orders(self):
self.ensure_one()
return {
'name': self.name,
'type': 'ir.actions.act_window',
'res_model': 'sale.order',
'view_mode': 'list,form',
'domain': [('x_fc_job_sort_id', '=', self.id)],
'context': {'default_x_fc_job_sort_id': self.id},
}

View File

@@ -110,6 +110,16 @@ class SaleOrder(models.Model):
help="Customer's internal job number for cross-referencing.",
tracking=True,
)
x_fc_job_sort_id = fields.Many2one(
'fp.so.job.sort',
string='Job Sorting',
ondelete='set null',
tracking=True,
help='Free-form bucket that groups this SO in the "Sale Orders '
'by Sorting" list view. Quick-create from the dropdown — '
'each shop slices its backlog differently (customer programme, '
'priority, week, etc.).',
)
x_fc_planned_start_date = fields.Date(
string='Planned Start Date', tracking=True,
)
@@ -151,6 +161,16 @@ class SaleOrder(models.Model):
string='Deadline',
compute='_compute_deadline_countdown',
)
# Drives the colour of the Deadline column. Computed in the same pass
# as x_fc_deadline_countdown so the buckets always agree with the
# human-readable countdown string.
x_fc_deadline_urgency = fields.Selection(
[('overdue', 'Overdue'),
('urgent', 'Due within 2 days'),
('safe', 'More than 2 days')],
string='Deadline Urgency',
compute='_compute_deadline_countdown',
)
x_fc_order_completion_date = fields.Date(
string='Order Completion Date',
compute='_compute_order_completion_date',
@@ -263,6 +283,136 @@ class SaleOrder(models.Model):
compute='_compute_invoiced_amount',
currency_field='currency_id',
)
# Single "Job Status" pill rendered in the SO list. Pipeline order:
# Draft → Awaiting Parts → Parts Partial → Ready to Start →
# <Step Name> → Ready to Ship → Ship Booked → In Transit →
# Delivered → Invoiced → Paid → Cancelled.
# Rendered as an Html field so each kind can carry its own tint via
# an .fp-kind-* class — Bootstrap's 5 decoration-* slots aren't
# enough to give every phase a distinct colour. SCSS bundle at
# static/src/scss/fp_job_status_pill.scss owns the colour map.
x_fc_fp_job_status = fields.Html(
string='Job Status',
compute='_compute_fp_job_status',
sanitize=False,
help='Single at-a-glance pill that advances through the order '
'lifecycle: receiving → WO progress → shipping → invoicing.',
)
x_fc_fp_job_status_kind = fields.Selection(
[('muted', 'Draft (grey)'),
('warning', 'Awaiting / Partial (amber)'),
('primary', 'Ready / Milestone (purple)'),
('info', 'Active Work (blue)'),
('shipping', 'Shipping (cyan)'),
('delivered', 'Delivered (teal)'),
('invoiced', 'Invoiced (lime)'),
('paid', 'Paid (green bold)'),
('danger', 'Cancelled (red)')],
string='Job Status Kind',
compute='_compute_fp_job_status',
help='Colour category that backs the Job Status pill — also '
'usable for filtering / grouping in the list search panel.',
)
@api.depends(
'state',
'x_fc_receiving_status',
'x_fc_wo_completion',
'invoice_ids.state',
'invoice_ids.payment_state',
'invoice_ids.move_type',
)
def _compute_fp_job_status(self):
from markupsafe import Markup as _Markup
from markupsafe import escape as _escape
for so in self:
label, kind = self._fp_resolve_job_status(so)
so.x_fc_fp_job_status_kind = kind
so.x_fc_fp_job_status = _Markup(
'<span class="fp-job-status fp-kind-%s">%s</span>'
) % (_Markup(kind), _escape(label))
@staticmethod
def _fp_resolve_job_status(so):
# Terminal SO states first.
if so.state == 'cancel':
return ('Cancelled', 'danger')
if so.state in ('draft', 'sent'):
return ('Draft', 'muted')
# Invoice phase (terminal positive states).
posted = so.invoice_ids.filtered(
lambda m: m.state == 'posted'
and m.move_type in ('out_invoice', 'out_refund')
)
if posted and all(
m.payment_state in ('paid', 'in_payment') for m in posted
):
return ('Paid', 'paid')
# Shipping phase signals — read once.
ship_status = None
if 'x_fc_receiving_ids' in so._fields:
for r in so.x_fc_receiving_ids:
ship = (
r.x_fc_outbound_shipment_id
if 'x_fc_outbound_shipment_id' in r._fields else False
)
if not ship:
continue
# Latch the most-advanced status across all receivings.
rank = {None: 0, 'booked': 1, 'in_transit': 2, 'delivered': 3}
cur = (
'delivered' if ship.status == 'delivered'
else 'in_transit' if ship.status == 'shipped'
else 'booked' if ship.status in ('confirmed', 'draft')
else None
)
if rank[cur] > rank[ship_status]:
ship_status = cur
if posted and ship_status == 'delivered':
return ('Invoiced', 'invoiced')
if ship_status == 'delivered':
return ('Delivered', 'delivered')
if ship_status == 'in_transit':
return ('In Transit', 'shipping')
# WO phase — figure out total steps and the current step name.
tot = 0
current_step_name = None
Job = so.env.get('fp.job')
if Job is not None and so.name:
jobs = Job.sudo().search([('origin', '=', so.name)])
if jobs:
steps = jobs.mapped('step_ids').sorted(
lambda s: (s.job_id.id, s.sequence)
)
tot = len(steps)
# Priority: in_progress → paused → next ready/pending.
current = (
steps.filtered(lambda s: s.state == 'in_progress')[:1]
or steps.filtered(lambda s: s.state == 'paused')[:1]
or steps.filtered(lambda s: s.state in ('ready', 'pending'))[:1]
)
current_step_name = current.name if current else None
all_steps_done = tot > 0 and current_step_name is None
if all_steps_done:
if ship_status == 'booked':
return ('Ship Booked', 'shipping')
return ('Ready to Ship', 'primary')
if current_step_name:
return (current_step_name, 'info')
# Receiving phase (no WO yet).
recv = so.x_fc_receiving_status or 'not_received'
if recv == 'received':
return ('Ready to Start', 'primary')
if recv == 'partial':
return ('Parts Partial', 'warning')
return ('Awaiting Parts', 'warning')
@api.depends('x_fc_lead_time_min_days', 'x_fc_lead_time_max_days', 'x_fc_rush_order')
def _compute_lead_time_display(self):
@@ -536,9 +686,11 @@ class SaleOrder(models.Model):
def _compute_deadline_countdown(self):
from datetime import datetime
now = fields.Datetime.now()
TWO_DAYS = 2 * 86400 # seconds threshold for "urgent"
for rec in self:
if not rec.commitment_date:
rec.x_fc_deadline_countdown = False
rec.x_fc_deadline_urgency = False
continue
target = rec.commitment_date
if isinstance(target, datetime):
@@ -549,12 +701,13 @@ class SaleOrder(models.Model):
secs = int(delta.total_seconds())
if secs == 0:
rec.x_fc_deadline_countdown = 'due now'
rec.x_fc_deadline_urgency = 'overdue'
continue
past = secs < 0
secs = abs(secs)
days = secs // 86400
hours = (secs % 86400) // 3600
mins = (secs % 3600) // 60
abs_secs = abs(secs)
days = abs_secs // 86400
hours = (abs_secs % 86400) // 3600
mins = (abs_secs % 3600) // 60
bits = []
if days:
bits.append('%dd' % days)
@@ -566,6 +719,12 @@ class SaleOrder(models.Model):
rec.x_fc_deadline_countdown = (
'overdue %s' % phrase if past else 'in %s' % phrase
)
if past:
rec.x_fc_deadline_urgency = 'overdue'
elif secs <= TWO_DAYS:
rec.x_fc_deadline_urgency = 'urgent'
else:
rec.x_fc_deadline_urgency = 'safe'
@api.depends(
'order_line.x_fc_effective_part_deadline',

View File

@@ -42,3 +42,5 @@ access_fp_part_revision_bump_manager,fp.part.revision.bump.manager,model_fp_part
access_fp_part_material_user,fp.part.material.user,model_fp_part_material,base.group_user,1,0,0,0
access_fp_part_material_estimator,fp.part.material.estimator,model_fp_part_material,fusion_plating_configurator.group_fp_estimator,1,1,1,0
access_fp_part_material_manager,fp.part.material.manager,model_fp_part_material,fusion_plating.group_fusion_plating_manager,1,1,1,1
access_fp_so_job_sort_user,fp.so.job.sort.user,model_fp_so_job_sort,base.group_user,1,1,1,0
access_fp_so_job_sort_manager,fp.so.job.sort.manager,model_fp_so_job_sort,fusion_plating.group_fusion_plating_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
42 access_fp_part_material_user fp.part.material.user model_fp_part_material base.group_user 1 0 0 0
43 access_fp_part_material_estimator fp.part.material.estimator model_fp_part_material fusion_plating_configurator.group_fp_estimator 1 1 1 0
44 access_fp_part_material_manager fp.part.material.manager model_fp_part_material fusion_plating.group_fusion_plating_manager 1 1 1 1
45 access_fp_so_job_sort_user fp.so.job.sort.user model_fp_so_job_sort base.group_user 1 1 1 0
46 access_fp_so_job_sort_manager fp.so.job.sort.manager model_fp_so_job_sort fusion_plating.group_fusion_plating_manager 1 1 1 1

View File

@@ -0,0 +1,68 @@
// =============================================================================
// Fusion Plating — Job Status pill on the SO list
// Copyright 2026 Nexa Systems Inc. · License OPL-1
//
// One pill per row, one colour per phase, vibrant + saturated so phases
// pop at a glance against both the light and dark Odoo bundles. Same
// hue map for both modes — saturated 500-level Tailwind hues with white
// text give consistent contrast against either page background.
// =============================================================================
// ----- Vibrant tints (light + dark) -----
$_fp-muted-bg : #6b7280; // slate
$_fp-warning-bg : #f59e0b; // amber
$_fp-primary-bg : #8b5cf6; // violet
$_fp-info-bg : #3b82f6; // blue
$_fp-shipping-bg : #06b6d4; // cyan
$_fp-delivered-bg : #14b8a6; // teal
$_fp-invoiced-bg : #84cc16; // lime
$_fp-paid-bg : #16a34a; // green
$_fp-danger-bg : #ef4444; // red
// Matching glow shadows — darker tone of the same hue for a subtle
// drop-shadow that gives the pill a "lifted" feel without being noisy.
$_fp-muted-glow : rgba(31, 41, 55, 0.35);
$_fp-warning-glow : rgba(180, 83, 9, 0.45);
$_fp-primary-glow : rgba(91, 33, 182, 0.45);
$_fp-info-glow : rgba(29, 78, 216, 0.45);
$_fp-shipping-glow : rgba(14, 116, 144, 0.45);
$_fp-delivered-glow : rgba(15, 118, 110, 0.45);
$_fp-invoiced-glow : rgba(101, 163, 13, 0.45);
$_fp-paid-glow : rgba(21, 128, 61, 0.5);
$_fp-danger-glow : rgba(185, 28, 28, 0.45);
// =============================================================================
// Pill base
// =============================================================================
.fp-job-status {
display: inline-block;
padding: 0.4em 0.95em;
border-radius: 999px;
font-weight: 600;
font-size: 0.82em;
line-height: 1.25;
letter-spacing: 0.015em;
white-space: nowrap;
text-align: center;
min-width: 72px;
color: #ffffff !important;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
}
// =============================================================================
// Per-kind tints — same map applies to light + dark bundles. White text
// gives consistent contrast against any saturated mid-tone hue.
// =============================================================================
.fp-kind-muted { background-color: $_fp-muted-bg; box-shadow: 0 1px 3px $_fp-muted-glow; }
.fp-kind-warning { background-color: $_fp-warning-bg; box-shadow: 0 1px 3px $_fp-warning-glow; }
.fp-kind-primary { background-color: $_fp-primary-bg; box-shadow: 0 1px 3px $_fp-primary-glow; }
.fp-kind-info { background-color: $_fp-info-bg; box-shadow: 0 1px 3px $_fp-info-glow; }
.fp-kind-shipping { background-color: $_fp-shipping-bg; box-shadow: 0 1px 3px $_fp-shipping-glow; }
.fp-kind-delivered { background-color: $_fp-delivered-bg; box-shadow: 0 1px 3px $_fp-delivered-glow; }
.fp-kind-invoiced { background-color: $_fp-invoiced-bg; box-shadow: 0 1px 3px $_fp-invoiced-glow; }
.fp-kind-paid {
background-color: $_fp-paid-bg;
box-shadow: 0 1px 4px $_fp-paid-glow;
font-weight: 700;
}
.fp-kind-danger { background-color: $_fp-danger-bg; box-shadow: 0 1px 3px $_fp-danger-glow; }

View File

@@ -0,0 +1,261 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Job Sorting:
- Section model views (list/form) under Configuration → Sales.
- Alternate SO list ("Sale Orders by Sorting") grouped by job sort
with foldable sections and create-from-here support.
-->
<odoo>
<!-- ===== Section management (Configuration) ===== -->
<record id="view_fp_so_job_sort_list" model="ir.ui.view">
<field name="name">fp.so.job.sort.list</field>
<field name="model">fp.so.job.sort</field>
<field name="arch" type="xml">
<list string="Job Sorting" editable="bottom">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="color" widget="color_picker"/>
<field name="fold" widget="boolean_toggle"/>
<field name="sale_order_count"/>
<field name="active" widget="boolean_toggle" optional="hide"/>
</list>
</field>
</record>
<record id="view_fp_so_job_sort_form" model="ir.ui.view">
<field name="name">fp.so.job.sort.form</field>
<field name="model">fp.so.job.sort</field>
<field name="arch" type="xml">
<form string="Job Sorting">
<sheet>
<div class="oe_button_box" name="button_box">
<button name="action_view_sale_orders" type="object"
class="oe_stat_button" icon="fa-shopping-cart">
<field name="sale_order_count" widget="statinfo"
string="Sale Orders"/>
</button>
</div>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" placeholder="e.g. Rush Orders"/></h1>
</div>
<group>
<group>
<field name="sequence"/>
<field name="color" widget="color_picker"/>
<field name="fold"/>
</group>
<group>
<field name="active"/>
</group>
</group>
<field name="description"
placeholder="What kinds of orders belong in this section?"/>
</sheet>
</form>
</field>
</record>
<record id="action_fp_so_job_sort" model="ir.actions.act_window">
<field name="name">Job Sorting</field>
<field name="res_model">fp.so.job.sort</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_fp_so_job_sort"
name="Job Sorting"
parent="fusion_plating.menu_fp_config_pricing_billing"
action="action_fp_so_job_sort"
sequence="25"/>
<!-- ===== Kanban grouped by Job Sorting =====
Groups SOs into foldable columns by x_fc_job_sort_id.
Drag-drop between columns rewrites the bucket; quick-create on
the column header creates a new fp.so.job.sort row. Wired into
the existing Sale Orders action below so it shows up in the
view-switcher next to the flat list. -->
<record id="view_sale_order_kanban_fp_by_sorting" model="ir.ui.view">
<field name="name">sale.order.kanban.fp.by_sorting</field>
<field name="model">sale.order</field>
<field name="arch" type="xml">
<kanban default_group_by="x_fc_job_sort_id"
group_create="true"
group_edit="true"
group_delete="true"
quick_create="false"
sample="1">
<field name="name"/>
<field name="partner_id"/>
<field name="amount_total"/>
<field name="currency_id"/>
<field name="x_fc_part_numbers_summary"/>
<field name="x_fc_customer_job_number"/>
<field name="x_fc_deadline_countdown"/>
<field name="x_fc_deadline_urgency"/>
<field name="x_fc_fp_job_status"/>
<field name="state"/>
<templates>
<t t-name="card">
<div class="o_kanban_card_content p-2">
<div class="d-flex justify-content-between align-items-start mb-1">
<strong><field name="name"/></strong>
<span t-att-class="'badge ' + (
record.x_fc_deadline_urgency.raw_value == 'overdue' and 'text-bg-danger' or
record.x_fc_deadline_urgency.raw_value == 'urgent' and 'text-bg-warning' or
record.x_fc_deadline_urgency.raw_value == 'safe' and 'text-bg-success' or
'text-bg-light')"
t-if="record.x_fc_deadline_countdown.raw_value">
<field name="x_fc_deadline_countdown"/>
</span>
</div>
<div class="text-muted small mb-1">
<field name="partner_id"/>
</div>
<div class="small mb-1" t-if="record.x_fc_part_numbers_summary.raw_value">
<i class="fa fa-cube me-1"/>
<field name="x_fc_part_numbers_summary"/>
</div>
<div class="small mb-2" t-if="record.x_fc_customer_job_number.raw_value">
<i class="fa fa-hashtag me-1"/>
<field name="x_fc_customer_job_number"/>
</div>
<div class="d-flex justify-content-between align-items-center">
<field name="x_fc_fp_job_status" widget="html"/>
<strong>
<field name="amount_total" widget="monetary"
options="{'currency_field': 'currency_id'}"/>
</strong>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<!-- ===== Sale Orders by Sorting (alternate SO list) ===== -->
<!-- Duplicate of view_sale_order_list_fp but renamed and intended
to be opened with group_by=x_fc_job_sort_id by default so the
user sees foldable sections per Job Sorting bucket. -->
<record id="view_sale_order_list_fp_by_sorting" model="ir.ui.view">
<field name="name">sale.order.list.fp.by_sorting</field>
<field name="model">sale.order</field>
<field name="priority">99</field>
<field name="arch" type="xml">
<list string="Sale Orders by Sorting" create="0"
decoration-info="state == 'draft'"
decoration-muted="state == 'cancel'"
decoration-danger="x_fc_is_late_forecast">
<header>
<button name="%(action_fp_direct_order_wizard)d"
type="action"
string="New Order"
class="btn-primary"
display="always"/>
</header>
<field name="name" optional="show"/>
<field name="partner_id" optional="show"/>
<field name="x_fc_po_number" optional="show"/>
<field name="x_fc_customer_job_number" optional="show"/>
<field name="x_fc_job_sort_id" optional="show"
options="{'no_create_edit': False, 'no_open': True}"/>
<field name="x_fc_internal_deadline" optional="show"/>
<field name="commitment_date" string="Customer Deadline"
optional="show"/>
<field name="x_fc_order_completion_date" string="Completion"
optional="show"/>
<field name="x_fc_is_late_forecast" optional="hide"
widget="boolean_toggle"/>
<field name="x_fc_deadline_urgency" column_invisible="1"/>
<field name="x_fc_deadline_countdown" optional="show"
decoration-danger="x_fc_deadline_urgency == 'overdue'"
decoration-warning="x_fc_deadline_urgency == 'urgent'"
decoration-success="x_fc_deadline_urgency == 'safe'"/>
<field name="x_fc_wo_completion" optional="show"/>
<field name="x_fc_planned_start_date" optional="hide"/>
<field name="x_fc_part_numbers_summary" string="Part"
optional="show"/>
<field name="amount_total" sum="Total" optional="show"/>
<field name="x_fc_invoiced_amount" sum="Invoiced"
optional="hide"
widget="monetary"
options="{'currency_field': 'currency_id'}"/>
<field name="x_fc_fp_job_status" widget="html"
string="Job Status" optional="show" readonly="1"/>
<field name="x_fc_receiving_status" widget="badge"
optional="hide"
decoration-warning="x_fc_receiving_status == 'not_received'"
decoration-info="x_fc_receiving_status == 'partial'"
decoration-success="x_fc_receiving_status == 'received'"/>
<field name="x_fc_delivery_method" optional="hide"/>
<field name="currency_id" column_invisible="1"/>
<field name="state" widget="badge" optional="show"/>
</list>
</field>
</record>
<!-- Search view for the alternate list: surface "Group by Job
Sorting" as a search-default filter. -->
<record id="view_sale_order_search_fp_by_sorting" model="ir.ui.view">
<field name="name">sale.order.search.fp.by_sorting</field>
<field name="model">sale.order</field>
<field name="arch" type="xml">
<search string="Sale Orders by Sorting">
<field name="name"/>
<field name="partner_id"/>
<field name="x_fc_part_numbers_summary" string="Part"/>
<field name="x_fc_customer_job_number"/>
<field name="x_fc_po_number"/>
<field name="x_fc_job_sort_id"/>
<filter name="late_forecast" string="Late Forecast"
domain="[('x_fc_is_late_forecast','=',True)]"/>
<filter name="cancelled" string="Cancelled"
domain="[('state','=','cancel')]"/>
<separator/>
<group>
<filter name="group_by_job_sort"
string="Job Sorting"
context="{'group_by': 'x_fc_job_sort_id'}"/>
<filter name="group_by_customer"
string="Customer"
context="{'group_by': 'partner_id'}"/>
<filter name="group_by_state"
string="Status"
context="{'group_by': 'state'}"/>
</group>
</search>
</field>
</record>
<!-- Append the kanban view to the existing Sale Orders action so
users can switch from the flat list to the grouped-by-sorting
kanban (foldable columns, drag-drop bucket reassignment) via
the view-switcher icon in the top-right of the SO list. -->
<record id="action_fp_sale_orders" model="ir.actions.act_window">
<field name="view_mode">list,kanban,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_fp')}),
(0, 0, {'view_mode': 'kanban', 'view_id': ref('view_sale_order_kanban_fp_by_sorting')})]"/>
</record>
<record id="action_fp_sale_orders_by_sorting" model="ir.actions.act_window">
<field name="name">Sale Orders (by Sorting)</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form</field>
<field name="view_id" ref="view_sale_order_list_fp_by_sorting"/>
<field name="search_view_id" ref="view_sale_order_search_fp_by_sorting"/>
<field name="domain">[('state', 'not in', ('draft', 'sent'))]</field>
<field name="context">{'search_default_group_by_job_sort': 1}</field>
</record>
<menuitem id="menu_fp_sale_orders_by_sorting"
name="Sale Orders (by Sorting)"
parent="fusion_plating_configurator.menu_fp_sales"
action="action_fp_sale_orders_by_sorting"
sequence="12"/>
</odoo>

View File

@@ -123,6 +123,15 @@
<field name="commitment_date" string="Delivery Date"
readonly="state in ('cancel',)"/>
</xpath>
<!-- Job Sorting sits right under Payment Terms — a free-form
bucket that groups the SO in the "Sale Orders by Sorting"
list. Quick-create from the dropdown. -->
<xpath expr="//group[@name='order_details']/field[@name='payment_term_id']" position="after">
<field name="x_fc_job_sort_id"
options="{'no_create_edit': False, 'no_open': True}"
placeholder="Type to create a new bucket..."/>
</xpath>
<xpath expr="//notebook" position="inside">
<page string="Plating" name="plating_tab">
<!-- Multi-part summary: read-only list of every order line
@@ -368,19 +377,29 @@
class="btn-primary"
display="always"/>
</header>
<field name="name"/>
<field name="partner_id"/>
<field name="x_fc_po_number"/>
<field name="name" optional="show"/>
<field name="partner_id" optional="show"/>
<field name="x_fc_po_number" optional="show"/>
<field name="x_fc_customer_job_number" optional="show"/>
<field name="x_fc_internal_deadline" optional="show"/>
<field name="commitment_date" string="Customer Deadline" optional="show"/>
<field name="x_fc_order_completion_date" string="Completion" optional="show"/>
<field name="x_fc_is_late_forecast" optional="hide" widget="boolean_toggle"/>
<field name="x_fc_deadline_countdown" optional="show"/>
<field name="x_fc_deadline_urgency" column_invisible="1"/>
<field name="x_fc_deadline_countdown" optional="show"
decoration-danger="x_fc_deadline_urgency == 'overdue'"
decoration-warning="x_fc_deadline_urgency == 'urgent'"
decoration-success="x_fc_deadline_urgency == 'safe'"/>
<field name="x_fc_wo_completion" optional="show"/>
<field name="x_fc_planned_start_date" optional="hide"/>
<field name="x_fc_part_catalog_id" optional="hide"/>
<field name="amount_total" sum="Total"/>
<!-- "Part" column — walks order_line.x_fc_part_catalog_id
and shows a compact summary (e.g. "M1234, M5678
(+3 more)"). The header x_fc_part_catalog_id field
is rarely populated in the configurator flow; the
line carries the authoritative part link. -->
<field name="x_fc_part_numbers_summary" string="Part"
optional="show"/>
<field name="amount_total" sum="Total" optional="show"/>
<field name="x_fc_invoiced_amount" sum="Invoiced" optional="hide"
widget="monetary"
options="{'currency_field': 'currency_id'}"/>
@@ -390,12 +409,21 @@
<field name="x_fc_margin_percent" optional="hide"
widget="percentage"/>
<field name="x_fc_is_blanket_order" optional="hide"/>
<!-- Single Job Status pill. Renders as HTML with a
per-kind class (.fp-kind-*) so every phase carries
its own distinct tint — see
static/src/scss/fp_job_status_pill.scss. -->
<field name="x_fc_fp_job_status" widget="html"
string="Job Status" optional="show"
readonly="1"/>
<field name="x_fc_receiving_status" widget="badge"
optional="hide"
decoration-warning="x_fc_receiving_status == 'not_received'"
decoration-info="x_fc_receiving_status == 'partial'"
decoration-success="x_fc_receiving_status == 'received'"/>
<field name="x_fc_delivery_method" optional="hide"/>
<field name="currency_id" column_invisible="1"/>
<field name="state" widget="badge"/>
<field name="state" widget="badge" optional="show"/>
</list>
</field>
</record>
@@ -465,8 +493,8 @@
<field name="model">sale.order</field>
<field name="arch" type="xml">
<list string="Quotations" decoration-muted="state == 'cancel'">
<field name="name"/>
<field name="partner_id"/>
<field name="name" optional="show"/>
<field name="partner_id" optional="show"/>
<field name="x_fc_part_numbers_summary" optional="show"/>
<field name="x_fc_po_number" optional="hide"/>
<field name="x_fc_customer_job_number" optional="hide"/>
@@ -474,15 +502,16 @@
<field name="validity_date" string="Expires" optional="show"/>
<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="amount_total" sum="Total" optional="show"/>
<field name="x_fc_is_signed" widget="boolean_toggle"
string="Signed" optional="show"/>
<field name="x_fc_email_status" widget="badge"
optional="show"
decoration-info="x_fc_email_status == 'sent'"
decoration-warning="x_fc_email_status == 'opened'"
decoration-success="x_fc_email_status == 'won'"/>
<field name="currency_id" column_invisible="1"/>
<field name="state" widget="badge"/>
<field name="state" widget="badge" optional="show"/>
</list>
</field>
</record>
@@ -582,7 +611,10 @@
</field>
</record>
<!-- ===== Window Action — Confirmed Sale Orders ===== -->
<!-- ===== Window Action — Confirmed Sale Orders =====
The kanban view_mode + kanban view_id are appended in
fp_so_job_sort_views.xml after the kanban view is defined, so
we don't hit a missing-ref at module load. -->
<record id="action_fp_sale_orders" model="ir.actions.act_window">
<field name="name">Sale Orders</field>
<field name="res_model">sale.order</field>

View File

@@ -79,6 +79,13 @@ class FpDirectOrderWizard(models.Model):
help="Customer's internal job number for cross-referencing. "
"Appears on work orders and invoices.",
)
job_sort_id = fields.Many2one(
'fp.so.job.sort',
string='Job Sorting',
help='Free-form bucket that groups the new SO in the '
'"Sale Orders by Sorting" list. Type a new label in the '
'dropdown to create a section on the fly.',
)
# ---- Scheduling ----
planned_start_date = fields.Date(
@@ -533,6 +540,7 @@ class FpDirectOrderWizard(models.Model):
'x_fc_po_pending': self.po_pending,
'x_fc_po_expected_date': self.po_expected_date or False,
'x_fc_customer_job_number': self.customer_job_number or False,
'x_fc_job_sort_id': self.job_sort_id.id or False,
'x_fc_planned_start_date': self.planned_start_date,
'x_fc_internal_deadline': self.internal_deadline,
'x_fc_lead_time_min_days': self.lead_time_min_days or 0,

View File

@@ -70,6 +70,9 @@
options="{'no_create_edit': True}"
invisible="not partner_id"/>
<field name="customer_job_number"/>
<field name="job_sort_id"
options="{'no_create_edit': False, 'no_open': True}"
placeholder="Type to create a new bucket..."/>
</group>
<group string="Purchase Order">
<field name="po_number"