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_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',
'version': '19.0.7.7.0',
'version': '19.0.7.8.0',
'category': 'Manufacturing/Plating',
'summary': 'Certificate registry for CoC, thickness reports, and quality documents.',
'description': """

View File

@@ -594,8 +594,41 @@ class FpCertificate(models.Model):
_logger.warning(
'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.'))
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):
"""Render the CoC PDF via the bound report action, OPTIONALLY
merge the Fischerscope thickness report PDF (uploaded by the

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
{
'name': 'Fusion Plating — Native Jobs',
'version': '19.0.10.16.8',
'version': '19.0.10.16.9',
'category': 'Manufacturing/Plating',
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
'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
happy.
Phase A — mirrors x_fc_carrier_id and x_fc_outbound_shipment_id
from the linked receiving so the delivery carries the shipping
choices made at receipt time. Shipping crew can override later.
Auto-populates everything we can resolve from upstream
records so the shipping crew doesn't have to re-type
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()
if self.delivery_id:
return
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}
if 'x_fc_job_id' in Delivery._fields:
vals['x_fc_job_id'] = self.id
if 'job_ref' in Delivery._fields:
vals['job_ref'] = self.name
if 'x_fc_job_id' not in Delivery._fields \
and 'job_ref' not in Delivery._fields:
_logger.warning(
"Job %s: fusion.plating.delivery has no job link field; "
"delivery created without job back-reference.", self.name,
)
# Mirror outbound carrier + shipment from the SO's first
# receiving record. If there are multiple receivings (split
# shipments), the shipping crew can change either field on the
# delivery form. Defensive: skip when fields aren't present
# (older instance) or no receiving exists.
if (self.sale_order_id
and 'x_fc_receiving_ids' in self.sale_order_id._fields
and self.sale_order_id.x_fc_receiving_ids):
recv = self.sale_order_id.x_fc_receiving_ids[:1]
# Delivery address + contact details from the SO. shipping
# partner is preferred (that's where parts physically go);
# fall back to the SO's main partner when no separate ship-to.
so = self.sale_order_id
ship_to = (so.partner_shipping_id or so.partner_id) if so else False
if ship_to:
if 'delivery_address_id' in Delivery._fields:
vals['delivery_address_id'] = ship_to.id
if 'contact_name' in Delivery._fields and ship_to.name:
vals['contact_name'] = ship_to.name
if 'contact_phone' in Delivery._fields:
vals['contact_phone'] = ship_to.phone or ship_to.mobile or ''
# Scheduled date — operator can adjust; this just primes it
# so they're not staring at a blank field.
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 \
and 'x_fc_carrier_id' in recv._fields \
and recv.x_fc_carrier_id:
@@ -1713,13 +1752,21 @@ class FpJob(models.Model):
vals['x_fc_outbound_shipment_id'] = (
recv.x_fc_outbound_shipment_id.id
)
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,
)
# CoC PDF — if a cert for this job is already issued and
# the delivery field accepts an attachment, link it. The
# cert's action_issue also calls _fp_sync_to_delivery for
# the case where the cert issues AFTER the delivery exists.
Cert = self.env.get('fp.certificate')
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):
"""Auto-create one draft fp.certificate per type returned by

View File

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

View File

@@ -260,6 +260,48 @@ class FpDelivery(models.Model):
def _fp_parent_counter_field(self):
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
def create(self, vals_list):
"""Parent-derived name (DLV-<parent>[-NN]) with legacy-sequence

View File

@@ -55,6 +55,17 @@
invisible="state in ('delivered','cancelled')"/>
<button name="action_reset_to_draft" string="Reset to Draft" type="object"
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"
statusbar_visible="draft,scheduled,en_route,delivered"/>
</header>