feat(configurator): fill per-line logic (price lookup, desc template, rev bump)

Task A4. Expands fp.direct.order.line with: part related fields,
optional new-revision block, additional treatment M2M, per-line
deadline + rush flag, description template + free-text, onchange
auto-price-lookup from customer price list, onchange template
suggestion (part > customer > coating), and _get_or_bump_revision
helper that will be called by the SO-creation loop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-19 20:32:34 -04:00
parent 057157587d
commit 9423a93961

View File

@@ -3,7 +3,8 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from odoo import api, fields, models
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class FpDirectOrderLine(models.TransientModel):
@@ -18,16 +19,48 @@ class FpDirectOrderLine(models.TransientModel):
)
sequence = fields.Integer(default=10)
# ---- Part ----
part_catalog_id = fields.Many2one(
'fp.part.catalog',
string='Part',
required=True,
)
part_number = fields.Char(
related='part_catalog_id.part_number', readonly=True,
)
part_revision = fields.Char(
related='part_catalog_id.revision', readonly=True,
)
surface_area = fields.Float(
related='part_catalog_id.surface_area', readonly=True, digits=(12, 4),
)
surface_area_uom = fields.Selection(
related='part_catalog_id.surface_area_uom', readonly=True,
)
# ---- New revision (optional) ----
create_new_revision = fields.Boolean(
string='This is a New Revision',
help='Check if the customer sent an updated drawing or 3D model. '
'A new part revision will be created and linked to this line.',
)
new_drawing_file = fields.Binary(string='New Drawing / 3D Model')
new_drawing_filename = fields.Char(string='Filename')
revision_note = fields.Char(string='Revision Note')
# ---- Treatments ----
coating_config_id = fields.Many2one(
'fp.coating.config',
string='Primary Treatment',
required=True,
)
treatment_ids = fields.Many2many(
'fp.treatment',
string='Additional Treatments',
help='Extra pre/post treatments applied to this line.',
)
# ---- Qty / price ----
quantity = fields.Integer(string='Qty', default=1, required=True)
currency_id = fields.Many2one(related='wizard_id.currency_id')
unit_price = fields.Monetary(
@@ -40,7 +73,129 @@ class FpDirectOrderLine(models.TransientModel):
compute='_compute_line_subtotal',
)
# ---- Scheduling / fulfilment ----
part_deadline = fields.Date(
string='Part Deadline',
help='Per-line deadline. Defaults to SO customer deadline if blank.',
)
rush_order = fields.Boolean(string='Rush')
# ---- Description ----
description_template_id = fields.Many2one(
'fp.sale.description.template',
string='Description Template',
)
line_description = fields.Text(
string='Line Description',
help='This text becomes the description of the sale order line. '
'Edit freely — your changes override the template.',
)
# ---- Computes ----
@api.depends('quantity', 'unit_price')
def _compute_line_subtotal(self):
for rec in self:
rec.line_subtotal = (rec.quantity or 0) * (rec.unit_price or 0.0)
# ---- Onchange ----
@api.onchange('coating_config_id', 'quantity', 'part_catalog_id')
def _onchange_lookup_price(self):
"""Auto-fill unit_price from customer price list when available."""
if self.unit_price:
return
partner = self.wizard_id.partner_id
if not (partner and self.coating_config_id):
return
price = self.env['fp.customer.price.list']._find_price(
partner.id,
self.coating_config_id.id,
quantity=self.quantity or 1,
)
if price:
self.unit_price = price.unit_price
@api.onchange('description_template_id')
def _onchange_description_template(self):
if self.description_template_id:
self.line_description = self.description_template_id.description
@api.onchange('part_catalog_id', 'coating_config_id')
def _onchange_suggest_template(self):
"""Offer a sensible default template — part-specific wins.
Priority (first non-empty result wins):
1. This part's lowest-sequence active template
2. This customer's templates (no part)
3. This coating's templates (no part)
4. Don't auto-pick — user has to choose
"""
if self.description_template_id or self.line_description:
return
Template = self.env['fp.sale.description.template']
partner = self.wizard_id.partner_id
if self.part_catalog_id:
match = Template.search([
('active', '=', True),
('part_catalog_id', '=', self.part_catalog_id.id),
], order='sequence', limit=1)
if match:
self.description_template_id = match.id
self.line_description = match.description
return
if partner:
match = Template.search([
('active', '=', True),
('part_catalog_id', '=', False),
('partner_id', '=', partner.id),
], order='sequence', limit=1)
if match:
self.description_template_id = match.id
self.line_description = match.description
return
if self.coating_config_id:
match = Template.search([
('active', '=', True),
('part_catalog_id', '=', False),
('partner_id', '=', False),
('coating_config_id', '=', self.coating_config_id.id),
], order='sequence', limit=1)
if match:
self.description_template_id = match.id
self.line_description = match.description
# ---- Helpers ----
def _get_or_bump_revision(self):
"""Return the part to use for the SO line, optionally bumping revision."""
self.ensure_one()
part = self.part_catalog_id
if not self.create_new_revision:
return part
if not self.new_drawing_file:
raise UserError(_(
'Line %s: upload the new drawing before confirming.'
) % (part.name or part.part_number or '?'))
drawing_att = self.env['ir.attachment'].create({
'name': self.new_drawing_filename or 'drawing.pdf',
'datas': self.new_drawing_file,
'res_model': 'fp.part.catalog',
'res_id': part.id,
})
part.action_create_revision()
new_rev = self.env['fp.part.catalog'].search([
('parent_part_id', '=', (part.parent_part_id or part).id),
('is_latest_revision', '=', True),
], limit=1, order='revision_number desc')
if not new_rev:
return part
new_rev.write({'revision_note': self.revision_note or False})
fname = (self.new_drawing_filename or '').lower()
if fname.endswith(('.step', '.stp', '.stl', '.iges', '.igs', '.brep', '.brp')):
new_rev.model_attachment_id = drawing_att.id
else:
new_rev.drawing_attachment_ids = [(4, drawing_att.id)]
return new_rev