changes
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
|
|||||||
59
fusion_claims/tests/test_dashboard.py
Normal file
59
fusion_claims/tests/test_dashboard.py
Normal 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)
|
||||||
@@ -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': """
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 '
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user