feat(configurator): B2+B7+B8 - Express override helper + serial-bulk + DWG/OPEN buttons on sale.order.line

This commit is contained in:
gsinghpal
2026-05-26 21:11:15 -04:00
parent baf5c4158f
commit 299cae8a4e

View File

@@ -6,7 +6,7 @@
from datetime import timedelta
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.exceptions import UserError, ValidationError
class SaleOrderLine(models.Model):
@@ -775,3 +775,144 @@ class SaleOrderLine(models.Model):
readonly=True,
store=True,
)
# ============================================================
# Express Orders backend helpers (Phase B — 2026-05-26)
# ============================================================
def _fp_apply_express_overrides_to_job(self, job):
"""Convert Express per-line flags into fp.job.node.override + step instructions.
Called from sale_order._fp_auto_create_job() immediately after Job.create
(creates override rows; step instructions skipped if no steps yet), and
again from fp.job.action_confirm() after _generate_steps_from_recipe()
(override rows recreate identically; step instructions land this time).
Idempotent: pre-deletes prior masking/bake override rows on each call.
Algorithm:
- x_fc_masking_enabled=False → opt out of masking + de_masking nodes
- x_fc_bake_instructions empty → opt out of baking nodes
- x_fc_bake_instructions non-empty → keep baking + write text to step.instructions
"""
self.ensure_one()
if not job or not job.recipe_id:
return
recipe = job.recipe_id
Override = self.env['fp.job.node.override'].sudo()
# Idempotency: clear prior masking/bake override rows on this job
prior = Override.search([
('job_id', '=', job.id),
('node_id.default_kind', 'in', ('masking', 'de_masking', 'baking')),
])
if prior:
prior.unlink()
msgs = []
# 1. Masking — opt out of masking + de_masking AS A PAIR
if not self.x_fc_masking_enabled:
nodes = recipe._fp_all_nodes_with_kind(('masking', 'de_masking'))
for node in nodes:
Override.create({
'job_id': job.id,
'node_id': node.id,
'included': False,
})
if nodes:
msgs.append(_('Masking + de-masking steps opted out (per SO line)'))
# 2. Bake — empty = opt out; non-empty = keep + write step.instructions
bake_text = (self.x_fc_bake_instructions or '').strip()
bake_nodes = recipe._fp_all_nodes_with_kind(('baking',))
if not bake_text:
for node in bake_nodes:
Override.create({
'job_id': job.id,
'node_id': node.id,
'included': False,
})
if bake_nodes:
msgs.append(_('Baking steps opted out (per SO line)'))
else:
# Step instructions write only succeeds if steps exist. The
# helper is called twice — first call (before action_confirm)
# finds no steps and skips; second call (after step gen) lands.
bake_steps = job.step_ids.filtered(
lambda s: s.recipe_node_id.default_kind == 'baking'
)
if bake_steps:
bake_steps.sudo().write({'instructions': bake_text})
msgs.append(_('Bake step instructions set to: %s') % bake_text)
# 3. Audit chatter post on the job (only on the call that actually wrote)
if msgs:
job.sudo().message_post(body='\n'.join('' + m for m in msgs))
def action_open_serial_bulk_add(self):
"""Open the existing fp.serial.bulk.add.wizard targeting this SO line.
Express Orders surfaces this as the inline '+ bulk' button on the
Part cell's serial row (post-confirm). The wizard model and its
action already handle both sale.order.line and fp.direct.order.line.
"""
self.ensure_one()
action = self.env.ref(
'fusion_plating_configurator.action_fp_serial_bulk_add_wizard'
).read()[0]
action['context'] = {
'default_target_model': 'sale.order.line',
'default_target_id': self.id,
'default_qty_expected': int(self.product_uom_qty or 0),
}
return action
def action_open_part(self):
"""Open the linked fp.part.catalog form in a modal."""
self.ensure_one()
if not self.x_fc_part_catalog_id:
return False
return {
'type': 'ir.actions.act_window',
'name': self.x_fc_part_catalog_id.display_name,
'res_model': 'fp.part.catalog',
'view_mode': 'form',
'res_id': self.x_fc_part_catalog_id.id,
'target': 'new',
}
def action_upload_drawing(self):
"""Attach a file (via context) to the line's part as a drawing.
Frontend calling pattern: read file picker → base64-encode →
set context['fp_drawing_file'] + context['fp_drawing_filename'] →
call this method. The drawing lives on the PART (not the line)
so future orders for the same part reuse it.
"""
self.ensure_one()
if not self.x_fc_part_catalog_id:
raise UserError(_('Pick or create a part on this line first.'))
file_data = self.env.context.get('fp_drawing_file')
filename = self.env.context.get('fp_drawing_filename', 'drawing.pdf')
if not file_data:
raise UserError(_('No file data received.'))
att = self.env['ir.attachment'].sudo().create({
'name': filename,
'datas': file_data,
'res_model': 'fp.part.catalog',
'res_id': self.x_fc_part_catalog_id.id,
})
self.x_fc_part_catalog_id.sudo().write({
'drawing_attachment_ids': [(4, att.id)],
})
self.x_fc_part_catalog_id.sudo().message_post(body=_(
'Drawing "%(name)s" uploaded by %(user)s from line %(seq)s on SO %(so)s.'
) % {
'name': filename,
'user': self.env.user.display_name,
'seq': self.sequence or self.id,
'so': self.order_id.name,
})
return {'type': 'ir.actions.client', 'tag': 'reload'}