changes
This commit is contained in:
@@ -1862,6 +1862,10 @@ class SaleOrder(models.Model):
|
||||
string='Previous Status Before Hold',
|
||||
help='Status before the application was put on hold (for resuming)',
|
||||
)
|
||||
x_fc_previous_status_before_withdrawal = fields.Char(
|
||||
string='Status Before Withdrawal',
|
||||
help='Records the status before withdrawal for audit trail.',
|
||||
)
|
||||
|
||||
x_fc_status_before_delivery = fields.Char(
|
||||
string='Status Before Delivery',
|
||||
@@ -2327,6 +2331,20 @@ class SaleOrder(models.Model):
|
||||
help='Date when Page 11 was signed',
|
||||
)
|
||||
|
||||
page11_sign_request_ids = fields.One2many(
|
||||
'fusion.page11.sign.request', 'sale_order_id',
|
||||
string='Page 11 Signing Requests',
|
||||
)
|
||||
page11_sign_request_count = fields.Integer(
|
||||
compute='_compute_page11_sign_request_count',
|
||||
string='Signing Requests',
|
||||
)
|
||||
page11_sign_status = fields.Selection([
|
||||
('none', 'Not Requested'),
|
||||
('sent', 'Pending Signature'),
|
||||
('signed', 'Signed'),
|
||||
], compute='_compute_page11_sign_request_count', string='Page 11 Remote Status')
|
||||
|
||||
# ==========================================================================
|
||||
# PAGE 12 SIGNATURE TRACKING (Authorizer + Vendor Signature)
|
||||
# Page 12 must be signed by: Authorizer (OT) and Vendor (our company)
|
||||
@@ -3120,11 +3138,49 @@ class SaleOrder(models.Model):
|
||||
self.ensure_one()
|
||||
return self._action_open_document('x_fc_original_application', 'Original ADP Application')
|
||||
|
||||
@api.depends('page11_sign_request_ids', 'page11_sign_request_ids.state')
|
||||
def _compute_page11_sign_request_count(self):
|
||||
for order in self:
|
||||
requests = order.page11_sign_request_ids
|
||||
order.page11_sign_request_count = len(requests)
|
||||
signed = requests.filtered(lambda r: r.state == 'signed')
|
||||
pending = requests.filtered(lambda r: r.state == 'sent')
|
||||
if signed:
|
||||
order.page11_sign_status = 'signed'
|
||||
elif pending:
|
||||
order.page11_sign_status = 'sent'
|
||||
else:
|
||||
order.page11_sign_status = 'none'
|
||||
|
||||
def action_open_signed_pages(self):
|
||||
"""Open the Page 11 & 12 PDF."""
|
||||
self.ensure_one()
|
||||
return self._action_open_document('x_fc_signed_pages_11_12', 'Page 11 & 12 (Signed)')
|
||||
|
||||
|
||||
def action_request_page11_signature(self):
|
||||
"""Open the wizard to send Page 11 for remote signing."""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Request Page 11 Signature',
|
||||
'res_model': 'fusion_claims.send.page11.wizard',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {'default_sale_order_id': self.id},
|
||||
}
|
||||
|
||||
def action_view_page11_requests(self):
|
||||
"""Open the list of Page 11 signing requests."""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Page 11 Signing Requests',
|
||||
'res_model': 'fusion.page11.sign.request',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('sale_order_id', '=', self.id)],
|
||||
'context': {'default_sale_order_id': self.id},
|
||||
}
|
||||
|
||||
def action_open_final_application(self):
|
||||
"""Open the Final Submitted Application PDF."""
|
||||
self.ensure_one()
|
||||
@@ -3686,6 +3742,41 @@ class SaleOrder(models.Model):
|
||||
|
||||
return True
|
||||
|
||||
def action_resubmit_from_withdrawn(self):
|
||||
"""Return a withdrawn application to Ready for Submission for correction and resubmission."""
|
||||
self.ensure_one()
|
||||
|
||||
if self.x_fc_adp_application_status != 'withdrawn':
|
||||
raise UserError("This action is only available for withdrawn applications.")
|
||||
|
||||
self.with_context(skip_status_validation=True).write({
|
||||
'x_fc_adp_application_status': 'ready_submission',
|
||||
})
|
||||
|
||||
user_name = self.env.user.name
|
||||
resubmit_date = fields.Date.today().strftime('%B %d, %Y')
|
||||
|
||||
message_body = f'''
|
||||
<div class="alert alert-info" role="alert">
|
||||
<h5 class="alert-heading"><i class="fa fa-repeat"></i> Application Returned for Resubmission</h5>
|
||||
<ul>
|
||||
<li><strong>Returned By:</strong> {user_name}</li>
|
||||
<li><strong>Date:</strong> {resubmit_date}</li>
|
||||
<li><strong>Status Returned To:</strong> Ready for Submission</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<p class="mb-0"><i class="fa fa-info-circle"></i> Make corrections and click <strong>Submit Application</strong> to resubmit.</p>
|
||||
</div>
|
||||
'''
|
||||
|
||||
self.message_post(
|
||||
body=Markup(message_body),
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def action_set_ready_to_bill(self):
|
||||
"""Open the Ready to Bill wizard to collect POD and delivery date.
|
||||
|
||||
@@ -4520,6 +4611,12 @@ class SaleOrder(models.Model):
|
||||
if 'x_fc_device_placement' in self.env['account.move.line']._fields:
|
||||
line_vals['x_fc_device_placement'] = line.x_fc_device_placement
|
||||
|
||||
# Copy deduction fields so export verification can recalculate correctly
|
||||
if 'x_fc_deduction_type' in self.env['account.move.line']._fields:
|
||||
line_vals['x_fc_deduction_type'] = line.x_fc_deduction_type or 'none'
|
||||
if 'x_fc_deduction_value' in self.env['account.move.line']._fields:
|
||||
line_vals['x_fc_deduction_value'] = line.x_fc_deduction_value or 0
|
||||
|
||||
# Store BOTH portions on invoice line (for display)
|
||||
if 'x_fc_adp_portion' in self.env['account.move.line']._fields:
|
||||
line_vals['x_fc_adp_portion'] = adp_portion
|
||||
@@ -5170,13 +5267,13 @@ class SaleOrder(models.Model):
|
||||
f'border-bottom:2px solid #4a5568;{font}"'
|
||||
)
|
||||
cell_style = (
|
||||
'style="padding:7px 10px;font-size:12px;color:#2d3748;'
|
||||
'border-bottom:1px solid #e2e8f0;"'
|
||||
'style="padding:7px 10px;font-size:12px;'
|
||||
'border-bottom:1px solid rgba(128,128,128,0.15);"'
|
||||
)
|
||||
alt_row = 'style="background:#f7fafc;"'
|
||||
alt_row = 'style="background:rgba(128,128,128,0.06);"'
|
||||
amt_style = (
|
||||
'style="padding:7px 10px;font-size:12px;color:#2d3748;'
|
||||
'border-bottom:1px solid #e2e8f0;text-align:right;"'
|
||||
'style="padding:7px 10px;font-size:12px;'
|
||||
'border-bottom:1px solid rgba(128,128,128,0.15);text-align:right;"'
|
||||
)
|
||||
hdr_r = hdr_style.replace('text-align:left', 'text-align:right')
|
||||
|
||||
@@ -5187,9 +5284,9 @@ class SaleOrder(models.Model):
|
||||
|
||||
html = (
|
||||
'<div style="margin:20px 0;">'
|
||||
f'<h3 style="color:#1a202c;font-size:15px;font-weight:700;'
|
||||
f'<h3 style="font-size:15px;font-weight:700;'
|
||||
f'margin:0 0 10px 0;{font}">Approved Items</h3>'
|
||||
'<table style="width:100%;border-collapse:collapse;border:1px solid #e2e8f0;">'
|
||||
'<table style="width:100%;border-collapse:collapse;border:1px solid rgba(128,128,128,0.25);">'
|
||||
'<thead><tr>'
|
||||
f'<th {hdr_style}>S/N</th>'
|
||||
f'<th {hdr_style}>ADP Code</th>'
|
||||
@@ -5241,13 +5338,13 @@ class SaleOrder(models.Model):
|
||||
colspan = 5
|
||||
total_style = (
|
||||
'style="padding:8px 10px;font-size:12px;font-weight:700;'
|
||||
'color:#1a202c;border-top:2px solid #2d3748;text-align:right;"'
|
||||
'border-top:2px solid rgba(128,128,128,0.3);text-align:right;"'
|
||||
)
|
||||
total_label_style = (
|
||||
f'style="padding:8px 10px;font-size:12px;font-weight:700;'
|
||||
f'color:#1a202c;border-top:2px solid #2d3748;text-align:right;"'
|
||||
'style="padding:8px 10px;font-size:12px;font-weight:700;'
|
||||
'border-top:2px solid rgba(128,128,128,0.3);text-align:right;"'
|
||||
)
|
||||
html += f'<tr style="background:#edf2f7;">'
|
||||
html += '<tr style="background:rgba(128,128,128,0.08);">'
|
||||
html += f'<td colspan="{colspan}" {total_label_style}>Total</td>'
|
||||
html += f'<td {total_style}>${total_adp:,.2f}</td>'
|
||||
html += f'<td {total_style}>${total_client:,.2f}</td>'
|
||||
@@ -5529,8 +5626,13 @@ class SaleOrder(models.Model):
|
||||
_logger.error(f"Failed to send case closed email for {self.name}: {e}")
|
||||
return False
|
||||
|
||||
def _send_withdrawal_email(self, reason=None):
|
||||
"""Send notification when application is withdrawn."""
|
||||
def _send_withdrawal_email(self, reason=None, intent=None):
|
||||
"""Send notification when application is withdrawn.
|
||||
|
||||
Args:
|
||||
reason: Free-text reason for withdrawal.
|
||||
intent: 'cancel' or 'resubmit' — determines email wording.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not self._is_email_notifications_enabled():
|
||||
return False
|
||||
@@ -5542,17 +5644,34 @@ class SaleOrder(models.Model):
|
||||
client_name = (recipients.get('client') or self.partner_id).name or 'Client'
|
||||
sales_rep_name = (recipients.get('sales_rep') or self.env.user).name
|
||||
|
||||
note_text = 'This application has been withdrawn from the Assistive Devices Program.'
|
||||
if intent == 'cancel':
|
||||
note_text = ('This application has been permanently withdrawn and cancelled. '
|
||||
'The sale order and all related invoices have been cancelled.')
|
||||
title = 'Application Withdrawn & Cancelled'
|
||||
subject_suffix = 'Withdrawn & Cancelled'
|
||||
note_color = '#dc3545'
|
||||
elif intent == 'resubmit':
|
||||
note_text = ('This application has been withdrawn for correction and will be resubmitted. '
|
||||
'The application has been returned to Ready for Submission status.')
|
||||
title = 'Application Withdrawn for Correction'
|
||||
subject_suffix = 'Withdrawn for Correction'
|
||||
note_color = '#d69e2e'
|
||||
else:
|
||||
note_text = 'This application has been withdrawn from the Assistive Devices Program.'
|
||||
title = 'Application Withdrawn'
|
||||
subject_suffix = 'Withdrawn'
|
||||
note_color = '#d69e2e'
|
||||
|
||||
if reason:
|
||||
note_text += f'<br/><strong>Reason:</strong> {reason}'
|
||||
|
||||
body_html = self._email_build(
|
||||
title='Application Withdrawn',
|
||||
title=title,
|
||||
summary=f'The ADP application for <strong>{client_name}</strong> has been withdrawn.',
|
||||
email_type='attention',
|
||||
sections=[('Case Details', self._build_case_detail_rows())],
|
||||
note=note_text,
|
||||
note_color='#d69e2e',
|
||||
note_color=note_color,
|
||||
button_url=f'{self.get_base_url()}/web#id={self.id}&model=sale.order&view_type=form',
|
||||
sender_name=sales_rep_name,
|
||||
)
|
||||
@@ -5560,12 +5679,12 @@ class SaleOrder(models.Model):
|
||||
email_cc = ', '.join(cc_emails) if to_emails else ', '.join(cc_emails[1:])
|
||||
try:
|
||||
self.env['mail.mail'].sudo().create({
|
||||
'subject': f'Application Withdrawn - {client_name} - {self.name}',
|
||||
'subject': f'Application {subject_suffix} - {client_name} - {self.name}',
|
||||
'body_html': body_html,
|
||||
'email_to': email_to, 'email_cc': email_cc,
|
||||
'model': 'sale.order', 'res_id': self.id,
|
||||
}).send()
|
||||
self._email_chatter_log('Application Withdrawn email sent', email_to, email_cc)
|
||||
self._email_chatter_log(f'{title} email sent', email_to, email_cc)
|
||||
return True
|
||||
except Exception as e:
|
||||
_logger.error(f"Failed to send withdrawal email for {self.name}: {e}")
|
||||
@@ -5862,7 +5981,10 @@ class SaleOrder(models.Model):
|
||||
'x_fc_proof_of_delivery',
|
||||
'x_fc_approval_letter',
|
||||
]
|
||||
doc_changes = {f: vals.get(f) for f in document_fields if f in vals and vals.get(f)}
|
||||
if self.env.context.get('skip_document_chatter'):
|
||||
doc_changes = {}
|
||||
else:
|
||||
doc_changes = {f: vals.get(f) for f in document_fields if f in vals and vals.get(f)}
|
||||
|
||||
# Preserve old documents in chatter BEFORE they get replaced or deleted
|
||||
# This ensures document history is maintained for audit purposes
|
||||
@@ -5885,7 +6007,7 @@ class SaleOrder(models.Model):
|
||||
|
||||
for order in self:
|
||||
for field_name in document_fields:
|
||||
if field_name in vals and field_name not in correction_handled:
|
||||
if field_name in vals and field_name not in correction_handled and not self.env.context.get('skip_document_chatter'):
|
||||
old_data = getattr(order, field_name, None)
|
||||
new_data = vals.get(field_name)
|
||||
label = document_labels.get(field_name, field_name)
|
||||
@@ -6584,96 +6706,6 @@ class SaleOrder(models.Model):
|
||||
except Exception as e:
|
||||
_logger.error(f" Failed to sync serial to invoice line {inv_line.id}: {e}")
|
||||
|
||||
def action_sync_adp_fields(self):
|
||||
"""Manual action to sync all ADP fields to invoices."""
|
||||
synced_invoices = 0
|
||||
for order in self:
|
||||
# First sync Studio fields to FC fields on the SO itself
|
||||
order._sync_studio_to_fc_fields()
|
||||
|
||||
# Then sync to invoices
|
||||
invoices = order.invoice_ids.filtered(lambda inv: inv.state != 'cancel')
|
||||
if invoices:
|
||||
order._sync_fields_to_invoices()
|
||||
synced_invoices += len(invoices)
|
||||
|
||||
# Force refresh of the view
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': 'Fields Synchronized',
|
||||
'message': f'Synced ADP fields from {len(self)} sale order(s) to {synced_invoices} invoice(s). Please refresh the page to see updated values.',
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
}
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _cron_sync_adp_fields(self):
|
||||
"""Cron job to sync ADP fields from Sale Orders to Invoices.
|
||||
|
||||
Processes all ADP sales created/modified in the last 7 days.
|
||||
Uses dynamic field mappings from Settings.
|
||||
"""
|
||||
from datetime import timedelta
|
||||
cutoff_date = fields.Datetime.now() - timedelta(days=7)
|
||||
|
||||
# Get field mappings
|
||||
mappings = self._get_field_mappings()
|
||||
sale_type_field = self.env['ir.config_parameter'].sudo().get_param(
|
||||
'fusion_claims.field_sale_type', 'x_fc_sale_type'
|
||||
)
|
||||
|
||||
# Build domain - check FC sale type fields
|
||||
domain = [('write_date', '>=', cutoff_date)]
|
||||
or_conditions = []
|
||||
|
||||
# Check FC sale type field
|
||||
if sale_type_field in self._fields:
|
||||
or_conditions.append((sale_type_field, 'in', ['adp', 'adp_odsp', 'ADP', 'ADP/ODSP']))
|
||||
|
||||
# Check claim number fields
|
||||
claim_field = mappings.get('so_claim_number', 'x_fc_claim_number')
|
||||
if claim_field in self._fields:
|
||||
or_conditions.append((claim_field, '!=', False))
|
||||
|
||||
# Combine with OR - each '|' must be a separate element in the domain list
|
||||
if or_conditions:
|
||||
# Add (n-1) OR operators for n conditions
|
||||
for _ in range(len(or_conditions) - 1):
|
||||
domain.append('|')
|
||||
# Add all conditions
|
||||
for cond in or_conditions:
|
||||
domain.append(cond)
|
||||
|
||||
try:
|
||||
orders = self.search(domain)
|
||||
except Exception as e:
|
||||
_logger.error(f"Error searching for ADP orders: {e}")
|
||||
# Fallback to simpler search
|
||||
orders = self.search([
|
||||
('write_date', '>=', cutoff_date),
|
||||
('invoice_ids', '!=', False),
|
||||
])
|
||||
|
||||
synced_count = 0
|
||||
error_count = 0
|
||||
|
||||
for order in orders:
|
||||
try:
|
||||
# Only sync if it's an ADP sale
|
||||
if order._is_adp_sale() or order.x_fc_claim_number:
|
||||
order._sync_studio_to_fc_fields()
|
||||
order._sync_fields_to_invoices()
|
||||
synced_count += 1
|
||||
except Exception as e:
|
||||
error_count += 1
|
||||
_logger.warning(f"Failed to sync order {order.name}: {e}")
|
||||
|
||||
_logger.info(f"Fusion Claims sync complete: {synced_count} orders synced, {error_count} errors")
|
||||
return synced_count
|
||||
|
||||
# ==========================================================================
|
||||
# EMAIL SEND OVERRIDE (Use ADP templates for ADP sales)
|
||||
# ==========================================================================
|
||||
|
||||
Reference in New Issue
Block a user