changes
This commit is contained in:
@@ -267,10 +267,10 @@ Each phase bumps the patch version (19.0.3.0.0 → .1.0 → .2.0 → .3.0 → .4
|
||||
|
||||
These are flagged as assumptions to confirm against the live model, not blockers:
|
||||
|
||||
1. **Stage timestamp sources** — the 5 stages on `fp.portal.job` may not all have explicit timestamp fields. Confirm: do we need to add fields, or pull from the underlying `fp.job` / chatter? If fields are missing, add them as `Datetime` on `fp.portal.job` with `compute=` from the underlying records.
|
||||
1. **Stage timestamp sources** — RESOLVED 2026-05-17 Phase 3 investigation: `fp.portal.job` is intentionally decoupled from `fp.job` (no `job_id` link). Existing Date fields cover received/shipped only; the 3 middle stages had no timestamps. Decision: **Option B** — added per-stage `Datetime` fields (`received_at`, `in_progress_started_at`, `qc_started_at`, `ready_to_ship_at`, `shipped_at`) with a `write()` override that snapshots `fields.Datetime.now()` on state change. Idempotent — won't overwrite if already set.
|
||||
2. **Operator name surfacing** — customers seeing operator names is a privacy / policy question. Default in the spec is to show first-initial + last-name (e.g., "D. Mendez"). Confirm with EN Plating before shipping.
|
||||
3. **Stage-notes copy** — the example notes in the mockup ("Tank 4 · 45 min cycle · Day 3 of 7") are made up. Confirm what info is reasonable to share with the customer per stage.
|
||||
4. **Document linking edge cases** — what shows when there are 0 documents in a category? Spec assumes a "Will appear when…" placeholder card per the approved mockup. Verify EN Plating doesn't want the placeholder hidden entirely when empty.
|
||||
4. **Document linking edge cases** — RESOLVED 2026-05-17: `fp.portal.job` has no link to `sale.order` / `quote_request` / `part_catalog`, so V1 cannot reach PO / Drawing / Spec documents through the portal job alone. Decision: **Option C for V1** — surface only the directly-attached fields (`coc_attachment_id`, `packing_list_attachment_id`); render the From-You / Specifications / Quality (when CoC missing) / Shipping (when packing missing) groups as placeholder rows ("Will appear when ..." messaging per the approved mockup). V2 (separate change) will add `sale_order_id` Many2one to `fp.portal.job` and pull PO/drawing/spec docs via that link.
|
||||
5. **Dark mode** — explicitly deferred. If a customer logs in with `color_scheme=dark` set, what should they see? Default Bootstrap dark fallback is ugly. Suggest: force the portal to `color_scheme=light` for `share=True` users, or add a `prefers-color-scheme: light` meta tag. Document the choice during Phase 1.
|
||||
|
||||
---
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating',
|
||||
'version': '19.0.20.0.0',
|
||||
'version': '19.0.20.1.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
||||
'description': """
|
||||
|
||||
@@ -33,6 +33,11 @@ class FpProcessNode(models.Model):
|
||||
_parent_name = 'parent_id'
|
||||
_order = 'parent_path, sequence, id'
|
||||
_rec_name = 'display_name'
|
||||
# Search by both name and code in m2o autocomplete pickers (e.g. the
|
||||
# Process / Recipe field on the direct-order wizard line). Without
|
||||
# this, typing the recipe code (e.g. "ENP-STEEL-BASIC") didn't match
|
||||
# because display_name composes from `name` alone for recipe roots.
|
||||
_rec_names_search = ['name', 'code']
|
||||
|
||||
# ---- Identity & hierarchy ------------------------------------------------
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating — Certificates',
|
||||
'version': '19.0.6.0.0',
|
||||
'version': '19.0.6.1.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Certificate registry for CoC, thickness reports, and quality documents.',
|
||||
'description': """
|
||||
|
||||
@@ -15,9 +15,10 @@ class ResConfigSettings(models.TransientModel):
|
||||
x_fc_owner_user_id = fields.Many2one(
|
||||
related='company_id.x_fc_owner_user_id', readonly=False,
|
||||
)
|
||||
x_fc_coc_signature_override = fields.Binary(
|
||||
related='company_id.x_fc_coc_signature_override', readonly=False,
|
||||
)
|
||||
# x_fc_coc_signature_override was retired 2026-05-17 — cert
|
||||
# signatures now come from the certifier user's Plating Signature
|
||||
# only. Field stays on res.company (no migration) but is no longer
|
||||
# exposed in settings.
|
||||
x_fc_nadcap_logo = fields.Binary(
|
||||
related='company_id.x_fc_nadcap_logo', readonly=False,
|
||||
)
|
||||
|
||||
@@ -22,12 +22,14 @@
|
||||
<field name="x_fc_owner_user_id"
|
||||
options="{'no_create': True, 'no_open': True}"/>
|
||||
</setting>
|
||||
<setting id="fp_coc_sig_override"
|
||||
string="Signature Override Image"
|
||||
help="Upload a scanned signature here to override the owner user's employee signature (useful if they don't have an HR record).">
|
||||
<field name="x_fc_coc_signature_override"
|
||||
widget="image" class="oe_avatar"/>
|
||||
</setting>
|
||||
<!-- Removed 2026-05-17: the "Signature Override Image"
|
||||
setting was retired. Cert signatures now come
|
||||
exclusively from the certifier user's Plating
|
||||
Signature (Preferences → My Profile →
|
||||
x_fc_signature_image). The DB column on
|
||||
res.company stays in place for now (no
|
||||
migration) but is no longer read by any
|
||||
report. -->
|
||||
</block>
|
||||
|
||||
<block title="Accreditation Logos"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating — Configurator',
|
||||
'version': '19.0.21.0.0',
|
||||
'version': '19.0.21.4.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Quotation configurator with part catalog, coating configs, and formula-based pricing engine.',
|
||||
'description': """
|
||||
|
||||
@@ -62,7 +62,7 @@ class FpDirectOrderLine(models.Model):
|
||||
process_variant_id = fields.Many2one(
|
||||
'fusion.plating.process.node',
|
||||
string='Process Variant',
|
||||
domain="[('parent_id', '=', False), ('node_type', '=', 'recipe')]",
|
||||
domain="[('id', 'in', recipe_choice_ids)]",
|
||||
ondelete='set null',
|
||||
help='Pick any recipe — the part\'s own variant, another part\'s '
|
||||
'recipe, or a template from the library. Cross-part picks '
|
||||
@@ -70,6 +70,44 @@ class FpDirectOrderLine(models.Model):
|
||||
'scoped. Use the Customize button to open the Process '
|
||||
'Composer for the chosen variant.',
|
||||
)
|
||||
# Pre-computed pick-list backing the process_variant_id m2o.
|
||||
# Scope: parent recipes (templates) + this part's own variants +
|
||||
# any recipe previously used on this customer's SO lines. Replaces
|
||||
# the previous wide-open domain that exposed every recipe in the
|
||||
# system. Recomputed on (part, customer) change.
|
||||
recipe_choice_ids = fields.Many2many(
|
||||
'fusion.plating.process.node',
|
||||
compute='_compute_recipe_choice_ids',
|
||||
string='Allowed Recipes (computed)',
|
||||
)
|
||||
|
||||
@api.depends('part_catalog_id', 'wizard_id.partner_id')
|
||||
def _compute_recipe_choice_ids(self):
|
||||
Node = self.env['fusion.plating.process.node']
|
||||
SOL = self.env['sale.order.line']
|
||||
for rec in self:
|
||||
ids = set()
|
||||
# 1) Templates — the "parent recipes" the operator sees first.
|
||||
templates = Node.search([
|
||||
('parent_id', '=', False),
|
||||
('node_type', '=', 'recipe'),
|
||||
('is_template', '=', True),
|
||||
])
|
||||
ids.update(templates.ids)
|
||||
# 2) This part's own variants (scoped recipes already cloned
|
||||
# onto the part).
|
||||
if rec.part_catalog_id:
|
||||
ids.update(rec.part_catalog_id.process_variant_ids.ids)
|
||||
# 3) Recipes previously used on this customer's SO lines.
|
||||
# Capped to avoid sweeping every history row on big customers.
|
||||
if rec.wizard_id and rec.wizard_id.partner_id:
|
||||
used = SOL.search([
|
||||
('order_id.partner_id', '=', rec.wizard_id.partner_id.id),
|
||||
('x_fc_process_variant_id', '!=', False),
|
||||
], order='create_date desc', limit=500
|
||||
).mapped('x_fc_process_variant_id')
|
||||
ids.update(used.ids)
|
||||
rec.recipe_choice_ids = [(6, 0, list(ids))]
|
||||
save_as_default_process = fields.Boolean(
|
||||
string='Set as Part Default',
|
||||
help='When ticked, the chosen process variant becomes this part\'s '
|
||||
@@ -152,13 +190,33 @@ class FpDirectOrderLine(models.Model):
|
||||
# fusion_plating_quality (where customer_spec_id field lives).
|
||||
has_default_spec = bool(getattr(
|
||||
part, 'x_fc_default_customer_spec_id', False))
|
||||
# New-part auto-suggest: if no default spec exists, this is
|
||||
# likely a first-time use of the part. Auto-tick the
|
||||
# push_to_defaults toggle so whatever Sarah picks becomes
|
||||
# the saved default — surface a warning popup so she knows.
|
||||
# `is_one_off` always wins (operator opted out of catalog
|
||||
# persistence), so don't auto-tick in that case.
|
||||
# The quality inherit's _onchange_part_default_spec ALSO
|
||||
# falls back to the most recent SO line for (part, customer)
|
||||
# when the part default is empty. If that lookup will find
|
||||
# a hit, this is NOT a first-time use from the operator's
|
||||
# perspective — the spec will silently pre-fill from history.
|
||||
# Suppress the warning in that case so we don't pop a
|
||||
# misleading "no saved specification" alert right when the
|
||||
# spec actually does auto-fill.
|
||||
has_history_spec = False
|
||||
partner = rec.wizard_id and rec.wizard_id.partner_id
|
||||
SOL = self.env['sale.order.line']
|
||||
if (partner
|
||||
and 'x_fc_customer_spec_id' in SOL._fields):
|
||||
has_history_spec = bool(SOL.search_count([
|
||||
('x_fc_part_catalog_id', '=', part.id),
|
||||
('order_id.partner_id', '=', partner.id),
|
||||
('x_fc_customer_spec_id', '!=', False),
|
||||
]))
|
||||
# New-part auto-suggest: if no default spec exists AND no
|
||||
# SO-line history exists, this is genuinely a first-time use
|
||||
# of the part. Auto-tick the push_to_defaults toggle so
|
||||
# whatever Sarah picks becomes the saved default — surface a
|
||||
# warning popup so she knows. `is_one_off` always wins
|
||||
# (operator opted out of catalog persistence), so don't
|
||||
# auto-tick in that case.
|
||||
if (not has_default_spec
|
||||
and not has_history_spec
|
||||
and not rec.is_one_off
|
||||
and not rec.push_to_defaults):
|
||||
rec.push_to_defaults = True
|
||||
@@ -512,17 +570,61 @@ class FpDirectOrderLine(models.Model):
|
||||
def _onchange_part_defaults(self):
|
||||
"""Seed defaults when a part is picked.
|
||||
|
||||
Order of precedence for "remember last entered" fields
|
||||
(process_variant_id, unit_price, tax_ids):
|
||||
1. What the operator already typed on this line — never clobber
|
||||
2. Most recent SO line for (part_catalog_id, partner) — the
|
||||
"last entered" carry-over so repeat orders feel sticky
|
||||
3. Fall back to product / part defaults
|
||||
|
||||
Spec auto-fill is handled by an inherit in fusion_plating_quality
|
||||
(the customer_spec_id field lives there).
|
||||
"""
|
||||
if not self.part_catalog_id:
|
||||
return
|
||||
# Seed default taxes from the FP-SERVICE product, fiscal-position
|
||||
# mapped from the customer. Only fills when the user hasn't set
|
||||
# taxes manually.
|
||||
self._fp_seed_from_last_so_line()
|
||||
# Fall back to product taxes if no prior SO line found and the
|
||||
# operator hasn't set taxes manually.
|
||||
if not self.tax_ids:
|
||||
self._seed_default_taxes()
|
||||
|
||||
def _fp_seed_from_last_so_line(self):
|
||||
"""Carry the (process variant, unit price, taxes) from the most
|
||||
recent SO line for this (part, customer) onto the wizard line.
|
||||
|
||||
Skips any field the operator has already filled. Quietly no-ops
|
||||
when no prior SO line exists, when the part has no partner yet,
|
||||
or when the line is for a one-off part (no expectation of
|
||||
history). Called from the part_catalog_id onchange.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not self.part_catalog_id or not self.wizard_id:
|
||||
return
|
||||
partner = self.wizard_id.partner_id
|
||||
if not partner:
|
||||
return
|
||||
recent = self.env['sale.order.line'].search([
|
||||
('x_fc_part_catalog_id', '=', self.part_catalog_id.id),
|
||||
('order_id.partner_id', '=', partner.id),
|
||||
], order='create_date desc', limit=1)
|
||||
if not recent:
|
||||
return
|
||||
# Process variant — only if the line doesn't already have a pick.
|
||||
# The part's default still applies as a fallback in
|
||||
# _onchange_part_clears_variant above; this beats it when the
|
||||
# customer's last SO had a specific variant.
|
||||
if not self.process_variant_id and recent.x_fc_process_variant_id:
|
||||
self.process_variant_id = recent.x_fc_process_variant_id
|
||||
# Unit price — only when blank/zero. Avoids overwriting a
|
||||
# quote-driven or hand-typed price.
|
||||
if not self.unit_price and recent.price_unit:
|
||||
self.unit_price = recent.price_unit
|
||||
# Taxes — only when blank. The downstream
|
||||
# _seed_default_taxes() fallback handles the no-prior-line case.
|
||||
# NB: SO line field is `tax_ids` (Odoo 19 renamed from tax_id).
|
||||
if not self.tax_ids and recent.tax_ids:
|
||||
self.tax_ids = [(6, 0, recent.tax_ids.ids)]
|
||||
|
||||
def _seed_default_taxes(self):
|
||||
"""Pick taxes from the FP-SERVICE product, mapped through the
|
||||
customer's fiscal position when one is set."""
|
||||
@@ -690,9 +792,11 @@ class FpDirectOrderLine(models.Model):
|
||||
return clone
|
||||
|
||||
def _fp_apply_recipe_polish(self):
|
||||
"""Post-write step: auto-clone any cross-part recipe pick and
|
||||
honour the Save-as-Default toggle. Called from create() and
|
||||
write()."""
|
||||
"""Post-write step: auto-clone any cross-part recipe pick, set
|
||||
the freshly-cloned recipe as the part's default IF this is the
|
||||
part's very first variant, and honour the manual Save-as-Default
|
||||
toggle for repeat orders that explicitly want to flip the
|
||||
default. Called from create() and write()."""
|
||||
for line in self:
|
||||
if not line.part_catalog_id or not line.process_variant_id:
|
||||
continue
|
||||
@@ -703,7 +807,19 @@ class FpDirectOrderLine(models.Model):
|
||||
if clone and clone.id != recipe.id:
|
||||
line.process_variant_id = clone.id
|
||||
recipe = clone
|
||||
if line.save_as_default_process and recipe.part_catalog_id:
|
||||
# Auto-default rule: if the part has no other variants
|
||||
# besides the one we just attached, this recipe becomes its
|
||||
# default automatically. Estimators don't have to remember
|
||||
# to tick "Save as Default" for first-time parts.
|
||||
other_variants = (
|
||||
line.part_catalog_id.process_variant_ids - recipe
|
||||
)
|
||||
should_auto_default = (
|
||||
bool(recipe.part_catalog_id)
|
||||
and not other_variants
|
||||
)
|
||||
if (line.save_as_default_process or should_auto_default) \
|
||||
and recipe.part_catalog_id:
|
||||
line.part_catalog_id.action_set_default_variant(recipe.id)
|
||||
|
||||
def action_customize_process(self):
|
||||
|
||||
@@ -552,13 +552,19 @@ class FpDirectOrderWizard(models.Model):
|
||||
resolved_parts[line.id] = part
|
||||
# Build the line header. Specification is optional; when
|
||||
# missing, drop it from the header rather than printing
|
||||
# "False - PartName Rev A".
|
||||
# "False - PartName Rev A". Same defensive treatment for
|
||||
# part identifier and revision — `%s` on a False/NULL field
|
||||
# used to print the literal string "False".
|
||||
spec = getattr(line, 'customer_spec_id', False)
|
||||
spec_label = (spec.display_name if spec else '') or _('No spec')
|
||||
header = '%s - %s Rev %s (x%d)' % (
|
||||
# Prefer part_number (set on every saved part); fall back to
|
||||
# name; skip the segment entirely if both are missing.
|
||||
part_label = part.part_number or part.name or ''
|
||||
rev_suffix = (' Rev %s' % part.revision) if part.revision else ''
|
||||
header = '%s - %s%s (x%d)' % (
|
||||
spec_label,
|
||||
part.name,
|
||||
part.revision,
|
||||
part_label or _('Unspecified part'),
|
||||
rev_suffix,
|
||||
line.quantity,
|
||||
)
|
||||
extended = (line.line_description or '').strip()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
{
|
||||
'name': 'Fusion Plating — Native Jobs',
|
||||
'version': '19.0.10.2.0',
|
||||
'version': '19.0.10.8.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
|
||||
'author': 'Nexa Systems Inc.',
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<field name="header_line" eval="False"/>
|
||||
<field name="header_spacing">0</field>
|
||||
<field name="disable_shrinking" eval="True"/>
|
||||
<!-- dpi=300 calibrated — see CLAUDE.md rule 14, 600 broke layout. -->
|
||||
<field name="dpi">300</field>
|
||||
</record>
|
||||
|
||||
@@ -47,6 +48,14 @@
|
||||
reads (so `_so or ...` doesn't NameError). We
|
||||
then override the ones we have data for. -->
|
||||
<t t-call="fusion_plating_reports.report_fp_wo_sticker_defaults"/>
|
||||
<!-- Multi-line trigger: parent SO has 2+ part-bearing lines.
|
||||
Even though this job is for a single specific part (jobs
|
||||
are grouped by recipe+part+coating+thickness+SN), the
|
||||
consolidated PO sticker is the requested behaviour. -->
|
||||
<t t-set="_so_part_lines" t-value="job.sale_order_id
|
||||
and job.sale_order_id.order_line.filtered(lambda l: l.x_fc_part_catalog_id)
|
||||
or job.env['sale.order.line']"/>
|
||||
<t t-set="_multi_line" t-value="len(_so_part_lines) >= 2"/>
|
||||
<!-- Pre-resolve the variables the shared inner template
|
||||
expects, sourcing them from fp.job's native fields. -->
|
||||
<t t-set="_order_id" t-value="job.name"/>
|
||||
@@ -54,13 +63,13 @@
|
||||
<t t-set="_scan_path" t-value="'/fp/job/'"/>
|
||||
<t t-set="_mo" t-value="False"/>
|
||||
<t t-set="_so" t-value="job.sale_order_id"/>
|
||||
<t t-set="_line" t-value="job.sale_order_line_ids[:1]"/>
|
||||
<t t-set="_part" t-value="('part_catalog_id' in job._fields and job.part_catalog_id) or False"/>
|
||||
<t t-set="_spec" t-value="('customer_spec_id' in job._fields and job.customer_spec_id) or False"/>
|
||||
<t t-set="_process" t-value="job.recipe_id or False"/>
|
||||
<t t-set="_due" t-value="job.date_deadline or False"/>
|
||||
<t t-set="_qty" t-value="job.qty"/>
|
||||
<t t-set="_qty_total" t-value="job.qty"/>
|
||||
<t t-set="_line" t-value="False if _multi_line else job.sale_order_line_ids[:1]"/>
|
||||
<t t-set="_part" t-value="False if _multi_line else (('part_catalog_id' in job._fields and job.part_catalog_id) or False)"/>
|
||||
<t t-set="_spec" t-value="False if _multi_line else (('customer_spec_id' in job._fields and job.customer_spec_id) or False)"/>
|
||||
<t t-set="_process" t-value="False if _multi_line else (job.recipe_id or False)"/>
|
||||
<t t-set="_due" t-value="(job.sale_order_id and job.sale_order_id.commitment_date) if _multi_line else (job.date_deadline or False)"/>
|
||||
<t t-set="_qty" t-value="sum(_so_part_lines.mapped('product_uom_qty')) if _multi_line else job.qty"/>
|
||||
<t t-set="_qty_total" t-value="1 if _multi_line else job.qty"/>
|
||||
<t t-set="_partner_name" t-value="job.partner_id.name"/>
|
||||
<!-- The fp.job's own name (WH/JOB/00033) is already
|
||||
printed in the header as "WO #...", so suppress
|
||||
@@ -91,25 +100,31 @@
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="job">
|
||||
<t t-call="fusion_plating_reports.report_fp_wo_sticker_defaults"/>
|
||||
<t t-set="_so_part_lines" t-value="job.sale_order_id
|
||||
and job.sale_order_id.order_line.filtered(lambda l: l.x_fc_part_catalog_id)
|
||||
or job.env['sale.order.line']"/>
|
||||
<t t-set="_multi_line" t-value="len(_so_part_lines) >= 2"/>
|
||||
<t t-set="_order_id" t-value="job.name"/>
|
||||
<t t-set="_scan_id" t-value="job.id"/>
|
||||
<t t-set="_scan_path" t-value="'/fp/job/'"/>
|
||||
<t t-set="_mo" t-value="False"/>
|
||||
<t t-set="_so" t-value="job.sale_order_id"/>
|
||||
<t t-set="_line" t-value="job.sale_order_line_ids[:1]"/>
|
||||
<t t-set="_part" t-value="('part_catalog_id' in job._fields and job.part_catalog_id) or False"/>
|
||||
<t t-set="_spec" t-value="('customer_spec_id' in job._fields and job.customer_spec_id) or False"/>
|
||||
<t t-set="_process" t-value="job.recipe_id or False"/>
|
||||
<t t-set="_due" t-value="job.date_deadline or False"/>
|
||||
<t t-set="_qty" t-value="job.qty"/>
|
||||
<t t-set="_qty_total" t-value="job.qty"/>
|
||||
<t t-set="_line" t-value="False if _multi_line else job.sale_order_line_ids[:1]"/>
|
||||
<t t-set="_part" t-value="False if _multi_line else (('part_catalog_id' in job._fields and job.part_catalog_id) or False)"/>
|
||||
<t t-set="_spec" t-value="False if _multi_line else (('customer_spec_id' in job._fields and job.customer_spec_id) or False)"/>
|
||||
<t t-set="_process" t-value="False if _multi_line else (job.recipe_id or False)"/>
|
||||
<t t-set="_due" t-value="(job.sale_order_id and job.sale_order_id.commitment_date) if _multi_line else (job.date_deadline or False)"/>
|
||||
<t t-set="_qty" t-value="sum(_so_part_lines.mapped('product_uom_qty')) if _multi_line else job.qty"/>
|
||||
<t t-set="_qty_total" t-value="1 if _multi_line else job.qty"/>
|
||||
<t t-set="_partner_name" t-value="job.partner_id.name"/>
|
||||
<t t-set="_mo_ref" t-value="''"/>
|
||||
<!-- Internal override: read x_fc_internal_description from
|
||||
the first linked SO line. -->
|
||||
<t t-set="_notes_content" t-value="(job.sale_order_line_ids[:1]
|
||||
and 'x_fc_internal_description' in job.sale_order_line_ids[:1]._fields
|
||||
and job.sale_order_line_ids[:1].x_fc_internal_description) or '-'"/>
|
||||
the first linked SO line. Multi-line PO blanks it
|
||||
since each line has its own description. -->
|
||||
<t t-set="_notes_content" t-value="'-' if _multi_line else
|
||||
((job.sale_order_line_ids[:1]
|
||||
and 'x_fc_internal_description' in job.sale_order_line_ids[:1]._fields
|
||||
and job.sale_order_line_ids[:1].x_fc_internal_description) or '-')"/>
|
||||
<t t-call="fusion_plating_reports.report_fp_wo_sticker_inner"/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
<div class="page fp-wo-detail">
|
||||
<style>
|
||||
.fp-wo-detail { font-family: Arial, sans-serif; font-size: 9pt; color: #000; }
|
||||
.fp-wo-detail h1 { text-align: center; font-size: 18pt; margin: 0 0 14px 0; font-weight: bold; color: #1a4d80; }
|
||||
.fp-wo-detail h1 { text-align: center; font-size: 22pt; margin: 0 0 14px 0; font-weight: bold; color: #2e2e2e; }
|
||||
.fp-wo-detail h3 { font-size: 11pt; margin: 12px 0 4px 0; font-weight: bold; }
|
||||
.fp-wo-detail .fp-meta { font-size: 8.5pt; color: #444; margin-bottom: 6px; }
|
||||
.fp-wo-detail table.bordered,
|
||||
@@ -134,8 +134,8 @@
|
||||
keeps captions glued to their image. */
|
||||
.fp-wo-detail .fp-photo-section { margin-top: 18px; }
|
||||
.fp-wo-detail .fp-photo-section h2 {
|
||||
font-size: 13pt; font-weight: bold; color: #1a4d80;
|
||||
margin: 0 0 8px 0; border-bottom: 2px solid #1a4d80;
|
||||
font-size: 13pt; font-weight: bold; color: #2e2e2e;
|
||||
margin: 0 0 8px 0; border-bottom: 2px solid #c1c1c1;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
.fp-wo-detail .fp-photo-grid {
|
||||
@@ -162,7 +162,7 @@
|
||||
font-size: 8pt; color: #444; line-height: 1.25;
|
||||
}
|
||||
.fp-wo-detail .fp-photo-ref {
|
||||
font-size: 8pt; color: #1a4d80; font-style: italic;
|
||||
font-size: 8pt; color: #4e4e4e; font-style: italic;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* Inline signature image inside the step
|
||||
@@ -243,6 +243,65 @@
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- ===== Contract Review (QA-005) block =====
|
||||
Surfaces the part's QA-005 audit trail on
|
||||
every WO Detail print. Per Rule 4 of the
|
||||
contract-review flow, on repeat orders the
|
||||
contract-review WO step is auto-completed
|
||||
at job creation using the reviewer's
|
||||
identity + date from the existing review;
|
||||
this block makes that audit visible to
|
||||
customers and inspectors. Hidden when no
|
||||
review exists (e.g. customer doesn't
|
||||
require contract review). -->
|
||||
<t t-set="review"
|
||||
t-value="('part_catalog_id' in job._fields and job.part_catalog_id
|
||||
and 'x_fc_contract_review_id' in job.part_catalog_id._fields
|
||||
and job.part_catalog_id.x_fc_contract_review_id) or False"/>
|
||||
<t t-if="review">
|
||||
<t t-set="_signer"
|
||||
t-value="review.s30_signed_by or review.s20_signed_by"/>
|
||||
<t t-set="_signed_dt"
|
||||
t-value="review.s30_signed_date or review.s20_signed_date"/>
|
||||
<t t-set="_initials_src"
|
||||
t-value="(_signer and _signer.name) or ''"/>
|
||||
<t t-set="_initials"
|
||||
t-value="''.join([w[:1].upper() for w in _initials_src.split()[:3]])"/>
|
||||
<table class="bordered" style="margin-top: 8px;">
|
||||
<tr>
|
||||
<th colspan="4" style="font-size: 10pt;">Contract Review (QA-005)</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="width: 28%;">Status</th>
|
||||
<th style="width: 30%;">Reviewer</th>
|
||||
<th style="width: 14%;">Initials</th>
|
||||
<th style="width: 28%;">Date Reviewed</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<t t-if="review.state == 'complete'">
|
||||
<span style="font-weight: bold; color: #2e2e2e;">QA-005 Approved</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span style="color: #aa0000;">Pending — <span t-esc="dict(review._fields['state'].selection).get(review.state, review.state)"/></span>
|
||||
</t>
|
||||
</td>
|
||||
<td>
|
||||
<span t-esc="(_signer and _signer.name) or '—'"/>
|
||||
</td>
|
||||
<td>
|
||||
<span style="font-weight: bold;" t-esc="_initials or '—'"/>
|
||||
</td>
|
||||
<td>
|
||||
<t t-if="_signed_dt">
|
||||
<span t-esc="job.fp_format_local(_signed_dt, '%Y-%m-%d')"/>
|
||||
</t>
|
||||
<t t-else="">—</t>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</t>
|
||||
|
||||
<div class="fp-spec">Specification(s):
|
||||
<span style="font-weight: normal;"
|
||||
t-esc="(job.recipe_id and job.recipe_id.name) or '—'"/>
|
||||
@@ -481,21 +540,30 @@
|
||||
<div style="page-break-before: always;"/>
|
||||
<div style="height: 8mm;"/>
|
||||
|
||||
<!-- Certifier = the job's plating manager. Pulls
|
||||
their Plating Signature (`x_fc_signature_image`)
|
||||
from Preferences → My Profile. Falls back to
|
||||
the company owner's signature, then to the
|
||||
settings override only if no user has one. -->
|
||||
<t t-set="certifier_user" t-value="job.manager_id or (('x_fc_owner_user_id' in company._fields and company.x_fc_owner_user_id) or False)"/>
|
||||
<!-- Certifier = the company's QA Manager, set in
|
||||
Settings → Fusion Plating → Contract Review.
|
||||
Falls back to the job's plating manager, then
|
||||
the company owner, then the settings signature
|
||||
override. Pulls the certifier's Plating
|
||||
Signature (`x_fc_signature_image`) from
|
||||
Preferences → My Profile.
|
||||
Resolution priority added 2026-05-17 per
|
||||
ops request — was auto-defaulting to the
|
||||
current user (whoever the job manager
|
||||
happened to be) which signed every cert as
|
||||
the wrong person. -->
|
||||
<t t-set="_qa_managers" t-value="('x_fc_qa_manager_user_ids' in company._fields and company.x_fc_qa_manager_user_ids) or False"/>
|
||||
<t t-set="certifier_user" t-value="(_qa_managers and _qa_managers[:1])
|
||||
or job.manager_id
|
||||
or (('x_fc_owner_user_id' in company._fields and company.x_fc_owner_user_id) or False)"/>
|
||||
<t t-set="signature_img" t-value="False"/>
|
||||
<t t-if="certifier_user and 'x_fc_signature_image' in certifier_user._fields and certifier_user.x_fc_signature_image">
|
||||
<t t-set="signature_img" t-value="certifier_user.x_fc_signature_image"/>
|
||||
</t>
|
||||
<!-- Final fallback: company-level override for sites
|
||||
whose certifier hasn't uploaded their signature yet. -->
|
||||
<t t-if="not signature_img and 'x_fc_coc_signature_override' in company._fields and company.x_fc_coc_signature_override">
|
||||
<t t-set="signature_img" t-value="company.x_fc_coc_signature_override"/>
|
||||
</t>
|
||||
<!-- Signature Override Image fallback retired
|
||||
2026-05-17. If no certifier user has uploaded
|
||||
their Plating Signature, the cert prints
|
||||
without a signature image (Name still shows). -->
|
||||
<t t-set="signer_name" t-value="(certifier_user and certifier_user.name) or ''"/>
|
||||
|
||||
<t t-set="_cust_stmt" t-value="(job.partner_id and 'x_fc_cert_statement' in job.partner_id._fields and job.partner_id.x_fc_cert_statement) or False"/>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?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.
|
||||
|
||||
Adds a leading "Part #" column to the customer portal's Sales Order
|
||||
products table. Reads sale.order.line.x_fc_part_catalog_id.part_number
|
||||
(defined in fusion_plating_configurator). The existing second column
|
||||
keeps line.name — the customer-facing description.
|
||||
|
||||
Layout after this inherit:
|
||||
| Part # | Description | Quantity | Unit Price | [Disc] | [Taxes] | Amount |
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<template id="sale_order_portal_content_fp_part_column"
|
||||
inherit_id="sale.sale_order_portal_content">
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Header: Part # before "Products" -->
|
||||
<!-- ============================================================ -->
|
||||
<xpath expr="//th[@id='product_name_header']" position="before">
|
||||
<th class="text-start" id="product_part_header">Part #</th>
|
||||
</xpath>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Product rows: Part # cell before the description cell. -->
|
||||
<!-- Reads from x_fc_part_catalog_id; service / freight / display -->
|
||||
<!-- lines with no part catalog show an empty cell. -->
|
||||
<!-- ============================================================ -->
|
||||
<xpath expr="//td[@name='td_product_name']" position="before">
|
||||
<td name="td_product_part" t-att-class="padding_class">
|
||||
<span t-if="'x_fc_part_catalog_id' in line._fields and line.x_fc_part_catalog_id"
|
||||
t-out="line.x_fc_part_catalog_id.part_number"/>
|
||||
</td>
|
||||
</xpath>
|
||||
|
||||
<!-- Combo header rows: empty Part # cell (the part lives on the
|
||||
child lines, not the combo header). -->
|
||||
<xpath expr="//td[@name='td_combo_name']" position="before">
|
||||
<td name="td_combo_part" t-att-class="padding_class"/>
|
||||
</xpath>
|
||||
|
||||
<!-- Collapsed section group rows: empty Part # cell. -->
|
||||
<xpath expr="//td[@name='td_section_group_name']" position="before">
|
||||
<td name="td_section_group_part"/>
|
||||
</xpath>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Section / subsection rows: extend the name-cell colspan by -->
|
||||
<!-- one so the new Part # column is absorbed into the section -->
|
||||
<!-- title band (sections span every column before the section -->
|
||||
<!-- total cell). The "not show_section_total" branch already -->
|
||||
<!-- uses 99 so no change needed there. -->
|
||||
<!-- ============================================================ -->
|
||||
<xpath expr="//t[@t-set='section_name_colspan'][1]" position="attributes">
|
||||
<attribute name="t-value">4 + (1 if display_discount else 0) + (1 if display_taxes else 0)</attribute>
|
||||
</xpath>
|
||||
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating — Quality (QMS)',
|
||||
'version': '19.0.6.2.0',
|
||||
'version': '19.0.6.6.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Native QMS for plating shops: NCR, CAPA, calibration, AVL, FAIR, '
|
||||
'internal audits, customer specs, document control. CE + EE compatible.',
|
||||
@@ -128,6 +128,10 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
||||
'fusion_plating_quality/static/src/scss/fp_quality_dashboard.scss',
|
||||
'fusion_plating_quality/static/src/xml/fp_quality_dashboard.xml',
|
||||
'fusion_plating_quality/static/src/js/fp_quality_dashboard.js',
|
||||
# Contract Review flow — forced redirect into QA-005 after
|
||||
# an estimator creates a part under a toggle-on customer.
|
||||
# Pairs with the bus.bus push in fp_part_catalog.py:create.
|
||||
'fusion_plating_quality/static/src/js/contract_review_redirect.js',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
|
||||
@@ -23,11 +23,30 @@ class FpDirectOrderLine(models.Model):
|
||||
|
||||
@api.onchange('part_catalog_id')
|
||||
def _onchange_part_default_spec(self):
|
||||
"""Pre-fill the line's specification from the part's default."""
|
||||
"""Pre-fill the line's specification.
|
||||
|
||||
Priority (first non-empty result wins, matches the same
|
||||
"remember last entered" pattern used by process / unit_price /
|
||||
tax / thickness in the base wizard line):
|
||||
1. What the operator already typed on this line — never clobber
|
||||
2. Most recent SO line for (part, customer) where a spec
|
||||
was set — the "repeat order" carry-over
|
||||
3. Part's stored default — x_fc_default_customer_spec_id
|
||||
"""
|
||||
for rec in self:
|
||||
if (rec.part_catalog_id
|
||||
and rec.part_catalog_id.x_fc_default_customer_spec_id
|
||||
and not rec.customer_spec_id):
|
||||
if not rec.part_catalog_id or rec.customer_spec_id:
|
||||
continue
|
||||
partner = rec.wizard_id.partner_id if rec.wizard_id else False
|
||||
if partner:
|
||||
recent = self.env['sale.order.line'].search([
|
||||
('x_fc_part_catalog_id', '=', rec.part_catalog_id.id),
|
||||
('order_id.partner_id', '=', partner.id),
|
||||
('x_fc_customer_spec_id', '!=', False),
|
||||
], order='create_date desc', limit=1)
|
||||
if recent:
|
||||
rec.customer_spec_id = recent.x_fc_customer_spec_id
|
||||
continue
|
||||
if rec.part_catalog_id.x_fc_default_customer_spec_id:
|
||||
rec.customer_spec_id = (
|
||||
rec.part_catalog_id.x_fc_default_customer_spec_id
|
||||
)
|
||||
@@ -37,12 +56,22 @@ class FpDirectOrderWizard(models.Model):
|
||||
_inherit = 'fp.direct.order.wizard'
|
||||
|
||||
def action_create_order(self):
|
||||
"""Carry customer_spec_id from each wizard line to its SO line.
|
||||
"""Carry customer_spec_id from each wizard line to its SO line,
|
||||
and (when the operator opted in via push_to_defaults) save the
|
||||
spec back to the part as its new default so the next order
|
||||
auto-fills without relying on a SO-line history lookup.
|
||||
|
||||
The base method (in configurator) builds the SO with all the
|
||||
coating/treatment/process fields. We can't insert spec into the
|
||||
vals dict from here without a circular dep, so post-create we
|
||||
pair wizard lines to SO lines by sequence and patch.
|
||||
|
||||
The push-to-defaults block mirrors the base wizard's thickness
|
||||
push (action_create_order's "6. Push-to-defaults" loop) — spec
|
||||
lives here in the quality module so the back-write lives here
|
||||
too. Only fills when the part default is currently empty so we
|
||||
never clobber an existing default that the part-form user set
|
||||
explicitly.
|
||||
"""
|
||||
result = super().action_create_order()
|
||||
if self.sale_order_id:
|
||||
@@ -55,4 +84,15 @@ class FpDirectOrderWizard(models.Model):
|
||||
for wiz_line, so_line in zip(wiz_lines, so_lines):
|
||||
if wiz_line.customer_spec_id and not so_line.x_fc_customer_spec_id:
|
||||
so_line.x_fc_customer_spec_id = wiz_line.customer_spec_id.id
|
||||
# Spec push-to-default — only on first-time parts that
|
||||
# had the toggle auto-ticked (or manually ticked). Skip
|
||||
# one-off parts and parts that already have a default.
|
||||
if (wiz_line.push_to_defaults
|
||||
and not wiz_line.is_one_off
|
||||
and wiz_line.customer_spec_id
|
||||
and wiz_line.part_catalog_id
|
||||
and not wiz_line.part_catalog_id.x_fc_default_customer_spec_id):
|
||||
wiz_line.part_catalog_id.x_fc_default_customer_spec_id = (
|
||||
wiz_line.customer_spec_id.id
|
||||
)
|
||||
return result
|
||||
|
||||
@@ -208,11 +208,8 @@ class FpPartCatalog(models.Model):
|
||||
% part_label,
|
||||
'message': _(
|
||||
'Customer %(c)s requires a Contract Review '
|
||||
'(QA-005) on new parts. The review record '
|
||||
'has been pre-created — open it using the '
|
||||
'Contract Review smart button at the top '
|
||||
'of the part form, or from your Activities '
|
||||
'inbox.'
|
||||
'(QA-005) on new parts. Opening the review '
|
||||
'form now…'
|
||||
) % {'c': customer_label},
|
||||
'type': 'warning',
|
||||
'sticky': True,
|
||||
@@ -224,6 +221,30 @@ class FpPartCatalog(models.Model):
|
||||
'review notification', part.id, exc_info=True,
|
||||
)
|
||||
|
||||
# 3) Forced redirect into the QA-005 form. The frontend
|
||||
# listener (see fusion_plating_quality/static/src/js/
|
||||
# contract_review_redirect.js) intercepts this bus
|
||||
# notification and calls action.doAction() to navigate.
|
||||
# Rule 3 of the contract-review flow: customer-toggle-on
|
||||
# parts force the QA-005 to open right after save so the
|
||||
# estimator can complete (or visibly defer) the review
|
||||
# before moving on.
|
||||
try:
|
||||
Bus._sendone(
|
||||
self.env.user.partner_id,
|
||||
'fusion_plating.contract_review_redirect',
|
||||
{
|
||||
'review_id': review.id,
|
||||
'part_id': part.id,
|
||||
'part_label': part_label,
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
_logger.warning(
|
||||
'fp.part.catalog %s: could not push contract-'
|
||||
'review redirect bus message', part.id, exc_info=True,
|
||||
)
|
||||
|
||||
# ---- Actions -------------------------------------------------------------
|
||||
|
||||
def action_start_contract_review(self):
|
||||
|
||||
@@ -19,7 +19,7 @@ class ResCompany(models.Model):
|
||||
'res_company_qa_assistant_rel',
|
||||
'company_id',
|
||||
'user_id',
|
||||
string='QA Assistant Signers',
|
||||
string='Planning Signers',
|
||||
domain=[('share', '=', False)],
|
||||
help='Users authorised to sign Section 2.0 (Planning / Production '
|
||||
'Review) on a Contract Review. Plating Managers can sign '
|
||||
@@ -30,11 +30,13 @@ class ResCompany(models.Model):
|
||||
'res_company_qa_manager_rel',
|
||||
'company_id',
|
||||
'user_id',
|
||||
string='QA Manager Signers',
|
||||
string='QA Manager',
|
||||
domain=[('share', '=', False)],
|
||||
help='Users authorised to sign Section 3.0 (Quality Review) on a '
|
||||
'Contract Review. Plating Managers can sign regardless of '
|
||||
'this list.',
|
||||
help='QA Manager(s) for the shop. Signs Section 3.0 (Quality '
|
||||
'Review) on a Contract Review AND is the default Certified '
|
||||
'By signer on the Work Order Detail report (first user in '
|
||||
'the list is used for the cert signature). Plating Managers '
|
||||
'can sign Contract Reviews regardless of this list.',
|
||||
)
|
||||
|
||||
def _fp_get_qa_signers(self, section):
|
||||
|
||||
@@ -15,10 +15,10 @@ class ResConfigSettings(models.TransientModel):
|
||||
x_fc_qa_assistant_user_ids = fields.Many2many(
|
||||
related='company_id.x_fc_qa_assistant_user_ids',
|
||||
readonly=False,
|
||||
string='QA Assistant Signers',
|
||||
string='Planning Signers',
|
||||
)
|
||||
x_fc_qa_manager_user_ids = fields.Many2many(
|
||||
related='company_id.x_fc_qa_manager_user_ids',
|
||||
readonly=False,
|
||||
string='QA Manager Signers',
|
||||
string='QA Manager',
|
||||
)
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/** @odoo-module **/
|
||||
/*
|
||||
* Copyright 2026 Nexa Systems Inc.
|
||||
* License OPL-1 (Odoo Proprietary License v1.0)
|
||||
* Part of the Fusion Plating product family.
|
||||
*
|
||||
* Forces the QA-005 Contract Review form to open right after an
|
||||
* estimator creates a new fp.part.catalog under a customer that has
|
||||
* x_fc_contract_review_required = True. Rule 3 of the contract-review
|
||||
* flow.
|
||||
*
|
||||
* Server side (fusion_plating_quality/models/fp_part_catalog.py):
|
||||
* after auto-creating the fp.contract.review record on part create,
|
||||
* push a bus notification of type "fusion_plating.contract_review_redirect"
|
||||
* with payload {review_id, part_id, part_label}.
|
||||
*
|
||||
* Frontend side (this file): a service subscribes to that bus type
|
||||
* on session start. When a payload arrives, dispatch an ir.actions.
|
||||
* act_window opening the review's form so the user lands on the QA-005
|
||||
* automatically. They can still close it and come back later — the
|
||||
* WO-step gate (rule 5) is the backstop.
|
||||
*/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const contractReviewRedirectService = {
|
||||
dependencies: ["bus_service", "action"],
|
||||
start(env, { bus_service: bus, action }) {
|
||||
bus.subscribe(
|
||||
"fusion_plating.contract_review_redirect",
|
||||
(payload) => {
|
||||
if (!payload || !payload.review_id) {
|
||||
return;
|
||||
}
|
||||
action.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
res_model: "fp.contract.review",
|
||||
res_id: payload.review_id,
|
||||
view_mode: "form",
|
||||
views: [[false, "form"]],
|
||||
target: "current",
|
||||
});
|
||||
},
|
||||
);
|
||||
bus.start();
|
||||
},
|
||||
};
|
||||
|
||||
registry
|
||||
.category("services")
|
||||
.add("fusion_plating_contract_review_redirect", contractReviewRedirectService);
|
||||
@@ -22,7 +22,7 @@
|
||||
permissions are affected. Plating Managers can
|
||||
always sign regardless of these lists.">
|
||||
<setting id="fp_qa_assistant_signers"
|
||||
string="QA Assistant Signers"
|
||||
string="Planning Signers"
|
||||
help="Users authorised to sign Section 2.0
|
||||
(Planning / Production Review). Typically
|
||||
one or two people. Leave empty if any
|
||||
@@ -33,10 +33,13 @@
|
||||
widget="many2many_tags"/>
|
||||
</setting>
|
||||
<setting id="fp_qa_manager_signers"
|
||||
string="QA Manager Signers"
|
||||
help="Users authorised to sign Section 3.0
|
||||
(Quality Review). Typically the QA
|
||||
Manager for the shop.">
|
||||
string="QA Manager"
|
||||
help="QA Manager(s) for the shop. Signs
|
||||
Section 3.0 (Quality Review) of the
|
||||
Contract Review, AND appears as the
|
||||
Certified By signer on the Work Order
|
||||
Detail report. The first user in the
|
||||
list is used for the cert signature.">
|
||||
<field name="x_fc_qa_manager_user_ids"
|
||||
widget="many2many_tags"/>
|
||||
</setting>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
{
|
||||
'name': 'Fusion Plating — Reports',
|
||||
'version': '19.0.11.1.0',
|
||||
'version': '19.0.11.14.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'PDF reports for Fusion Plating: quote, SO, WO, packing, BoL, CoC, invoice, receipt, quality + compliance.',
|
||||
'depends': [
|
||||
|
||||
@@ -307,6 +307,9 @@
|
||||
<field name="header_line" eval="False"/>
|
||||
<field name="header_spacing">0</field>
|
||||
<field name="disable_shrinking" eval="True"/>
|
||||
<!-- dpi=300 is the calibrated value for this paperformat; the
|
||||
sticker inner's px-based geometry is tuned against it. Do
|
||||
NOT bump (see CLAUDE.md rule 14 — 600 broke layout). -->
|
||||
<field name="dpi">300</field>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -6,14 +6,17 @@
|
||||
|
||||
Shared CSS for all Fusion Plating reports (portrait + landscape).
|
||||
|
||||
The primary colour is driven by the active company's
|
||||
res.company.primary_color field (Settings → Company → Report Layout),
|
||||
falling back to #1d1f1e when the company has no brand colour set.
|
||||
Section-header band: #c1c1c1 (neutral grey) with #4e4e4e text
|
||||
Document titles (h2/h4): #4e4e4e
|
||||
Hardcoded — used to follow `res.company.primary_color` but the
|
||||
client wanted a uniform neutral palette across every FP report
|
||||
regardless of company branding. `fp_primary` is kept in scope for
|
||||
any per-report template that still wants the company colour.
|
||||
|
||||
To keep section-header markup concise in individual report files,
|
||||
a utility class `.fp-header-primary` is exposed — apply that class
|
||||
to any `<th>` or `<td>` that should render as a primary-coloured
|
||||
section banner (e.g. CARGO DESCRIPTION, PAYMENT DETAILS).
|
||||
to any `<th>` or `<td>` that should render as a section banner
|
||||
(e.g. CARGO DESCRIPTION, PAYMENT DETAILS).
|
||||
-->
|
||||
<odoo>
|
||||
<!-- ============================================================= -->
|
||||
@@ -24,10 +27,28 @@
|
||||
<t t-set="fp_primary" t-value="(_fp_company.primary_color if _fp_company else False) or '#1d1f1e'"/>
|
||||
<style>
|
||||
.fp-report { font-family: Arial, sans-serif; font-size: 10pt; color: #000; }
|
||||
.fp-report table { width: 100%; border-collapse: collapse; margin-bottom: 10px; }
|
||||
.fp-report table.bordered, .fp-report table.bordered th, .fp-report table.bordered td { border: 1px solid #000; }
|
||||
.fp-report th { background-color: <t t-out="fp_primary"/>; color: white; padding: 6px 8px; font-weight: bold; text-align: center; font-size: 9pt; }
|
||||
.fp-report td { padding: 6px 8px; vertical-align: top; font-size: 10pt; }
|
||||
.fp-report table { width: 100%; border-collapse: collapse; border-spacing: 0; margin-bottom: 10px; }
|
||||
/* Standard collapse + longhand borders + background-clip.
|
||||
Tried border-collapse:separate with single-side-per-cell
|
||||
(right+bottom on cell, top+left on table) to fix wkhtmltopdf's
|
||||
slightly-lighter-verticals quirk — but the `separate` model
|
||||
makes column widths drift between tables with different
|
||||
column counts, so tables stacked on the page no longer
|
||||
line up at the outer edges. Reverted. The collapse pattern
|
||||
gives correct alignment; the verticals may render a hair
|
||||
softer than horizontals on entech wkhtmltopdf but that's
|
||||
the less-bad trade-off vs misaligned tables. */
|
||||
.fp-report table.bordered { border: 0; border-collapse: collapse; border-spacing: 0; }
|
||||
.fp-report table.bordered th,
|
||||
.fp-report table.bordered td {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #000;
|
||||
background-clip: padding-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.fp-report th { background-color: #c1c1c1; color: #1d1f1e; padding: 6px 8px; font-weight: bold; text-align: center; font-size: 9pt; background-clip: padding-box; }
|
||||
.fp-report td { padding: 6px 8px; vertical-align: top; font-size: 10pt; background-clip: padding-box; }
|
||||
.fp-report .text-center { text-align: center; }
|
||||
.fp-report .text-end { text-align: right; }
|
||||
.fp-report .text-start { text-align: left; }
|
||||
@@ -35,20 +56,45 @@
|
||||
.fp-report .client-bg { background-color: #fff3e0; }
|
||||
.fp-report .section-row { background-color: #f0f0f0; font-weight: bold; }
|
||||
.fp-report .note-row { font-style: italic; color: #555; font-size: 9pt; }
|
||||
.fp-report h4 { color: <t t-out="fp_primary"/>; margin: 0 0 15px 0; font-size: 16pt; }
|
||||
.fp-report .totals-table { border: 1px solid #000; border-collapse: collapse; }
|
||||
.fp-report .totals-table td { border: 1px solid #000; padding: 6px 8px; }
|
||||
.fp-report h4 { color: #2e2e2e; margin: 0 0 15px 0; font-size: 20pt; }
|
||||
.fp-report .totals-table { border: 0; border-collapse: collapse; border-spacing: 0; }
|
||||
.fp-report .totals-table td {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #000;
|
||||
padding: 6px 8px;
|
||||
background-clip: padding-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.fp-report .info-header { background-color: #f5f5f5; color: #333; }
|
||||
.fp-report .adp-header { background-color: #e3f2fd; color: #333; }
|
||||
.fp-report .highlight-box { border: 2px solid <t t-out="fp_primary"/>; background-color: #eaf2f8; padding: 10px; margin: 10px 0; }
|
||||
.fp-report .fp-header-primary { background-color: <t t-out="fp_primary"/>; color: white; }
|
||||
.fp-report .highlight-box { border: 2px solid #c1c1c1; background-color: #f5f5f5; padding: 10px; margin: 10px 0; }
|
||||
.fp-report .fp-header-primary { background-color: #c1c1c1; color: #1d1f1e; }
|
||||
.fp-report .paid-stamp { color: #28a745; font-size: 36pt; font-weight: bold; border: 4px solid #28a745; padding: 10px 20px; transform: rotate(-8deg); display: inline-block; }
|
||||
.fp-report .status-ok { color: #2e7d32; font-weight: bold; }
|
||||
.fp-report .status-warning { color: #f57f17; font-weight: bold; }
|
||||
.fp-report .status-fail { color: #c62828; font-weight: bold; }
|
||||
.fp-report .sig-line { border-bottom: 1px solid #000; height: 60px; margin-bottom: 4px; }
|
||||
.fp-report .sig-table { width: 100%; border-collapse: collapse; margin-top: 16px; border: 1px solid #000; page-break-inside: avoid; break-inside: avoid; }
|
||||
.fp-report .sig-table .sig-cell { padding: 14px 12px 8px 12px; vertical-align: top; border: 1px solid #000; page-break-inside: avoid; break-inside: avoid; }
|
||||
.fp-report .sig-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
margin-top: 16px;
|
||||
border: 0;
|
||||
page-break-inside: avoid;
|
||||
break-inside: avoid;
|
||||
}
|
||||
.fp-report .sig-table .sig-cell {
|
||||
padding: 14px 12px 8px 12px;
|
||||
vertical-align: top;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #000;
|
||||
background-clip: padding-box;
|
||||
box-sizing: border-box;
|
||||
page-break-inside: avoid;
|
||||
break-inside: avoid;
|
||||
}
|
||||
.fp-report .small-muted { font-size: 8pt; color: #666; }
|
||||
.fp-report .fp-cell-mid { vertical-align: middle !important; }
|
||||
.fp-report .fp-keep-together { page-break-inside: avoid; break-inside: avoid; }
|
||||
@@ -65,9 +111,18 @@
|
||||
<t t-set="fp_primary" t-value="(_fp_company.primary_color if _fp_company else False) or '#1d1f1e'"/>
|
||||
<style>
|
||||
.fp-landscape { font-family: Arial, sans-serif; font-size: 10pt; color: #000; }
|
||||
.fp-landscape table { width: 100%; border-collapse: collapse; margin-bottom: 6px; }
|
||||
.fp-landscape table.bordered, .fp-landscape table.bordered th, .fp-landscape table.bordered td { border: 1px solid #000; }
|
||||
.fp-landscape th { background-color: <t t-out="fp_primary"/>; color: white; padding: 4px 8px; font-weight: bold; font-size: 9pt; }
|
||||
.fp-landscape table { width: 100%; border-collapse: collapse; border-spacing: 0; margin-bottom: 6px; }
|
||||
/* Standard collapse + longhand + background-clip — see comment in fp_portrait_styles. */
|
||||
.fp-landscape table.bordered { border: 0; border-collapse: collapse; border-spacing: 0; }
|
||||
.fp-landscape table.bordered th,
|
||||
.fp-landscape table.bordered td {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #000;
|
||||
background-clip: padding-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.fp-landscape th { background-color: #c1c1c1; color: #1d1f1e; padding: 4px 8px; font-weight: bold; font-size: 9pt; background-clip: padding-box; }
|
||||
.fp-landscape td { padding: 4px 8px; vertical-align: top; font-size: 9.5pt; }
|
||||
.fp-landscape .text-center { text-align: center; }
|
||||
.fp-landscape .text-end { text-align: right; }
|
||||
@@ -76,13 +131,13 @@
|
||||
.fp-landscape .client-bg { background-color: #fff3e0; }
|
||||
.fp-landscape .section-row { background-color: #f0f0f0; font-weight: bold; }
|
||||
.fp-landscape .note-row { font-style: italic; color: #555; }
|
||||
.fp-landscape h2 { color: <t t-out="fp_primary"/>; margin: 4px 0; font-size: 18pt; }
|
||||
.fp-landscape h2 { color: #2e2e2e; margin: 4px 0; font-size: 22pt; }
|
||||
.fp-landscape .info-table td { padding: 8px 12px; font-size: 11pt; }
|
||||
.fp-landscape .info-table th { background-color: #f5f5f5; color: #333; font-size: 10pt; padding: 6px 12px; }
|
||||
.fp-landscape .totals-table { border: 1px solid #000; }
|
||||
.fp-landscape .totals-table td { border: 1px solid #000; padding: 8px 12px; font-size: 11pt; }
|
||||
.fp-landscape .highlight-box { border: 2px solid <t t-out="fp_primary"/>; background-color: #eaf2f8; padding: 6px 10px; margin: 6px 0; font-size: 9pt; }
|
||||
.fp-landscape .fp-header-primary { background-color: <t t-out="fp_primary"/>; color: white; }
|
||||
.fp-landscape .highlight-box { border: 2px solid #c1c1c1; background-color: #f5f5f5; padding: 6px 10px; margin: 6px 0; font-size: 9pt; }
|
||||
.fp-landscape .fp-header-primary { background-color: #c1c1c1; color: #1d1f1e; }
|
||||
.fp-landscape .paid-stamp { color: #28a745; font-size: 42pt; font-weight: bold; border: 4px solid #28a745; padding: 10px 20px; transform: rotate(-8deg); display: inline-block; }
|
||||
.fp-landscape .status-ok { color: #2e7d32; font-weight: bold; }
|
||||
.fp-landscape .status-warning { color: #f57f17; font-weight: bold; }
|
||||
|
||||
@@ -26,15 +26,16 @@
|
||||
<!-- ================================================================== -->
|
||||
<template id="coc_body">
|
||||
<t t-set="is_fr" t-value="LANG == 'fr'"/>
|
||||
<t t-set="owner_sig" t-value="False"/>
|
||||
<t t-if="company.x_fc_owner_user_id">
|
||||
<t t-set="_emp" t-value="company.x_fc_owner_user_id.employee_ids[:1]"/>
|
||||
<t t-if="_emp and 'signature' in _emp._fields">
|
||||
<t t-set="owner_sig" t-value="_emp['signature']"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-set="signature_img" t-value="company.x_fc_coc_signature_override or owner_sig"/>
|
||||
<t t-set="signer_name" t-value="doc.certified_by_id.name or (company.x_fc_owner_user_id.name if company.x_fc_owner_user_id else '')"/>
|
||||
<!-- Signer + signature resolution (2026-05-17): unified with the
|
||||
WO Detail certifier pattern. Signer = cert's certified_by
|
||||
user; falls back to the company owner. Signature image is
|
||||
that user's Plating Signature (x_fc_signature_image from
|
||||
Preferences → My Profile). The previous HR Employee
|
||||
signature lookup was retired in favour of this single
|
||||
source so all FP reports pull from the same field. -->
|
||||
<t t-set="signer_user" t-value="doc.certified_by_id or company.x_fc_owner_user_id or False"/>
|
||||
<t t-set="signature_img" t-value="(signer_user and 'x_fc_signature_image' in signer_user._fields and signer_user.x_fc_signature_image) or False"/>
|
||||
<t t-set="signer_name" t-value="(signer_user and signer_user.name) or ''"/>
|
||||
|
||||
<style>
|
||||
.fp-coc { font-family: Arial, sans-serif; font-size: 9pt; color: #000;
|
||||
|
||||
@@ -208,16 +208,13 @@
|
||||
|
||||
<hr class="heavy"/>
|
||||
|
||||
<!-- Sign-off block (re-uses owner_user_id signature pattern from coc_body) -->
|
||||
<t t-set="owner_sig" t-value="False"/>
|
||||
<t t-if="company.x_fc_owner_user_id">
|
||||
<t t-set="_emp" t-value="company.x_fc_owner_user_id.employee_ids[:1]"/>
|
||||
<t t-if="_emp and 'signature' in _emp._fields">
|
||||
<t t-set="owner_sig" t-value="_emp['signature']"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-set="signature_img" t-value="company.x_fc_coc_signature_override or owner_sig"/>
|
||||
<t t-set="signer_name" t-value="(doc.certified_by_id and doc.certified_by_id.name) or (company.x_fc_owner_user_id and company.x_fc_owner_user_id.name) or ''"/>
|
||||
<!-- Sign-off block — unified with WO Detail / CoC (2026-05-17).
|
||||
Signer = cert's certified_by user → falls back to company
|
||||
owner. Signature image = signer's Plating Signature
|
||||
(x_fc_signature_image from Preferences → My Profile). -->
|
||||
<t t-set="signer_user" t-value="doc.certified_by_id or company.x_fc_owner_user_id or False"/>
|
||||
<t t-set="signature_img" t-value="(signer_user and 'x_fc_signature_image' in signer_user._fields and signer_user.x_fc_signature_image) or False"/>
|
||||
<t t-set="signer_name" t-value="(signer_user and signer_user.name) or ''"/>
|
||||
|
||||
<!-- Sub 12c+ — cert statement: per-customer override → company default → hardcoded fallback -->
|
||||
<t t-set="_cust_stmt" t-value="('x_fc_cert_statement' in doc.partner_id._fields and doc.partner_id.x_fc_cert_statement) or False"/>
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
<div class="page">
|
||||
|
||||
<h4>
|
||||
<span t-if="doc.move_type == 'out_invoice' and doc.state == 'posted'">Invoice </span>
|
||||
<span t-elif="doc.move_type == 'out_invoice' and doc.state == 'draft'">Draft Invoice </span>
|
||||
<span t-elif="doc.move_type == 'out_refund'">Credit Note </span>
|
||||
<span t-elif="doc.move_type == 'in_invoice'">Vendor Bill </span>
|
||||
<span t-if="doc.move_type == 'out_invoice' and doc.state == 'posted'">Invoice # </span>
|
||||
<span t-elif="doc.move_type == 'out_invoice' and doc.state == 'draft'">Draft Invoice # </span>
|
||||
<span t-elif="doc.move_type == 'out_refund'">Credit Note # </span>
|
||||
<span t-elif="doc.move_type == 'in_invoice'">Vendor Bill # </span>
|
||||
<span t-field="doc.name"/>
|
||||
</h4>
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
<span t-field="doc.amount_tax" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="background-color: #eaf2f8;">
|
||||
<tr style="background-color: #c1c1c1;">
|
||||
<td><strong>Grand Total</strong></td>
|
||||
<td class="text-end"><strong>
|
||||
<span t-field="doc.amount_total" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
@@ -205,10 +205,10 @@
|
||||
<div class="page">
|
||||
|
||||
<h2 style="text-align: left;">
|
||||
<span t-if="doc.move_type == 'out_invoice' and doc.state == 'posted'">Invoice </span>
|
||||
<span t-elif="doc.move_type == 'out_invoice' and doc.state == 'draft'">Draft Invoice </span>
|
||||
<span t-elif="doc.move_type == 'out_refund'">Credit Note </span>
|
||||
<span t-elif="doc.move_type == 'in_invoice'">Vendor Bill </span>
|
||||
<span t-if="doc.move_type == 'out_invoice' and doc.state == 'posted'">Invoice # </span>
|
||||
<span t-elif="doc.move_type == 'out_invoice' and doc.state == 'draft'">Draft Invoice # </span>
|
||||
<span t-elif="doc.move_type == 'out_refund'">Credit Note # </span>
|
||||
<span t-elif="doc.move_type == 'in_invoice'">Vendor Bill # </span>
|
||||
<span t-field="doc.name"/>
|
||||
</h2>
|
||||
|
||||
@@ -341,7 +341,7 @@
|
||||
<span t-field="doc.amount_tax" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="background-color: #eaf2f8;">
|
||||
<tr style="background-color: #c1c1c1;">
|
||||
<td><strong>Grand Total</strong></td>
|
||||
<td class="text-end"><strong>
|
||||
<span t-field="doc.amount_total" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<div class="page">
|
||||
|
||||
<h4>
|
||||
Packing Slip —
|
||||
Packing Slip #
|
||||
<span t-field="doc.name"/>
|
||||
</h4>
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
<div class="page">
|
||||
|
||||
<h2 style="text-align: left;">
|
||||
Packing Slip —
|
||||
Packing Slip #
|
||||
<span t-field="doc.name"/>
|
||||
</h2>
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
|
||||
<!-- Title -->
|
||||
<h4>
|
||||
<span t-if="doc.state in ['draft','sent']">Quotation </span>
|
||||
<span t-else="">Sales Order </span>
|
||||
<span t-if="doc.state in ['draft','sent']">Quotation # </span>
|
||||
<span t-else="">Sales Order # </span>
|
||||
<span t-field="doc.name"/>
|
||||
</h4>
|
||||
|
||||
@@ -213,7 +213,7 @@
|
||||
<span t-field="doc.amount_tax" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="background-color: #eaf2f8;">
|
||||
<tr style="background-color: #c1c1c1;">
|
||||
<td><strong>Grand Total</strong></td>
|
||||
<td class="text-end"><strong>
|
||||
<span t-field="doc.amount_total" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
@@ -282,8 +282,8 @@
|
||||
|
||||
<!-- Title -->
|
||||
<h2 style="text-align: left;">
|
||||
<span t-if="doc.state in ['draft','sent']">Quotation </span>
|
||||
<span t-else="">Sales Order </span>
|
||||
<span t-if="doc.state in ['draft','sent']">Quotation # </span>
|
||||
<span t-else="">Sales Order # </span>
|
||||
<span t-field="doc.name"/>
|
||||
</h2>
|
||||
|
||||
@@ -492,7 +492,7 @@
|
||||
<span t-field="doc.amount_tax" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="background-color: #eaf2f8;">
|
||||
<tr style="background-color: #c1c1c1;">
|
||||
<td><strong>Grand Total</strong></td>
|
||||
<td class="text-end"><strong>
|
||||
<span t-field="doc.amount_total" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
|
||||
@@ -95,17 +95,27 @@
|
||||
<!-- Notes content — outer can pre-set this (e.g. the Internal
|
||||
variant passes line.x_fc_internal_description). Otherwise
|
||||
falls back to line.name (customer-facing description per
|
||||
Sub 2 Q6), then to part.name. -->
|
||||
<t t-set="_notes_content" t-value="_notes_content
|
||||
or (_line and _line.name)
|
||||
or (_part and _part.name)
|
||||
or '-'"/>
|
||||
Sub 2 Q6), then to part.name. Strip en/em-dash, smart
|
||||
quotes, and ellipsis defensively for the wkhtmltopdf font
|
||||
path on entech — same treatment as thickness above, which
|
||||
otherwise turns "—" into the "â€"" mojibake. -->
|
||||
<t t-set="_notes_raw" t-value="_notes_content
|
||||
or (_line and _line.name)
|
||||
or (_part and _part.name)
|
||||
or '-'"/>
|
||||
<t t-set="_notes_content" t-value="_notes_raw
|
||||
.replace(u'—', '-').replace(u'–', '-')
|
||||
.replace(u'‘', "'").replace(u'’', "'")
|
||||
.replace(u'“', '"').replace(u'”', '"')
|
||||
.replace(u'…', '...')"/>
|
||||
<!-- Inline the QR as base64 data URI so wkhtmltopdf doesn't need
|
||||
to fetch /report/barcode/ over the network during rendering.
|
||||
600x600 source at 300dpi print = ~515ppi effective — high-def
|
||||
scan reliability for the 4x6" label. -->
|
||||
1000x1000 source — Odoo core caps barcode area at 1.2M pixels
|
||||
(`width * height > 1200000` raises "Barcode too large"), so we
|
||||
stay under that ceiling. 1000x1000 at the 31mm wrapper gives
|
||||
~821ppi effective — far above the 203dpi thermal printer. -->
|
||||
<t t-set="_qr_src" t-value="env['ir.actions.report'].barcode_data_uri(
|
||||
'QR', _scan_url, width=600, height=600)"/>
|
||||
'QR', _scan_url, width=1000, height=1000)"/>
|
||||
|
||||
<style>
|
||||
@page { margin: 0; size: 152mm 102mm; }
|
||||
@@ -173,7 +183,8 @@
|
||||
adds around the QR pattern. We render the image larger than
|
||||
the wrapper and offset so the wrapper clips that border out.
|
||||
Wrapper 365px = ~30.9mm at 300dpi (30% larger than the
|
||||
previous 280px). 600x600 source = high-def at print scale. ---- */
|
||||
previous 280px). 1000x1000 source = print-sharp at the
|
||||
paperformat DPI (under Odoo's 1.2M-pixel barcode cap). ---- */
|
||||
.fp-sticker-qr-wrap {
|
||||
width: 365px;
|
||||
height: 365px;
|
||||
@@ -237,7 +248,13 @@
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.fp-sticker-strong { font-weight: 700; }
|
||||
/* Values used to be bold via .fp-sticker-strong (PO/Part#/Qty).
|
||||
Per ops, only the field title and the WO# header should be
|
||||
bold; values stay regular weight. Font sizes unchanged from
|
||||
the original layout — bumping them broke wkhtmltopdf's row
|
||||
packing on entech, so we accept the same visual weight as
|
||||
before. -->
|
||||
.fp-sticker-strong { font-weight: 400; }
|
||||
.fp-sticker-muted { color: #555; font-size: 30pt; }
|
||||
/* Notes column on the right side of the body. */
|
||||
.fp-notes-label {
|
||||
@@ -327,7 +344,10 @@
|
||||
<tr>
|
||||
<td class="fp-sticker-label">Part #:</td>
|
||||
<td class="fp-sticker-value">
|
||||
<t t-if="_part">
|
||||
<t t-if="_multi_line">
|
||||
<span class="fp-sticker-strong">Multiple Line Items</span>
|
||||
</t>
|
||||
<t t-elif="_part">
|
||||
<span class="fp-sticker-strong"
|
||||
t-esc="_part.part_number"/>
|
||||
<t t-if="_part.revision">
|
||||
@@ -408,6 +428,11 @@
|
||||
<t t-set="_scan_path" t-value="False"/>
|
||||
<t t-set="_notes_content" t-value="False"/>
|
||||
<t t-set="_qty_total" t-value="False"/>
|
||||
<!-- _multi_line = True signals "this PO has multiple part lines";
|
||||
Part # prints "Multiple Line Items", line-specific fields
|
||||
(SN/Thickness/Notes) auto-resolve to "-" via _line=False, and
|
||||
Qty shows the SO-line sum (outer sets _qty + _qty_total=1). -->
|
||||
<t t-set="_multi_line" t-value="False"/>
|
||||
</template>
|
||||
|
||||
<!-- ========== Outer template — mrp.workorder entry ========== -->
|
||||
@@ -454,24 +479,50 @@
|
||||
<template id="report_fp_so_sticker">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="so">
|
||||
<t t-foreach="so.order_line.filtered(lambda l: l.x_fc_part_catalog_id)"
|
||||
t-as="line">
|
||||
<t t-set="_part_lines"
|
||||
t-value="so.order_line.filtered(lambda l: l.x_fc_part_catalog_id)"/>
|
||||
<t t-if="len(_part_lines) >= 2">
|
||||
<!-- Multi-line PO: one consolidated sticker.
|
||||
Part # prints "Multiple Line Items", Qty is the
|
||||
sum of all part-line qtys. Per-box loop disabled
|
||||
(_qty_total=1) — the consolidated label is the
|
||||
master-skid label, not per-physical-box. -->
|
||||
<t t-call="fusion_plating_reports.report_fp_wo_sticker_defaults"/>
|
||||
<t t-set="_order_id" t-value="so.name"/>
|
||||
<t t-set="_scan_id" t-value="line.id"/>
|
||||
<t t-set="_scan_id" t-value="_part_lines[:1].id"/>
|
||||
<t t-set="_scan_path" t-value="'/fp/so-line/'"/>
|
||||
<t t-set="_mo" t-value="False"/>
|
||||
<t t-set="_so" t-value="so"/>
|
||||
<t t-set="_line" t-value="line"/>
|
||||
<t t-set="_part" t-value="line.x_fc_part_catalog_id"/>
|
||||
<t t-set="_spec" t-value="line.x_fc_customer_spec_id"/>
|
||||
<t t-set="_due" t-value="line.x_fc_part_deadline or so.commitment_date or False"/>
|
||||
<t t-set="_qty" t-value="line.product_uom_qty"/>
|
||||
<t t-set="_qty_total" t-value="line.product_uom_qty"/>
|
||||
<t t-set="_line" t-value="False"/>
|
||||
<t t-set="_part" t-value="False"/>
|
||||
<t t-set="_spec" t-value="False"/>
|
||||
<t t-set="_due" t-value="so.commitment_date or False"/>
|
||||
<t t-set="_qty" t-value="sum(_part_lines.mapped('product_uom_qty'))"/>
|
||||
<t t-set="_qty_total" t-value="1"/>
|
||||
<t t-set="_partner_name" t-value="so.partner_id.name"/>
|
||||
<t t-set="_mo_ref" t-value="''"/>
|
||||
<t t-set="_multi_line" t-value="True"/>
|
||||
<t t-call="fusion_plating_reports.report_fp_wo_sticker_inner"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-foreach="_part_lines" t-as="line">
|
||||
<t t-call="fusion_plating_reports.report_fp_wo_sticker_defaults"/>
|
||||
<t t-set="_order_id" t-value="so.name"/>
|
||||
<t t-set="_scan_id" t-value="line.id"/>
|
||||
<t t-set="_scan_path" t-value="'/fp/so-line/'"/>
|
||||
<t t-set="_mo" t-value="False"/>
|
||||
<t t-set="_so" t-value="so"/>
|
||||
<t t-set="_line" t-value="line"/>
|
||||
<t t-set="_part" t-value="line.x_fc_part_catalog_id"/>
|
||||
<t t-set="_spec" t-value="line.x_fc_customer_spec_id"/>
|
||||
<t t-set="_due" t-value="line.x_fc_part_deadline or so.commitment_date or False"/>
|
||||
<t t-set="_qty" t-value="line.product_uom_qty"/>
|
||||
<t t-set="_qty_total" t-value="line.product_uom_qty"/>
|
||||
<t t-set="_partner_name" t-value="so.partner_id.name"/>
|
||||
<t t-set="_mo_ref" t-value="''"/>
|
||||
<t t-call="fusion_plating_reports.report_fp_wo_sticker_inner"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
@@ -484,27 +535,52 @@
|
||||
<template id="report_fp_so_sticker_internal">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="so">
|
||||
<t t-foreach="so.order_line.filtered(lambda l: l.x_fc_part_catalog_id)"
|
||||
t-as="line">
|
||||
<t t-set="_part_lines"
|
||||
t-value="so.order_line.filtered(lambda l: l.x_fc_part_catalog_id)"/>
|
||||
<t t-if="len(_part_lines) >= 2">
|
||||
<!-- Multi-line PO: one consolidated sticker.
|
||||
Notes column blanked ("-") because each line has
|
||||
its own internal description. -->
|
||||
<t t-call="fusion_plating_reports.report_fp_wo_sticker_defaults"/>
|
||||
<t t-set="_order_id" t-value="so.name"/>
|
||||
<t t-set="_scan_id" t-value="line.id"/>
|
||||
<t t-set="_scan_id" t-value="_part_lines[:1].id"/>
|
||||
<t t-set="_scan_path" t-value="'/fp/so-line/'"/>
|
||||
<t t-set="_mo" t-value="False"/>
|
||||
<t t-set="_so" t-value="so"/>
|
||||
<t t-set="_line" t-value="line"/>
|
||||
<t t-set="_part" t-value="line.x_fc_part_catalog_id"/>
|
||||
<t t-set="_spec" t-value="line.x_fc_customer_spec_id"/>
|
||||
<t t-set="_due" t-value="line.x_fc_part_deadline or so.commitment_date or False"/>
|
||||
<t t-set="_qty" t-value="line.product_uom_qty"/>
|
||||
<t t-set="_qty_total" t-value="line.product_uom_qty"/>
|
||||
<t t-set="_line" t-value="False"/>
|
||||
<t t-set="_part" t-value="False"/>
|
||||
<t t-set="_spec" t-value="False"/>
|
||||
<t t-set="_due" t-value="so.commitment_date or False"/>
|
||||
<t t-set="_qty" t-value="sum(_part_lines.mapped('product_uom_qty'))"/>
|
||||
<t t-set="_qty_total" t-value="1"/>
|
||||
<t t-set="_partner_name" t-value="so.partner_id.name"/>
|
||||
<t t-set="_mo_ref" t-value="''"/>
|
||||
<!-- Internal override: read x_fc_internal_description -->
|
||||
<t t-set="_notes_content" t-value="('x_fc_internal_description' in line._fields
|
||||
and line.x_fc_internal_description) or '-'"/>
|
||||
<t t-set="_multi_line" t-value="True"/>
|
||||
<t t-set="_notes_content" t-value="'-'"/>
|
||||
<t t-call="fusion_plating_reports.report_fp_wo_sticker_inner"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-foreach="_part_lines" t-as="line">
|
||||
<t t-call="fusion_plating_reports.report_fp_wo_sticker_defaults"/>
|
||||
<t t-set="_order_id" t-value="so.name"/>
|
||||
<t t-set="_scan_id" t-value="line.id"/>
|
||||
<t t-set="_scan_path" t-value="'/fp/so-line/'"/>
|
||||
<t t-set="_mo" t-value="False"/>
|
||||
<t t-set="_so" t-value="so"/>
|
||||
<t t-set="_line" t-value="line"/>
|
||||
<t t-set="_part" t-value="line.x_fc_part_catalog_id"/>
|
||||
<t t-set="_spec" t-value="line.x_fc_customer_spec_id"/>
|
||||
<t t-set="_due" t-value="line.x_fc_part_deadline or so.commitment_date or False"/>
|
||||
<t t-set="_qty" t-value="line.product_uom_qty"/>
|
||||
<t t-set="_qty_total" t-value="line.product_uom_qty"/>
|
||||
<t t-set="_partner_name" t-value="so.partner_id.name"/>
|
||||
<t t-set="_mo_ref" t-value="''"/>
|
||||
<!-- Internal override: read x_fc_internal_description -->
|
||||
<t t-set="_notes_content" t-value="('x_fc_internal_description' in line._fields
|
||||
and line.x_fc_internal_description) or '-'"/>
|
||||
<t t-call="fusion_plating_reports.report_fp_wo_sticker_inner"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<div class="page">
|
||||
|
||||
<h4>
|
||||
Work Order Traveller —
|
||||
Work Order Traveller #
|
||||
<span t-field="doc.name"/>
|
||||
</h4>
|
||||
|
||||
@@ -265,7 +265,7 @@
|
||||
<div class="page">
|
||||
|
||||
<h2 style="text-align: left;">
|
||||
Work Order Traveller —
|
||||
Work Order Traveller #
|
||||
<span t-field="doc.name"/>
|
||||
</h2>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user