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 datetime import timedelta
|
||||||
|
|
||||||
from odoo import _, api, fields, models
|
from odoo import _, api, fields, models
|
||||||
from odoo.exceptions import ValidationError
|
from odoo.exceptions import UserError, ValidationError
|
||||||
|
|
||||||
|
|
||||||
class SaleOrderLine(models.Model):
|
class SaleOrderLine(models.Model):
|
||||||
@@ -775,3 +775,144 @@ class SaleOrderLine(models.Model):
|
|||||||
readonly=True,
|
readonly=True,
|
||||||
store=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