This commit is contained in:
gsinghpal
2026-05-21 03:42:46 -04:00
parent 1314f4581d
commit 53fd6114e7
10 changed files with 2624 additions and 28 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -2,3 +2,4 @@
from . import test_signed_pages_gate from . import test_signed_pages_gate
from . import test_application_received_wizard from . import test_application_received_wizard
from . import test_dashboard

View File

@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
from odoo.tests.common import TransactionCase, tagged
@tagged('-at_install', 'post_install', 'fusion_claims')
class TestFusionClaimsDashboard(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.Dashboard = cls.env['fusion.claims.dashboard']
cls.User = cls.env['res.users']
cls.Partner = cls.env['res.partner']
# Manager user (sees everything)
cls.manager = cls.User.create({
'name': 'Test Dashboard Manager',
'login': 'test_dash_mgr',
'group_ids': [
(4, cls.env.ref('fusion_claims.group_fusion_claims_manager').id),
(4, cls.env.ref('sales_team.group_sale_salesman').id),
],
})
# Sales rep (sees only own cases)
cls.salesrep = cls.User.create({
'name': 'Test Dashboard Salesrep',
'login': 'test_dash_rep',
'group_ids': [
(4, cls.env.ref('fusion_claims.group_fusion_claims_user').id),
(4, cls.env.ref('sales_team.group_sale_salesman').id),
],
})
cls.partner = cls.Partner.create({'name': 'Test Client'})
def test_dashboard_record_creates(self):
dashboard = self.Dashboard.create({})
self.assertTrue(dashboard.id, "Dashboard record should be creatable")
self.assertEqual(dashboard.name, 'Dashboard')
def test_role_filter_empty_for_manager(self):
dashboard = self.Dashboard.with_user(self.manager).create({})
self.assertEqual(dashboard._role_filter_domain(), [],
"Manager should see all cases (empty domain)")
def test_role_filter_restricts_for_salesrep(self):
dashboard = self.Dashboard.with_user(self.salesrep).create({})
domain = dashboard._role_filter_domain()
self.assertEqual(domain, [('user_id', '=', self.salesrep.id)],
"Sales rep should see only their own SOs")
def test_is_manager_true_for_manager(self):
dashboard = self.Dashboard.with_user(self.manager).create({})
self.assertTrue(dashboard.is_manager)
def test_is_manager_false_for_salesrep(self):
dashboard = self.Dashboard.with_user(self.salesrep).create({})
self.assertFalse(dashboard.is_manager)

View File

@@ -5,7 +5,7 @@
{ {
'name': 'Fusion Plating — Certificates', 'name': 'Fusion Plating — Certificates',
'version': '19.0.7.7.0', 'version': '19.0.7.8.0',
'category': 'Manufacturing/Plating', 'category': 'Manufacturing/Plating',
'summary': 'Certificate registry for CoC, thickness reports, and quality documents.', 'summary': 'Certificate registry for CoC, thickness reports, and quality documents.',
'description': """ 'description': """

View File

@@ -594,8 +594,41 @@ class FpCertificate(models.Model):
_logger.warning( _logger.warning(
'Cert %s: PDF render failed: %s', rec.name, e, 'Cert %s: PDF render failed: %s', rec.name, e,
) )
# Back-fill the CoC attachment onto the linked delivery
# if one exists already. Job._fp_create_delivery handles
# the create-time case (cert issued before delivery
# spawned); this handles the inverse (delivery spawned
# first, cert issued later). Best-effort.
try:
rec._fp_sync_coc_to_delivery()
except Exception as e:
_logger.warning(
'Cert %s: CoC->delivery sync failed: %s',
rec.name, e,
)
rec.message_post(body=_('Certificate issued.')) rec.message_post(body=_('Certificate issued.'))
def _fp_sync_coc_to_delivery(self):
"""Push this CoC's attachment onto its job's delivery so the
shipping crew sees the CoC ready to print without hunting for
the cert. Only acts on `coc` certs with an attachment_id;
delivery field must exist and be empty (don't overwrite an
operator's manual choice).
"""
self.ensure_one()
if self.certificate_type != 'coc' or not self.attachment_id:
return
job = self.x_fc_job_id if 'x_fc_job_id' in self._fields else False
if not job or not job.delivery_id:
return
delivery = job.delivery_id.sudo()
if 'coc_attachment_id' not in delivery._fields:
return
if delivery.coc_attachment_id:
# Operator already picked one; don't overwrite.
return
delivery.coc_attachment_id = self.attachment_id.id
def _fp_render_and_attach_pdf(self): def _fp_render_and_attach_pdf(self):
"""Render the CoC PDF via the bound report action, OPTIONALLY """Render the CoC PDF via the bound report action, OPTIONALLY
merge the Fischerscope thickness report PDF (uploaded by the merge the Fischerscope thickness report PDF (uploaded by the

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0) # License OPL-1 (Odoo Proprietary License v1.0)
{ {
'name': 'Fusion Plating — Native Jobs', 'name': 'Fusion Plating — Native Jobs',
'version': '19.0.10.16.8', 'version': '19.0.10.16.9',
'category': 'Manufacturing/Plating', 'category': 'Manufacturing/Plating',
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.', 'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
'author': 'Nexa Systems Inc.', 'author': 'Nexa Systems Inc.',

View File

@@ -1675,34 +1675,73 @@ class FpJob(models.Model):
look up by job_ref. Setting both ends keeps every consumer look up by job_ref. Setting both ends keeps every consumer
happy. happy.
Phase A — mirrors x_fc_carrier_id and x_fc_outbound_shipment_id Auto-populates everything we can resolve from upstream
from the linked receiving so the delivery carries the shipping records so the shipping crew doesn't have to re-type
choices made at receipt time. Shipping crew can override later. addresses / contacts / dates that already exist on the SO:
- delivery_address_id, contact_name, contact_phone — SO's
partner_shipping_id (falls back to partner_id)
- scheduled_date — SO.commitment_date
- source_facility_id — job.facility_id
- x_fc_carrier_id, x_fc_outbound_shipment_id — from the
SO's first receiving record (set at receive time)
- coc_attachment_id — issued cert.attachment_id for this
job (if a CoC is already issued before delivery exists;
otherwise the cert's action_issue back-fills it later)
Everything skips silently when the source field doesn't
exist or the source value is blank, so older install
topologies and partially-configured jobs still get a
delivery — just less pre-filled.
""" """
self.ensure_one() self.ensure_one()
if self.delivery_id: if self.delivery_id:
return return
Delivery = self.env['fusion.plating.delivery'].sudo() Delivery = self.env['fusion.plating.delivery'].sudo()
vals = self._fp_resolve_delivery_defaults(Delivery)
try:
delivery = Delivery.create(vals)
self.delivery_id = delivery.id
except Exception as e:
_logger.warning(
"Job %s: failed to auto-create delivery: %s", self.name, e,
)
def _fp_resolve_delivery_defaults(self, Delivery):
"""Build the create-vals for a fresh delivery, OR the
write-vals for refreshing an existing one. Centralised so
the create path, the per-cert post-issue sync, and any
future 'Refresh from Source' button all stay consistent.
"""
self.ensure_one()
vals = {'partner_id': self.partner_id.id} vals = {'partner_id': self.partner_id.id}
if 'x_fc_job_id' in Delivery._fields: if 'x_fc_job_id' in Delivery._fields:
vals['x_fc_job_id'] = self.id vals['x_fc_job_id'] = self.id
if 'job_ref' in Delivery._fields: if 'job_ref' in Delivery._fields:
vals['job_ref'] = self.name vals['job_ref'] = self.name
if 'x_fc_job_id' not in Delivery._fields \ # Delivery address + contact details from the SO. shipping
and 'job_ref' not in Delivery._fields: # partner is preferred (that's where parts physically go);
_logger.warning( # fall back to the SO's main partner when no separate ship-to.
"Job %s: fusion.plating.delivery has no job link field; " so = self.sale_order_id
"delivery created without job back-reference.", self.name, ship_to = (so.partner_shipping_id or so.partner_id) if so else False
) if ship_to:
# Mirror outbound carrier + shipment from the SO's first if 'delivery_address_id' in Delivery._fields:
# receiving record. If there are multiple receivings (split vals['delivery_address_id'] = ship_to.id
# shipments), the shipping crew can change either field on the if 'contact_name' in Delivery._fields and ship_to.name:
# delivery form. Defensive: skip when fields aren't present vals['contact_name'] = ship_to.name
# (older instance) or no receiving exists. if 'contact_phone' in Delivery._fields:
if (self.sale_order_id vals['contact_phone'] = ship_to.phone or ship_to.mobile or ''
and 'x_fc_receiving_ids' in self.sale_order_id._fields # Scheduled date — operator can adjust; this just primes it
and self.sale_order_id.x_fc_receiving_ids): # so they're not staring at a blank field.
recv = self.sale_order_id.x_fc_receiving_ids[:1] if so and so.commitment_date and 'scheduled_date' in Delivery._fields:
vals['scheduled_date'] = so.commitment_date
# Source facility comes from the job (where it was plated).
if self.facility_id and 'source_facility_id' in Delivery._fields:
vals['source_facility_id'] = self.facility_id.id
# Outbound carrier + shipment mirrored from the SO's first
# receiving record (the crew chose these at receipt time).
if (so and 'x_fc_receiving_ids' in so._fields
and so.x_fc_receiving_ids):
recv = so.x_fc_receiving_ids[:1]
if 'x_fc_carrier_id' in Delivery._fields \ if 'x_fc_carrier_id' in Delivery._fields \
and 'x_fc_carrier_id' in recv._fields \ and 'x_fc_carrier_id' in recv._fields \
and recv.x_fc_carrier_id: and recv.x_fc_carrier_id:
@@ -1713,13 +1752,21 @@ class FpJob(models.Model):
vals['x_fc_outbound_shipment_id'] = ( vals['x_fc_outbound_shipment_id'] = (
recv.x_fc_outbound_shipment_id.id recv.x_fc_outbound_shipment_id.id
) )
try: # CoC PDF — if a cert for this job is already issued and
delivery = Delivery.create(vals) # the delivery field accepts an attachment, link it. The
self.delivery_id = delivery.id # cert's action_issue also calls _fp_sync_to_delivery for
except Exception as e: # the case where the cert issues AFTER the delivery exists.
_logger.warning( Cert = self.env.get('fp.certificate')
"Job %s: failed to auto-create delivery: %s", self.name, e, if Cert is not None and 'coc_attachment_id' in Delivery._fields:
) issued_cert = Cert.sudo().search([
('x_fc_job_id', '=', self.id),
('certificate_type', '=', 'coc'),
('state', '=', 'issued'),
('attachment_id', '!=', False),
], order='issue_date desc, id desc', limit=1)
if issued_cert and issued_cert.attachment_id:
vals['coc_attachment_id'] = issued_cert.attachment_id.id
return vals
def _fp_create_certificates(self): def _fp_create_certificates(self):
"""Auto-create one draft fp.certificate per type returned by """Auto-create one draft fp.certificate per type returned by

View File

@@ -5,7 +5,7 @@
{ {
'name': 'Fusion Plating — Logistics', 'name': 'Fusion Plating — Logistics',
'version': '19.0.3.9.0', 'version': '19.0.3.10.0',
'category': 'Manufacturing/Plating', 'category': 'Manufacturing/Plating',
'summary': ( 'summary': (
'Pickup & delivery for plating shops: vehicle master, driver ' 'Pickup & delivery for plating shops: vehicle master, driver '

View File

@@ -260,6 +260,48 @@ class FpDelivery(models.Model):
def _fp_parent_counter_field(self): def _fp_parent_counter_field(self):
return 'x_fc_pn_delivery_count' return 'x_fc_pn_delivery_count'
def action_refresh_from_source(self):
"""Re-pull delivery address / contact / scheduled date / source
facility / carrier / CoC from the linked job → SO → receiving →
cert chain. Only fills BLANK fields — never overwrites operator
edits. Use when an upstream value changed after the delivery
was auto-created, or to backfill an old delivery that was
created before the auto-populate hook existed.
"""
for rec in self:
job = (rec.x_fc_job_id
if 'x_fc_job_id' in rec._fields else False)
if not job:
# Fall back via job_ref Char if M2O is empty (older data)
if rec.job_ref and 'fp.job' in self.env:
job = self.env['fp.job'].sudo().search(
[('name', '=', rec.job_ref)], limit=1,
)
if not job:
raise UserError(_(
'Delivery %s has no linked job — nothing to '
'refresh from.'
) % rec.name)
Delivery = rec.env['fusion.plating.delivery']
defaults = job._fp_resolve_delivery_defaults(Delivery)
# Drop fields the operator already filled — never clobber
# manual edits. Includes the partner/job links since those
# are non-overridable.
fill = {
k: v for k, v in defaults.items()
if v and not rec[k]
}
if not fill:
rec.message_post(body=_(
'Refresh from source: nothing to update — every '
'field already populated.'
))
continue
rec.sudo().write(fill)
rec.message_post(body=_(
'Refresh from source filled: %s'
) % ', '.join(sorted(fill.keys())))
@api.model_create_multi @api.model_create_multi
def create(self, vals_list): def create(self, vals_list):
"""Parent-derived name (DLV-<parent>[-NN]) with legacy-sequence """Parent-derived name (DLV-<parent>[-NN]) with legacy-sequence

View File

@@ -55,6 +55,17 @@
invisible="state in ('delivered','cancelled')"/> invisible="state in ('delivered','cancelled')"/>
<button name="action_reset_to_draft" string="Reset to Draft" type="object" <button name="action_reset_to_draft" string="Reset to Draft" type="object"
invisible="state != 'cancelled'"/> invisible="state != 'cancelled'"/>
<!-- Pulls delivery address / contact / scheduled
date / source facility / carrier / CoC from
the job → SO → receiving → cert chain. Only
fills BLANK fields, never overwrites operator
edits. Useful when upstream data changed or
to backfill an old delivery. -->
<button name="action_refresh_from_source"
string="Refresh from Source"
type="object" class="btn-secondary"
icon="fa-refresh"
invisible="state in ('delivered','cancelled')"/>
<field name="state" widget="statusbar" <field name="state" widget="statusbar"
statusbar_visible="draft,scheduled,en_route,delivered"/> statusbar_visible="draft,scheduled,en_route,delivered"/>
</header> </header>