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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user