feat(fusion_plating): quote-to-cash infra, notifications, wizards, Tier 1 plating features

Quote-to-cash PDF reports (portrait + landscape variants, 16 new actions):
- Quotation / Sales Order, Work Order Traveller, Packing Slip, Bill of Lading,
  Certificate of Conformance (portrait added), Invoice, Payment Receipt
- Shared fp_portrait_styles + fp_landscape_styles base templates

Workflow gap fixes (fusion_plating_bridge_mrp):
- Auto-assign recipe from SO coating config in MrpProduction.action_confirm
- Auto-create draft CoC (fp.certificate) on MrpProduction.button_mark_done

Notifications overhaul (fusion_plating_notifications v2.0):
- Expanded TRIGGER_EVENTS to 7 (added quote_sent, mo_complete, shipped, payment_received)
- Shared _dispatch method replaces three duplicated send helpers
- Auto-attach PDF reports per template config (quote, SO, CoC, invoice, receipt, BoL)
- Rebuilt 7 email templates with fusion_claims accent-bar design
  (info/success color-coded, theme-safe, 600px max-width)
- New hooks: MrpProduction done, FpDelivery mark_delivered, AccountPayment post,
  SaleOrder action_quotation_send

Wizards (fusion_plating_configurator):
- fp.direct.order.wizard — skip quotation for repeat customers with PO in hand;
  optional new-revision drawing upload bumps fp.part.catalog revision and links
  new rev to the SO; creates + confirms the SO in one step
- fp.part.catalog.import.wizard — 3-step CSV import with dry-run preview,
  tolerant parsing (customer by name/email/xmlid, human-readable selections),
  duplicate detection, create-missing-customers option, single transaction commit
- Partner form stat buttons: Direct Order, Import Parts
- CSV template download button

Tier 1 practical plating features:
- T1.1 Hydrogen bake window enforcement (fp.coating.config.requires_bake_relief,
  auto-create fusion.plating.bake.window on plating WO finish, FpDelivery lockout
  when window is open)
- T1.2 Bath replenishment rules + pending suggestion queue
  (fusion.plating.bath.replenishment.rule + .suggestion, hook on bath log line
  create, operator Apply / Dismiss actions)
- T1.3 Rack/fixture library (fusion.plating.rack with MTO counter, strip
  schedule, lifecycle: active → needs_strip → stripping → retired)
- T1.4 Rework / strip-and-replate MOs (x_fc_is_rework, x_fc_original_production_id,
  Create Rework stat button on completed MOs)
- T1.5 Parts location (x_fc_current_location computed on mrp.production —
  "In progress: Alkaline Clean" / "Queued: Bake Oven" / "Ready to Ship")

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-16 23:41:12 -04:00
parent 7c7ef06057
commit d3dd6376a6
51 changed files with 5231 additions and 197 deletions

View File

@@ -5,16 +5,9 @@
{
'name': 'Fusion Plating — Notifications',
'version': '19.0.1.0.0',
'version': '19.0.2.0.0',
'category': 'Manufacturing/Plating',
'summary': 'Auto-email notifications at workflow milestones with configurable templates and audit log.',
'description': """
Fusion Plating — Notifications
================================
Automated email notifications triggered at key workflow events:
SO confirmation, parts received, invoice posted, and more.
""",
'summary': 'Auto-email notifications at workflow milestones with configurable templates, PDF attachments, and audit log.',
'author': 'Nexa Systems Inc.',
'website': 'https://www.nexasystems.ca',
'maintainer': 'Nexa Systems Inc.',
@@ -26,8 +19,12 @@ SO confirmation, parts received, invoice posted, and more.
'fusion_plating_configurator',
'fusion_plating_receiving',
'fusion_plating_invoicing',
'fusion_plating_bridge_mrp',
'fusion_plating_logistics',
'fusion_plating_reports',
'sale_management',
'account',
'mrp',
'mail',
],
'data': [

View File

@@ -1,11 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="fp_notif_quote_sent" model="fp.notification.template">
<field name="name">Quotation Sent</field>
<field name="trigger_event">quote_sent</field>
<field name="mail_template_id" ref="fp_mail_template_quote_sent"/>
<field name="active" eval="True"/>
<field name="attach_quotation" eval="True"/>
</record>
<record id="fp_notif_so_confirmed" model="fp.notification.template">
<field name="name">Order Confirmation</field>
<field name="trigger_event">so_confirmed</field>
<field name="mail_template_id" ref="fp_mail_template_so_confirmed"/>
<field name="active" eval="True"/>
<field name="attach_sale_order" eval="True"/>
</record>
<record id="fp_notif_parts_received" model="fp.notification.template">
@@ -15,6 +24,22 @@
<field name="active" eval="True"/>
</record>
<record id="fp_notif_mo_complete" model="fp.notification.template">
<field name="name">Manufacturing Complete</field>
<field name="trigger_event">mo_complete</field>
<field name="mail_template_id" ref="fp_mail_template_mo_complete"/>
<field name="active" eval="True"/>
</record>
<record id="fp_notif_shipped" model="fp.notification.template">
<field name="name">Shipped / Delivered</field>
<field name="trigger_event">shipped</field>
<field name="mail_template_id" ref="fp_mail_template_shipped"/>
<field name="active" eval="True"/>
<field name="attach_coc" eval="True"/>
<field name="attach_bol" eval="True"/>
</record>
<record id="fp_notif_invoice_posted" model="fp.notification.template">
<field name="name">Invoice Posted</field>
<field name="trigger_event">invoice_posted</field>
@@ -23,4 +48,12 @@
<field name="attach_invoice" eval="True"/>
</record>
<record id="fp_notif_payment_received" model="fp.notification.template">
<field name="name">Payment Received</field>
<field name="trigger_event">payment_received</field>
<field name="mail_template_id" ref="fp_mail_template_payment_received"/>
<field name="active" eval="True"/>
<field name="attach_receipt" eval="True"/>
</record>
</odoo>

View File

@@ -1,51 +1,404 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Mail templates use the Fusion-Claims accent-bar design:
- 4 px accent bar in status colour (info / success / attention / urgent)
- 600 px max-width, centred, Segoe UI stack
- Theme-safe: opacity + rgba grey, no fixed backgrounds
-->
<odoo noupdate="1">
<!-- ============================================================= -->
<!-- 1. Quote Sent (Info, #2B6CB0) -->
<!-- ============================================================= -->
<record id="fp_mail_template_quote_sent" model="mail.template">
<field name="name">FP: Quotation Sent</field>
<field name="model_id" ref="sale.model_sale_order"/>
<field name="subject">Quotation {{ object.name }} — EN Technologies</field>
<field name="email_from">{{ (object.company_id.email or user.email) }}</field>
<field name="email_to">{{ object.partner_id.email }}</field>
<field name="auto_delete" eval="True"/>
<field name="body_html" type="html">
<div style="font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif; max-width: 600px; margin: 0 auto; padding: 32px 24px;">
<div style="height: 4px; background-color: #2B6CB0; margin-bottom: 28px;"></div>
<div style="font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: #2B6CB0; font-weight: 600; margin-bottom: 8px;">
EN Technologies
</div>
<h2 style="margin: 0 0 8px 0; font-size: 22px; font-weight: bold;">Quotation Ready</h2>
<p style="margin: 0 0 20px 0; font-size: 15px; opacity: 0.65;">
Hi <t t-out="object.partner_id.name or ''"/>, your quotation is attached for review.
</p>
<table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
<tr style="border-bottom: 2px solid rgba(128,128,128,0.35);">
<th style="text-align: left; padding: 8px 4px; font-size: 12px; text-transform: uppercase; opacity: 0.55; font-weight: 600;">Detail</th>
<th style="text-align: right; padding: 8px 4px; font-size: 12px; text-transform: uppercase; opacity: 0.55; font-weight: 600;">Value</th>
</tr>
<tr style="border-bottom: 1px solid rgba(128,128,128,0.25);">
<td style="padding: 8px 4px;">Quote Reference</td>
<td style="padding: 8px 4px; text-align: right; font-family: monospace;"><t t-out="object.name"/></td>
</tr>
<tr style="border-bottom: 1px solid rgba(128,128,128,0.25); background: rgba(128,128,128,0.06);">
<td style="padding: 8px 4px;">Valid Until</td>
<td style="padding: 8px 4px; text-align: right;"><t t-out="object.validity_date or 'On request'"/></td>
</tr>
<tr style="border-bottom: 1px solid rgba(128,128,128,0.25);">
<td style="padding: 8px 4px;"><strong>Total</strong></td>
<td style="padding: 8px 4px; text-align: right; font-family: monospace;"><strong><t t-out="object.currency_id.symbol or ''"/><t t-out="'{:,.2f}'.format(object.amount_total)"/></strong></td>
</tr>
</table>
<div style="border-left: 3px solid #2B6CB0; padding: 12px 16px; margin: 20px 0; font-size: 14px;">
<strong>Next steps:</strong> Reply with a PO number or signed copy to accept. Once we receive your parts, we'll confirm receipt and begin production.
</div>
<div style="margin-top: 32px; font-size: 14px;">
Best regards,<br/>
<strong><t t-out="user.name or ''"/></strong><br/>
EN Technologies Inc.
</div>
<div style="margin-top: 40px; padding-top: 16px; border-top: 1px solid rgba(128,128,128,0.25); font-size: 11px; opacity: 0.5; text-align: center;">
This is an automated notification from EN Technologies production system.
</div>
</div>
</field>
</record>
<!-- ============================================================= -->
<!-- 2. Sales Order Confirmed (Success, #38a169) -->
<!-- ============================================================= -->
<record id="fp_mail_template_so_confirmed" model="mail.template">
<field name="name">FP: Order Confirmation</field>
<field name="model_id" ref="sale.model_sale_order"/>
<field name="subject">Order Confirmation — {{ object.name }}</field>
<field name="subject">Order Confirmed — {{ object.name }}</field>
<field name="email_from">{{ (object.company_id.email or user.email) }}</field>
<field name="email_to">{{ object.partner_id.email }}</field>
<field name="body_html" type="html">
<p>Dear {{ object.partner_id.name }},</p>
<p>Your order <strong>{{ object.name }}</strong> has been confirmed.</p>
<p>We will notify you when your parts have been received at our facility.</p>
<p>Thank you for your business.</p>
<p>— EN Technologies Inc.</p>
</field>
<field name="auto_delete" eval="True"/>
<field name="body_html" type="html">
<div style="font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif; max-width: 600px; margin: 0 auto; padding: 32px 24px;">
<div style="height: 4px; background-color: #38a169; margin-bottom: 28px;"></div>
<div style="font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: #38a169; font-weight: 600; margin-bottom: 8px;">
EN Technologies
</div>
<h2 style="margin: 0 0 8px 0; font-size: 22px; font-weight: bold;">Order Confirmed</h2>
<p style="margin: 0 0 20px 0; font-size: 15px; opacity: 0.65;">
Thank you, <t t-out="object.partner_id.name or ''"/>. We have your order and will notify you the moment your parts arrive at our facility.
</p>
<table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
<tr style="border-bottom: 2px solid rgba(128,128,128,0.35);">
<th style="text-align: left; padding: 8px 4px; font-size: 12px; text-transform: uppercase; opacity: 0.55; font-weight: 600;">Order</th>
<th style="text-align: right; padding: 8px 4px; font-size: 12px; text-transform: uppercase; opacity: 0.55; font-weight: 600;">Detail</th>
</tr>
<tr style="border-bottom: 1px solid rgba(128,128,128,0.25);">
<td style="padding: 8px 4px;">Reference</td>
<td style="padding: 8px 4px; text-align: right; font-family: monospace;"><t t-out="object.name"/></td>
</tr>
<tr style="border-bottom: 1px solid rgba(128,128,128,0.25); background: rgba(128,128,128,0.06);">
<td style="padding: 8px 4px;">Customer PO</td>
<td style="padding: 8px 4px; text-align: right;"><t t-out="object.x_fc_po_number or '—'"/></td>
</tr>
<tr style="border-bottom: 1px solid rgba(128,128,128,0.25);">
<td style="padding: 8px 4px;">Order Date</td>
<td style="padding: 8px 4px; text-align: right;"><t t-out="object.date_order.strftime('%b %d, %Y') if object.date_order else '—'"/></td>
</tr>
<tr style="border-bottom: 1px solid rgba(128,128,128,0.25); background: rgba(128,128,128,0.06);">
<td style="padding: 8px 4px;"><strong>Total</strong></td>
<td style="padding: 8px 4px; text-align: right; font-family: monospace;"><strong><t t-out="object.currency_id.symbol or ''"/><t t-out="'{:,.2f}'.format(object.amount_total)"/></strong></td>
</tr>
</table>
<div style="border-left: 3px solid #38a169; padding: 12px 16px; margin: 20px 0; font-size: 14px;">
<strong>What's next:</strong> Ship your parts to our facility. We'll inspect on arrival, run the process, and keep you posted at each milestone.
</div>
<div style="margin-top: 32px; font-size: 14px;">
Best regards,<br/>
<strong><t t-out="user.name or ''"/></strong><br/>
EN Technologies Inc.
</div>
<div style="margin-top: 40px; padding-top: 16px; border-top: 1px solid rgba(128,128,128,0.25); font-size: 11px; opacity: 0.5; text-align: center;">
This is an automated notification from EN Technologies production system.
</div>
</div>
</field>
</record>
<!-- ============================================================= -->
<!-- 3. Parts Received (Info, #2B6CB0) -->
<!-- ============================================================= -->
<record id="fp_mail_template_parts_received" model="mail.template">
<field name="name">FP: Parts Received</field>
<field name="model_id" eval="env['ir.model']._get_id('fp.receiving')"/>
<field name="subject">Parts Received — {{ object.name }}</field>
<field name="model_id" ref="fusion_plating_receiving.model_fp_receiving"/>
<field name="subject">Parts Received — {{ object.sale_order_id.name or object.name }}</field>
<field name="email_from">{{ (object.sale_order_id.company_id.email or user.email) }}</field>
<field name="email_to">{{ object.partner_id.email }}</field>
<field name="body_html" type="html">
<p>Dear {{ object.partner_id.name }},</p>
<p>We have received your parts for order <strong>{{ object.sale_order_id.name }}</strong>.</p>
<p>Quantity received: {{ object.received_qty }}</p>
<p>Your parts are now in our production queue. We will keep you updated on progress.</p>
<p>— EN Technologies Inc.</p>
</field>
<field name="auto_delete" eval="True"/>
<field name="body_html" type="html">
<div style="font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif; max-width: 600px; margin: 0 auto; padding: 32px 24px;">
<div style="height: 4px; background-color: #2B6CB0; margin-bottom: 28px;"></div>
<div style="font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: #2B6CB0; font-weight: 600; margin-bottom: 8px;">
EN Technologies
</div>
<h2 style="margin: 0 0 8px 0; font-size: 22px; font-weight: bold;">Parts Received</h2>
<p style="margin: 0 0 20px 0; font-size: 15px; opacity: 0.65;">
Good news, <t t-out="object.partner_id.name or ''"/>. Your parts have arrived at our facility and passed incoming inspection.
</p>
<table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
<tr style="border-bottom: 2px solid rgba(128,128,128,0.35);">
<th style="text-align: left; padding: 8px 4px; font-size: 12px; text-transform: uppercase; opacity: 0.55; font-weight: 600;">Detail</th>
<th style="text-align: right; padding: 8px 4px; font-size: 12px; text-transform: uppercase; opacity: 0.55; font-weight: 600;">Value</th>
</tr>
<tr style="border-bottom: 1px solid rgba(128,128,128,0.25);">
<td style="padding: 8px 4px;">Sale Order</td>
<td style="padding: 8px 4px; text-align: right; font-family: monospace;"><t t-out="object.sale_order_id.name or '—'"/></td>
</tr>
<tr style="border-bottom: 1px solid rgba(128,128,128,0.25); background: rgba(128,128,128,0.06);">
<td style="padding: 8px 4px;">Receiving Reference</td>
<td style="padding: 8px 4px; text-align: right; font-family: monospace;"><t t-out="object.name"/></td>
</tr>
<tr style="border-bottom: 1px solid rgba(128,128,128,0.25);">
<td style="padding: 8px 4px;">Qty Received</td>
<td style="padding: 8px 4px; text-align: right;"><t t-out="object.received_qty"/></td>
</tr>
</table>
<div style="border-left: 3px solid #2B6CB0; padding: 12px 16px; margin: 20px 0; font-size: 14px;">
<strong>Next step:</strong> Your job has entered our production queue. Expect a manufacturing-complete email once the job is through processing.
</div>
<div style="margin-top: 32px; font-size: 14px;">
Best regards,<br/>
<strong><t t-out="user.name or ''"/></strong><br/>
EN Technologies Inc.
</div>
<div style="margin-top: 40px; padding-top: 16px; border-top: 1px solid rgba(128,128,128,0.25); font-size: 11px; opacity: 0.5; text-align: center;">
This is an automated notification from EN Technologies production system.
</div>
</div>
</field>
</record>
<!-- ============================================================= -->
<!-- 4. Manufacturing Complete (Info, #2B6CB0) -->
<!-- ============================================================= -->
<record id="fp_mail_template_mo_complete" model="mail.template">
<field name="name">FP: Manufacturing Complete</field>
<field name="model_id" ref="mrp.model_mrp_production"/>
<field name="subject">Job Complete — {{ object.x_fc_portal_job_id.name or object.name }}</field>
<field name="email_from">{{ (object.company_id.email or user.email) }}</field>
<field name="email_to">{{ object.x_fc_portal_job_id.partner_id.email }}</field>
<field name="auto_delete" eval="True"/>
<field name="body_html" type="html">
<div style="font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif; max-width: 600px; margin: 0 auto; padding: 32px 24px;">
<div style="height: 4px; background-color: #2B6CB0; margin-bottom: 28px;"></div>
<div style="font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: #2B6CB0; font-weight: 600; margin-bottom: 8px;">
EN Technologies
</div>
<h2 style="margin: 0 0 8px 0; font-size: 22px; font-weight: bold;">Manufacturing Complete — Ready to Ship</h2>
<p style="margin: 0 0 20px 0; font-size: 15px; opacity: 0.65;">
Hi <t t-out="object.x_fc_portal_job_id.partner_id.name or ''"/>, your job has cleared production and quality. We are preparing it for shipment.
</p>
<table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
<tr style="border-bottom: 2px solid rgba(128,128,128,0.35);">
<th style="text-align: left; padding: 8px 4px; font-size: 12px; text-transform: uppercase; opacity: 0.55; font-weight: 600;">Detail</th>
<th style="text-align: right; padding: 8px 4px; font-size: 12px; text-transform: uppercase; opacity: 0.55; font-weight: 600;">Value</th>
</tr>
<tr style="border-bottom: 1px solid rgba(128,128,128,0.25);">
<td style="padding: 8px 4px;">Job Reference</td>
<td style="padding: 8px 4px; text-align: right; font-family: monospace;"><t t-out="object.x_fc_portal_job_id.name or object.name"/></td>
</tr>
<tr style="border-bottom: 1px solid rgba(128,128,128,0.25); background: rgba(128,128,128,0.06);">
<td style="padding: 8px 4px;">Sale Order</td>
<td style="padding: 8px 4px; text-align: right; font-family: monospace;"><t t-out="object.origin or '—'"/></td>
</tr>
<tr style="border-bottom: 1px solid rgba(128,128,128,0.25);">
<td style="padding: 8px 4px;">Quantity</td>
<td style="padding: 8px 4px; text-align: right;"><t t-out="int(object.product_qty)"/></td>
</tr>
</table>
<div style="border-left: 3px solid #2B6CB0; padding: 12px 16px; margin: 20px 0; font-size: 14px;">
<strong>Next:</strong> Your Certificate of Conformance will be issued with the shipment. Delivery scheduling to follow.
</div>
<div style="margin-top: 32px; font-size: 14px;">
Best regards,<br/>
<strong><t t-out="user.name or ''"/></strong><br/>
EN Technologies Inc.
</div>
<div style="margin-top: 40px; padding-top: 16px; border-top: 1px solid rgba(128,128,128,0.25); font-size: 11px; opacity: 0.5; text-align: center;">
This is an automated notification from EN Technologies production system.
</div>
</div>
</field>
</record>
<!-- ============================================================= -->
<!-- 5. Shipped / Delivered (Success, #38a169) -->
<!-- ============================================================= -->
<record id="fp_mail_template_shipped" model="mail.template">
<field name="name">FP: Shipped / Delivered</field>
<field name="model_id" ref="fusion_plating_logistics.model_fusion_plating_delivery"/>
<field name="subject">Shipped — {{ object.job_ref or object.name }}</field>
<field name="email_from">{{ (object.company_id.email or user.email) }}</field>
<field name="email_to">{{ object.partner_id.email }}</field>
<field name="auto_delete" eval="True"/>
<field name="body_html" type="html">
<div style="font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif; max-width: 600px; margin: 0 auto; padding: 32px 24px;">
<div style="height: 4px; background-color: #38a169; margin-bottom: 28px;"></div>
<div style="font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: #38a169; font-weight: 600; margin-bottom: 8px;">
EN Technologies
</div>
<h2 style="margin: 0 0 8px 0; font-size: 22px; font-weight: bold;">Your Parts Have Shipped</h2>
<p style="margin: 0 0 20px 0; font-size: 15px; opacity: 0.65;">
Hi <t t-out="object.partner_id.name or ''"/>, your order has left our facility. Certificate of Conformance and Bill of Lading are attached for your records.
</p>
<table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
<tr style="border-bottom: 2px solid rgba(128,128,128,0.35);">
<th style="text-align: left; padding: 8px 4px; font-size: 12px; text-transform: uppercase; opacity: 0.55; font-weight: 600;">Shipment</th>
<th style="text-align: right; padding: 8px 4px; font-size: 12px; text-transform: uppercase; opacity: 0.55; font-weight: 600;">Detail</th>
</tr>
<tr style="border-bottom: 1px solid rgba(128,128,128,0.25);">
<td style="padding: 8px 4px;">Delivery Ref</td>
<td style="padding: 8px 4px; text-align: right; font-family: monospace;"><t t-out="object.name"/></td>
</tr>
<tr style="border-bottom: 1px solid rgba(128,128,128,0.25); background: rgba(128,128,128,0.06);">
<td style="padding: 8px 4px;">Job Reference</td>
<td style="padding: 8px 4px; text-align: right; font-family: monospace;"><t t-out="object.job_ref or '—'"/></td>
</tr>
<tr style="border-bottom: 1px solid rgba(128,128,128,0.25);">
<td style="padding: 8px 4px;">Delivered</td>
<td style="padding: 8px 4px; text-align: right;"><t t-out="object.delivered_at.strftime('%b %d, %Y %H:%M') if object.delivered_at else '—'"/></td>
</tr>
<tr style="border-bottom: 1px solid rgba(128,128,128,0.25); background: rgba(128,128,128,0.06);">
<td style="padding: 8px 4px;">Driver</td>
<td style="padding: 8px 4px; text-align: right;"><t t-out="object.assigned_driver_id.name or '—'"/></td>
</tr>
</table>
<div style="border-left: 3px solid #38a169; padding: 12px 16px; margin: 20px 0; font-size: 14px;">
<strong>Next:</strong> Your invoice will follow shortly. Please inspect your parts on receipt and contact us with any questions.
</div>
<div style="margin-top: 32px; font-size: 14px;">
Best regards,<br/>
<strong><t t-out="user.name or ''"/></strong><br/>
EN Technologies Inc.
</div>
<div style="margin-top: 40px; padding-top: 16px; border-top: 1px solid rgba(128,128,128,0.25); font-size: 11px; opacity: 0.5; text-align: center;">
This is an automated notification from EN Technologies production system.
</div>
</div>
</field>
</record>
<!-- ============================================================= -->
<!-- 6. Invoice Posted (Info, #2B6CB0) -->
<!-- ============================================================= -->
<record id="fp_mail_template_invoice_posted" model="mail.template">
<field name="name">FP: Invoice Notification</field>
<field name="model_id" ref="account.model_account_move"/>
<field name="subject">Invoice {{ object.name }} — EN Technologies</field>
<field name="email_from">{{ (object.company_id.email or user.email) }}</field>
<field name="email_to">{{ object.partner_id.email }}</field>
<field name="body_html" type="html">
<p>Dear {{ object.partner_id.name }},</p>
<p>Please find your invoice <strong>{{ object.name }}</strong> for amount <strong>{{ object.amount_total }}</strong>.</p>
<p>Thank you for your business.</p>
<p>— EN Technologies Inc.</p>
</field>
<field name="auto_delete" eval="True"/>
<field name="body_html" type="html">
<div style="font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif; max-width: 600px; margin: 0 auto; padding: 32px 24px;">
<div style="height: 4px; background-color: #2B6CB0; margin-bottom: 28px;"></div>
<div style="font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: #2B6CB0; font-weight: 600; margin-bottom: 8px;">
EN Technologies
</div>
<h2 style="margin: 0 0 8px 0; font-size: 22px; font-weight: bold;">Invoice Ready</h2>
<p style="margin: 0 0 20px 0; font-size: 15px; opacity: 0.65;">
Hi <t t-out="object.partner_id.name or ''"/>, please find invoice <t t-out="object.name"/> attached.
</p>
<table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
<tr style="border-bottom: 2px solid rgba(128,128,128,0.35);">
<th style="text-align: left; padding: 8px 4px; font-size: 12px; text-transform: uppercase; opacity: 0.55; font-weight: 600;">Invoice</th>
<th style="text-align: right; padding: 8px 4px; font-size: 12px; text-transform: uppercase; opacity: 0.55; font-weight: 600;">Detail</th>
</tr>
<tr style="border-bottom: 1px solid rgba(128,128,128,0.25);">
<td style="padding: 8px 4px;">Invoice Number</td>
<td style="padding: 8px 4px; text-align: right; font-family: monospace;"><t t-out="object.name"/></td>
</tr>
<tr style="border-bottom: 1px solid rgba(128,128,128,0.25); background: rgba(128,128,128,0.06);">
<td style="padding: 8px 4px;">Source Order</td>
<td style="padding: 8px 4px; text-align: right; font-family: monospace;"><t t-out="object.invoice_origin or '—'"/></td>
</tr>
<tr style="border-bottom: 1px solid rgba(128,128,128,0.25);">
<td style="padding: 8px 4px;">Invoice Date</td>
<td style="padding: 8px 4px; text-align: right;"><t t-out="object.invoice_date.strftime('%b %d, %Y') if object.invoice_date else '—'"/></td>
</tr>
<tr style="border-bottom: 1px solid rgba(128,128,128,0.25); background: rgba(128,128,128,0.06);">
<td style="padding: 8px 4px;">Due Date</td>
<td style="padding: 8px 4px; text-align: right;"><t t-out="object.invoice_date_due.strftime('%b %d, %Y') if object.invoice_date_due else '—'"/></td>
</tr>
<tr style="border-bottom: 1px solid rgba(128,128,128,0.25);">
<td style="padding: 8px 4px;"><strong>Amount Due</strong></td>
<td style="padding: 8px 4px; text-align: right; font-family: monospace;"><strong><t t-out="object.currency_id.symbol or ''"/><t t-out="'{:,.2f}'.format(object.amount_residual or object.amount_total)"/></strong></td>
</tr>
</table>
<div style="border-left: 3px solid #2B6CB0; padding: 12px 16px; margin: 20px 0; font-size: 14px;">
<strong>Payment:</strong> Please remit by the due date. Reference invoice number <t t-out="object.name"/> on your payment.
</div>
<div style="margin-top: 32px; font-size: 14px;">
Best regards,<br/>
<strong><t t-out="user.name or ''"/></strong><br/>
EN Technologies Inc.
</div>
<div style="margin-top: 40px; padding-top: 16px; border-top: 1px solid rgba(128,128,128,0.25); font-size: 11px; opacity: 0.5; text-align: center;">
This is an automated notification from EN Technologies production system.
</div>
</div>
</field>
</record>
<!-- ============================================================= -->
<!-- 7. Payment Received (Success, #38a169) -->
<!-- ============================================================= -->
<record id="fp_mail_template_payment_received" model="mail.template">
<field name="name">FP: Payment Received</field>
<field name="model_id" ref="account.model_account_payment"/>
<field name="subject">Payment Received — {{ object.name }}</field>
<field name="email_from">{{ (object.company_id.email or user.email) }}</field>
<field name="email_to">{{ object.partner_id.email }}</field>
<field name="auto_delete" eval="True"/>
<field name="body_html" type="html">
<div style="font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif; max-width: 600px; margin: 0 auto; padding: 32px 24px;">
<div style="height: 4px; background-color: #38a169; margin-bottom: 28px;"></div>
<div style="font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: #38a169; font-weight: 600; margin-bottom: 8px;">
EN Technologies
</div>
<h2 style="margin: 0 0 8px 0; font-size: 22px; font-weight: bold;">Payment Received — Thank You</h2>
<p style="margin: 0 0 20px 0; font-size: 15px; opacity: 0.65;">
Hi <t t-out="object.partner_id.name or ''"/>, we've received your payment. Your receipt is attached.
</p>
<table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
<tr style="border-bottom: 2px solid rgba(128,128,128,0.35);">
<th style="text-align: left; padding: 8px 4px; font-size: 12px; text-transform: uppercase; opacity: 0.55; font-weight: 600;">Payment</th>
<th style="text-align: right; padding: 8px 4px; font-size: 12px; text-transform: uppercase; opacity: 0.55; font-weight: 600;">Detail</th>
</tr>
<tr style="border-bottom: 1px solid rgba(128,128,128,0.25);">
<td style="padding: 8px 4px;">Receipt Number</td>
<td style="padding: 8px 4px; text-align: right; font-family: monospace;"><t t-out="object.name"/></td>
</tr>
<tr style="border-bottom: 1px solid rgba(128,128,128,0.25); background: rgba(128,128,128,0.06);">
<td style="padding: 8px 4px;">Payment Method</td>
<td style="padding: 8px 4px; text-align: right;"><t t-out="object.payment_method_line_id.name or '—'"/></td>
</tr>
<tr style="border-bottom: 1px solid rgba(128,128,128,0.25);">
<td style="padding: 8px 4px;">Payment Date</td>
<td style="padding: 8px 4px; text-align: right;"><t t-out="object.date.strftime('%b %d, %Y') if object.date else '—'"/></td>
</tr>
<tr style="border-bottom: 1px solid rgba(128,128,128,0.25); background: rgba(128,128,128,0.06);">
<td style="padding: 8px 4px;"><strong>Amount</strong></td>
<td style="padding: 8px 4px; text-align: right; font-family: monospace;"><strong><t t-out="object.currency_id.symbol or ''"/><t t-out="'{:,.2f}'.format(object.amount)"/></strong></td>
</tr>
</table>
<div style="border-left: 3px solid #38a169; padding: 12px 16px; margin: 20px 0; font-size: 14px;">
Your account balance has been updated. We appreciate your business.
</div>
<div style="margin-top: 32px; font-size: 14px;">
Best regards,<br/>
<strong><t t-out="user.name or ''"/></strong><br/>
EN Technologies Inc.
</div>
<div style="margin-top: 40px; padding-top: 16px; border-top: 1px solid rgba(128,128,128,0.25); font-size: 11px; opacity: 0.5; text-align: center;">
This is an automated notification from EN Technologies production system.
</div>
</div>
</field>
</record>
</odoo>

View File

@@ -8,3 +8,6 @@ from . import fp_notification_log
from . import sale_order
from . import fp_receiving
from . import account_move
from . import account_payment
from . import mrp_production
from . import fp_delivery

View File

@@ -3,56 +3,24 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
import logging
from odoo import models
_logger = logging.getLogger(__name__)
class AccountMove(models.Model):
_inherit = 'account.move'
def action_post(self):
res = super().action_post()
Dispatch = self.env['fp.notification.template']
for move in self:
if move.move_type == 'out_invoice' and move.partner_id:
# Find linked SO
so = False
if move.invoice_origin:
so = self.env['sale.order'].search(
[('name', '=', move.invoice_origin)], limit=1,
)
self._send_fp_notification(
'invoice_posted', move, move.partner_id, sale_order=so,
if move.move_type != 'out_invoice' or not move.partner_id:
continue
so = False
if move.invoice_origin:
so = self.env['sale.order'].search(
[('name', '=', move.invoice_origin)], limit=1,
)
Dispatch._dispatch(
'invoice_posted', move, move.partner_id, sale_order=so,
)
return res
def _send_fp_notification(self, trigger_event, record, partner, sale_order=None):
"""Send a notification email and log it."""
template = self.env['fp.notification.template'].search(
[('trigger_event', '=', trigger_event), ('active', '=', True)], limit=1,
)
if not template or not template.mail_template_id:
return
try:
template.mail_template_id.send_mail(record.id, force_send=False)
self.env['fp.notification.log'].create({
'template_id': template.id,
'trigger_event': trigger_event,
'sale_order_id': sale_order.id if sale_order else False,
'partner_id': partner.id if partner else False,
'recipient_email': partner.email if partner else '',
'status': 'sent',
})
except Exception as e:
_logger.warning('FP notification failed (%s): %s', trigger_event, e)
self.env['fp.notification.log'].create({
'template_id': template.id,
'trigger_event': trigger_event,
'sale_order_id': sale_order.id if sale_order else False,
'partner_id': partner.id if partner else False,
'recipient_email': partner.email if partner else '',
'status': 'failed',
'error_message': str(e),
})

View File

@@ -0,0 +1,30 @@
# -*- 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 models
class AccountPayment(models.Model):
_inherit = 'account.payment'
def action_post(self):
res = super().action_post()
Dispatch = self.env['fp.notification.template']
for pay in self:
# Only customer receipts (inbound payments from customers)
if pay.payment_type != 'inbound' or not pay.partner_id:
continue
if pay.partner_type != 'customer':
continue
so = False
inv = pay.reconciled_invoice_ids[:1]
if inv and inv.invoice_origin:
so = self.env['sale.order'].search(
[('name', '=', inv.invoice_origin)], limit=1,
)
Dispatch._dispatch(
'payment_received', pay, pay.partner_id, sale_order=so,
)
return res

View File

@@ -0,0 +1,31 @@
# -*- 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 models
class FpDelivery(models.Model):
_inherit = 'fusion.plating.delivery'
def action_mark_delivered(self):
res = super().action_mark_delivered()
Dispatch = self.env['fp.notification.template']
for rec in self:
if not rec.partner_id:
continue
so = False
if rec.job_ref:
# Delivery's job_ref is the MO name; find the SO via MO origin.
mo = self.env['mrp.production'].search(
[('name', '=', rec.job_ref)], limit=1,
)
if mo and mo.origin:
so = self.env['sale.order'].search(
[('name', '=', mo.origin)], limit=1,
)
Dispatch._dispatch(
'shipped', rec, rec.partner_id, sale_order=so,
)
return res

View File

@@ -5,15 +5,7 @@
from odoo import fields, models
TRIGGER_EVENTS = [
('so_confirmed', 'Order Confirmed'),
('parts_received', 'Parts Received'),
('mo_complete', 'Manufacturing Complete'),
('shipment', 'Shipment (Carrier)'),
('delivery', 'Delivery (Local)'),
('invoice_posted', 'Invoice Posted'),
('deposit_created', 'Deposit Required'),
]
from .fp_notification_template import TRIGGER_EVENTS
class FpNotificationLog(models.Model):

View File

@@ -3,15 +3,20 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from odoo import fields, models
import logging
from odoo import api, fields, models
_logger = logging.getLogger(__name__)
TRIGGER_EVENTS = [
('quote_sent', 'Quotation Sent'),
('so_confirmed', 'Order Confirmed'),
('parts_received', 'Parts Received'),
('mo_complete', 'Manufacturing Complete'),
('shipment', 'Shipment (Carrier)'),
('delivery', 'Delivery (Local)'),
('shipped', 'Shipped / Delivered'),
('invoice_posted', 'Invoice Posted'),
('payment_received', 'Payment Received'),
('deposit_created', 'Deposit Required'),
]
@@ -35,10 +40,14 @@ class FpNotificationTemplate(models.Model):
help='The Odoo mail template used to render and send the email.',
)
active = fields.Boolean(string='Active', default=True)
attach_quotation = fields.Boolean(string='Attach Quotation PDF')
attach_sale_order = fields.Boolean(string='Attach Sales Order PDF')
attach_coc = fields.Boolean(string='Attach CoC')
attach_thickness_report = fields.Boolean(string='Attach Thickness Report')
attach_invoice = fields.Boolean(string='Attach Invoice')
attach_receipt = fields.Boolean(string='Attach Payment Receipt')
attach_packing_list = fields.Boolean(string='Attach Packing List')
attach_bol = fields.Boolean(string='Attach Bill of Lading')
attach_pod = fields.Boolean(string='Attach Proof of Delivery')
cc_internal_ids = fields.Many2many(
'res.users', 'fp_notification_template_cc_rel',
@@ -49,3 +58,172 @@ class FpNotificationTemplate(models.Model):
('fp_notification_trigger_uniq', 'unique(trigger_event)',
'Only one notification template per trigger event.'),
]
# ------------------------------------------------------------------
# Central dispatch helper — called from every hook.
# ------------------------------------------------------------------
@api.model
def _dispatch(self, trigger_event, record, partner=None, sale_order=None,
extra_attachment_ids=None):
"""Look up the template for this trigger, render it, and send.
Also logs the attempt in fp.notification.log.
"""
template = self.search(
[('trigger_event', '=', trigger_event), ('active', '=', True)],
limit=1,
)
if not template or not template.mail_template_id:
return
partner = partner or getattr(record, 'partner_id', False)
# Build attachment list from template config
attachment_ids = list(extra_attachment_ids or [])
attachment_names = []
for att_id in template._collect_attachments(record):
attachment_ids.append(att_id)
if attachment_ids:
attachment_names = self.env['ir.attachment'].browse(attachment_ids).mapped('name')
Log = self.env['fp.notification.log']
try:
mail_id = template.mail_template_id.send_mail(
record.id,
force_send=False,
email_values={'attachment_ids': [(6, 0, attachment_ids)]} if attachment_ids else None,
)
Log.create({
'template_id': template.id,
'trigger_event': trigger_event,
'sale_order_id': sale_order.id if sale_order else False,
'partner_id': partner.id if partner else False,
'recipient_email': partner.email if partner else '',
'attachment_names': ', '.join(attachment_names) if attachment_names else '',
'status': 'sent',
'mail_mail_id': mail_id,
})
except Exception as exc:
_logger.warning('FP notification failed (%s): %s', trigger_event, exc)
Log.create({
'template_id': template.id,
'trigger_event': trigger_event,
'sale_order_id': sale_order.id if sale_order else False,
'partner_id': partner.id if partner else False,
'recipient_email': partner.email if partner else '',
'status': 'failed',
'error_message': str(exc),
})
def _collect_attachments(self, record):
"""Return a list of ir.attachment ids to attach to the email based
on the template's attach_* flags and the record's context.
"""
self.ensure_one()
Attachment = self.env['ir.attachment']
ids = []
# Resolve related records (MO, portal job, SO) from `record`
portal_job = None
production = None
sale_order = None
invoice = None
delivery = None
payment = None
model = record._name
if model == 'sale.order':
sale_order = record
portal_job = self.env['fusion.plating.portal.job'].search(
[('name', 'in', record.mapped('picking_ids.origin'))], limit=1,
) or None
elif model == 'account.move':
invoice = record
if record.invoice_origin:
sale_order = self.env['sale.order'].search(
[('name', '=', record.invoice_origin)], limit=1,
) or None
elif model == 'account.payment':
payment = record
invoice = record.reconciled_invoice_ids[:1]
if invoice and invoice.invoice_origin:
sale_order = self.env['sale.order'].search(
[('name', '=', invoice.invoice_origin)], limit=1,
) or None
elif model == 'mrp.production':
production = record
portal_job = record.x_fc_portal_job_id
if record.origin:
sale_order = self.env['sale.order'].search(
[('name', '=', record.origin)], limit=1,
) or None
elif model == 'fusion.plating.delivery':
delivery = record
if record.job_ref:
portal_job = self.env['fusion.plating.portal.job'].search(
[('name', '=', record.job_ref)], limit=1,
) or None
elif model == 'fp.receiving':
sale_order = record.sale_order_id
def _render_report(xmlid, rec):
"""Render a PDF report and return an attachment id."""
if not rec:
return None
try:
report = self.env.ref(xmlid, raise_if_not_found=False)
if not report:
return None
pdf_bytes, _fmt = self.env['ir.actions.report']._render_qweb_pdf(
xmlid, res_ids=rec.ids,
)
import base64
att = Attachment.create({
'name': f'{report.name} - {rec.display_name}.pdf',
'type': 'binary',
'datas': base64.b64encode(pdf_bytes),
'mimetype': 'application/pdf',
'res_model': rec._name,
'res_id': rec.id,
})
return att.id
except Exception as exc:
_logger.warning('Failed to render %s: %s', xmlid, exc)
return None
if self.attach_quotation and sale_order:
att = _render_report(
'fusion_plating_reports.action_report_fp_sale_portrait', sale_order,
)
if att:
ids.append(att)
if self.attach_sale_order and sale_order:
att = _render_report(
'fusion_plating_reports.action_report_fp_sale_portrait', sale_order,
)
if att:
ids.append(att)
if self.attach_coc and portal_job:
att = _render_report(
'fusion_plating_reports.action_report_coc', portal_job,
)
if att:
ids.append(att)
if self.attach_invoice and invoice:
att = _render_report(
'fusion_plating_reports.action_report_fp_invoice_portrait', invoice,
)
if att:
ids.append(att)
if self.attach_receipt and payment:
att = _render_report(
'fusion_plating_reports.action_report_fp_receipt_portrait', payment,
)
if att:
ids.append(att)
if self.attach_bol and delivery:
att = _render_report(
'fusion_plating_reports.action_report_fp_bol_portrait', delivery,
)
if att:
ids.append(att)
return ids

View File

@@ -3,50 +3,18 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
import logging
from odoo import models
_logger = logging.getLogger(__name__)
class FpReceiving(models.Model):
_inherit = 'fp.receiving'
def action_accept(self):
res = super().action_accept()
Dispatch = self.env['fp.notification.template']
for rec in self:
self._send_fp_notification(
Dispatch._dispatch(
'parts_received', rec, rec.partner_id,
sale_order=rec.sale_order_id,
)
return res
def _send_fp_notification(self, trigger_event, record, partner, sale_order=None):
"""Send a notification email and log it."""
template = self.env['fp.notification.template'].search(
[('trigger_event', '=', trigger_event), ('active', '=', True)], limit=1,
)
if not template or not template.mail_template_id:
return
try:
template.mail_template_id.send_mail(record.id, force_send=False)
self.env['fp.notification.log'].create({
'template_id': template.id,
'trigger_event': trigger_event,
'sale_order_id': sale_order.id if sale_order else False,
'partner_id': partner.id if partner else False,
'recipient_email': partner.email if partner else '',
'status': 'sent',
})
except Exception as e:
_logger.warning('FP notification failed (%s): %s', trigger_event, e)
self.env['fp.notification.log'].create({
'template_id': template.id,
'trigger_event': trigger_event,
'sale_order_id': sale_order.id if sale_order else False,
'partner_id': partner.id if partner else False,
'recipient_email': partner.email if partner else '',
'status': 'failed',
'error_message': str(e),
})

View File

@@ -0,0 +1,31 @@
# -*- 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 models
class MrpProduction(models.Model):
_inherit = 'mrp.production'
def button_mark_done(self):
res = super().button_mark_done()
Dispatch = self.env['fp.notification.template']
for mo in self:
partner = False
so = False
if mo.x_fc_portal_job_id:
partner = mo.x_fc_portal_job_id.partner_id
if mo.origin:
so = self.env['sale.order'].search(
[('name', '=', mo.origin)], limit=1,
)
if so and not partner:
partner = so.partner_id
if not partner:
continue
Dispatch._dispatch(
'mo_complete', mo, partner, sale_order=so,
)
return res

View File

@@ -3,49 +3,27 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
import logging
from odoo import models
_logger = logging.getLogger(__name__)
class SaleOrder(models.Model):
_inherit = 'sale.order'
def action_confirm(self):
res = super().action_confirm()
def action_quotation_send(self):
"""Fire the quote_sent trigger when a quotation is emailed."""
res = super().action_quotation_send()
Dispatch = self.env['fp.notification.template']
for order in self:
self._send_fp_notification(
'so_confirmed', order, order.partner_id, sale_order=order,
Dispatch._dispatch(
'quote_sent', order, order.partner_id, sale_order=order,
)
return res
def _send_fp_notification(self, trigger_event, record, partner, sale_order=None):
"""Send a notification email and log it."""
template = self.env['fp.notification.template'].search(
[('trigger_event', '=', trigger_event), ('active', '=', True)], limit=1,
)
if not template or not template.mail_template_id:
return
try:
template.mail_template_id.send_mail(record.id, force_send=False)
self.env['fp.notification.log'].create({
'template_id': template.id,
'trigger_event': trigger_event,
'sale_order_id': sale_order.id if sale_order else False,
'partner_id': partner.id if partner else False,
'recipient_email': partner.email if partner else '',
'status': 'sent',
})
except Exception as e:
_logger.warning('FP notification failed (%s): %s', trigger_event, e)
self.env['fp.notification.log'].create({
'template_id': template.id,
'trigger_event': trigger_event,
'sale_order_id': sale_order.id if sale_order else False,
'partner_id': partner.id if partner else False,
'recipient_email': partner.email if partner else '',
'status': 'failed',
'error_message': str(e),
})
def action_confirm(self):
res = super().action_confirm()
Dispatch = self.env['fp.notification.template']
for order in self:
Dispatch._dispatch(
'so_confirmed', order, order.partner_id, sale_order=order,
)
return res

View File

@@ -78,7 +78,7 @@
<filter name="filter_this_week" string="This Week"
domain="[('sent_date', '&gt;=', (context_today() - datetime.timedelta(days=context_today().weekday())).strftime('%Y-%m-%d'))]"/>
<separator/>
<group expand="1">
<group>
<filter name="group_trigger" string="Trigger Event"
context="{'group_by': 'trigger_event'}"/>
<filter name="group_status" string="Status"

View File

@@ -42,12 +42,16 @@
</group>
<group string="Attachments">
<group>
<field name="attach_quotation"/>
<field name="attach_sale_order"/>
<field name="attach_coc"/>
<field name="attach_thickness_report"/>
<field name="attach_invoice"/>
<field name="attach_receipt"/>
</group>
<group>
<field name="attach_packing_list"/>
<field name="attach_bol"/>
<field name="attach_pod"/>
</group>
</group>