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_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',
|
||||
'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': """
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 '
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user