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