feat(configurator): B2+B7+B8 - Express override helper + serial-bulk + DWG/OPEN buttons on sale.order.line
This commit is contained in:
@@ -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'}
|
||||
|
||||
Reference in New Issue
Block a user