This commit is contained in:
gsinghpal
2026-04-26 15:05:17 -04:00
parent 160198edb1
commit d9f58b9851
110 changed files with 6210 additions and 1182 deletions

View File

@@ -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',
}