changes
This commit is contained in:
@@ -167,23 +167,37 @@ class FpPartCatalog(models.Model):
|
||||
'Compose button to edit. When an order does not pick a '
|
||||
'specific variant, this one is used.',
|
||||
)
|
||||
process_variant_ids = fields.One2many(
|
||||
# Computed instead of plain One2many because the One2many `domain=`
|
||||
# was silently NOT being applied — `part.process_variant_ids` was
|
||||
# returning every node (root + children) for the part instead of
|
||||
# only the root recipe variants. Computing explicitly via search
|
||||
# is bulletproof and survives the Odoo 19 ORM rewrites. The store
|
||||
# is False because the underlying recipe-tree topology can change
|
||||
# outside this model (composer, drag/drop in editor, etc.) and we
|
||||
# want fresh reads.
|
||||
process_variant_ids = fields.Many2many(
|
||||
'fusion.plating.process.node',
|
||||
'part_catalog_id',
|
||||
compute='_compute_process_variant_ids',
|
||||
string='Process Variants',
|
||||
domain="[('parent_id', '=', False), ('node_type', '=', 'recipe')]",
|
||||
help='All recipe variants composed for this part. Each order line '
|
||||
'picks one (or falls back to the default).',
|
||||
help='Root recipe variants composed for this part. Each order '
|
||||
'line picks one (or falls back to the default).',
|
||||
)
|
||||
process_variant_count = fields.Integer(
|
||||
string='Variants',
|
||||
compute='_compute_process_variant_count',
|
||||
compute='_compute_process_variant_ids',
|
||||
)
|
||||
|
||||
@api.depends('process_variant_ids')
|
||||
def _compute_process_variant_count(self):
|
||||
@api.depends_context('uid')
|
||||
def _compute_process_variant_ids(self):
|
||||
Node = self.env['fusion.plating.process.node']
|
||||
for rec in self:
|
||||
rec.process_variant_count = len(rec.process_variant_ids)
|
||||
variants = Node.search([
|
||||
('part_catalog_id', '=', rec.id),
|
||||
('parent_id', '=', False),
|
||||
('node_type', '=', 'recipe'),
|
||||
])
|
||||
rec.process_variant_ids = variants
|
||||
rec.process_variant_count = len(variants)
|
||||
|
||||
# ---- Direct-order defaults (Phase C — C4) ----
|
||||
x_fc_default_coating_config_id = fields.Many2one(
|
||||
@@ -360,21 +374,25 @@ class FpPartCatalog(models.Model):
|
||||
[('part_catalog_id', '=', part.id)])
|
||||
|
||||
def _compute_workorder_count(self):
|
||||
SaleOrder = self.env['sale.order']
|
||||
Production = self.env['mrp.production']
|
||||
MrpWO = self.env.get('mrp.workorder')
|
||||
# Sub 11 — MRP gone; count fp.job.step rows scoped to this part's SOs.
|
||||
for part in self:
|
||||
part.workorder_count = 0
|
||||
if 'fp.job' not in self.env or 'fp.job.step' not in self.env:
|
||||
return
|
||||
SaleOrder = self.env['sale.order']
|
||||
Job = self.env['fp.job'].sudo()
|
||||
Step = self.env['fp.job.step'].sudo()
|
||||
for part in self:
|
||||
if MrpWO is None:
|
||||
part.workorder_count = 0
|
||||
continue
|
||||
so_names = SaleOrder.search(
|
||||
[('x_fc_part_catalog_id', '=', part.id)]
|
||||
).mapped('name')
|
||||
if not so_names:
|
||||
part.workorder_count = 0
|
||||
continue
|
||||
mos = Production.search([('origin', 'in', so_names)])
|
||||
part.workorder_count = sum(len(m.workorder_ids) for m in mos)
|
||||
jobs = Job.search([('origin', 'in', so_names)])
|
||||
if not jobs:
|
||||
continue
|
||||
part.workorder_count = Step.search_count(
|
||||
[('job_id', 'in', jobs.ids)])
|
||||
|
||||
def _compute_revision_count(self):
|
||||
for part in self:
|
||||
@@ -460,18 +478,20 @@ class FpPartCatalog(models.Model):
|
||||
}
|
||||
|
||||
def action_view_workorders(self):
|
||||
# Sub 11 — MRP gone; navigate to fp.job.step rows scoped to this part.
|
||||
self.ensure_one()
|
||||
so_names = self.env['sale.order'].search(
|
||||
[('x_fc_part_catalog_id', '=', self.id)]
|
||||
).mapped('name')
|
||||
mos = self.env['mrp.production'].search([('origin', 'in', so_names)])
|
||||
wo_ids = mos.mapped('workorder_ids').ids
|
||||
if 'fp.job' not in self.env or 'fp.job.step' not in self.env:
|
||||
return False
|
||||
jobs = self.env['fp.job'].sudo().search([('origin', 'in', so_names)])
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Work Orders — %s') % (self.part_number or self.name),
|
||||
'res_model': 'mrp.workorder',
|
||||
'domain': [('id', 'in', wo_ids)],
|
||||
'view_mode': 'list,form,kanban',
|
||||
'res_model': 'fp.job.step',
|
||||
'domain': [('job_id', 'in', jobs.ids)],
|
||||
'view_mode': 'list,form',
|
||||
}
|
||||
|
||||
def action_view_revisions(self):
|
||||
|
||||
@@ -605,6 +605,15 @@ class FpQuoteConfigurator(models.Model):
|
||||
'name': '%s — %s (x%d)' % (coating_name, part_name, self.quantity),
|
||||
'product_uom_qty': self.quantity,
|
||||
'price_unit': price / self.quantity if self.quantity else price,
|
||||
# Sub 11 fix — propagate part + coating to the LINE too.
|
||||
# fusion_plating_jobs._fp_auto_create_job filters lines
|
||||
# by x_fc_part_catalog_id; without it, no fp.job spawns.
|
||||
'x_fc_part_catalog_id': (
|
||||
self.part_catalog_id.id if self.part_catalog_id else False
|
||||
),
|
||||
'x_fc_coating_config_id': (
|
||||
self.coating_config_id.id if self.coating_config_id else False
|
||||
),
|
||||
})],
|
||||
}
|
||||
so = self.env['sale.order'].create(so_vals)
|
||||
|
||||
@@ -146,6 +146,37 @@ class SaleOrder(models.Model):
|
||||
# top of this stub during its own load pass.
|
||||
x_fc_workorder_count = fields.Integer(string='Work Orders')
|
||||
|
||||
# Smart-button visibility helpers (post-Sub 11). The BOM Items kanban
|
||||
# is only useful when the SO carries 2+ distinct parts; the By Job
|
||||
# Group kanban is only useful when at least one line is tagged with
|
||||
# x_fc_wo_group_tag. Default-hidden otherwise so the smart-button
|
||||
# row stays clean for the typical single-part SO.
|
||||
x_fc_distinct_part_count = fields.Integer(
|
||||
string='# Distinct Parts',
|
||||
compute='_compute_smart_button_visibility',
|
||||
)
|
||||
x_fc_has_wo_group_tag = fields.Boolean(
|
||||
string='Has Job Group Tag',
|
||||
compute='_compute_smart_button_visibility',
|
||||
)
|
||||
x_fc_wo_group_count = fields.Integer(
|
||||
string='# Job Groups',
|
||||
compute='_compute_smart_button_visibility',
|
||||
help='Distinct x_fc_wo_group_tag values across this SO\'s lines.',
|
||||
)
|
||||
|
||||
@api.depends('order_line.x_fc_part_catalog_id',
|
||||
'order_line.x_fc_wo_group_tag')
|
||||
def _compute_smart_button_visibility(self):
|
||||
for rec in self:
|
||||
parts = rec.order_line.mapped('x_fc_part_catalog_id')
|
||||
rec.x_fc_distinct_part_count = len(parts)
|
||||
tags = {
|
||||
t for t in rec.order_line.mapped('x_fc_wo_group_tag') if t
|
||||
}
|
||||
rec.x_fc_has_wo_group_tag = bool(tags)
|
||||
rec.x_fc_wo_group_count = len(tags)
|
||||
|
||||
# Sub 9 — process variant summary across order lines. Renders one
|
||||
# variant label when all lines share one, otherwise "Mixed (N)".
|
||||
x_fc_process_summary = fields.Char(
|
||||
@@ -192,42 +223,45 @@ class SaleOrder(models.Model):
|
||||
|
||||
@api.depends('name')
|
||||
def _compute_wo_completion(self):
|
||||
"""Batched: one grouped query across all records in self."""
|
||||
"""Batched: one grouped query across all records in self.
|
||||
|
||||
Sub 11 — MRP is gone; we count fp.job.step completion instead of
|
||||
mrp.workorder. The selection is the same shape: completed steps
|
||||
out of total steps across every fp.job for this SO.
|
||||
"""
|
||||
for rec in self:
|
||||
rec.x_fc_wo_completion = '0/0'
|
||||
names = [so.name for so in self if so.name]
|
||||
if not names:
|
||||
return
|
||||
WO = self.env['mrp.workorder'].sudo()
|
||||
rows = WO.read_group(
|
||||
[('production_id.origin', 'in', names)],
|
||||
['production_id.origin', 'state'],
|
||||
['production_id', 'state'],
|
||||
lazy=False,
|
||||
if 'fp.job.step' not in self.env or 'fp.job' not in self.env:
|
||||
return
|
||||
Job = self.env['fp.job'].sudo()
|
||||
Step = self.env['fp.job.step'].sudo()
|
||||
jobs = Job.search([('origin', 'in', names)])
|
||||
if not jobs:
|
||||
return
|
||||
job_to_origin = {j.id: j.origin for j in jobs}
|
||||
# Odoo 19 — use _read_group with aggregates=['__count'].
|
||||
rows = Step._read_group(
|
||||
domain=[('job_id', 'in', jobs.ids)],
|
||||
groupby=['job_id', 'state'],
|
||||
aggregates=['__count'],
|
||||
)
|
||||
# Build {origin: {'done': n, 'total': n}}
|
||||
# read_group returns production_id as (id, name) tuples; we need
|
||||
# to translate back to origin. Do a small lookup.
|
||||
mos = self.env['mrp.production'].sudo().search(
|
||||
[('origin', 'in', names)]
|
||||
)
|
||||
mo_to_origin = {m.id: m.origin for m in mos}
|
||||
totals = {} # {origin: [total, done]}
|
||||
for r in rows:
|
||||
mo_id = r['production_id'][0] if r['production_id'] else False
|
||||
origin = mo_to_origin.get(mo_id)
|
||||
for job_rec, state_val, count in rows:
|
||||
origin = job_to_origin.get(job_rec.id)
|
||||
if not origin:
|
||||
continue
|
||||
cnt = r['__count']
|
||||
bucket = totals.setdefault(origin, [0, 0])
|
||||
bucket[0] += cnt
|
||||
if r['state'] == 'done':
|
||||
bucket[1] += cnt
|
||||
bucket[0] += count
|
||||
if state_val == 'done':
|
||||
bucket[1] += count
|
||||
for rec in self:
|
||||
if not rec.name:
|
||||
continue
|
||||
tot, done = totals.get(rec.name, [0, 0])
|
||||
rec.x_fc_wo_completion = '%d/%d' % (done, tot) if tot else '0/0'
|
||||
rec.x_fc_wo_completion = f'{done}/{tot}' if tot else '0/0'
|
||||
|
||||
# ---- Phase F: quotes list view polish ----
|
||||
x_fc_follow_up_date = fields.Date(
|
||||
|
||||
Reference in New Issue
Block a user