561 lines
23 KiB
Python
561 lines
23 KiB
Python
# -*- 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
|