feat(promote-customer-spec): Phase B — two-picker SO line UX

Spec-side picker (x_fc_customer_spec_id / customer_spec_id) added on:
- sale.order.line (via quality inherit — onchange autofill, create()
  fallback to part default, _prepare_invoice_line carry)
- account.move.line (via quality inherit — invoice rendering)
- fp.part.catalog (via quality inherit — x_fc_default_customer_spec_id)
- fp.direct.order.line (via quality inherit — wizard picker + autofill)
- fp.direct.order.wizard (action_create_order post-creates spec on SO line)

Thickness picker switched to fp.recipe.thickness (replaces coating-scoped):
- sale.order.line.x_fc_thickness_id comodel + domain rewired to recipe
- account.move.line + fp.delivery same
- fp.direct.order.line.thickness_id same

View inherits in quality add Specification picker next to legacy
Primary Treatment column on:
- SO form line tree
- part catalog Default Treatments block
- direct-order wizard line tree + drawer

Wizard files (fp.contract.review.client.email.wizard) pulled from
entech into the repo — they were ahead of the repo. Quality __init__
now imports wizards/.

Legacy x_fc_coating_config_id + treatment_ids remain visible during
transition; Phase E removes them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-15 01:16:25 -04:00
parent c96f27b96c
commit 7cafab1b9f
23 changed files with 486 additions and 29 deletions

View File

@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from . import fp_contract_review_client_email_wizard

View File

@@ -0,0 +1,136 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class FpContractReviewClientEmailWizard(models.TransientModel):
"""Email-composer wizard for the Contract Review "Awaiting Client Info"
workflow. Pre-fills subject + body from the QA failure reason so the
QA Signer (Brett, or any other configured signer) can ping the
customer in a single click.
Sending the wizard:
1. Posts a chatter message of message_type='email' on the review
(the smart-button counter on the review form picks this up).
2. Sends the actual email via mail.mail to the customer's email.
3. Stamps `info_requested_date` on the review the first time, so
the form clearly shows when the request went out.
"""
_name = 'fp.contract.review.client.email.wizard'
_description = 'Contract Review — Email Client (Request Info)'
review_id = fields.Many2one(
'fp.contract.review',
string='Contract Review',
required=True,
ondelete='cascade',
)
customer_id = fields.Many2one(
'res.partner',
related='review_id.customer_id',
readonly=True,
)
recipient_email = fields.Char(
string='To',
required=True,
help='Customer contact email. Edit if the request needs to go to a '
'specific buyer / engineer.',
)
recipient_name = fields.Char(
string='Recipient Name',
)
subject = fields.Char(
string='Subject',
required=True,
)
body = fields.Html(
string='Message',
required=True,
sanitize=True,
)
@api.model
def default_get(self, fields_list):
vals = super().default_get(fields_list)
review_id = self.env.context.get('default_review_id')
if review_id:
review = self.env['fp.contract.review'].browse(review_id)
company = review.company_id or self.env.company
part_label = (review.part_id and review.part_id.display_name) or '-'
po_label = review.contract_po_number or review.quote_or_job_number or '-'
failure_html = review.qa_failure_reason or _(
'<p>(Reason not yet captured — type details here.)</p>'
)
if 'subject' in fields_list and not vals.get('subject'):
vals['subject'] = _(
'%(company)s — Information request for Contract Review '
'%(name)s (PO %(po)s)'
) % {
'company': company.name or '',
'name': review.name or '',
'po': po_label,
}
if 'body' in fields_list and not vals.get('body'):
vals['body'] = _(
'<p>Hello %(recipient)s,</p>'
'<p>We are reviewing your contract for <b>%(part)s</b> '
'(PO %(po)s) and need additional information to '
'finalise our QA-005 review.</p>'
'<p><b>Items requiring clarification:</b></p>'
'%(failure)s'
'<p>Please reply with the requested information at '
'your earliest convenience so we can complete the '
'review and proceed with production.</p>'
'<p>Thank you,<br/>%(company)s — Quality Team</p>'
) % {
'recipient': (review.customer_id.name or _('there')),
'part': part_label,
'po': po_label,
'failure': failure_html,
'company': company.name or '',
}
return vals
def action_send(self):
"""Send the email + post chatter + stamp request date."""
self.ensure_one()
if not self.recipient_email:
raise UserError(_(
'A recipient email is required. Set the customer\'s email '
'on their contact card or override here.'
))
review = self.review_id
# Post into the review's chatter as message_type='email' so the
# smart-button counter picks it up. message_post handles the
# actual mail.mail send when partner_ids / email_to is set.
review.message_post(
body=self.body,
subject=self.subject,
message_type='email',
subtype_xmlid='mail.mt_comment',
partner_ids=review.customer_id.ids if review.customer_id else [],
email_layout_xmlid='mail.mail_notification_light',
email_add_signature=True,
)
# Belt-and-braces direct send to the recipient_email when it
# differs from the partner's primary email (e.g. a buyer-specific
# address typed into the wizard).
partner_email = review.customer_id.email if review.customer_id else ''
if self.recipient_email and self.recipient_email != partner_email:
self.env['mail.mail'].sudo().create({
'subject': self.subject,
'body_html': self.body,
'email_from': self.env.user.email_formatted or
(review.company_id and review.company_id.email) or '',
'email_to': self.recipient_email,
'auto_delete': True,
'model': 'fp.contract.review',
'res_id': review.id,
}).send()
if not review.info_requested_date:
review.write({'info_requested_date': fields.Datetime.now()})
return {'type': 'ir.actions.act_window_close'}

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
-->
<odoo>
<record id="view_fp_contract_review_client_email_wizard_form" model="ir.ui.view">
<field name="name">fp.contract.review.client.email.wizard.form</field>
<field name="model">fp.contract.review.client.email.wizard</field>
<field name="arch" type="xml">
<form string="Email Client — Request Info">
<sheet>
<group>
<field name="review_id" readonly="1"/>
<field name="customer_id" readonly="1"/>
<field name="recipient_email"/>
<field name="recipient_name"/>
<field name="subject"/>
</group>
<separator string="Message"/>
<field name="body" placeholder="Compose the message to the client. The body has been pre-filled with the QA failure reason — edit as needed."/>
</sheet>
<footer>
<button name="action_send"
type="object"
string="Send Email"
class="btn-primary"
icon="fa-paper-plane"/>
<button special="cancel"
string="Cancel"
class="btn-secondary"/>
</footer>
</form>
</field>
</record>
</odoo>