changes
This commit is contained in:
@@ -27,3 +27,12 @@ from . import fusion_plating_kpi_value
|
||||
|
||||
# Phase 5 — Job Margin report.
|
||||
from . import report_fp_job_margin
|
||||
|
||||
# Phase 1 of MRP cut-out (Sub 11) — relocated from fusion_plating_bridge_mrp.
|
||||
# (fp.qc.checklist.template lives in fusion_plating_quality; can't depend
|
||||
# back on jobs without a cycle.)
|
||||
from . import fp_job_consumption
|
||||
# fp.work.role, fp.operator.proficiency, fp_process_node inherit, and the
|
||||
# hr.employee shop-roles inherit live in fusion_plating core so every
|
||||
# downstream module (cgp, bridge_mrp residue, etc.) sees them without a
|
||||
# transitive dep on jobs.
|
||||
|
||||
@@ -517,6 +517,12 @@ class FpJob(models.Model):
|
||||
if self.env.context.get('fp_jobs_migration'):
|
||||
return result
|
||||
for job in self:
|
||||
# Auto-generate steps from the recipe — was previously only
|
||||
# called by seed scripts, which meant real-life confirmed
|
||||
# jobs sat with zero operations. Idempotent: the generator
|
||||
# short-circuits when steps already exist.
|
||||
if job.recipe_id and not job.step_ids:
|
||||
job._generate_steps_from_recipe()
|
||||
job._fp_create_portal_job()
|
||||
job._fp_create_qc_check_if_needed()
|
||||
job._fp_create_racking_inspection()
|
||||
@@ -526,36 +532,28 @@ class FpJob(models.Model):
|
||||
def _fp_create_racking_inspection(self):
|
||||
"""Auto-create a draft racking inspection on job confirm.
|
||||
|
||||
Mirrors bridge_mrp's behaviour for MO confirm. Best-effort: the
|
||||
legacy fp.racking.inspection model still requires a production_id
|
||||
(mrp.production), so we can only create one when this job is
|
||||
bound to an MO via bridge_mrp. Otherwise we skip cleanly — Phase
|
||||
9 will flip the required-FK to fp.job.
|
||||
Phase 9 — production_id is now optional on fp.racking.inspection,
|
||||
so we always create one bound by `x_fc_job_id`. When the job is
|
||||
also linked to an MO (legacy bridge_mrp coexistence), populate
|
||||
production_id too so legacy reports keep working.
|
||||
|
||||
Idempotent — if an inspection already exists for this job, skip.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if 'fp.racking.inspection' not in self.env:
|
||||
return
|
||||
Inspection = self.env['fp.racking.inspection'].sudo()
|
||||
# The model still requires production_id today. If the job has
|
||||
# no MO link (which it won't in pure-native mode), skip rather
|
||||
# than crash. The link exists when fusion_plating_bridge_mrp is
|
||||
# installed and a production was created in parallel.
|
||||
production = False
|
||||
if 'production_id' in self._fields and self.production_id:
|
||||
production = self.production_id
|
||||
elif 'mrp_production_id' in self._fields and getattr(
|
||||
self, 'mrp_production_id', False):
|
||||
production = self.mrp_production_id
|
||||
if not production:
|
||||
_logger.debug(
|
||||
"Job %s: no MO link — skipping racking-inspection auto-create "
|
||||
"(required production_id not yet on fp.job).", self.name,
|
||||
)
|
||||
if 'x_fc_job_id' not in Inspection._fields:
|
||||
# Schema not yet upgraded — skip.
|
||||
return
|
||||
existing = Inspection.search([
|
||||
('x_fc_job_id', '=', self.id),
|
||||
], limit=1)
|
||||
if existing:
|
||||
return
|
||||
# Phase 6 (Sub 11) — production_id retired; bind by x_fc_job_id only.
|
||||
vals = {'x_fc_job_id': self.id}
|
||||
try:
|
||||
vals = {'production_id': production.id}
|
||||
if 'x_fc_job_id' in Inspection._fields:
|
||||
vals['x_fc_job_id'] = self.id
|
||||
Inspection.create(vals)
|
||||
except Exception as e:
|
||||
_logger.warning(
|
||||
|
||||
104
fusion_plating/fusion_plating_jobs/models/fp_job_consumption.py
Normal file
104
fusion_plating/fusion_plating_jobs/models/fp_job_consumption.py
Normal file
@@ -0,0 +1,104 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
#
|
||||
# Phase 1 (Sub 11) — relocated from fusion_plating_bridge_mrp.
|
||||
# MRP-flavoured fields (production_id, workorder_id) replaced by their
|
||||
# native fp.job / fp.job.step equivalents.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class FpJobConsumption(models.Model):
|
||||
"""A single consumable drawdown charged to a plating job.
|
||||
|
||||
Sources include bath replenishment applied against a job, masking tape
|
||||
rolls, PPE, nickel salts — anything that has a cost and should roll
|
||||
into job costing.
|
||||
|
||||
Kept deliberately lightweight: one row per event, cost derived from
|
||||
`product.standard_price` at log time (snapshot, not reactive).
|
||||
"""
|
||||
_name = 'fp.job.consumption'
|
||||
_description = 'Fusion Plating — Job Consumption'
|
||||
_order = 'logged_date desc, id desc'
|
||||
|
||||
job_id = fields.Many2one(
|
||||
'fp.job', string='Plating Job',
|
||||
required=True, ondelete='cascade', index=True,
|
||||
)
|
||||
step_id = fields.Many2one(
|
||||
'fp.job.step', string='Job Step',
|
||||
domain="[('job_id', '=', job_id)]",
|
||||
ondelete='set null',
|
||||
)
|
||||
product_id = fields.Many2one(
|
||||
'product.product', string='Product', required=True,
|
||||
domain="[('sale_ok', '=', False)]",
|
||||
)
|
||||
product_name = fields.Char(
|
||||
string='Product Name (snapshot)',
|
||||
help='Free-text product label if no inventory product is linked.',
|
||||
)
|
||||
quantity = fields.Float(string='Quantity', required=True, digits=(12, 3))
|
||||
uom_id = fields.Many2one(
|
||||
'uom.uom', string='UoM',
|
||||
)
|
||||
currency_id = fields.Many2one(
|
||||
'res.currency', required=True,
|
||||
default=lambda self: self.env.company.currency_id,
|
||||
)
|
||||
unit_cost = fields.Monetary(
|
||||
string='Unit Cost (snapshot)', currency_field='currency_id',
|
||||
help='Taken from product.standard_price at log time.',
|
||||
)
|
||||
total_cost = fields.Monetary(
|
||||
string='Total Cost', currency_field='currency_id',
|
||||
compute='_compute_total_cost', store=True,
|
||||
)
|
||||
logged_date = fields.Datetime(
|
||||
string='Logged', default=fields.Datetime.now,
|
||||
)
|
||||
logged_by_id = fields.Many2one(
|
||||
'res.users', string='Logged By', default=lambda self: self.env.user,
|
||||
)
|
||||
source = fields.Selection(
|
||||
[('replenishment', 'Bath Replenishment'),
|
||||
('masking', 'Masking Material'),
|
||||
('ppe', 'PPE / Consumables'),
|
||||
('chemistry', 'Process Chemistry'),
|
||||
('other', 'Other')],
|
||||
string='Source', default='other', required=True,
|
||||
)
|
||||
replenishment_id = fields.Many2one(
|
||||
'fusion.plating.bath.replenishment.suggestion',
|
||||
string='Replenishment Suggestion',
|
||||
ondelete='set null',
|
||||
)
|
||||
notes = fields.Char(string='Notes')
|
||||
|
||||
@api.depends('quantity', 'unit_cost')
|
||||
def _compute_total_cost(self):
|
||||
for rec in self:
|
||||
rec.total_cost = round((rec.quantity or 0) * (rec.unit_cost or 0), 2)
|
||||
|
||||
@api.depends('product_id', 'product_name', 'quantity', 'job_id')
|
||||
def _compute_display_name(self):
|
||||
for rec in self:
|
||||
label = rec.product_id.display_name or rec.product_name or 'Consumption'
|
||||
qty = ('%g' % rec.quantity) if rec.quantity else ''
|
||||
job = rec.job_id.name or ''
|
||||
bits = [label]
|
||||
if qty:
|
||||
bits.append('×' + qty)
|
||||
if job:
|
||||
bits.append('(%s)' % job)
|
||||
rec.display_name = ' '.join(bits)
|
||||
|
||||
@api.onchange('product_id')
|
||||
def _onchange_product(self):
|
||||
if self.product_id:
|
||||
self.product_name = self.product_id.display_name
|
||||
self.unit_cost = self.product_id.standard_price or 0.0
|
||||
self.uom_id = self.product_id.uom_id or False
|
||||
@@ -9,7 +9,7 @@
|
||||
# bridge_mrp keeps its version alive so legacy MO-flow keeps working.
|
||||
# Both coexist during the migration period.
|
||||
|
||||
from odoo import fields, models
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class FpJobNodeOverride(models.Model):
|
||||
@@ -35,6 +35,14 @@ class FpJobNodeOverride(models.Model):
|
||||
help='When True, this opt-in/out node is included in step generation.',
|
||||
)
|
||||
|
||||
@api.depends('job_id', 'node_id', 'included')
|
||||
def _compute_display_name(self):
|
||||
for rec in self:
|
||||
job = rec.job_id.display_name or '(no job)'
|
||||
node = rec.node_id.display_name or '(no node)'
|
||||
tag = 'included' if rec.included else 'excluded'
|
||||
rec.display_name = '%s · %s [%s]' % (job, node, tag)
|
||||
|
||||
_unique_job_node = models.Constraint(
|
||||
'unique(job_id, node_id)',
|
||||
'A job can only have one override per recipe node.',
|
||||
|
||||
@@ -13,6 +13,34 @@ from odoo.exceptions import UserError
|
||||
class FpJobStep(models.Model):
|
||||
_inherit = 'fp.job.step'
|
||||
|
||||
def button_start(self):
|
||||
"""Override — soft gate when parts haven't been received yet.
|
||||
|
||||
Doesn't block (parts could be in-transit late, manager wants
|
||||
the shop to start prep regardless), but posts a chatter warning
|
||||
on the job so the audit trail captures premature starts.
|
||||
"""
|
||||
result = super().button_start()
|
||||
for step in self:
|
||||
so = step.job_id.sale_order_id
|
||||
if not so:
|
||||
continue
|
||||
recv = so.x_fc_receiving_status if (
|
||||
'x_fc_receiving_status' in so._fields
|
||||
) else None
|
||||
if recv in (False, None, 'not_received'):
|
||||
step.job_id.message_post(body=_(
|
||||
'Step "%(step)s" started before parts were received '
|
||||
'(SO %(so)s — receiving status: %(status)s). '
|
||||
'Confirm the parts are physically on the floor before '
|
||||
'continuing.'
|
||||
) % {
|
||||
'step': step.name,
|
||||
'so': so.name or '',
|
||||
'status': recv or 'unknown',
|
||||
})
|
||||
return result
|
||||
|
||||
def button_pause(self):
|
||||
"""Pause an in-progress step (operator break, end of shift).
|
||||
|
||||
|
||||
@@ -2,18 +2,55 @@
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
#
|
||||
# Phase 3 — parallel job link on fp.racking.inspection.
|
||||
# Coexists with the legacy production_id (mrp.production) link.
|
||||
# Phase 3 / Phase 9 — native-job link on fp.racking.inspection.
|
||||
# Coexists with the legacy production_id (mrp.production) link; either
|
||||
# (or both) may be set.
|
||||
|
||||
from odoo import fields, models
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class FpRackingInspection(models.Model):
|
||||
_inherit = 'fp.racking.inspection'
|
||||
|
||||
x_fc_job_id = fields.Many2one(
|
||||
'fp.job',
|
||||
string='Plating Job',
|
||||
index=True,
|
||||
help='Native fp.job link. Coexists with the legacy production_id.',
|
||||
)
|
||||
# x_fc_job_id is declared in the base receiving module so its views
|
||||
# can reference it. We add help/depends here.
|
||||
|
||||
@api.depends('x_fc_job_id.name', 'partner_id.name')
|
||||
def _compute_name(self):
|
||||
for rec in self:
|
||||
if rec.x_fc_job_id:
|
||||
rec.name = _('Inspection — %s') % rec.x_fc_job_id.name
|
||||
else:
|
||||
rec.name = _('Racking Inspection')
|
||||
|
||||
@api.depends('x_fc_job_id.sale_order_id')
|
||||
def _compute_sale_order(self):
|
||||
for rec in self:
|
||||
so = (rec.x_fc_job_id.sale_order_id
|
||||
if rec.x_fc_job_id and rec.x_fc_job_id.sale_order_id
|
||||
else False)
|
||||
rec.sale_order_id = so or False
|
||||
rec.partner_id = so.partner_id if so else False
|
||||
|
||||
@api.constrains('x_fc_job_id')
|
||||
def _check_link_present(self):
|
||||
for rec in self:
|
||||
if not rec.x_fc_job_id:
|
||||
raise ValidationError(_(
|
||||
'Racking inspection must reference a plating job.'
|
||||
))
|
||||
|
||||
@api.constrains('x_fc_job_id')
|
||||
def _check_job_unique(self):
|
||||
for rec in self:
|
||||
if not rec.x_fc_job_id:
|
||||
continue
|
||||
dup = self.search_count([
|
||||
('x_fc_job_id', '=', rec.x_fc_job_id.id),
|
||||
('id', '!=', rec.id),
|
||||
])
|
||||
if dup:
|
||||
raise ValidationError(_(
|
||||
'Only one racking inspection per plating job.'
|
||||
))
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
# bridge_mrp's MO-creation hook handles the flow.
|
||||
|
||||
import logging
|
||||
|
||||
from markupsafe import Markup
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
@@ -18,6 +21,147 @@ _logger = logging.getLogger(__name__)
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = 'sale.order'
|
||||
|
||||
x_fc_fp_job_count = fields.Integer(
|
||||
string='Plating Jobs',
|
||||
compute='_compute_fp_job_count',
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Phase 4 (Sub 11) — workflow-stage field + assigned-manager field
|
||||
# relocated from fusion_plating_bridge_mrp. Field re-declared with
|
||||
# the same selection + compute pointer; jobs is now the source of
|
||||
# truth so Phase 5 can delete bridge_mrp without losing the field.
|
||||
# ------------------------------------------------------------------
|
||||
x_fc_workflow_stage = fields.Selection(
|
||||
[
|
||||
('draft', 'Quote'),
|
||||
('awaiting_parts', 'Parts'),
|
||||
('inspecting', 'Inspecting'),
|
||||
('accept_parts', 'Accept'),
|
||||
('assign_work', 'Assign'),
|
||||
('in_production', 'Production'),
|
||||
('ready_to_ship', 'Ready'),
|
||||
('shipped', 'Shipped'),
|
||||
('invoicing', 'Invoicing'),
|
||||
('paid', 'Paid'),
|
||||
('complete', 'Done'),
|
||||
('cancelled', 'Cancelled'),
|
||||
],
|
||||
compute='_compute_workflow_stage',
|
||||
string='Workflow Stage',
|
||||
help='Current position in the SO → Ship → Invoice workflow. '
|
||||
'Drives which next-step button is shown on the SO header.',
|
||||
)
|
||||
x_fc_assigned_manager_id = fields.Many2one(
|
||||
'res.users', string='Assigned Manager',
|
||||
help='The manager responsible for this job. Set when the job '
|
||||
'is confirmed (falls back to the salesperson).',
|
||||
tracking=True,
|
||||
)
|
||||
|
||||
def _compute_fp_job_count(self):
|
||||
Job = self.env['fp.job'].sudo()
|
||||
for so in self:
|
||||
so.x_fc_fp_job_count = Job.search_count(
|
||||
[('sale_order_id', '=', so.id)]
|
||||
)
|
||||
|
||||
def _compute_workflow_stage(self):
|
||||
"""Native-jobs override — walks fp.job state instead of mrp.production.
|
||||
|
||||
When `use_native_jobs` is on, the SO is fulfilled by `fp.job`
|
||||
records, not MRP MOs. The bridge_mrp compute reads `mrp.production`
|
||||
and would falsely stall the banner. We branch at the top: native
|
||||
mode → fp.job walker; legacy mode → super() (bridge_mrp).
|
||||
"""
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
native = ICP.get_param('fusion_plating_jobs.use_native_jobs') == 'True'
|
||||
if not native:
|
||||
return super()._compute_workflow_stage()
|
||||
|
||||
Job = self.env['fp.job']
|
||||
Delivery = self.env.get('fusion.plating.delivery')
|
||||
for so in self:
|
||||
if so.state == 'cancel':
|
||||
so.x_fc_workflow_stage = 'cancelled'
|
||||
continue
|
||||
if so.state in ('draft', 'sent'):
|
||||
so.x_fc_workflow_stage = 'draft'
|
||||
continue
|
||||
|
||||
jobs = Job.search([('sale_order_id', '=', so.id)])
|
||||
all_jobs_done = bool(jobs) and all(
|
||||
j.state == 'done' for j in jobs
|
||||
)
|
||||
|
||||
shipped = False
|
||||
if Delivery is not None and jobs:
|
||||
if 'x_fc_job_id' in Delivery._fields:
|
||||
shipped = bool(Delivery.search_count([
|
||||
('x_fc_job_id', 'in', jobs.ids),
|
||||
('state', '=', 'delivered'),
|
||||
]))
|
||||
|
||||
posted_invoices = so.invoice_ids.filtered(
|
||||
lambda i: i.state == 'posted'
|
||||
)
|
||||
has_posted_invoice = bool(posted_invoices)
|
||||
all_paid = has_posted_invoice and all(
|
||||
i.payment_state in ('paid', 'in_payment')
|
||||
for i in posted_invoices
|
||||
)
|
||||
|
||||
if shipped and all_paid:
|
||||
so.x_fc_workflow_stage = 'complete'
|
||||
continue
|
||||
if all_paid and not shipped:
|
||||
so.x_fc_workflow_stage = 'paid'
|
||||
continue
|
||||
if shipped and has_posted_invoice:
|
||||
so.x_fc_workflow_stage = 'invoicing'
|
||||
continue
|
||||
if shipped:
|
||||
so.x_fc_workflow_stage = 'shipped'
|
||||
continue
|
||||
if all_jobs_done:
|
||||
so.x_fc_workflow_stage = 'ready_to_ship'
|
||||
continue
|
||||
|
||||
recv_status = so.x_fc_receiving_status or 'not_received'
|
||||
if recv_status == 'not_received':
|
||||
so.x_fc_workflow_stage = 'awaiting_parts'
|
||||
continue
|
||||
if recv_status in ('partial', 'received'):
|
||||
so.x_fc_workflow_stage = 'inspecting'
|
||||
continue
|
||||
if recv_status == 'inspected':
|
||||
if not so.x_fc_assigned_manager_id and not jobs:
|
||||
so.x_fc_workflow_stage = 'assign_work'
|
||||
continue
|
||||
so.x_fc_workflow_stage = 'in_production'
|
||||
continue
|
||||
so.x_fc_workflow_stage = (
|
||||
'in_production' if jobs else 'awaiting_parts'
|
||||
)
|
||||
|
||||
def action_view_fp_jobs(self):
|
||||
self.ensure_one()
|
||||
jobs = self.env['fp.job'].search([('sale_order_id', '=', self.id)])
|
||||
action = {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Plating Jobs'),
|
||||
'res_model': 'fp.job',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('sale_order_id', '=', self.id)],
|
||||
'context': {'default_sale_order_id': self.id},
|
||||
}
|
||||
if len(jobs) == 1:
|
||||
action.update({
|
||||
'view_mode': 'form',
|
||||
'res_id': jobs.id,
|
||||
})
|
||||
return action
|
||||
|
||||
def action_confirm(self):
|
||||
result = super().action_confirm()
|
||||
# Only run when the native flag is on
|
||||
@@ -25,6 +169,22 @@ class SaleOrder(models.Model):
|
||||
if ICP.get_param('fusion_plating_jobs.use_native_jobs') == 'True':
|
||||
for so in self:
|
||||
so._fp_auto_create_job()
|
||||
# Auto-confirm any draft jobs we just created so steps
|
||||
# generate immediately (no manager click required).
|
||||
# Best-effort: an exception in side-effects shouldn't
|
||||
# block the SO confirm itself.
|
||||
draft_jobs = self.env['fp.job'].sudo().search([
|
||||
('sale_order_id', '=', so.id),
|
||||
('state', '=', 'draft'),
|
||||
])
|
||||
for job in draft_jobs:
|
||||
try:
|
||||
job.action_confirm()
|
||||
except Exception as exc:
|
||||
so.message_post(body=_(
|
||||
'Auto-confirm of fp.job %(job)s failed: %(err)s. '
|
||||
'Confirm manually from the job form.'
|
||||
) % {'job': job.name, 'err': exc})
|
||||
return result
|
||||
|
||||
def _fp_auto_create_job(self):
|
||||
@@ -121,3 +281,94 @@ class SaleOrder(models.Model):
|
||||
self.name, job.name, qty, (recipe.name if recipe else '-'),
|
||||
)
|
||||
return True
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Phase 4 (Sub 11) — workflow stage action buttons.
|
||||
# Native versions of bridge_mrp's action_fp_* methods. Drop the
|
||||
# mrp.production lookups; talk to fp.job and fp.receiving directly.
|
||||
# ------------------------------------------------------------------
|
||||
def action_fp_mark_inspected(self):
|
||||
"""Flip open receivings from draft → inspecting."""
|
||||
self.ensure_one()
|
||||
Recv = self.env.get('fp.receiving')
|
||||
if Recv is None:
|
||||
return False
|
||||
for rec in Recv.search([('sale_order_id', '=', self.id)]):
|
||||
if rec.state == 'draft':
|
||||
rec.state = 'inspecting'
|
||||
self.message_post(body=_('Parts marked as inspecting.'))
|
||||
return True
|
||||
|
||||
def action_fp_accept_parts(self):
|
||||
"""Mark receiving accepted; flip SO receiving status to inspected."""
|
||||
self.ensure_one()
|
||||
Recv = self.env.get('fp.receiving')
|
||||
if Recv is None:
|
||||
return False
|
||||
for rec in Recv.search([('sale_order_id', '=', self.id)]):
|
||||
if rec.state in ('draft', 'inspecting'):
|
||||
rec.state = 'accepted'
|
||||
if 'x_fc_receiving_status' in self._fields:
|
||||
self.x_fc_receiving_status = 'inspected'
|
||||
self.message_post(body=_('Parts accepted — ready to assign manager.'))
|
||||
return True
|
||||
|
||||
def action_fp_assign_to_me(self):
|
||||
"""Manager claims the SO and confirms its draft fp.jobs."""
|
||||
self.ensure_one()
|
||||
user = self.env.user
|
||||
self.x_fc_assigned_manager_id = user.id
|
||||
Job = self.env['fp.job']
|
||||
jobs = Job.search([
|
||||
('sale_order_id', '=', self.id),
|
||||
('state', '=', 'draft'),
|
||||
])
|
||||
for job in jobs:
|
||||
try:
|
||||
job.action_confirm()
|
||||
except Exception as exc:
|
||||
self.message_post(body=_(
|
||||
'Auto-confirm of fp.job %s failed: %s'
|
||||
) % (job.name, exc))
|
||||
if 'manager_id' in job._fields and not job.manager_id:
|
||||
job.manager_id = user.id
|
||||
self.message_post(
|
||||
body=Markup(_(
|
||||
'Job assigned to <b>%s</b>. %d plating job(s) released to the floor.'
|
||||
)) % (user.name, len(jobs)),
|
||||
)
|
||||
return True
|
||||
|
||||
def action_fp_mark_shipped(self):
|
||||
"""Mark linked deliveries delivered (triggers auto-invoice)."""
|
||||
self.ensure_one()
|
||||
Delivery = self.env.get('fusion.plating.delivery')
|
||||
if Delivery is None:
|
||||
return False
|
||||
Job = self.env['fp.job']
|
||||
jobs = Job.search([('sale_order_id', '=', self.id)])
|
||||
deliveries = Delivery.browse([])
|
||||
if 'x_fc_job_id' in Delivery._fields:
|
||||
deliveries = Delivery.search([
|
||||
('x_fc_job_id', 'in', jobs.ids),
|
||||
('state', '!=', 'delivered'),
|
||||
])
|
||||
for dlv in deliveries:
|
||||
dlv.action_mark_delivered()
|
||||
self.message_post(
|
||||
body=_(
|
||||
'%d delivery record(s) marked delivered. '
|
||||
'Invoice flow triggered per invoice strategy.'
|
||||
) % len(deliveries),
|
||||
)
|
||||
return True
|
||||
|
||||
def action_fp_open_shop_floor(self):
|
||||
"""Jump to the Plant Overview filtered to this SO's jobs."""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'fp_plant_overview',
|
||||
'name': _('Shop Floor — %s') % self.name,
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user