Initial commit
This commit is contained in:
560
fusion_claims/wizard/odsp_sa_mobility_wizard.py
Normal file
560
fusion_claims/wizard/odsp_sa_mobility_wizard.py
Normal file
@@ -0,0 +1,560 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
import base64
|
||||
import io
|
||||
import os
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import pdfrw
|
||||
except ImportError:
|
||||
pdfrw = None
|
||||
_logger.warning("pdfrw not installed. SA Mobility PDF filling will not work.")
|
||||
|
||||
|
||||
class SAMobilityPartLine(models.TransientModel):
|
||||
_name = 'fusion_claims.sa.mobility.part.line'
|
||||
_description = 'SA Mobility Parts Line'
|
||||
_order = 'sequence'
|
||||
|
||||
wizard_id = fields.Many2one('fusion_claims.sa.mobility.wizard', ondelete='cascade')
|
||||
sequence = fields.Integer(default=10)
|
||||
qty = fields.Float(string='Qty', digits=(12, 2))
|
||||
description = fields.Char(string='Description')
|
||||
unit_price = fields.Float(string='Unit Price', digits=(12, 2))
|
||||
tax_id = fields.Many2one('account.tax', string='Tax Type',
|
||||
domain=[('type_tax_use', '=', 'sale')])
|
||||
taxes = fields.Float(string='Taxes', digits=(12, 2),
|
||||
compute='_compute_taxes', store=True)
|
||||
amount = fields.Float(string='Amount', compute='_compute_amount', store=True)
|
||||
|
||||
@api.depends('qty', 'unit_price', 'tax_id')
|
||||
def _compute_taxes(self):
|
||||
for line in self:
|
||||
subtotal = line.qty * line.unit_price
|
||||
line.taxes = subtotal * line.tax_id.amount / 100 if line.tax_id else 0.0
|
||||
|
||||
@api.depends('qty', 'unit_price', 'taxes')
|
||||
def _compute_amount(self):
|
||||
for line in self:
|
||||
line.amount = (line.qty * line.unit_price) + line.taxes
|
||||
|
||||
|
||||
class SAMobilityLabourLine(models.TransientModel):
|
||||
_name = 'fusion_claims.sa.mobility.labour.line'
|
||||
_description = 'SA Mobility Labour Line'
|
||||
_order = 'sequence'
|
||||
|
||||
wizard_id = fields.Many2one('fusion_claims.sa.mobility.wizard', ondelete='cascade')
|
||||
sequence = fields.Integer(default=10)
|
||||
hours = fields.Float(string='Hours', digits=(12, 2))
|
||||
rate = fields.Float(string='Rate', digits=(12, 2))
|
||||
tax_id = fields.Many2one('account.tax', string='Tax Type',
|
||||
domain=[('type_tax_use', '=', 'sale')])
|
||||
taxes = fields.Float(string='Taxes', digits=(12, 2),
|
||||
compute='_compute_taxes', store=True)
|
||||
amount = fields.Float(string='Amount', compute='_compute_amount', store=True)
|
||||
|
||||
@api.depends('hours', 'rate', 'tax_id')
|
||||
def _compute_taxes(self):
|
||||
for line in self:
|
||||
subtotal = line.hours * line.rate
|
||||
line.taxes = subtotal * line.tax_id.amount / 100 if line.tax_id else 0.0
|
||||
|
||||
@api.depends('hours', 'rate', 'taxes')
|
||||
def _compute_amount(self):
|
||||
for line in self:
|
||||
line.amount = (line.hours * line.rate) + line.taxes
|
||||
|
||||
|
||||
class SAMobilityFeeLine(models.TransientModel):
|
||||
_name = 'fusion_claims.sa.mobility.fee.line'
|
||||
_description = 'SA Mobility Additional Fee Line'
|
||||
_order = 'sequence'
|
||||
|
||||
wizard_id = fields.Many2one('fusion_claims.sa.mobility.wizard', ondelete='cascade')
|
||||
sequence = fields.Integer(default=10)
|
||||
description = fields.Char(string='Description')
|
||||
rate = fields.Float(string='Rate', digits=(12, 2))
|
||||
tax_id = fields.Many2one('account.tax', string='Tax Type',
|
||||
domain=[('type_tax_use', '=', 'sale')])
|
||||
taxes = fields.Float(string='Taxes', digits=(12, 2),
|
||||
compute='_compute_taxes', store=True)
|
||||
amount = fields.Float(string='Amount', compute='_compute_amount', store=True)
|
||||
|
||||
@api.depends('rate', 'tax_id')
|
||||
def _compute_taxes(self):
|
||||
for line in self:
|
||||
line.taxes = line.rate * line.tax_id.amount / 100 if line.tax_id else 0.0
|
||||
|
||||
@api.depends('rate', 'taxes')
|
||||
def _compute_amount(self):
|
||||
for line in self:
|
||||
line.amount = line.rate + line.taxes
|
||||
|
||||
|
||||
class SAMobilityWizard(models.TransientModel):
|
||||
_name = 'fusion_claims.sa.mobility.wizard'
|
||||
_description = 'SA Mobility Form Filling Wizard'
|
||||
|
||||
sale_order_id = fields.Many2one('sale.order', required=True, readonly=True)
|
||||
|
||||
# --- Vendor section (auto-populated, read-only) ---
|
||||
vendor_name = fields.Char(string='Vendor Name', readonly=True)
|
||||
order_number = fields.Char(string='Order #', readonly=True)
|
||||
vendor_address = fields.Char(string='Vendor Address', readonly=True)
|
||||
primary_email = fields.Char(string='Primary Email', readonly=True)
|
||||
phone = fields.Char(string='Phone', readonly=True)
|
||||
secondary_email = fields.Char(string='Secondary Email', readonly=True)
|
||||
form_date = fields.Date(string='Date', default=fields.Date.today)
|
||||
|
||||
# --- Salesperson ---
|
||||
salesperson_name = fields.Char(string='Salesperson/Technician', readonly=True)
|
||||
service_date = fields.Date(string='Date of Service', default=fields.Date.today)
|
||||
|
||||
# --- Client section (auto-populated, read-only) ---
|
||||
client_last_name = fields.Char(string='Last Name', readonly=True)
|
||||
client_first_name = fields.Char(string='First Name', readonly=True)
|
||||
member_id = fields.Char(string='ODSP Member ID', size=9, readonly=True)
|
||||
client_address = fields.Char(string='Client Address', readonly=True)
|
||||
client_phone = fields.Char(string='Client Phone', readonly=True)
|
||||
|
||||
# --- User-editable fields ---
|
||||
relationship = fields.Selection([
|
||||
('self', 'Self'),
|
||||
('spouse', 'Spouse'),
|
||||
('dependent', 'Dependent'),
|
||||
], string='Relationship to Recipient', default='self', required=True)
|
||||
|
||||
device_type = fields.Selection([
|
||||
('manual_wheelchair', 'Manual Wheelchair'),
|
||||
('high_tech_wheelchair', 'High Technology Wheelchair'),
|
||||
('mobility_scooter', 'Mobility Scooter'),
|
||||
('walker', 'Walker'),
|
||||
('lifting_device', 'Lifting Device'),
|
||||
('other', 'Other'),
|
||||
], string='Device Type', required=True)
|
||||
device_other_description = fields.Char(
|
||||
string='Other Device Description',
|
||||
help='e.g. power chair, batteries, stairlift, ceiling lift',
|
||||
)
|
||||
|
||||
serial_number = fields.Char(string='Serial Number')
|
||||
year = fields.Char(string='Year')
|
||||
make = fields.Char(string='Make')
|
||||
model_name = fields.Char(string='Model')
|
||||
|
||||
warranty_in_effect = fields.Boolean(string='Warranty in Effect')
|
||||
warranty_description = fields.Char(string='Warranty Description')
|
||||
after_hours = fields.Boolean(string='After-hours/Weekend Work')
|
||||
|
||||
notes = fields.Text(
|
||||
string='Notes / Comments',
|
||||
help='Additional details about the request (filled into Notes/Comments area on Page 2)',
|
||||
)
|
||||
|
||||
sa_request_type = fields.Selection([
|
||||
('batteries', 'Batteries'),
|
||||
('repair', 'Repair / Maintenance'),
|
||||
], string='Request Type', required=True, default='repair',
|
||||
help='Controls email body template when sending to SA Mobility')
|
||||
|
||||
email_body_notes = fields.Text(
|
||||
string='Email Body Notes',
|
||||
help='Urgency or priority notes that appear at the top of the email body, '
|
||||
'right below the title. Use this for time-sensitive requests.',
|
||||
)
|
||||
|
||||
# --- Line items ---
|
||||
part_line_ids = fields.One2many(
|
||||
'fusion_claims.sa.mobility.part.line', 'wizard_id', string='Parts')
|
||||
labour_line_ids = fields.One2many(
|
||||
'fusion_claims.sa.mobility.labour.line', 'wizard_id', string='Labour')
|
||||
fee_line_ids = fields.One2many(
|
||||
'fusion_claims.sa.mobility.fee.line', 'wizard_id', string='Additional Fees')
|
||||
|
||||
# --- Computed totals ---
|
||||
parts_total = fields.Float(compute='_compute_totals', string='Parts Total')
|
||||
labour_total = fields.Float(compute='_compute_totals', string='Labour Total')
|
||||
fees_total = fields.Float(compute='_compute_totals', string='Fees Total')
|
||||
grand_total = fields.Float(compute='_compute_totals', string='Grand Total')
|
||||
|
||||
@api.depends('part_line_ids.amount', 'labour_line_ids.amount', 'fee_line_ids.amount')
|
||||
def _compute_totals(self):
|
||||
for wiz in self:
|
||||
wiz.parts_total = sum(wiz.part_line_ids.mapped('amount'))
|
||||
wiz.labour_total = sum(wiz.labour_line_ids.mapped('amount'))
|
||||
wiz.fees_total = sum(wiz.fee_line_ids.mapped('amount'))
|
||||
wiz.grand_total = wiz.parts_total + wiz.labour_total + wiz.fees_total
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
"""Pre-populate wizard from sale order context."""
|
||||
res = super().default_get(fields_list)
|
||||
order_id = self.env.context.get('active_id')
|
||||
if not order_id:
|
||||
return res
|
||||
|
||||
order = self.env['sale.order'].browse(order_id)
|
||||
company = order.company_id or self.env.company
|
||||
|
||||
# Vendor info
|
||||
res['sale_order_id'] = order.id
|
||||
res['vendor_name'] = company.name or ''
|
||||
res['order_number'] = order.name or ''
|
||||
addr_parts = [company.street or '', company.city or '', company.zip or '']
|
||||
res['vendor_address'] = ', '.join(p for p in addr_parts if p)
|
||||
sa_email = self.env['ir.config_parameter'].sudo().get_param(
|
||||
'fusion_claims.sa_mobility_email', 'samobility@ontario.ca')
|
||||
res['primary_email'] = company.email or sa_email
|
||||
res['phone'] = company.phone or ''
|
||||
res['secondary_email'] = order.user_id.email or ''
|
||||
|
||||
# Salesperson
|
||||
res['salesperson_name'] = order.user_id.name or ''
|
||||
|
||||
# Client info
|
||||
partner = order.partner_id
|
||||
if partner:
|
||||
name_parts = (partner.name or '').split(' ', 1)
|
||||
res['client_first_name'] = name_parts[0] if name_parts else ''
|
||||
res['client_last_name'] = name_parts[1] if len(name_parts) > 1 else ''
|
||||
addr_parts = [partner.street or '', partner.city or '', partner.zip or '']
|
||||
res['client_address'] = ', '.join(p for p in addr_parts if p)
|
||||
res['client_phone'] = partner.phone or ''
|
||||
res['member_id'] = order.x_fc_odsp_member_id or partner.x_fc_odsp_member_id or ''
|
||||
|
||||
# Restore saved device/form data from sale order (if previously filled)
|
||||
if order.x_fc_sa_device_type:
|
||||
res['relationship'] = order.x_fc_sa_relationship or 'self'
|
||||
res['device_type'] = order.x_fc_sa_device_type
|
||||
res['device_other_description'] = order.x_fc_sa_device_other or ''
|
||||
res['serial_number'] = order.x_fc_sa_serial_number or ''
|
||||
res['year'] = order.x_fc_sa_year or ''
|
||||
res['make'] = order.x_fc_sa_make or ''
|
||||
res['model_name'] = order.x_fc_sa_model or ''
|
||||
res['warranty_in_effect'] = order.x_fc_sa_warranty
|
||||
res['warranty_description'] = order.x_fc_sa_warranty_desc or ''
|
||||
res['after_hours'] = order.x_fc_sa_after_hours
|
||||
res['sa_request_type'] = order.x_fc_sa_request_type or 'repair'
|
||||
res['notes'] = order.x_fc_sa_notes or ''
|
||||
|
||||
# Pre-populate parts and labour from order lines
|
||||
part_lines = []
|
||||
labour_lines = []
|
||||
part_seq = 10
|
||||
labour_seq = 10
|
||||
for line in order.order_line.filtered(lambda l: not l.display_type):
|
||||
tax = line.tax_ids[:1]
|
||||
# Route LABOR product to labour tab
|
||||
if line.product_id and line.product_id.default_code == 'LABOR':
|
||||
labour_lines.append((0, 0, {
|
||||
'sequence': labour_seq,
|
||||
'hours': line.product_uom_qty,
|
||||
'rate': line.price_unit,
|
||||
'tax_id': tax.id if tax else False,
|
||||
}))
|
||||
labour_seq += 10
|
||||
else:
|
||||
part_lines.append((0, 0, {
|
||||
'sequence': part_seq,
|
||||
'qty': line.product_uom_qty,
|
||||
'description': line.product_id.name or line.name or '',
|
||||
'unit_price': line.price_unit,
|
||||
'tax_id': tax.id if tax else False,
|
||||
}))
|
||||
part_seq += 10
|
||||
if part_lines:
|
||||
res['part_line_ids'] = part_lines[:6]
|
||||
if labour_lines:
|
||||
res['labour_line_ids'] = labour_lines[:5]
|
||||
|
||||
return res
|
||||
|
||||
def _get_template_path(self):
|
||||
"""Get the path to the SA Mobility form template PDF."""
|
||||
module_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
return os.path.join(module_path, 'static', 'src', 'pdf', 'sa_mobility_form_template.pdf')
|
||||
|
||||
def _build_field_mapping(self):
|
||||
"""Build a dictionary mapping PDF form field names to values."""
|
||||
self.ensure_one()
|
||||
mapping = {}
|
||||
|
||||
# Vendor section
|
||||
mapping['Text 1'] = self.vendor_name or ''
|
||||
mapping['Text2'] = self.order_number or ''
|
||||
mapping['Text3'] = self.vendor_address or ''
|
||||
mapping['Text4'] = self.primary_email or ''
|
||||
mapping['Text5'] = self.phone or ''
|
||||
mapping['Text6'] = self.secondary_email or ''
|
||||
mapping['Text7'] = fields.Date.to_string(self.form_date) if self.form_date else ''
|
||||
|
||||
# Salesperson
|
||||
mapping['Text8'] = self.salesperson_name or ''
|
||||
mapping['Text9'] = fields.Date.to_string(self.service_date) if self.service_date else ''
|
||||
|
||||
# Client
|
||||
mapping['Text10'] = self.client_last_name or ''
|
||||
mapping['Text11'] = self.client_first_name or ''
|
||||
|
||||
# Member ID - 9 individual digit boxes (Text12-Text20)
|
||||
member = (self.member_id or '').ljust(9)
|
||||
for i in range(9):
|
||||
mapping[f'Text{12 + i}'] = member[i] if i < len(self.member_id or '') else ''
|
||||
|
||||
mapping['Text21'] = self.client_address or ''
|
||||
mapping['Text22'] = self.client_phone or ''
|
||||
|
||||
# Relationship checkboxes
|
||||
mapping['Check Box16'] = self.relationship == 'self'
|
||||
mapping['Check Box17'] = self.relationship == 'spouse'
|
||||
mapping['Check Box18'] = self.relationship == 'dependent'
|
||||
|
||||
# Device type checkboxes
|
||||
device_map = {
|
||||
'manual_wheelchair': 'Check Box19',
|
||||
'high_tech_wheelchair': 'Check Box20',
|
||||
'mobility_scooter': 'Check Box21',
|
||||
'walker': 'Check Box22',
|
||||
'lifting_device': 'Check Box23',
|
||||
'other': 'Check Box24',
|
||||
}
|
||||
for dtype, cb_name in device_map.items():
|
||||
mapping[cb_name] = self.device_type == dtype
|
||||
|
||||
mapping['Text23'] = self.device_other_description or ''
|
||||
mapping['Text24'] = self.serial_number or ''
|
||||
mapping['Text25'] = self.year or ''
|
||||
mapping['Text26'] = self.make or ''
|
||||
mapping['Text27'] = self.model_name or ''
|
||||
|
||||
# Warranty
|
||||
mapping['Check Box26'] = bool(self.warranty_in_effect)
|
||||
mapping['Check Box28'] = not self.warranty_in_effect
|
||||
mapping['Text28'] = self.warranty_description or ''
|
||||
|
||||
# After hours
|
||||
mapping['Check Box27'] = bool(self.after_hours)
|
||||
mapping['Check Box29'] = not self.after_hours
|
||||
|
||||
# Parts lines (up to 6 rows): Text30-Text59
|
||||
for idx, line in enumerate(self.part_line_ids[:6]):
|
||||
base = 30 + (idx * 5)
|
||||
mapping[f'Text{base}'] = str(int(line.qty)) if line.qty == int(line.qty) else str(line.qty)
|
||||
mapping[f'Text{base + 1}'] = line.description or ''
|
||||
mapping[f'Text{base + 2}'] = f'${line.unit_price:.2f}'
|
||||
mapping[f'Text{base + 3}'] = f'${line.taxes:.2f}' if line.taxes else ''
|
||||
mapping[f'Text{base + 4}'] = f'${line.amount:.2f}'
|
||||
mapping['Text60'] = f'${self.parts_total:.2f}'
|
||||
|
||||
# Labour lines (up to 5 rows): Text61-Text80
|
||||
for idx, line in enumerate(self.labour_line_ids[:5]):
|
||||
base = 61 + (idx * 4)
|
||||
mapping[f'Text{base}'] = str(line.hours)
|
||||
mapping[f'Text{base + 1}'] = f'${line.rate:.2f}'
|
||||
mapping[f'Text{base + 2}'] = f'${line.taxes:.2f}' if line.taxes else ''
|
||||
mapping[f'Text{base + 3}'] = f'${line.amount:.2f}'
|
||||
mapping['Text81'] = f'${self.labour_total:.2f}'
|
||||
|
||||
# Additional fees (up to 4 rows): Text82-Text97
|
||||
for idx, line in enumerate(self.fee_line_ids[:4]):
|
||||
base = 82 + (idx * 4)
|
||||
mapping[f'Text{base}'] = line.description or ''
|
||||
mapping[f'Text{base + 1}'] = f'${line.rate:.2f}'
|
||||
mapping[f'Text{base + 2}'] = f'${line.taxes:.2f}' if line.taxes else ''
|
||||
mapping[f'Text{base + 3}'] = f'${line.amount:.2f}'
|
||||
mapping['Text98'] = f'${self.fees_total:.2f}'
|
||||
|
||||
# Estimated totals summary
|
||||
mapping['Text99'] = f'${self.parts_total:.2f}'
|
||||
mapping['Text100'] = f'${self.labour_total:.2f}'
|
||||
mapping['Text101'] = f'${self.fees_total:.2f}'
|
||||
mapping['Text102'] = f'${self.grand_total:.2f}'
|
||||
|
||||
# Page 2 - Notes/Comments area
|
||||
mapping['Text1'] = self.notes or ''
|
||||
|
||||
return mapping
|
||||
|
||||
def _fill_pdf(self):
|
||||
"""Fill the SA Mobility PDF template using pdfrw AcroForm field filling."""
|
||||
self.ensure_one()
|
||||
if not pdfrw:
|
||||
raise UserError(_("pdfrw library is not installed. Cannot fill PDF forms."))
|
||||
|
||||
template_path = self._get_template_path()
|
||||
if not os.path.exists(template_path):
|
||||
raise UserError(_("SA Mobility form template not found at %s") % template_path)
|
||||
|
||||
mapping = self._build_field_mapping()
|
||||
template = pdfrw.PdfReader(template_path)
|
||||
|
||||
for page in template.pages:
|
||||
annotations = page.get('/Annots')
|
||||
if not annotations:
|
||||
continue
|
||||
for annot in annotations:
|
||||
if annot.get('/Subtype') != '/Widget':
|
||||
continue
|
||||
field_name = annot.get('/T')
|
||||
if not field_name:
|
||||
continue
|
||||
# pdfrw wraps field names in parentheses
|
||||
clean_name = field_name.strip('()')
|
||||
if clean_name not in mapping:
|
||||
continue
|
||||
|
||||
value = mapping[clean_name]
|
||||
if isinstance(value, bool):
|
||||
# Checkbox field
|
||||
if value:
|
||||
annot.update(pdfrw.PdfDict(
|
||||
V=pdfrw.PdfName('Yes'),
|
||||
AS=pdfrw.PdfName('Yes'),
|
||||
))
|
||||
else:
|
||||
annot.update(pdfrw.PdfDict(
|
||||
V=pdfrw.PdfName('Off'),
|
||||
AS=pdfrw.PdfName('Off'),
|
||||
))
|
||||
else:
|
||||
# Text field
|
||||
annot.update(pdfrw.PdfDict(
|
||||
V=pdfrw.PdfString.encode(str(value)),
|
||||
AP='',
|
||||
))
|
||||
|
||||
# Mark form as not needing appearance regeneration
|
||||
if template.Root.AcroForm:
|
||||
template.Root.AcroForm.update(pdfrw.PdfDict(NeedAppearances=pdfrw.PdfObject('true')))
|
||||
|
||||
output = io.BytesIO()
|
||||
writer = pdfrw.PdfWriter()
|
||||
writer.trailer = template
|
||||
writer.write(output)
|
||||
return output.getvalue()
|
||||
|
||||
def _save_form_data(self):
|
||||
"""Persist user-editable wizard data to sale order for future sessions."""
|
||||
self.ensure_one()
|
||||
self.sale_order_id.with_context(skip_all_validations=True).write({
|
||||
'x_fc_sa_relationship': self.relationship,
|
||||
'x_fc_sa_device_type': self.device_type,
|
||||
'x_fc_sa_device_other': self.device_other_description or '',
|
||||
'x_fc_sa_serial_number': self.serial_number or '',
|
||||
'x_fc_sa_year': self.year or '',
|
||||
'x_fc_sa_make': self.make or '',
|
||||
'x_fc_sa_model': self.model_name or '',
|
||||
'x_fc_sa_warranty': self.warranty_in_effect,
|
||||
'x_fc_sa_warranty_desc': self.warranty_description or '',
|
||||
'x_fc_sa_after_hours': self.after_hours,
|
||||
'x_fc_sa_request_type': self.sa_request_type,
|
||||
'x_fc_sa_notes': self.notes or '',
|
||||
})
|
||||
|
||||
def action_fill_and_attach(self):
|
||||
"""Fill the SA Mobility PDF and attach to the sale order via chatter."""
|
||||
self.ensure_one()
|
||||
order = self.sale_order_id
|
||||
|
||||
self._save_form_data()
|
||||
pdf_data = self._fill_pdf()
|
||||
filename = f'SA_Mobility_Form_{order.name}.pdf'
|
||||
|
||||
attachment = self.env['ir.attachment'].create({
|
||||
'name': filename,
|
||||
'type': 'binary',
|
||||
'datas': base64.b64encode(pdf_data),
|
||||
'res_model': 'sale.order',
|
||||
'res_id': order.id,
|
||||
'mimetype': 'application/pdf',
|
||||
})
|
||||
|
||||
# Update ODSP status if appropriate
|
||||
if order.x_fc_sa_status == 'quotation':
|
||||
order.x_fc_sa_status = 'form_ready'
|
||||
|
||||
order.message_post(
|
||||
body=_("SA Mobility form filled and attached."),
|
||||
message_type='comment',
|
||||
attachment_ids=[attachment.id],
|
||||
)
|
||||
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
def action_fill_attach_and_send(self):
|
||||
"""Fill PDF, attach to order, and send email to SA Mobility."""
|
||||
self.ensure_one()
|
||||
order = self.sale_order_id
|
||||
|
||||
self._save_form_data()
|
||||
pdf_data = self._fill_pdf()
|
||||
filename = f'SA_Mobility_Form_{order.name}.pdf'
|
||||
|
||||
# Attach to sale order
|
||||
attachment = self.env['ir.attachment'].create({
|
||||
'name': filename,
|
||||
'type': 'binary',
|
||||
'datas': base64.b64encode(pdf_data),
|
||||
'res_model': 'sale.order',
|
||||
'res_id': order.id,
|
||||
'mimetype': 'application/pdf',
|
||||
})
|
||||
|
||||
# Generate quotation PDF and attach
|
||||
att_ids = [attachment.id]
|
||||
try:
|
||||
report = self.env.ref('sale.action_report_saleorder')
|
||||
pdf_content, _ct = report._render_qweb_pdf(report.id, [order.id])
|
||||
quot_att = self.env['ir.attachment'].create({
|
||||
'name': f'Quotation_{order.name}.pdf',
|
||||
'type': 'binary',
|
||||
'datas': base64.b64encode(pdf_content),
|
||||
'res_model': 'sale.order',
|
||||
'res_id': order.id,
|
||||
'mimetype': 'application/pdf',
|
||||
})
|
||||
att_ids.append(quot_att.id)
|
||||
except Exception as e:
|
||||
_logger.warning(f"Could not generate quotation PDF for {order.name}: {e}")
|
||||
|
||||
# Build and send email
|
||||
order._send_sa_mobility_email(
|
||||
request_type=self.sa_request_type,
|
||||
device_description=self._get_device_label(),
|
||||
attachment_ids=att_ids,
|
||||
email_body_notes=self.email_body_notes,
|
||||
)
|
||||
|
||||
# Update ODSP status
|
||||
if order.x_fc_sa_status in ('quotation', 'form_ready'):
|
||||
order.x_fc_sa_status = 'submitted_to_sa'
|
||||
|
||||
sa_email = order._get_sa_mobility_email()
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Email Sent'),
|
||||
'message': _('SA Mobility form emailed to %s.') % sa_email,
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
'next': {'type': 'ir.actions.act_window_close'},
|
||||
},
|
||||
}
|
||||
|
||||
def _get_device_label(self):
|
||||
"""Get human-readable device description."""
|
||||
self.ensure_one()
|
||||
labels = dict(self._fields['device_type'].selection)
|
||||
label = labels.get(self.device_type, '')
|
||||
if self.device_type == 'other' and self.device_other_description:
|
||||
label = self.device_other_description
|
||||
return label
|
||||
Reference in New Issue
Block a user