update
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Claims',
|
||||
'version': '19.0.7.3.0',
|
||||
'version': '19.0.8.0.0',
|
||||
'category': 'Sales',
|
||||
'summary': 'Complete ADP Claims Management with Dashboard, Sales Integration, Billing Automation, and Two-Stage Verification.',
|
||||
'description': """
|
||||
@@ -97,7 +97,6 @@
|
||||
'data/mail_activity_type_data.xml',
|
||||
'data/ir_cron_data.xml',
|
||||
'data/ir_actions_server_data.xml',
|
||||
'data/stock_location_data.xml',
|
||||
'data/product_labor_data.xml',
|
||||
'wizard/status_change_reason_wizard_views.xml',
|
||||
'views/res_company_views.xml',
|
||||
@@ -129,6 +128,7 @@
|
||||
'wizard/odsp_pre_approved_wizard_views.xml',
|
||||
'wizard/odsp_ready_delivery_wizard_views.xml',
|
||||
'wizard/send_page11_wizard_views.xml',
|
||||
'wizard/adp_import_wizard_views.xml',
|
||||
'views/res_partner_views.xml',
|
||||
'views/pdf_template_inherit_views.xml',
|
||||
'views/dashboard_views.xml',
|
||||
@@ -138,7 +138,6 @@
|
||||
'views/adp_claims_views.xml',
|
||||
'views/submission_history_views.xml',
|
||||
'views/product_template_adp_views.xml',
|
||||
'views/fusion_loaner_views.xml',
|
||||
'views/page11_sign_request_views.xml',
|
||||
'views/technician_task_views.xml',
|
||||
'report/report_actions.xml',
|
||||
|
||||
@@ -1,41 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2024-2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Claim Assistant product family.
|
||||
-->
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Loaner Stock Location -->
|
||||
<record id="stock_location_loaner" model="stock.location">
|
||||
<field name="name">Loaner Stock</field>
|
||||
<field name="usage">internal</field>
|
||||
<field name="location_id" ref="stock.stock_location_stock"/>
|
||||
</record>
|
||||
|
||||
<!-- Sequence for Loaner Checkout -->
|
||||
<record id="seq_loaner_checkout" model="ir.sequence">
|
||||
<field name="name">Loaner Checkout Sequence</field>
|
||||
<field name="code">fusion.loaner.checkout</field>
|
||||
<field name="prefix">LOAN/</field>
|
||||
<field name="padding">5</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
<!-- Loaner Product Categories -->
|
||||
<record id="product_category_loaner" model="product.category">
|
||||
<field name="name">Loaner Equipment</field>
|
||||
</record>
|
||||
<record id="product_category_loaner_rollator" model="product.category">
|
||||
<field name="name">Rollators</field>
|
||||
<field name="parent_id" ref="product_category_loaner"/>
|
||||
</record>
|
||||
<record id="product_category_loaner_wheelchair" model="product.category">
|
||||
<field name="name">Wheelchairs</field>
|
||||
<field name="parent_id" ref="product_category_loaner"/>
|
||||
</record>
|
||||
<record id="product_category_loaner_powerchair" model="product.category">
|
||||
<field name="name">Powerchairs</field>
|
||||
<field name="parent_id" ref="product_category_loaner"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -18,8 +18,6 @@ from . import account_move_line
|
||||
from . import account_payment
|
||||
from . import account_payment_method_line
|
||||
from . import submission_history
|
||||
from . import fusion_loaner_checkout
|
||||
from . import fusion_loaner_history
|
||||
from . import client_profile
|
||||
from . import adp_application_data
|
||||
from . import xml_parser
|
||||
|
||||
@@ -8,7 +8,7 @@ import re
|
||||
import base64
|
||||
import logging
|
||||
import zipfile
|
||||
from datetime import date, datetime
|
||||
from datetime import date, datetime, timedelta
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
@@ -20,7 +20,30 @@ class ADPExportRecord(models.Model):
|
||||
_name = 'fusion_claims.adp.export.record'
|
||||
_description = 'ADP Export File Record'
|
||||
_inherit = ['fusion_claims.adp.posting.schedule.mixin']
|
||||
_order = 'export_date desc, id desc'
|
||||
_order = 'year desc, month asc, posting_period_date desc, export_date desc, id desc'
|
||||
|
||||
_GROUPBY_ORDER = {
|
||||
'year': 'year desc',
|
||||
'month': 'month asc',
|
||||
'posting_period_label': 'posting_period_label asc',
|
||||
'posting_period_date': 'posting_period_date desc',
|
||||
'user_id': 'user_id asc',
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _read_group(self, domain, groupby=(), aggregates=(), having=(), offset=0, limit=None, order=None):
|
||||
if not order and groupby:
|
||||
parts = []
|
||||
for field in groupby:
|
||||
fname = field.split(':')[0] if isinstance(field, str) else field
|
||||
if fname in self._GROUPBY_ORDER:
|
||||
parts.append(self._GROUPBY_ORDER[fname])
|
||||
if parts:
|
||||
order = ', '.join(parts)
|
||||
return super()._read_group(
|
||||
domain, groupby=groupby, aggregates=aggregates,
|
||||
having=having, offset=offset, limit=limit, order=order,
|
||||
)
|
||||
|
||||
name = fields.Char(
|
||||
string='Filename',
|
||||
@@ -61,6 +84,12 @@ class ADPExportRecord(models.Model):
|
||||
store=True,
|
||||
help='Numeric month for proper ordering',
|
||||
)
|
||||
payment_date = fields.Date(
|
||||
string='Payment Date',
|
||||
compute='_compute_period_fields',
|
||||
store=True,
|
||||
help='Expected payment date (posting day + 10 calendar days = Monday)',
|
||||
)
|
||||
|
||||
file_data = fields.Binary(
|
||||
string='Export File',
|
||||
@@ -100,6 +129,9 @@ class ADPExportRecord(models.Model):
|
||||
default=lambda self: self.env.company,
|
||||
index=True,
|
||||
)
|
||||
file_preview = fields.Text(
|
||||
string='File Content',
|
||||
)
|
||||
notes = fields.Text(
|
||||
string='Notes',
|
||||
)
|
||||
@@ -110,20 +142,93 @@ class ADPExportRecord(models.Model):
|
||||
ppd = record.posting_period_date
|
||||
if ppd:
|
||||
record.year = str(ppd.year)
|
||||
record.month = ppd.strftime('%B')
|
||||
record.month = '%02d - %s' % (ppd.month, ppd.strftime('%B'))
|
||||
record.month_number = ppd.month
|
||||
record.posting_period_label = ppd.strftime('%b %d, %Y')
|
||||
record.posting_period_label = 'Posting Schedule - %s' % ppd.strftime('%d/%m/%Y')
|
||||
record.payment_date = ppd + timedelta(days=10)
|
||||
else:
|
||||
record.year = ''
|
||||
record.month = ''
|
||||
record.month_number = 0
|
||||
record.posting_period_label = ''
|
||||
record.payment_date = False
|
||||
|
||||
@api.depends('invoice_ids')
|
||||
def _compute_invoice_count(self):
|
||||
for record in self:
|
||||
record.invoice_count = len(record.invoice_ids)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
records = super().create(vals_list)
|
||||
for record in records:
|
||||
record._process_file_data()
|
||||
return records
|
||||
|
||||
def _process_file_data(self):
|
||||
"""Extract preview text and auto-link invoices from file content."""
|
||||
self.ensure_one()
|
||||
if not self.file_data:
|
||||
return
|
||||
|
||||
try:
|
||||
raw = base64.b64decode(self.file_data)
|
||||
text = raw.decode('utf-8', errors='replace')
|
||||
except Exception:
|
||||
return
|
||||
|
||||
vals = {'file_preview': text}
|
||||
|
||||
lines = text.strip().splitlines()
|
||||
if lines and not self.line_count:
|
||||
vals['line_count'] = len(lines)
|
||||
|
||||
if not self.invoice_ids:
|
||||
invoice_numbers = set()
|
||||
for line in lines:
|
||||
parts = line.split(',')
|
||||
if len(parts) > 3 and parts[3].strip():
|
||||
invoice_numbers.add(parts[3].strip())
|
||||
|
||||
if invoice_numbers:
|
||||
invoices = self.env['account.move'].search([
|
||||
('name', 'in', list(invoice_numbers)),
|
||||
])
|
||||
if invoices:
|
||||
vals['invoice_ids'] = [(6, 0, invoices.ids)]
|
||||
|
||||
self.write(vals)
|
||||
|
||||
@api.model
|
||||
def _get_posting_period_for_file(self, file_date):
|
||||
"""Determine which posting period a file belongs to.
|
||||
|
||||
If the file was created/exported ON a posting date, it belongs to that
|
||||
period. If it was created AFTER the posting date (even one day later),
|
||||
it falls into the NEXT posting period because that posting has already
|
||||
been submitted.
|
||||
|
||||
Handles dates before the configured base date correctly using Python's
|
||||
floor division for negative offsets.
|
||||
"""
|
||||
if file_date is None:
|
||||
file_date = date.today()
|
||||
elif hasattr(file_date, 'date'):
|
||||
file_date = file_date.date()
|
||||
|
||||
base_date = self._get_adp_posting_base_date()
|
||||
frequency = self._get_adp_posting_frequency()
|
||||
if frequency <= 0:
|
||||
frequency = 14
|
||||
|
||||
days_diff = (file_date - base_date).days
|
||||
cycles_passed = days_diff // frequency
|
||||
current_posting = base_date + timedelta(days=cycles_passed * frequency)
|
||||
|
||||
if file_date <= current_posting:
|
||||
return current_posting
|
||||
return current_posting + timedelta(days=frequency)
|
||||
|
||||
def action_download(self):
|
||||
"""Download the export file."""
|
||||
self.ensure_one()
|
||||
@@ -259,9 +364,9 @@ class ADPExportRecord(models.Model):
|
||||
try:
|
||||
vendor_code, file_date = self._parse_export_filename(doc.name)
|
||||
if file_date:
|
||||
posting_date = self._get_current_posting_date(file_date)
|
||||
posting_date = self._get_posting_period_for_file(file_date)
|
||||
else:
|
||||
posting_date = self._get_current_posting_date(
|
||||
posting_date = self._get_posting_period_for_file(
|
||||
doc.create_date.date() if doc.create_date else date.today()
|
||||
)
|
||||
|
||||
|
||||
@@ -1,823 +0,0 @@
|
||||
# -*- 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, ValidationError
|
||||
from markupsafe import Markup
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionLoanerCheckout(models.Model):
|
||||
"""Track loaner equipment checkouts and returns."""
|
||||
_name = 'fusion.loaner.checkout'
|
||||
_description = 'Loaner Equipment Checkout'
|
||||
_order = 'checkout_date desc, id desc'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin', 'fusion.email.builder.mixin']
|
||||
|
||||
# =========================================================================
|
||||
# REFERENCE FIELDS
|
||||
# =========================================================================
|
||||
|
||||
name = fields.Char(
|
||||
string='Reference',
|
||||
required=True,
|
||||
copy=False,
|
||||
readonly=True,
|
||||
default=lambda self: _('New'),
|
||||
)
|
||||
sale_order_id = fields.Many2one(
|
||||
'sale.order',
|
||||
string='Sale Order',
|
||||
ondelete='set null',
|
||||
tracking=True,
|
||||
)
|
||||
partner_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Client',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
authorizer_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Authorizer',
|
||||
help='Therapist/Authorizer associated with this loaner',
|
||||
)
|
||||
sales_rep_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Sales Rep',
|
||||
default=lambda self: self.env.user,
|
||||
tracking=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# PRODUCT & SERIAL
|
||||
# =========================================================================
|
||||
|
||||
product_id = fields.Many2one(
|
||||
'product.product',
|
||||
string='Product',
|
||||
required=True,
|
||||
domain="[('x_fc_can_be_loaned', '=', True)]",
|
||||
tracking=True,
|
||||
)
|
||||
lot_id = fields.Many2one(
|
||||
'stock.lot',
|
||||
string='Serial Number',
|
||||
domain="[('product_id', '=', product_id)]",
|
||||
tracking=True,
|
||||
)
|
||||
product_description = fields.Text(
|
||||
string='Product Description',
|
||||
related='product_id.description_sale',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# DATES
|
||||
# =========================================================================
|
||||
|
||||
checkout_date = fields.Date(
|
||||
string='Checkout Date',
|
||||
required=True,
|
||||
default=fields.Date.context_today,
|
||||
tracking=True,
|
||||
)
|
||||
loaner_period_days = fields.Integer(
|
||||
string='Loaner Period (Days)',
|
||||
default=7,
|
||||
help='Number of free loaner days before rental conversion',
|
||||
)
|
||||
expected_return_date = fields.Date(
|
||||
string='Expected Return Date',
|
||||
compute='_compute_expected_return_date',
|
||||
store=True,
|
||||
)
|
||||
actual_return_date = fields.Date(
|
||||
string='Actual Return Date',
|
||||
tracking=True,
|
||||
)
|
||||
days_out = fields.Integer(
|
||||
string='Days Out',
|
||||
compute='_compute_days_out',
|
||||
)
|
||||
days_overdue = fields.Integer(
|
||||
string='Days Overdue',
|
||||
compute='_compute_days_overdue',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# STATUS
|
||||
# =========================================================================
|
||||
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('checked_out', 'Checked Out'),
|
||||
('overdue', 'Overdue'),
|
||||
('rental_pending', 'Rental Conversion Pending'),
|
||||
('returned', 'Returned'),
|
||||
('converted_rental', 'Converted to Rental'),
|
||||
('lost', 'Lost/Write-off'),
|
||||
], string='Status', default='draft', tracking=True, required=True)
|
||||
|
||||
# =========================================================================
|
||||
# LOCATION
|
||||
# =========================================================================
|
||||
|
||||
delivery_address = fields.Text(
|
||||
string='Delivery Address',
|
||||
help='Where the loaner was delivered',
|
||||
)
|
||||
return_location_id = fields.Many2one(
|
||||
'stock.location',
|
||||
string='Return Location',
|
||||
domain="[('usage', '=', 'internal')]",
|
||||
help='Where the loaner was returned to (store, warehouse, etc.)',
|
||||
tracking=True,
|
||||
)
|
||||
checked_out_by_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Checked Out By',
|
||||
default=lambda self: self.env.user,
|
||||
)
|
||||
returned_to_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Returned To',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# CHECKOUT CONDITION
|
||||
# =========================================================================
|
||||
|
||||
checkout_condition = fields.Selection([
|
||||
('excellent', 'Excellent'),
|
||||
('good', 'Good'),
|
||||
('fair', 'Fair'),
|
||||
('needs_repair', 'Needs Repair'),
|
||||
], string='Checkout Condition', default='excellent')
|
||||
checkout_notes = fields.Text(
|
||||
string='Checkout Notes',
|
||||
)
|
||||
checkout_photo_ids = fields.Many2many(
|
||||
'ir.attachment',
|
||||
'fusion_loaner_checkout_photo_rel',
|
||||
'checkout_id',
|
||||
'attachment_id',
|
||||
string='Checkout Photos',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# RETURN CONDITION
|
||||
# =========================================================================
|
||||
|
||||
return_condition = fields.Selection([
|
||||
('excellent', 'Excellent'),
|
||||
('good', 'Good'),
|
||||
('fair', 'Fair'),
|
||||
('needs_repair', 'Needs Repair'),
|
||||
('damaged', 'Damaged'),
|
||||
], string='Return Condition')
|
||||
return_notes = fields.Text(
|
||||
string='Return Notes',
|
||||
)
|
||||
return_photo_ids = fields.Many2many(
|
||||
'ir.attachment',
|
||||
'fusion_loaner_return_photo_rel',
|
||||
'checkout_id',
|
||||
'attachment_id',
|
||||
string='Return Photos',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# REMINDER TRACKING
|
||||
# =========================================================================
|
||||
|
||||
reminder_day5_sent = fields.Boolean(
|
||||
string='Day 5 Reminder Sent',
|
||||
default=False,
|
||||
)
|
||||
reminder_day8_sent = fields.Boolean(
|
||||
string='Day 8 Warning Sent',
|
||||
default=False,
|
||||
)
|
||||
reminder_day10_sent = fields.Boolean(
|
||||
string='Day 10 Final Notice Sent',
|
||||
default=False,
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# RENTAL CONVERSION
|
||||
# =========================================================================
|
||||
|
||||
rental_order_id = fields.Many2one(
|
||||
'sale.order',
|
||||
string='Rental Order',
|
||||
help='Sale order created when loaner converted to rental',
|
||||
)
|
||||
rental_conversion_date = fields.Date(
|
||||
string='Rental Conversion Date',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# STOCK MOVES
|
||||
# =========================================================================
|
||||
|
||||
checkout_move_id = fields.Many2one(
|
||||
'stock.move',
|
||||
string='Checkout Stock Move',
|
||||
)
|
||||
return_move_id = fields.Many2one(
|
||||
'stock.move',
|
||||
string='Return Stock Move',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# HISTORY
|
||||
# =========================================================================
|
||||
|
||||
history_ids = fields.One2many(
|
||||
'fusion.loaner.history',
|
||||
'checkout_id',
|
||||
string='History',
|
||||
)
|
||||
history_count = fields.Integer(
|
||||
compute='_compute_history_count',
|
||||
string='History Count',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# COMPUTED FIELDS
|
||||
# =========================================================================
|
||||
|
||||
@api.depends('checkout_date', 'loaner_period_days')
|
||||
def _compute_expected_return_date(self):
|
||||
for record in self:
|
||||
if record.checkout_date and record.loaner_period_days:
|
||||
record.expected_return_date = record.checkout_date + timedelta(days=record.loaner_period_days)
|
||||
else:
|
||||
record.expected_return_date = False
|
||||
|
||||
@api.depends('checkout_date', 'actual_return_date')
|
||||
def _compute_days_out(self):
|
||||
today = fields.Date.today()
|
||||
for record in self:
|
||||
if record.checkout_date:
|
||||
end_date = record.actual_return_date or today
|
||||
record.days_out = (end_date - record.checkout_date).days
|
||||
else:
|
||||
record.days_out = 0
|
||||
|
||||
@api.depends('expected_return_date', 'actual_return_date', 'state')
|
||||
def _compute_days_overdue(self):
|
||||
today = fields.Date.today()
|
||||
for record in self:
|
||||
if record.state in ('returned', 'converted_rental', 'lost'):
|
||||
record.days_overdue = 0
|
||||
elif record.expected_return_date:
|
||||
end_date = record.actual_return_date or today
|
||||
overdue = (end_date - record.expected_return_date).days
|
||||
record.days_overdue = max(0, overdue)
|
||||
else:
|
||||
record.days_overdue = 0
|
||||
|
||||
def _compute_history_count(self):
|
||||
for record in self:
|
||||
record.history_count = len(record.history_ids)
|
||||
|
||||
# =========================================================================
|
||||
# ONCHANGE
|
||||
# =========================================================================
|
||||
|
||||
@api.onchange('product_id')
|
||||
def _onchange_product_id(self):
|
||||
if self.product_id:
|
||||
self.loaner_period_days = self.product_id.x_fc_loaner_period_days or 7
|
||||
self.lot_id = False
|
||||
|
||||
@api.onchange('sale_order_id')
|
||||
def _onchange_sale_order_id(self):
|
||||
if self.sale_order_id:
|
||||
self.partner_id = self.sale_order_id.partner_id
|
||||
self.authorizer_id = self.sale_order_id.x_fc_authorizer_id
|
||||
self.sales_rep_id = self.sale_order_id.user_id
|
||||
self.delivery_address = self.sale_order_id.partner_shipping_id.contact_address if self.sale_order_id.partner_shipping_id else ''
|
||||
|
||||
# =========================================================================
|
||||
# CRUD
|
||||
# =========================================================================
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get('name', _('New')) == _('New'):
|
||||
vals['name'] = self.env['ir.sequence'].next_by_code('fusion.loaner.checkout') or _('New')
|
||||
records = super().create(vals_list)
|
||||
for record in records:
|
||||
record._log_history('create', 'Loaner checkout created')
|
||||
return records
|
||||
|
||||
# =========================================================================
|
||||
# ACTIONS
|
||||
# =========================================================================
|
||||
|
||||
def action_checkout(self):
|
||||
"""Confirm the loaner checkout."""
|
||||
self.ensure_one()
|
||||
if self.state != 'draft':
|
||||
raise UserError(_("Can only checkout from draft state."))
|
||||
|
||||
if not self.product_id:
|
||||
raise UserError(_("Please select a product."))
|
||||
|
||||
self.write({'state': 'checked_out'})
|
||||
self._log_history('checkout', f'Loaner checked out to {self.partner_id.name}')
|
||||
# Stock move is non-blocking -- use savepoint so failure doesn't roll back checkout
|
||||
try:
|
||||
with self.env.cr.savepoint():
|
||||
self._create_checkout_stock_move()
|
||||
except Exception as e:
|
||||
_logger.warning(f"Stock move failed for checkout {self.name} (non-blocking): {e}")
|
||||
self._send_checkout_email()
|
||||
|
||||
# Post to chatter
|
||||
self.message_post(
|
||||
body=Markup(
|
||||
'<div class="alert alert-success">'
|
||||
f'<strong>Loaner Checked Out</strong><br/>'
|
||||
f'Product: {self.product_id.name}<br/>'
|
||||
f'Serial: {self.lot_id.name if self.lot_id else "N/A"}<br/>'
|
||||
f'Expected Return: {self.expected_return_date}'
|
||||
'</div>'
|
||||
),
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def action_return(self):
|
||||
"""Open return wizard."""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _('Return Loaner'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'fusion.loaner.return.wizard',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_checkout_id': self.id,
|
||||
},
|
||||
}
|
||||
|
||||
def action_process_return(self, return_condition, return_notes=None, return_photos=None, return_location_id=None):
|
||||
"""Process the loaner return."""
|
||||
self.ensure_one()
|
||||
if self.state not in ('checked_out', 'overdue', 'rental_pending'):
|
||||
raise UserError(_("Cannot return a loaner that is not checked out."))
|
||||
|
||||
vals = {
|
||||
'state': 'returned',
|
||||
'actual_return_date': fields.Date.today(),
|
||||
'return_condition': return_condition,
|
||||
'return_notes': return_notes,
|
||||
'returned_to_id': self.env.user.id,
|
||||
}
|
||||
if return_location_id:
|
||||
vals['return_location_id'] = return_location_id
|
||||
if return_photos:
|
||||
vals['return_photo_ids'] = [(6, 0, return_photos)]
|
||||
|
||||
self.write(vals)
|
||||
self._log_history('return', f'Loaner returned in {return_condition} condition')
|
||||
try:
|
||||
with self.env.cr.savepoint():
|
||||
self._create_return_stock_move()
|
||||
except Exception as e:
|
||||
_logger.warning(f"Stock move failed for return {self.name} (non-blocking): {e}")
|
||||
self._send_return_email()
|
||||
|
||||
# Post to chatter
|
||||
self.message_post(
|
||||
body=Markup(
|
||||
'<div class="alert alert-info">'
|
||||
f'<strong>Loaner Returned</strong><br/>'
|
||||
f'Condition: {return_condition}<br/>'
|
||||
f'Days Out: {self.days_out}'
|
||||
'</div>'
|
||||
),
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def action_mark_lost(self):
|
||||
"""Mark loaner as lost."""
|
||||
self.ensure_one()
|
||||
self.write({'state': 'lost'})
|
||||
self._log_history('lost', 'Loaner marked as lost/write-off')
|
||||
|
||||
self.message_post(
|
||||
body=Markup(
|
||||
'<div class="alert alert-danger">'
|
||||
'<strong>Loaner Marked as Lost</strong><br/>'
|
||||
f'Product: {self.product_id.name}<br/>'
|
||||
f'Serial: {self.lot_id.name if self.lot_id else "N/A"}'
|
||||
'</div>'
|
||||
),
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
def action_convert_to_rental(self):
|
||||
"""Flag for rental conversion."""
|
||||
self.ensure_one()
|
||||
self.write({
|
||||
'state': 'rental_pending',
|
||||
'rental_conversion_date': fields.Date.today(),
|
||||
})
|
||||
self._log_history('rental_pending', 'Loaner flagged for rental conversion')
|
||||
self._send_rental_conversion_email()
|
||||
|
||||
def action_view_history(self):
|
||||
"""View loaner history."""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _('Loaner History'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'fusion.loaner.history',
|
||||
'view_mode': 'tree,form',
|
||||
'domain': [('checkout_id', '=', self.id)],
|
||||
'context': {'default_checkout_id': self.id},
|
||||
}
|
||||
|
||||
def action_view_sale_order(self):
|
||||
self.ensure_one()
|
||||
if not self.sale_order_id:
|
||||
return
|
||||
return {
|
||||
'name': self.sale_order_id.name,
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'sale.order',
|
||||
'view_mode': 'form',
|
||||
'res_id': self.sale_order_id.id,
|
||||
}
|
||||
|
||||
def action_view_partner(self):
|
||||
self.ensure_one()
|
||||
if not self.partner_id:
|
||||
return
|
||||
return {
|
||||
'name': self.partner_id.name,
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'res.partner',
|
||||
'view_mode': 'form',
|
||||
'res_id': self.partner_id.id,
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# STOCK MOVES
|
||||
# =========================================================================
|
||||
|
||||
def _get_loaner_location(self):
|
||||
"""Get the loaner stock location."""
|
||||
location = self.env.ref('fusion_claims.stock_location_loaner', raise_if_not_found=False)
|
||||
if not location:
|
||||
# Fallback to main stock
|
||||
location = self.env.ref('stock.stock_location_stock')
|
||||
return location
|
||||
|
||||
def _get_customer_location(self):
|
||||
"""Get customer location for stock moves."""
|
||||
return self.env.ref('stock.stock_location_customers')
|
||||
|
||||
def _create_checkout_stock_move(self):
|
||||
"""Create stock move for checkout. Non-blocking -- checkout proceeds even if move fails."""
|
||||
if not self.lot_id:
|
||||
return # No serial tracking
|
||||
|
||||
try:
|
||||
source_location = self._get_loaner_location()
|
||||
dest_location = self._get_customer_location()
|
||||
|
||||
move_vals = {
|
||||
'name': f'Loaner Checkout: {self.name}',
|
||||
'product_id': self.product_id.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': self.product_id.uom_id.id,
|
||||
'location_id': source_location.id,
|
||||
'location_dest_id': dest_location.id,
|
||||
'origin': self.name,
|
||||
'company_id': self.company_id.id,
|
||||
'procure_method': 'make_to_stock',
|
||||
}
|
||||
move = self.env['stock.move'].sudo().create(move_vals)
|
||||
move._action_confirm()
|
||||
move._action_assign()
|
||||
|
||||
# Set the lot on move line
|
||||
if move.move_line_ids:
|
||||
move.move_line_ids.write({'lot_id': self.lot_id.id})
|
||||
|
||||
move._action_done()
|
||||
self.checkout_move_id = move.id
|
||||
except Exception as e:
|
||||
_logger.warning(f"Could not create checkout stock move (non-blocking): {e}")
|
||||
|
||||
def _create_return_stock_move(self):
|
||||
"""Create stock move for return. Uses return_location_id if set, otherwise Loaner Stock."""
|
||||
if not self.lot_id:
|
||||
return
|
||||
|
||||
try:
|
||||
source_location = self._get_customer_location()
|
||||
dest_location = self.return_location_id or self._get_loaner_location()
|
||||
|
||||
move_vals = {
|
||||
'name': f'Loaner Return: {self.name}',
|
||||
'product_id': self.product_id.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': self.product_id.uom_id.id,
|
||||
'location_id': source_location.id,
|
||||
'location_dest_id': dest_location.id,
|
||||
'origin': self.name,
|
||||
'company_id': self.company_id.id,
|
||||
'procure_method': 'make_to_stock',
|
||||
}
|
||||
move = self.env['stock.move'].sudo().create(move_vals)
|
||||
move._action_confirm()
|
||||
move._action_assign()
|
||||
|
||||
if move.move_line_ids:
|
||||
move.move_line_ids.write({'lot_id': self.lot_id.id})
|
||||
|
||||
move._action_done()
|
||||
self.return_move_id = move.id
|
||||
except Exception as e:
|
||||
_logger.warning(f"Could not create return stock move: {e}")
|
||||
|
||||
# =========================================================================
|
||||
# HISTORY LOGGING
|
||||
# =========================================================================
|
||||
|
||||
def _log_history(self, action, notes=None):
|
||||
"""Log action to history."""
|
||||
self.ensure_one()
|
||||
self.env['fusion.loaner.history'].create({
|
||||
'checkout_id': self.id,
|
||||
'lot_id': self.lot_id.id if self.lot_id else False,
|
||||
'action': action,
|
||||
'notes': notes,
|
||||
})
|
||||
|
||||
# =========================================================================
|
||||
# EMAIL METHODS
|
||||
# =========================================================================
|
||||
|
||||
def _get_email_recipients(self):
|
||||
"""Get all email recipients for loaner notifications."""
|
||||
recipients = {
|
||||
'client_email': self.partner_id.email if self.partner_id else None,
|
||||
'authorizer_email': self.authorizer_id.email if self.authorizer_id else None,
|
||||
'sales_rep_email': self.sales_rep_id.email if self.sales_rep_id else None,
|
||||
'office_emails': [],
|
||||
}
|
||||
|
||||
# Get office emails from company
|
||||
company = self.company_id or self.env.company
|
||||
office_partners = company.sudo().x_fc_office_notification_ids
|
||||
recipients['office_emails'] = [p.email for p in office_partners if p.email]
|
||||
|
||||
return recipients
|
||||
|
||||
def _send_checkout_email(self):
|
||||
"""Send checkout confirmation email to all parties."""
|
||||
self.ensure_one()
|
||||
recipients = self._get_email_recipients()
|
||||
to_emails = [e for e in [recipients['client_email'], recipients['authorizer_email']] if e]
|
||||
cc_emails = [e for e in [recipients['sales_rep_email']] if e] + recipients['office_emails']
|
||||
if not to_emails:
|
||||
return False
|
||||
|
||||
client_name = self.partner_id.name or 'Client'
|
||||
product_name = self.product_id.name or 'Product'
|
||||
expected_return = self.expected_return_date.strftime('%B %d, %Y') if self.expected_return_date else 'N/A'
|
||||
|
||||
body_html = self._email_build(
|
||||
title='Loaner Equipment Checkout',
|
||||
summary=f'Loaner equipment has been checked out for <strong>{client_name}</strong>.',
|
||||
email_type='info',
|
||||
sections=[('Loaner Details', [
|
||||
('Reference', self.name),
|
||||
('Product', product_name),
|
||||
('Serial Number', self.lot_id.name if self.lot_id else None),
|
||||
('Checkout Date', self.checkout_date.strftime('%B %d, %Y') if self.checkout_date else None),
|
||||
('Expected Return', expected_return),
|
||||
('Loaner Period', f'{self.loaner_period_days} days'),
|
||||
])],
|
||||
note='<strong>Important:</strong> Please return the loaner equipment by the expected return date. '
|
||||
'If not returned on time, rental charges may apply.',
|
||||
note_color='#d69e2e',
|
||||
)
|
||||
try:
|
||||
self.env['mail.mail'].sudo().create({
|
||||
'subject': f'Loaner Checkout - {product_name} - {self.name}',
|
||||
'body_html': body_html,
|
||||
'email_to': ', '.join(to_emails),
|
||||
'email_cc': ', '.join(cc_emails) if cc_emails else '',
|
||||
'model': 'fusion.loaner.checkout', 'res_id': self.id,
|
||||
}).send()
|
||||
return True
|
||||
except Exception as e:
|
||||
_logger.error(f"Failed to send checkout email for {self.name}: {e}")
|
||||
return False
|
||||
|
||||
def _send_return_email(self):
|
||||
"""Send return confirmation email."""
|
||||
self.ensure_one()
|
||||
recipients = self._get_email_recipients()
|
||||
to_emails = [e for e in [recipients['client_email']] if e]
|
||||
cc_emails = [e for e in [recipients['sales_rep_email']] if e]
|
||||
if not to_emails:
|
||||
return False
|
||||
|
||||
client_name = self.partner_id.name or 'Client'
|
||||
product_name = self.product_id.name or 'Product'
|
||||
|
||||
body_html = self._email_build(
|
||||
title='Loaner Equipment Returned',
|
||||
summary=f'Thank you for returning the loaner equipment, <strong>{client_name}</strong>.',
|
||||
email_type='success',
|
||||
sections=[('Return Details', [
|
||||
('Reference', self.name),
|
||||
('Product', product_name),
|
||||
('Return Date', self.actual_return_date.strftime('%B %d, %Y') if self.actual_return_date else None),
|
||||
('Condition', self.return_condition or None),
|
||||
('Days Out', str(self.days_out)),
|
||||
])],
|
||||
)
|
||||
try:
|
||||
self.env['mail.mail'].sudo().create({
|
||||
'subject': f'Loaner Returned - {product_name} - {self.name}',
|
||||
'body_html': body_html,
|
||||
'email_to': ', '.join(to_emails),
|
||||
'email_cc': ', '.join(cc_emails) if cc_emails else '',
|
||||
'model': 'fusion.loaner.checkout', 'res_id': self.id,
|
||||
}).send()
|
||||
return True
|
||||
except Exception as e:
|
||||
_logger.error(f"Failed to send return email for {self.name}: {e}")
|
||||
return False
|
||||
|
||||
def _send_rental_conversion_email(self):
|
||||
"""Send rental conversion notification."""
|
||||
self.ensure_one()
|
||||
recipients = self._get_email_recipients()
|
||||
to_emails = [e for e in [recipients['client_email'], recipients['authorizer_email']] if e]
|
||||
cc_emails = [e for e in [recipients['sales_rep_email']] if e] + recipients['office_emails']
|
||||
if not to_emails and not cc_emails:
|
||||
return False
|
||||
|
||||
client_name = self.partner_id.name or 'Client'
|
||||
product_name = self.product_id.name or 'Product'
|
||||
weekly_rate = self.product_id.x_fc_rental_price_weekly or 0
|
||||
monthly_rate = self.product_id.x_fc_rental_price_monthly or 0
|
||||
|
||||
body_html = self._email_build(
|
||||
title='Loaner Rental Conversion Notice',
|
||||
summary=f'The loaner equipment for <strong>{client_name}</strong> has exceeded the free loaner period.',
|
||||
email_type='urgent',
|
||||
sections=[('Equipment Details', [
|
||||
('Reference', self.name),
|
||||
('Product', product_name),
|
||||
('Days Out', str(self.days_out)),
|
||||
('Days Overdue', str(self.days_overdue)),
|
||||
('Weekly Rental Rate', f'${weekly_rate:.2f}'),
|
||||
('Monthly Rental Rate', f'${monthly_rate:.2f}'),
|
||||
])],
|
||||
note='<strong>Action required:</strong> Please return the equipment or contact us to arrange '
|
||||
'a rental agreement. Rental charges will apply until the equipment is returned.',
|
||||
note_color='#c53030',
|
||||
)
|
||||
email_to = ', '.join(to_emails) if to_emails else ', '.join(cc_emails[:1])
|
||||
email_cc = ', '.join(cc_emails) if to_emails else ', '.join(cc_emails[1:])
|
||||
try:
|
||||
self.env['mail.mail'].sudo().create({
|
||||
'subject': f'Loaner Rental Conversion - {product_name} - {self.name}',
|
||||
'body_html': body_html,
|
||||
'email_to': email_to, 'email_cc': email_cc,
|
||||
'model': 'fusion.loaner.checkout', 'res_id': self.id,
|
||||
}).send()
|
||||
return True
|
||||
except Exception as e:
|
||||
_logger.error(f"Failed to send rental conversion email for {self.name}: {e}")
|
||||
return False
|
||||
|
||||
def _send_reminder_email(self, reminder_type):
|
||||
"""Send reminder email based on type (day5, day8, day10)."""
|
||||
self.ensure_one()
|
||||
recipients = self._get_email_recipients()
|
||||
client_name = self.partner_id.name or 'Client'
|
||||
product_name = self.product_id.name or 'Product'
|
||||
expected_return = self.expected_return_date.strftime('%B %d, %Y') if self.expected_return_date else 'N/A'
|
||||
|
||||
if reminder_type == 'day5':
|
||||
to_emails = [e for e in [recipients['sales_rep_email']] if e] + recipients['office_emails']
|
||||
cc_emails = []
|
||||
subject = f'Loaner Reminder: {product_name} - Day 5'
|
||||
email_type = 'attention'
|
||||
message = (f'The loaner equipment for {client_name} has been out for 5 days. '
|
||||
f'Please follow up to arrange return.')
|
||||
elif reminder_type == 'day8':
|
||||
to_emails = [e for e in [recipients['client_email']] if e]
|
||||
cc_emails = [e for e in [recipients['sales_rep_email']] if e] + recipients['office_emails']
|
||||
subject = f'Loaner Return Reminder - {product_name}'
|
||||
email_type = 'attention'
|
||||
message = (f'Your loaner equipment has been out for 8 days. '
|
||||
f'Please return it soon or it may be converted to a rental.')
|
||||
else:
|
||||
to_emails = [e for e in [recipients['client_email'], recipients['authorizer_email']] if e]
|
||||
cc_emails = [e for e in [recipients['sales_rep_email']] if e] + recipients['office_emails']
|
||||
subject = f'Loaner Return Required - {product_name}'
|
||||
email_type = 'urgent'
|
||||
message = (f'Your loaner equipment has been out for {self.days_out} days. '
|
||||
f'If not returned, rental charges will apply.')
|
||||
|
||||
if not to_emails:
|
||||
return False
|
||||
|
||||
body_html = self._email_build(
|
||||
title='Loaner Equipment Reminder',
|
||||
summary=message,
|
||||
email_type=email_type,
|
||||
sections=[('Loaner Details', [
|
||||
('Reference', self.name),
|
||||
('Client', client_name),
|
||||
('Product', product_name),
|
||||
('Days Out', str(self.days_out)),
|
||||
('Expected Return', expected_return),
|
||||
])],
|
||||
)
|
||||
try:
|
||||
self.env['mail.mail'].sudo().create({
|
||||
'subject': subject,
|
||||
'body_html': body_html,
|
||||
'email_to': ', '.join(to_emails),
|
||||
'email_cc': ', '.join(cc_emails) if cc_emails else '',
|
||||
'model': 'fusion.loaner.checkout', 'res_id': self.id,
|
||||
}).send()
|
||||
return True
|
||||
except Exception as e:
|
||||
_logger.error(f"Failed to send {reminder_type} reminder for {self.name}: {e}")
|
||||
return False
|
||||
|
||||
# =========================================================================
|
||||
# CRON METHODS
|
||||
# =========================================================================
|
||||
|
||||
@api.model
|
||||
def _cron_check_overdue_loaners(self):
|
||||
"""Daily cron to check for overdue loaners and send reminders."""
|
||||
today = fields.Date.today()
|
||||
|
||||
# Find all active loaners
|
||||
active_loaners = self.search([
|
||||
('state', 'in', ['checked_out', 'overdue', 'rental_pending']),
|
||||
])
|
||||
|
||||
for loaner in active_loaners:
|
||||
days_out = loaner.days_out
|
||||
|
||||
# Update overdue status
|
||||
if loaner.state == 'checked_out' and loaner.expected_return_date and today > loaner.expected_return_date:
|
||||
loaner.write({'state': 'overdue'})
|
||||
loaner._log_history('overdue', f'Loaner is now overdue by {loaner.days_overdue} days')
|
||||
|
||||
# Day 5 reminder
|
||||
if days_out >= 5 and not loaner.reminder_day5_sent:
|
||||
loaner._send_reminder_email('day5')
|
||||
loaner.reminder_day5_sent = True
|
||||
loaner._log_history('reminder_sent', 'Day 5 reminder sent')
|
||||
|
||||
# Day 8 warning
|
||||
if days_out >= 8 and not loaner.reminder_day8_sent:
|
||||
loaner._send_reminder_email('day8')
|
||||
loaner.reminder_day8_sent = True
|
||||
loaner._log_history('reminder_sent', 'Day 8 rental warning sent')
|
||||
|
||||
# Day 10 final notice
|
||||
if days_out >= 10 and not loaner.reminder_day10_sent:
|
||||
loaner._send_reminder_email('day10')
|
||||
loaner.reminder_day10_sent = True
|
||||
loaner._log_history('reminder_sent', 'Day 10 final notice sent')
|
||||
|
||||
# Flag for rental conversion
|
||||
if loaner.state != 'rental_pending':
|
||||
loaner.action_convert_to_rental()
|
||||
@@ -1,105 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionLoanerHistory(models.Model):
|
||||
"""Audit trail for loaner equipment actions."""
|
||||
_name = 'fusion.loaner.history'
|
||||
_description = 'Loaner History Log'
|
||||
_order = 'action_date desc, id desc'
|
||||
|
||||
# =========================================================================
|
||||
# REFERENCE FIELDS
|
||||
# =========================================================================
|
||||
|
||||
checkout_id = fields.Many2one(
|
||||
'fusion.loaner.checkout',
|
||||
string='Checkout Record',
|
||||
ondelete='cascade',
|
||||
required=True,
|
||||
)
|
||||
lot_id = fields.Many2one(
|
||||
'stock.lot',
|
||||
string='Serial Number',
|
||||
help='The serial number this action relates to',
|
||||
)
|
||||
product_id = fields.Many2one(
|
||||
'product.product',
|
||||
string='Product',
|
||||
related='checkout_id.product_id',
|
||||
store=True,
|
||||
)
|
||||
partner_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Client',
|
||||
related='checkout_id.partner_id',
|
||||
store=True,
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# ACTION DETAILS
|
||||
# =========================================================================
|
||||
|
||||
action = fields.Selection([
|
||||
('create', 'Created'),
|
||||
('checkout', 'Checked Out'),
|
||||
('return', 'Returned'),
|
||||
('condition_update', 'Condition Updated'),
|
||||
('reminder_sent', 'Reminder Sent'),
|
||||
('overdue', 'Marked Overdue'),
|
||||
('rental_pending', 'Rental Conversion Pending'),
|
||||
('rental_converted', 'Converted to Rental'),
|
||||
('lost', 'Marked as Lost'),
|
||||
('note', 'Note Added'),
|
||||
], string='Action', required=True)
|
||||
|
||||
action_date = fields.Datetime(
|
||||
string='Date/Time',
|
||||
default=fields.Datetime.now,
|
||||
required=True,
|
||||
)
|
||||
user_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='User',
|
||||
default=lambda self: self.env.user,
|
||||
required=True,
|
||||
)
|
||||
notes = fields.Text(
|
||||
string='Notes',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# DISPLAY
|
||||
# =========================================================================
|
||||
|
||||
def _get_action_label(self):
|
||||
"""Get human-readable action label."""
|
||||
action_labels = dict(self._fields['action'].selection)
|
||||
return action_labels.get(self.action, self.action)
|
||||
|
||||
def name_get(self):
|
||||
result = []
|
||||
for record in self:
|
||||
name = f"{record.checkout_id.name} - {record._get_action_label()}"
|
||||
result.append((record.id, name))
|
||||
return result
|
||||
|
||||
# =========================================================================
|
||||
# SEARCH BY SERIAL
|
||||
# =========================================================================
|
||||
|
||||
@api.model
|
||||
def get_history_by_serial(self, lot_id):
|
||||
"""Get all history for a specific serial number."""
|
||||
return self.search([('lot_id', '=', lot_id)], order='action_date desc')
|
||||
|
||||
@api.model
|
||||
def get_history_by_product(self, product_id):
|
||||
"""Get all history for a specific product."""
|
||||
return self.search([('product_id', '=', product_id)], order='action_date desc')
|
||||
@@ -65,86 +65,6 @@ class ProductTemplate(models.Model):
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# LOANER PRODUCT FIELDS
|
||||
# ==========================================================================
|
||||
|
||||
x_fc_can_be_loaned = fields.Boolean(
|
||||
string='Can be Loaned',
|
||||
default=False,
|
||||
help='If checked, this product can be loaned out to clients',
|
||||
)
|
||||
x_fc_loaner_period_days = fields.Integer(
|
||||
string='Loaner Period (Days)',
|
||||
default=7,
|
||||
help='Default number of free loaner days before rental conversion',
|
||||
)
|
||||
x_fc_rental_price_weekly = fields.Float(
|
||||
string='Weekly Rental Price',
|
||||
digits='Product Price',
|
||||
help='Rental price per week if loaner converts to rental',
|
||||
)
|
||||
x_fc_rental_price_monthly = fields.Float(
|
||||
string='Monthly Rental Price',
|
||||
digits='Product Price',
|
||||
help='Rental price per month if loaner converts to rental',
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# LOANER EQUIPMENT FIELDS
|
||||
# ==========================================================================
|
||||
|
||||
x_fc_equipment_type = fields.Selection([
|
||||
('type_1_walker', 'Type 1 Walker'),
|
||||
('type_2_mw', 'Type 2 MW'),
|
||||
('type_2_pw', 'Type 2 PW'),
|
||||
('type_2_walker', 'Type 2 Walker'),
|
||||
('type_3_mw', 'Type 3 MW'),
|
||||
('type_3_pw', 'Type 3 PW'),
|
||||
('type_3_walker', 'Type 3 Walker'),
|
||||
('type_4_mw', 'Type 4 MW'),
|
||||
('type_5_mw', 'Type 5 MW'),
|
||||
('ceiling_lift', 'Ceiling Lift'),
|
||||
('mobility_scooter', 'Mobility Scooter'),
|
||||
('patient_lift', 'Patient Lift'),
|
||||
('transport_wheelchair', 'Transport Wheelchair'),
|
||||
('standard_wheelchair', 'Standard Wheelchair'),
|
||||
('power_wheelchair', 'Power Wheelchair'),
|
||||
('cushion', 'Cushion'),
|
||||
('backrest', 'Backrest'),
|
||||
('stairlift', 'Stairlift'),
|
||||
('others', 'Others'),
|
||||
], string='Equipment Type')
|
||||
|
||||
x_fc_wheelchair_category = fields.Selection([
|
||||
('type_1', 'Type 1'),
|
||||
('type_2', 'Type 2'),
|
||||
('type_3', 'Type 3'),
|
||||
('type_4', 'Type 4'),
|
||||
('type_5', 'Type 5'),
|
||||
], string='Wheelchair Category')
|
||||
|
||||
x_fc_seat_width = fields.Char(string='Seat Width')
|
||||
x_fc_seat_depth = fields.Char(string='Seat Depth')
|
||||
x_fc_seat_height = fields.Char(string='Seat Height')
|
||||
|
||||
x_fc_storage_location = fields.Selection([
|
||||
('warehouse', 'Warehouse'),
|
||||
('westin_brampton', 'Westin Brampton'),
|
||||
('mobility_etobicoke', 'Mobility Etobicoke'),
|
||||
('scarborough_storage', 'Scarborough Storage'),
|
||||
('client_loaned', 'Client/Loaned'),
|
||||
('rented_out', 'Rented Out'),
|
||||
], string='Storage Location')
|
||||
|
||||
x_fc_listing_type = fields.Selection([
|
||||
('owned', 'Owned'),
|
||||
('borrowed', 'Borrowed'),
|
||||
], string='Listing Type')
|
||||
|
||||
x_fc_asset_number = fields.Char(string='Asset Number')
|
||||
x_fc_package_info = fields.Text(string='Package Information')
|
||||
|
||||
# ==========================================================================
|
||||
# ONCHANGE / CONSTRAINTS
|
||||
# ==========================================================================
|
||||
|
||||
@@ -263,96 +263,6 @@ class SaleOrder(models.Model):
|
||||
action['res_id'] = self.x_fc_technician_task_ids.id
|
||||
return action
|
||||
|
||||
# LOANER EQUIPMENT TRACKING
|
||||
# ==========================================================================
|
||||
x_fc_loaner_checkout_ids = fields.One2many(
|
||||
'fusion.loaner.checkout',
|
||||
'sale_order_id',
|
||||
string='Loaner Checkouts',
|
||||
help='Loaner equipment checked out for this order',
|
||||
)
|
||||
x_fc_loaner_count = fields.Integer(
|
||||
string='Loaners',
|
||||
compute='_compute_loaner_count',
|
||||
)
|
||||
x_fc_active_loaner_count = fields.Integer(
|
||||
string='Active Loaners',
|
||||
compute='_compute_loaner_count',
|
||||
)
|
||||
x_fc_has_overdue_loaner = fields.Boolean(
|
||||
string='Has Overdue Loaner',
|
||||
compute='_compute_loaner_count',
|
||||
help='True if any active loaner is past its expected return date',
|
||||
)
|
||||
|
||||
@api.depends('x_fc_loaner_checkout_ids', 'x_fc_loaner_checkout_ids.state',
|
||||
'x_fc_loaner_checkout_ids.expected_return_date')
|
||||
def _compute_loaner_count(self):
|
||||
"""Compute loaner counts and overdue status for this order."""
|
||||
today = fields.Date.today()
|
||||
for order in self:
|
||||
active = order.x_fc_loaner_checkout_ids.filtered(
|
||||
lambda l: l.state in ('checked_out', 'overdue', 'rental_pending')
|
||||
)
|
||||
order.x_fc_loaner_count = len(order.x_fc_loaner_checkout_ids)
|
||||
order.x_fc_active_loaner_count = len(active)
|
||||
order.x_fc_has_overdue_loaner = any(
|
||||
l.state == 'overdue' or (l.expected_return_date and l.expected_return_date < today)
|
||||
for l in active
|
||||
)
|
||||
|
||||
def action_view_loaners(self):
|
||||
"""Open the loaner checkouts for this order."""
|
||||
self.ensure_one()
|
||||
action = {
|
||||
'name': 'Loaner Checkouts',
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'fusion.loaner.checkout',
|
||||
'view_mode': 'tree,form',
|
||||
'domain': [('sale_order_id', '=', self.id)],
|
||||
'context': {'default_sale_order_id': self.id},
|
||||
}
|
||||
if len(self.x_fc_loaner_checkout_ids) == 1:
|
||||
action['view_mode'] = 'form'
|
||||
action['res_id'] = self.x_fc_loaner_checkout_ids.id
|
||||
return action
|
||||
|
||||
def action_checkout_loaner(self):
|
||||
"""Open the loaner checkout wizard."""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': 'Checkout Loaner',
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'fusion.loaner.checkout.wizard',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_sale_order_id': self.id,
|
||||
'default_partner_id': self.partner_id.id,
|
||||
'default_authorizer_id': self.x_fc_authorizer_id.id if self.x_fc_authorizer_id else False,
|
||||
},
|
||||
}
|
||||
|
||||
def action_checkin_loaner(self):
|
||||
"""Open the return wizard for the active loaner on this order."""
|
||||
self.ensure_one()
|
||||
active_loaners = self.x_fc_loaner_checkout_ids.filtered(
|
||||
lambda l: l.state in ('checked_out', 'overdue', 'rental_pending')
|
||||
)
|
||||
if not active_loaners:
|
||||
raise UserError("No active loaners to check in for this order.")
|
||||
if len(active_loaners) == 1:
|
||||
return active_loaners.action_return()
|
||||
# Multiple active loaners - show the list so user can pick which one to return
|
||||
return {
|
||||
'name': 'Return Loaner',
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'fusion.loaner.checkout',
|
||||
'view_mode': 'tree,form',
|
||||
'domain': [('id', 'in', active_loaners.ids)],
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
def action_ready_for_delivery(self):
|
||||
"""Open the task scheduling form to schedule a delivery task.
|
||||
|
||||
|
||||
@@ -21,12 +21,6 @@ access_fusion_ready_for_submission_wizard,fusion.ready.for.submission.wizard.use
|
||||
access_fusion_ready_to_bill_wizard,fusion.ready.to.bill.wizard.user,model_fusion_claims_ready_to_bill_wizard,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_fusion_submission_history_user,fusion.submission.history.user,model_fusion_submission_history,sales_team.group_sale_salesman,1,1,1,0
|
||||
access_fusion_submission_history_manager,fusion.submission.history.manager,model_fusion_submission_history,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_loaner_checkout_user,fusion.loaner.checkout.user,model_fusion_loaner_checkout,sales_team.group_sale_salesman,1,1,1,0
|
||||
access_fusion_loaner_checkout_manager,fusion.loaner.checkout.manager,model_fusion_loaner_checkout,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_loaner_history_user,fusion.loaner.history.user,model_fusion_loaner_history,sales_team.group_sale_salesman,1,0,0,0
|
||||
access_fusion_loaner_history_manager,fusion.loaner.history.manager,model_fusion_loaner_history,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_loaner_checkout_wizard,fusion.loaner.checkout.wizard.user,model_fusion_loaner_checkout_wizard,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_fusion_loaner_return_wizard,fusion.loaner.return.wizard.user,model_fusion_loaner_return_wizard,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_fusion_ready_for_delivery_wizard,fusion.ready.for.delivery.wizard.user,model_fusion_ready_for_delivery_wizard,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_fusion_client_profile_user,fusion.client.profile.user,model_fusion_client_profile,sales_team.group_sale_salesman,1,1,1,0
|
||||
access_fusion_client_profile_manager,fusion.client.profile.manager,model_fusion_client_profile,sales_team.group_sale_manager,1,1,1,1
|
||||
@@ -68,4 +62,5 @@ access_fusion_page11_sign_request_user,fusion.page11.sign.request.user,model_fus
|
||||
access_fusion_page11_sign_request_manager,fusion.page11.sign.request.manager,model_fusion_page11_sign_request,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_page11_sign_request_public,fusion.page11.sign.request.public,model_fusion_page11_sign_request,base.group_public,1,0,0,0
|
||||
access_fusion_send_page11_wizard_user,fusion_claims.send.page11.wizard.user,model_fusion_claims_send_page11_wizard,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_fusion_send_page11_wizard_manager,fusion_claims.send.page11.wizard.manager,model_fusion_claims_send_page11_wizard,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_send_page11_wizard_manager,fusion_claims.send.page11.wizard.manager,model_fusion_claims_send_page11_wizard,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_adp_import_wizard_user,fusion_claims.adp.import.wizard.user,model_fusion_claims_adp_import_wizard,account.group_account_invoice,1,1,1,1
|
||||
|
@@ -756,4 +756,16 @@ html.dark, .o_dark {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.adp_file_preview {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -220,8 +220,7 @@
|
||||
<field name="x_fc_client_portion_total" widget="monetary" sum="Total Client" optional="show"/>
|
||||
<field name="amount_total" widget="monetary" sum="Grand Total" optional="show"/>
|
||||
|
||||
<!-- Loaner / Status -->
|
||||
<field name="x_fc_loaner_count" string="Loaners" optional="hide"/>
|
||||
<!-- Status -->
|
||||
<field name="x_fc_on_hold_date" optional="hide"/>
|
||||
<field name="x_fc_case_locked" optional="hide"/>
|
||||
<field name="state" widget="badge" decoration-success="state == 'sale'"
|
||||
@@ -1733,105 +1732,73 @@ else:
|
||||
parent="menu_adp_claims_root"
|
||||
sequence="20"/>
|
||||
|
||||
<!-- Top-level quick access -->
|
||||
<menuitem id="menu_adp_all_orders" name="All ADP Orders" parent="menu_fc_adp"
|
||||
action="action_adp_orders_all" sequence="1"/>
|
||||
<menuitem id="menu_adp_invoices" name="ADP Invoices" parent="menu_fc_adp"
|
||||
action="action_adp_invoices" sequence="2"/>
|
||||
<menuitem id="menu_adp_client_invoices" name="ADP Client Invoices" parent="menu_fc_adp"
|
||||
action="action_adp_client_invoices" sequence="3"/>
|
||||
<menuitem id="menu_adp_export_files" name="Export Files" parent="menu_fc_adp"
|
||||
action="action_adp_export_records" sequence="4"/>
|
||||
|
||||
<menuitem id="menu_adp_quotations"
|
||||
name="Quotation Stage"
|
||||
<!-- Orders & Billing -->
|
||||
<menuitem id="menu_adp_orders_billing"
|
||||
name="Orders & Billing"
|
||||
parent="menu_fc_adp"
|
||||
sequence="5"/>
|
||||
<menuitem id="menu_adp_invoices" name="ADP Invoices" parent="menu_adp_orders_billing"
|
||||
action="action_adp_invoices" sequence="1"/>
|
||||
<menuitem id="menu_adp_client_invoices" name="Client Invoices" parent="menu_adp_orders_billing"
|
||||
action="action_adp_client_invoices" sequence="2"/>
|
||||
<menuitem id="menu_adp_ready_billing" name="Ready for Billing" parent="menu_adp_orders_billing"
|
||||
action="action_adp_ready_billing" sequence="3"/>
|
||||
<menuitem id="menu_adp_billed" name="Billed to ADP" parent="menu_adp_orders_billing"
|
||||
action="action_adp_billed" sequence="4"/>
|
||||
<menuitem id="menu_adp_export_files" name="Claim Submission Files" parent="menu_adp_orders_billing"
|
||||
action="action_adp_export_records" sequence="5"/>
|
||||
|
||||
<!-- Pre-Submission Pipeline -->
|
||||
<menuitem id="menu_adp_pre_submission"
|
||||
name="Pre-Submission"
|
||||
parent="menu_fc_adp"
|
||||
action="action_adp_quotations"
|
||||
sequence="10"/>
|
||||
<menuitem id="menu_adp_quotations" name="Quotation Stage" parent="menu_adp_pre_submission"
|
||||
action="action_adp_quotations" sequence="1"/>
|
||||
<menuitem id="menu_adp_assessment_scheduled" name="Assessment Scheduled" parent="menu_adp_pre_submission"
|
||||
action="action_adp_assessment_scheduled" sequence="2"/>
|
||||
<menuitem id="menu_adp_assessment_completed" name="Waiting for Application" parent="menu_adp_pre_submission"
|
||||
action="action_adp_assessment_completed" sequence="3"/>
|
||||
<menuitem id="menu_adp_application_received" name="Application Received" parent="menu_adp_pre_submission"
|
||||
action="action_adp_application_received" sequence="4"/>
|
||||
<menuitem id="menu_adp_ready_submission" name="Ready for Submission" parent="menu_adp_pre_submission"
|
||||
action="action_adp_ready_submission" sequence="5"/>
|
||||
|
||||
<menuitem id="menu_adp_assessment_scheduled"
|
||||
name="Assessment Scheduled"
|
||||
<!-- ADP Review -->
|
||||
<menuitem id="menu_adp_review"
|
||||
name="ADP Review"
|
||||
parent="menu_fc_adp"
|
||||
action="action_adp_assessment_scheduled"
|
||||
sequence="12"/>
|
||||
|
||||
<menuitem id="menu_adp_assessment_completed"
|
||||
name="Waiting for Application"
|
||||
parent="menu_fc_adp"
|
||||
action="action_adp_assessment_completed"
|
||||
sequence="14"/>
|
||||
|
||||
<menuitem id="menu_adp_application_received"
|
||||
name="Application Received"
|
||||
parent="menu_fc_adp"
|
||||
action="action_adp_application_received"
|
||||
sequence="16"/>
|
||||
|
||||
<menuitem id="menu_adp_ready_submission"
|
||||
name="Ready for Submission"
|
||||
parent="menu_fc_adp"
|
||||
action="action_adp_ready_submission"
|
||||
sequence="18"/>
|
||||
|
||||
<menuitem id="menu_adp_pending_approval"
|
||||
name="Application Submitted"
|
||||
parent="menu_fc_adp"
|
||||
action="action_adp_pending_approval"
|
||||
sequence="20"/>
|
||||
<menuitem id="menu_adp_pending_approval" name="Application Submitted" parent="menu_adp_review"
|
||||
action="action_adp_pending_approval" sequence="1"/>
|
||||
<menuitem id="menu_adp_accepted" name="Accepted by ADP" parent="menu_adp_review"
|
||||
action="action_adp_accepted" sequence="2"/>
|
||||
<menuitem id="menu_adp_approved" name="Application Approved" parent="menu_adp_review"
|
||||
action="action_adp_approved" sequence="3"/>
|
||||
<menuitem id="menu_adp_rejected" name="Rejected by ADP" parent="menu_adp_review"
|
||||
action="action_adp_rejected" sequence="4"/>
|
||||
<menuitem id="menu_adp_needs_correction" name="Needs Correction" parent="menu_adp_review"
|
||||
action="action_adp_needs_correction" sequence="5"/>
|
||||
|
||||
<menuitem id="menu_adp_accepted"
|
||||
name="Accepted by ADP"
|
||||
<!-- Fulfillment -->
|
||||
<menuitem id="menu_adp_fulfillment"
|
||||
name="Fulfillment"
|
||||
parent="menu_fc_adp"
|
||||
action="action_adp_accepted"
|
||||
sequence="21"/>
|
||||
|
||||
<menuitem id="menu_adp_rejected"
|
||||
name="Rejected by ADP"
|
||||
parent="menu_fc_adp"
|
||||
action="action_adp_rejected"
|
||||
sequence="22"/>
|
||||
|
||||
<menuitem id="menu_adp_needs_correction"
|
||||
name="Needs Correction"
|
||||
parent="menu_fc_adp"
|
||||
action="action_adp_needs_correction"
|
||||
sequence="23"/>
|
||||
|
||||
<menuitem id="menu_adp_approved"
|
||||
name="Application Approved"
|
||||
parent="menu_fc_adp"
|
||||
action="action_adp_approved"
|
||||
sequence="25"/>
|
||||
|
||||
<menuitem id="menu_adp_ready_delivery"
|
||||
name="Ready for Delivery"
|
||||
parent="menu_fc_adp"
|
||||
action="action_adp_ready_delivery"
|
||||
sequence="27"/>
|
||||
|
||||
<menuitem id="menu_adp_ready_billing"
|
||||
name="Ready for Billing"
|
||||
parent="menu_fc_adp"
|
||||
action="action_adp_ready_billing"
|
||||
sequence="30"/>
|
||||
<menuitem id="menu_adp_ready_delivery" name="Ready for Delivery" parent="menu_adp_fulfillment"
|
||||
action="action_adp_ready_delivery" sequence="1"/>
|
||||
<menuitem id="menu_adp_closed" name="Case Closed" parent="menu_adp_fulfillment"
|
||||
action="action_adp_closed" sequence="2"/>
|
||||
|
||||
<menuitem id="menu_adp_billed"
|
||||
name="Billed to ADP"
|
||||
parent="menu_fc_adp"
|
||||
action="action_adp_billed"
|
||||
sequence="35"/>
|
||||
|
||||
<menuitem id="menu_adp_closed"
|
||||
name="Case Closed"
|
||||
parent="menu_fc_adp"
|
||||
action="action_adp_closed"
|
||||
sequence="40"/>
|
||||
|
||||
<!-- ADP Special Statuses -->
|
||||
<!-- Special Statuses -->
|
||||
<menuitem id="menu_adp_special_statuses"
|
||||
name="Special Statuses"
|
||||
parent="menu_fc_adp"
|
||||
sequence="50"/>
|
||||
|
||||
<menuitem id="menu_adp_on_hold" name="On Hold" parent="menu_adp_special_statuses"
|
||||
action="action_adp_on_hold" sequence="10"/>
|
||||
<menuitem id="menu_adp_withdrawn" name="Withdrawn" parent="menu_adp_special_statuses"
|
||||
@@ -2037,6 +2004,8 @@ else:
|
||||
action="action_device_import_wizard" sequence="20"/>
|
||||
<menuitem id="menu_import_xml_files" name="Import XML Files" parent="menu_adp_config"
|
||||
action="action_xml_import_wizard" sequence="30"/>
|
||||
<menuitem id="menu_adp_import_files" name="Import Submission Files" parent="menu_adp_config"
|
||||
action="action_adp_import_wizard" sequence="35"/>
|
||||
<menuitem id="menu_fusion_claims_settings" name="Settings" parent="menu_adp_config"
|
||||
action="action_fusion_claims_settings" sequence="90"/>
|
||||
|
||||
|
||||
@@ -12,10 +12,12 @@
|
||||
<field name="name">fusion_claims.adp.export.record.list</field>
|
||||
<field name="model">fusion_claims.adp.export.record</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="ADP Export Files" default_order="export_date desc">
|
||||
<list string="ADP Claim Submission Files"
|
||||
default_order="year desc, month asc, posting_period_date desc, export_date desc">
|
||||
<field name="name"/>
|
||||
<field name="export_date"/>
|
||||
<field name="posting_period_label" string="Posting Period"/>
|
||||
<field name="payment_date" string="Payment Date"/>
|
||||
<field name="vendor_code"/>
|
||||
<field name="line_count" string="Lines"/>
|
||||
<field name="invoice_count" string="Invoices"/>
|
||||
@@ -27,6 +29,44 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- ADP EXPORT RECORD: Kanban View -->
|
||||
<!-- ===================================================================== -->
|
||||
<record id="view_adp_export_record_kanban" model="ir.ui.view">
|
||||
<field name="name">fusion_claims.adp.export.record.kanban</field>
|
||||
<field name="model">fusion_claims.adp.export.record</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban class="o_kanban_mobile" default_order="year desc, month asc, posting_period_date desc, export_date desc">
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<strong><field name="name"/></strong>
|
||||
<field name="vendor_code" class="text-muted"/>
|
||||
</div>
|
||||
<div class="text-muted small mb-1">
|
||||
<field name="posting_period_label"/>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="small">
|
||||
<span class="me-3"><i class="fa fa-list-ol me-1"/>
|
||||
<field name="line_count"/> lines</span>
|
||||
<span><i class="fa fa-file-text-o me-1"/>
|
||||
<field name="invoice_count"/> invoices</span>
|
||||
</div>
|
||||
<field name="user_id" widget="many2one_avatar_user"/>
|
||||
</div>
|
||||
<div class="text-muted small mt-1">
|
||||
<i class="fa fa-calendar me-1"/><field name="export_date"/>
|
||||
</div>
|
||||
<field name="year" invisible="1"/>
|
||||
<field name="month" invisible="1"/>
|
||||
<field name="month_number" invisible="1"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- ADP EXPORT RECORD: Form View -->
|
||||
<!-- ===================================================================== -->
|
||||
@@ -34,20 +74,29 @@
|
||||
<field name="name">fusion_claims.adp.export.record.form</field>
|
||||
<field name="model">fusion_claims.adp.export.record</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="ADP Export File">
|
||||
<form string="ADP Claim Submission File">
|
||||
<header>
|
||||
<button name="action_download" string="Download File"
|
||||
type="object" class="btn-primary" icon="fa-download"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_invoices" type="object"
|
||||
class="oe_stat_button" icon="fa-pencil-square-o"
|
||||
invisible="invoice_count == 0">
|
||||
<field name="invoice_count" widget="statinfo"
|
||||
string="Invoices"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<h1><field name="name" readonly="1"/></h1>
|
||||
<h1><field name="name"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group string="Export Details">
|
||||
<field name="export_date" readonly="1"/>
|
||||
<field name="posting_period_date" readonly="1"/>
|
||||
<field name="posting_period_label" readonly="1"/>
|
||||
<field name="payment_date" readonly="1"/>
|
||||
<field name="vendor_code" readonly="1"/>
|
||||
<field name="line_count" readonly="1"/>
|
||||
</group>
|
||||
@@ -59,13 +108,20 @@
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="File Preview" name="preview"
|
||||
invisible="not file_preview">
|
||||
<field name="file_preview" readonly="1" nolabel="1"
|
||||
widget="text"/>
|
||||
</page>
|
||||
<page string="Exported Invoices" name="invoices">
|
||||
<field name="invoice_ids" readonly="1" nolabel="1">
|
||||
<list string="Invoices" create="0" delete="0">
|
||||
<field name="name" string="Invoice"/>
|
||||
<field name="partner_id" string="Customer"/>
|
||||
<field name="partner_id" string="Customer"
|
||||
widget="many2one"/>
|
||||
<field name="invoice_date"/>
|
||||
<field name="amount_total" string="Total"/>
|
||||
<field name="currency_id" column_invisible="True"/>
|
||||
<field name="amount_total" string="Total" sum="Grand Total"/>
|
||||
<field name="state" widget="badge"
|
||||
decoration-success="state == 'posted'"
|
||||
decoration-info="state == 'draft'"/>
|
||||
@@ -88,7 +144,7 @@
|
||||
<field name="name">fusion_claims.adp.export.record.search</field>
|
||||
<field name="model">fusion_claims.adp.export.record</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="ADP Export Files">
|
||||
<search string="ADP Claim Submission Files">
|
||||
<field name="name"/>
|
||||
<field name="vendor_code"/>
|
||||
<field name="user_id"/>
|
||||
@@ -98,7 +154,7 @@
|
||||
<filter string="Month" name="group_month"
|
||||
context="{'group_by': 'month'}"/>
|
||||
<filter string="Posting Period" name="group_posting"
|
||||
context="{'group_by': 'posting_period_date'}"/>
|
||||
context="{'group_by': 'posting_period_label'}"/>
|
||||
<filter string="Exported By" name="group_user"
|
||||
context="{'group_by': 'user_id'}"/>
|
||||
</search>
|
||||
@@ -109,17 +165,17 @@
|
||||
<!-- ADP EXPORT RECORD: Action -->
|
||||
<!-- ===================================================================== -->
|
||||
<record id="action_adp_export_records" model="ir.actions.act_window">
|
||||
<field name="name">Export Files</field>
|
||||
<field name="name">Claim Submission Files</field>
|
||||
<field name="res_model">fusion_claims.adp.export.record</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="view_mode">list,kanban,form</field>
|
||||
<field name="search_view_id" ref="view_adp_export_record_search"/>
|
||||
<field name="context">{'search_default_group_year': 1, 'search_default_group_month': 1, 'search_default_group_posting': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No export files yet
|
||||
No claim submission files yet
|
||||
</p>
|
||||
<p>
|
||||
ADP export files will appear here after you export invoices using the
|
||||
ADP claim submission files will appear here after you export invoices using the
|
||||
<strong>Export ADP</strong> button on ADP portion invoices.
|
||||
</p>
|
||||
</field>
|
||||
|
||||
@@ -1,525 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2024-2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
-->
|
||||
<odoo>
|
||||
<!-- ===================================================================== -->
|
||||
<!-- LOANER CHECKOUT VIEWS -->
|
||||
<!-- ===================================================================== -->
|
||||
|
||||
<!-- List View -->
|
||||
<record id="view_fusion_loaner_checkout_list" model="ir.ui.view">
|
||||
<field name="name">fusion.loaner.checkout.list</field>
|
||||
<field name="model">fusion.loaner.checkout</field>
|
||||
<field name="arch" type="xml">
|
||||
<list decoration-danger="state == 'overdue'"
|
||||
decoration-warning="state == 'rental_pending'"
|
||||
decoration-muted="state in ('returned', 'lost')"
|
||||
default_order="checkout_date desc, id desc">
|
||||
<field name="name"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="product_id"/>
|
||||
<field name="lot_id" optional="show"/>
|
||||
<field name="checkout_date"/>
|
||||
<field name="expected_return_date"/>
|
||||
<field name="actual_return_date" optional="hide"/>
|
||||
<field name="days_out"/>
|
||||
<field name="days_overdue" optional="hide"/>
|
||||
<field name="sales_rep_id" optional="hide"/>
|
||||
<field name="checkout_condition" optional="hide"/>
|
||||
<field name="return_condition" optional="hide"/>
|
||||
<field name="sale_order_id" optional="hide"/>
|
||||
<field name="state" widget="badge"
|
||||
decoration-info="state == 'draft'"
|
||||
decoration-success="state == 'checked_out'"
|
||||
decoration-danger="state in ('overdue', 'lost')"
|
||||
decoration-warning="state == 'rental_pending'"
|
||||
decoration-muted="state in ('returned', 'converted_rental')"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Form View -->
|
||||
<record id="view_fusion_loaner_checkout_form" model="ir.ui.view">
|
||||
<field name="name">fusion.loaner.checkout.form</field>
|
||||
<field name="model">fusion.loaner.checkout</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
<button name="action_checkout" type="object" string="Confirm Checkout"
|
||||
class="btn-primary" invisible="state != 'draft'"/>
|
||||
<button name="action_return" type="object" string="Return Loaner"
|
||||
class="btn-success" invisible="state not in ('checked_out', 'overdue', 'rental_pending')"/>
|
||||
<field name="state" widget="statusbar"
|
||||
statusbar_visible="draft,checked_out,returned"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_sale_order" type="object"
|
||||
class="oe_stat_button" icon="fa-file-text-o"
|
||||
invisible="not sale_order_id">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_text">Sale Order</span>
|
||||
</div>
|
||||
</button>
|
||||
<button name="action_view_partner" type="object"
|
||||
class="oe_stat_button" icon="fa-user">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_text">Contact</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" readonly="1"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group string="Client Information">
|
||||
<field name="partner_id"/>
|
||||
<field name="sales_rep_id"/>
|
||||
<field name="sale_order_id"/>
|
||||
</group>
|
||||
<group string="Product">
|
||||
<field name="product_id"/>
|
||||
<field name="lot_id"/>
|
||||
<field name="loaner_period_days"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group string="Dates">
|
||||
<field name="checkout_date"/>
|
||||
<field name="expected_return_date"/>
|
||||
<field name="actual_return_date"/>
|
||||
<field name="days_out"/>
|
||||
</group>
|
||||
<group string="Condition">
|
||||
<field name="checkout_condition"/>
|
||||
<field name="checkout_notes"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Search View -->
|
||||
<record id="view_fusion_loaner_checkout_search" model="ir.ui.view">
|
||||
<field name="name">fusion.loaner.checkout.search</field>
|
||||
<field name="model">fusion.loaner.checkout</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Loaners">
|
||||
<field name="name"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="product_id"/>
|
||||
<field name="lot_id" string="Serial Number"/>
|
||||
<field name="sale_order_id"/>
|
||||
<field name="sales_rep_id"/>
|
||||
<separator/>
|
||||
<!-- Status Filters -->
|
||||
<filter string="Draft" name="filter_draft"
|
||||
domain="[('state', '=', 'draft')]"/>
|
||||
<filter string="Checked Out" name="filter_checked_out"
|
||||
domain="[('state', '=', 'checked_out')]"/>
|
||||
<filter string="Overdue" name="filter_overdue"
|
||||
domain="[('state', '=', 'overdue')]"/>
|
||||
<filter string="Rental Pending" name="filter_rental_pending"
|
||||
domain="[('state', '=', 'rental_pending')]"/>
|
||||
<filter string="Returned" name="filter_returned"
|
||||
domain="[('state', '=', 'returned')]"/>
|
||||
<filter string="Converted to Rental" name="filter_converted"
|
||||
domain="[('state', '=', 'converted_rental')]"/>
|
||||
<filter string="Lost" name="filter_lost"
|
||||
domain="[('state', '=', 'lost')]"/>
|
||||
<separator/>
|
||||
<!-- Quick Filters -->
|
||||
<filter string="Active Loaners" name="filter_active"
|
||||
domain="[('state', 'in', ['checked_out', 'overdue', 'rental_pending'])]"/>
|
||||
<filter string="Needs Attention" name="filter_attention"
|
||||
domain="[('state', 'in', ['overdue', 'rental_pending'])]"/>
|
||||
<filter string="My Loaners" name="filter_my_loaners"
|
||||
domain="[('sales_rep_id', '=', uid)]"/>
|
||||
<separator/>
|
||||
<!-- Condition Filters -->
|
||||
<filter string="Needs Repair (Checkout)" name="filter_checkout_repair"
|
||||
domain="[('checkout_condition', '=', 'needs_repair')]"/>
|
||||
<filter string="Damaged (Return)" name="filter_return_damaged"
|
||||
domain="[('return_condition', 'in', ['needs_repair', 'damaged'])]"/>
|
||||
<separator/>
|
||||
<!-- Group By -->
|
||||
<filter string="Status" name="group_state"
|
||||
context="{'group_by': 'state'}"/>
|
||||
<filter string="Client" name="group_client"
|
||||
context="{'group_by': 'partner_id'}"/>
|
||||
<filter string="Product" name="group_product"
|
||||
context="{'group_by': 'product_id'}"/>
|
||||
<filter string="Sales Rep" name="group_sales_rep"
|
||||
context="{'group_by': 'sales_rep_id'}"/>
|
||||
<filter string="Checkout Condition" name="group_checkout_condition"
|
||||
context="{'group_by': 'checkout_condition'}"/>
|
||||
<filter string="Return Condition" name="group_return_condition"
|
||||
context="{'group_by': 'return_condition'}"/>
|
||||
<filter string="Checkout Month" name="group_checkout_month"
|
||||
context="{'group_by': 'checkout_date:month'}"/>
|
||||
<filter string="Return Month" name="group_return_month"
|
||||
context="{'group_by': 'actual_return_date:month'}"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- LOANER HISTORY VIEWS -->
|
||||
<!-- ===================================================================== -->
|
||||
|
||||
<record id="view_fusion_loaner_history_list" model="ir.ui.view">
|
||||
<field name="name">fusion.loaner.history.list</field>
|
||||
<field name="model">fusion.loaner.history</field>
|
||||
<field name="arch" type="xml">
|
||||
<list default_order="action_date desc, id desc">
|
||||
<field name="action_date"/>
|
||||
<field name="checkout_id"/>
|
||||
<field name="partner_id" optional="show"/>
|
||||
<field name="product_id" optional="show"/>
|
||||
<field name="lot_id" optional="hide"/>
|
||||
<field name="action" widget="badge"
|
||||
decoration-info="action in ('create', 'note')"
|
||||
decoration-success="action in ('checkout', 'return')"
|
||||
decoration-warning="action in ('reminder_sent', 'overdue', 'rental_pending')"
|
||||
decoration-danger="action in ('lost', 'condition_update')"/>
|
||||
<field name="user_id"/>
|
||||
<field name="notes" optional="show"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- History Search View -->
|
||||
<record id="view_fusion_loaner_history_search" model="ir.ui.view">
|
||||
<field name="name">fusion.loaner.history.search</field>
|
||||
<field name="model">fusion.loaner.history</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Loaner History">
|
||||
<field name="checkout_id"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="product_id"/>
|
||||
<field name="lot_id" string="Serial Number"/>
|
||||
<field name="user_id"/>
|
||||
<separator/>
|
||||
<!-- Action Type Filters -->
|
||||
<filter string="Checkouts" name="filter_checkout"
|
||||
domain="[('action', '=', 'checkout')]"/>
|
||||
<filter string="Returns" name="filter_return"
|
||||
domain="[('action', '=', 'return')]"/>
|
||||
<filter string="Reminders" name="filter_reminders"
|
||||
domain="[('action', '=', 'reminder_sent')]"/>
|
||||
<filter string="Overdue" name="filter_overdue"
|
||||
domain="[('action', '=', 'overdue')]"/>
|
||||
<filter string="Rental Conversions" name="filter_rental"
|
||||
domain="[('action', 'in', ['rental_pending', 'rental_converted'])]"/>
|
||||
<filter string="Lost" name="filter_lost"
|
||||
domain="[('action', '=', 'lost')]"/>
|
||||
<separator/>
|
||||
<!-- Group By -->
|
||||
<filter string="Action Type" name="group_action"
|
||||
context="{'group_by': 'action'}"/>
|
||||
<filter string="Client" name="group_client"
|
||||
context="{'group_by': 'partner_id'}"/>
|
||||
<filter string="Product" name="group_product"
|
||||
context="{'group_by': 'product_id'}"/>
|
||||
<filter string="User" name="group_user"
|
||||
context="{'group_by': 'user_id'}"/>
|
||||
<filter string="Month" name="group_month"
|
||||
context="{'group_by': 'action_date:month'}"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- WIZARD VIEWS -->
|
||||
<!-- ===================================================================== -->
|
||||
|
||||
<!-- Checkout Wizard -->
|
||||
<record id="view_loaner_checkout_wizard_form" model="ir.ui.view">
|
||||
<field name="name">fusion.loaner.checkout.wizard.form</field>
|
||||
<field name="model">fusion.loaner.checkout.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group>
|
||||
<field name="partner_id"/>
|
||||
<field name="product_id"/>
|
||||
<field name="lot_id"/>
|
||||
<field name="checkout_date"/>
|
||||
<field name="loaner_period_days"/>
|
||||
<field name="checkout_condition" widget="radio"/>
|
||||
<field name="checkout_notes"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="action_checkout" type="object" string="Checkout Loaner" class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Return Wizard -->
|
||||
<record id="view_loaner_return_wizard_form" model="ir.ui.view">
|
||||
<field name="name">fusion.loaner.return.wizard.form</field>
|
||||
<field name="model">fusion.loaner.return.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group>
|
||||
<field name="checkout_id" readonly="1"/>
|
||||
<field name="return_date"/>
|
||||
<field name="return_condition" widget="radio"/>
|
||||
<field name="return_notes"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="action_return" type="object" string="Confirm Return" class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- LOANER PRODUCTS VIEWS -->
|
||||
<!-- ===================================================================== -->
|
||||
|
||||
<!-- Loaner Products List View -->
|
||||
<record id="view_fusion_loaner_products_list" model="ir.ui.view">
|
||||
<field name="name">product.template.loaner.list</field>
|
||||
<field name="model">product.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="x_fc_equipment_type" optional="show"/>
|
||||
<field name="x_fc_wheelchair_category" optional="show"/>
|
||||
<field name="x_fc_seat_width" optional="show"/>
|
||||
<field name="x_fc_seat_depth" optional="show"/>
|
||||
<field name="x_fc_seat_height" optional="hide"/>
|
||||
<field name="x_fc_storage_location" optional="show"/>
|
||||
<field name="x_fc_listing_type" optional="show"/>
|
||||
<field name="x_fc_asset_number" optional="hide"/>
|
||||
<field name="active" column_invisible="True"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Loaner Products Search View -->
|
||||
<record id="view_fusion_loaner_products_search" model="ir.ui.view">
|
||||
<field name="name">product.template.loaner.search</field>
|
||||
<field name="model">product.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Loaner Products">
|
||||
<field name="name"/>
|
||||
<field name="x_fc_equipment_type"/>
|
||||
<field name="x_fc_asset_number"/>
|
||||
<separator/>
|
||||
<filter string="Owned" name="filter_owned"
|
||||
domain="[('x_fc_listing_type', '=', 'owned')]"/>
|
||||
<filter string="Borrowed" name="filter_borrowed"
|
||||
domain="[('x_fc_listing_type', '=', 'borrowed')]"/>
|
||||
<separator/>
|
||||
<filter string="Archived" name="filter_archived"
|
||||
domain="[('active', '=', False)]"/>
|
||||
<separator/>
|
||||
<filter string="Equipment Type" name="group_equipment_type"
|
||||
context="{'group_by': 'x_fc_equipment_type'}"/>
|
||||
<filter string="Wheelchair Category" name="group_wheelchair_category"
|
||||
context="{'group_by': 'x_fc_wheelchair_category'}"/>
|
||||
<filter string="Storage Location" name="group_storage_location"
|
||||
context="{'group_by': 'x_fc_storage_location'}"/>
|
||||
<filter string="Listing Type" name="group_listing_type"
|
||||
context="{'group_by': 'x_fc_listing_type'}"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- ACTIONS -->
|
||||
<!-- ===================================================================== -->
|
||||
|
||||
<record id="action_fusion_loaner_checkout" model="ir.actions.act_window">
|
||||
<field name="name">Loaner Equipment</field>
|
||||
<field name="res_model">fusion.loaner.checkout</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_fusion_loaner_checkout_search"/>
|
||||
<field name="context">{'search_default_filter_active': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">No loaner checkouts yet</p>
|
||||
<p>Track loaner equipment issued to clients during assessments or trials.</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fusion_loaner_all" model="ir.actions.act_window">
|
||||
<field name="name">All Loaners</field>
|
||||
<field name="res_model">fusion.loaner.checkout</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_fusion_loaner_checkout_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">No loaner checkouts yet</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fusion_loaner_overdue" model="ir.actions.act_window">
|
||||
<field name="name">Overdue Loaners</field>
|
||||
<field name="res_model">fusion.loaner.checkout</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_fusion_loaner_checkout_search"/>
|
||||
<field name="context">{'search_default_filter_attention': 1}</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fusion_loaner_returned" model="ir.actions.act_window">
|
||||
<field name="name">Returned Loaners</field>
|
||||
<field name="res_model">fusion.loaner.checkout</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_fusion_loaner_checkout_search"/>
|
||||
<field name="context">{'search_default_filter_returned': 1}</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fusion_loaner_history" model="ir.actions.act_window">
|
||||
<field name="name">Loaner History</field>
|
||||
<field name="res_model">fusion.loaner.history</field>
|
||||
<field name="view_mode">list</field>
|
||||
<field name="search_view_id" ref="view_fusion_loaner_history_search"/>
|
||||
</record>
|
||||
|
||||
<!-- Action: Loaner Products (products that can be loaned) -->
|
||||
<record id="action_fusion_loaner_products" model="ir.actions.act_window">
|
||||
<field name="name">Loaner Products</field>
|
||||
<field name="res_model">product.template</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="view_id" ref="view_fusion_loaner_products_list"/>
|
||||
<field name="search_view_id" ref="view_fusion_loaner_products_search"/>
|
||||
<field name="domain">[('x_fc_can_be_loaned', '=', True)]</field>
|
||||
<field name="context">{'default_x_fc_can_be_loaned': True, 'default_sale_ok': False, 'default_purchase_ok': False, 'default_rent_ok': True}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No loaner products configured yet
|
||||
</p>
|
||||
<p>
|
||||
Mark products as "Loaner" in the product form to add them here.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- MENUS -->
|
||||
<!-- ===================================================================== -->
|
||||
|
||||
<menuitem id="menu_loaner_root"
|
||||
name="Loaners"
|
||||
parent="menu_adp_claims_root"
|
||||
sequence="58"/>
|
||||
|
||||
<menuitem id="menu_loaner_active"
|
||||
name="Active Loaners"
|
||||
parent="menu_loaner_root"
|
||||
action="action_fusion_loaner_checkout"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem id="menu_loaner_all"
|
||||
name="All Loaners"
|
||||
parent="menu_loaner_root"
|
||||
action="action_fusion_loaner_all"
|
||||
sequence="15"/>
|
||||
|
||||
<menuitem id="menu_loaner_overdue"
|
||||
name="Overdue / Attention"
|
||||
parent="menu_loaner_root"
|
||||
action="action_fusion_loaner_overdue"
|
||||
sequence="18"/>
|
||||
|
||||
<menuitem id="menu_loaner_returned"
|
||||
name="Returned"
|
||||
parent="menu_loaner_root"
|
||||
action="action_fusion_loaner_returned"
|
||||
sequence="19"/>
|
||||
|
||||
<menuitem id="menu_loaner_history"
|
||||
name="Loaner History"
|
||||
parent="menu_loaner_root"
|
||||
action="action_fusion_loaner_history"
|
||||
sequence="20"/>
|
||||
|
||||
<menuitem id="menu_loaner_products"
|
||||
name="Loaner Products"
|
||||
parent="menu_loaner_root"
|
||||
action="action_fusion_loaner_products"
|
||||
sequence="30"/>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- PRODUCT TEMPLATE LOANER FIELDS -->
|
||||
<!-- ===================================================================== -->
|
||||
|
||||
<!-- Add "Can be Loaned" checkbox next to other product type checkboxes -->
|
||||
<record id="view_product_template_loaner_checkbox" model="ir.ui.view">
|
||||
<field name="name">product.template.loaner.checkbox</field>
|
||||
<field name="model">product.template</field>
|
||||
<field name="inherit_id" ref="product.product_template_form_view"/>
|
||||
<field name="priority">50</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='options']" position="inside">
|
||||
<span class="d-inline-flex">
|
||||
<field name="x_fc_can_be_loaned"/>
|
||||
<label for="x_fc_can_be_loaned" string="Loaner"/>
|
||||
</span>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Loaner Settings tab (only visible when Can be Loaned is checked) -->
|
||||
<record id="view_product_template_loaner_form" model="ir.ui.view">
|
||||
<field name="name">product.template.loaner.form</field>
|
||||
<field name="model">product.template</field>
|
||||
<field name="inherit_id" ref="product.product_template_form_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//page[@name='sales']" position="after">
|
||||
<page string="Loaner Settings" name="loaner_settings" invisible="not x_fc_can_be_loaned">
|
||||
<group>
|
||||
<group string="Loaner Period">
|
||||
<field name="x_fc_loaner_period_days"/>
|
||||
</group>
|
||||
<group string="Rental Pricing (if not returned)">
|
||||
<field name="x_fc_rental_price_weekly"/>
|
||||
<field name="x_fc_rental_price_monthly"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group string="Equipment Details">
|
||||
<field name="x_fc_equipment_type"/>
|
||||
<field name="x_fc_wheelchair_category"/>
|
||||
<field name="x_fc_listing_type"/>
|
||||
<field name="x_fc_asset_number"/>
|
||||
</group>
|
||||
<group string="Dimensions">
|
||||
<field name="x_fc_seat_width"/>
|
||||
<field name="x_fc_seat_depth"/>
|
||||
<field name="x_fc_seat_height"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group string="Location">
|
||||
<field name="x_fc_storage_location"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Package Information">
|
||||
<field name="x_fc_package_info" nolabel="1" colspan="2"/>
|
||||
</group>
|
||||
<group string="Security Deposit">
|
||||
<group>
|
||||
<field name="x_fc_security_deposit_type"/>
|
||||
<field name="x_fc_security_deposit_amount"
|
||||
invisible="x_fc_security_deposit_type != 'fixed'"/>
|
||||
<field name="x_fc_security_deposit_percent"
|
||||
invisible="x_fc_security_deposit_type != 'percentage'"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
@@ -1074,13 +1074,6 @@
|
||||
invisible="not x_fc_is_mod_sale or x_fc_mod_client_invoice_count == 0">
|
||||
<field name="x_fc_mod_client_invoice_count" widget="statinfo" string="Client Invoice"/>
|
||||
</button>
|
||||
|
||||
<!-- Loaner Equipment Button -->
|
||||
<button name="action_view_loaners" type="object"
|
||||
class="oe_stat_button" icon="fa-wheelchair"
|
||||
invisible="x_fc_loaner_count == 0">
|
||||
<field name="x_fc_loaner_count" widget="statinfo" string="Loaners"/>
|
||||
</button>
|
||||
|
||||
<!-- Technician Tasks Button -->
|
||||
<button name="action_view_technician_tasks" type="object"
|
||||
@@ -1186,7 +1179,6 @@
|
||||
<field name="x_fc_device_verification_complete" invisible="1"/>
|
||||
<field name="x_fc_submission_verified" invisible="1"/>
|
||||
<field name="x_fc_adp_application_status" invisible="1"/>
|
||||
<field name="x_fc_active_loaner_count" invisible="1"/>
|
||||
<field name="x_fc_early_delivery" invisible="1"/>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
@@ -1340,36 +1332,6 @@
|
||||
|
||||
</xpath>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- LOANER BUTTONS - Positioned AFTER all standard Odoo buttons -->
|
||||
<!-- (after Cancel, before the statusbar widget) -->
|
||||
<!-- ============================================================ -->
|
||||
<xpath expr="//header/field[@name='state']" position="before">
|
||||
<field name="x_fc_is_adp_sale" invisible="1"/>
|
||||
<field name="x_fc_active_loaner_count" invisible="1"/>
|
||||
<field name="x_fc_has_overdue_loaner" invisible="1"/>
|
||||
|
||||
<!-- Checkout Loaner: only when NO active loaners -->
|
||||
<button name="action_checkout_loaner" type="object"
|
||||
string="Checkout Loaner" class="btn-secondary"
|
||||
icon="fa-wheelchair"
|
||||
invisible="not x_fc_is_adp_sale or x_fc_active_loaner_count > 0 or x_fc_adp_application_status not in ('assessment_scheduled', 'assessment_completed', 'waiting_for_application', 'application_received', 'ready_submission', 'submitted', 'resubmitted', 'accepted', 'approved', 'approved_deduction', 'ready_delivery')"
|
||||
help="Checkout loaner equipment for the client"/>
|
||||
|
||||
<!-- Checkin Loaner: GREEN when within return period -->
|
||||
<button name="action_checkin_loaner" type="object"
|
||||
string="Checkin Loaner" class="fc-btn-status-good"
|
||||
icon="fa-clock-o"
|
||||
invisible="not x_fc_is_adp_sale or x_fc_active_loaner_count == 0 or x_fc_has_overdue_loaner"
|
||||
help="Loaner within return period - click to check in"/>
|
||||
|
||||
<!-- Checkin Loaner: RED when past return period -->
|
||||
<button name="action_checkin_loaner" type="object"
|
||||
string="Checkin Loaner" class="fc-btn-status-bad"
|
||||
icon="fa-exclamation-circle"
|
||||
invisible="not x_fc_is_adp_sale or x_fc_active_loaner_count == 0 or not x_fc_has_overdue_loaner"
|
||||
help="Loaner OVERDUE - click to check in"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -17,8 +17,6 @@ from . import application_received_wizard
|
||||
from . import ready_for_submission_wizard
|
||||
from . import ready_to_bill_wizard
|
||||
from . import field_mapping_config_wizard
|
||||
from . import loaner_checkout_wizard
|
||||
from . import loaner_return_wizard
|
||||
from . import ready_for_delivery_wizard
|
||||
from . import xml_import_wizard
|
||||
from . import send_to_mod_wizard
|
||||
@@ -30,4 +28,5 @@ from . import odsp_discretionary_wizard
|
||||
from . import odsp_pre_approved_wizard
|
||||
from . import odsp_ready_delivery_wizard
|
||||
from . import odsp_submit_to_odsp_wizard
|
||||
from . import send_page11_wizard
|
||||
from . import send_page11_wizard
|
||||
from . import adp_import_wizard
|
||||
@@ -429,7 +429,7 @@ class FusionCentralExportWizard(models.TransientModel):
|
||||
"""Save export file to the ADP Export Records model (filestore-backed)."""
|
||||
try:
|
||||
ExportRecord = self.env['fusion_claims.adp.export.record']
|
||||
posting_date = ExportRecord._get_current_posting_date(self.export_date)
|
||||
posting_date = ExportRecord._get_posting_period_for_file(self.export_date)
|
||||
|
||||
ExportRecord.create({
|
||||
'name': filename,
|
||||
|
||||
163
fusion_claims/wizard/adp_import_wizard.py
Normal file
163
fusion_claims/wizard/adp_import_wizard.py
Normal file
@@ -0,0 +1,163 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2025 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Claim Assistant product family.
|
||||
|
||||
import base64
|
||||
import io
|
||||
import logging
|
||||
import zipfile
|
||||
from datetime import date
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ADPImportWizard(models.TransientModel):
|
||||
_name = 'fusion_claims.adp.import.wizard'
|
||||
_description = 'Import ADP Export Files'
|
||||
|
||||
txt_files = fields.Many2many(
|
||||
'ir.attachment',
|
||||
string='TXT Files',
|
||||
help='Select one or more ADP export .txt files to import',
|
||||
)
|
||||
zip_file = fields.Binary(string='ZIP File')
|
||||
zip_filename = fields.Char()
|
||||
result_message = fields.Text(string='Import Results', readonly=True)
|
||||
state = fields.Selection([
|
||||
('draft', 'Upload'),
|
||||
('done', 'Done'),
|
||||
], default='draft')
|
||||
|
||||
def action_import(self):
|
||||
"""Process uploaded files and create export records."""
|
||||
self.ensure_one()
|
||||
if not self.txt_files and not self.zip_file:
|
||||
raise UserError(_('Please upload .txt files or a ZIP file.'))
|
||||
|
||||
ExportRecord = self.env['fusion_claims.adp.export.record']
|
||||
imported = 0
|
||||
skipped = 0
|
||||
errors = []
|
||||
|
||||
file_list, skipped_non_txt = self._collect_files()
|
||||
|
||||
for filename, file_data_b64 in file_list:
|
||||
try:
|
||||
existing = ExportRecord.search([('name', '=', filename)], limit=1)
|
||||
if existing:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
vendor_code, file_date = ExportRecord._parse_export_filename(filename)
|
||||
posting_date = ExportRecord._get_posting_period_for_file(
|
||||
file_date if file_date else date.today()
|
||||
)
|
||||
|
||||
ExportRecord.create({
|
||||
'name': filename,
|
||||
'filename': filename,
|
||||
'file_data': file_data_b64,
|
||||
'export_date': fields.Datetime.now(),
|
||||
'posting_period_date': posting_date,
|
||||
'vendor_code': vendor_code or '',
|
||||
'user_id': self.env.uid,
|
||||
'company_id': self.env.company.id,
|
||||
'notes': 'Imported on %s' % date.today(),
|
||||
})
|
||||
imported += 1
|
||||
|
||||
except Exception as e:
|
||||
errors.append('%s: %s' % (filename, str(e)))
|
||||
_logger.exception('Error importing file %s', filename)
|
||||
|
||||
lines = [
|
||||
'Import Complete!',
|
||||
'- Files imported: %d' % imported,
|
||||
'- Files skipped (already exist): %d' % skipped,
|
||||
'- Non-txt files ignored: %d' % skipped_non_txt,
|
||||
]
|
||||
if errors:
|
||||
lines.append('\nErrors (%d):' % len(errors))
|
||||
for err in errors:
|
||||
lines.append(' - %s' % err)
|
||||
|
||||
self.result_message = '\n'.join(lines)
|
||||
self.state = 'done'
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': self._name,
|
||||
'view_mode': 'form',
|
||||
'res_id': self.id,
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
def _collect_files(self):
|
||||
"""Collect all .txt files from both individual uploads and ZIP.
|
||||
|
||||
Returns (file_list, skipped_count) where file_list is
|
||||
a list of (filename, base64_data) tuples.
|
||||
"""
|
||||
files = []
|
||||
skipped_types = []
|
||||
|
||||
for attachment in self.txt_files:
|
||||
name = attachment.name or ''
|
||||
if not name.lower().endswith('.txt'):
|
||||
skipped_types.append(name)
|
||||
continue
|
||||
files.append((name, attachment.datas))
|
||||
|
||||
if skipped_types and not files and not self.zip_file:
|
||||
raise UserError(_(
|
||||
'Only .txt files are supported. Skipped: %s',
|
||||
', '.join(skipped_types),
|
||||
))
|
||||
|
||||
if self.zip_file:
|
||||
files.extend(self._extract_txt_from_zip())
|
||||
|
||||
return files, len(skipped_types)
|
||||
|
||||
def _extract_txt_from_zip(self):
|
||||
"""Scan all folders/subfolders in the ZIP and extract .txt files.
|
||||
|
||||
Returns list of (filename, base64_data) tuples.
|
||||
Uses only the base filename (no folder path) as the record name.
|
||||
"""
|
||||
raw = base64.b64decode(self.zip_file)
|
||||
buf = io.BytesIO(raw)
|
||||
|
||||
if not zipfile.is_zipfile(buf):
|
||||
raise UserError(_('The uploaded file is not a valid ZIP archive.'))
|
||||
|
||||
buf.seek(0)
|
||||
results = []
|
||||
seen = set()
|
||||
|
||||
with zipfile.ZipFile(buf, 'r') as zf:
|
||||
for entry in zf.infolist():
|
||||
if entry.is_dir():
|
||||
continue
|
||||
lower = entry.filename.lower()
|
||||
if not lower.endswith('.txt'):
|
||||
continue
|
||||
if lower.startswith('__macosx'):
|
||||
continue
|
||||
|
||||
basename = entry.filename.rsplit('/', 1)[-1]
|
||||
if not basename or basename in seen:
|
||||
continue
|
||||
seen.add(basename)
|
||||
|
||||
data = zf.read(entry.filename)
|
||||
results.append((basename, base64.b64encode(data).decode('ascii')))
|
||||
|
||||
if not results:
|
||||
raise UserError(_('No .txt files found in the ZIP archive.'))
|
||||
|
||||
return results
|
||||
45
fusion_claims/wizard/adp_import_wizard_views.xml
Normal file
45
fusion_claims/wizard/adp_import_wizard_views.xml
Normal file
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_adp_import_wizard_form" model="ir.ui.view">
|
||||
<field name="name">fusion_claims.adp.import.wizard.form</field>
|
||||
<field name="model">fusion_claims.adp.import.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Import Claim Submission Files">
|
||||
<group invisible="state != 'draft'">
|
||||
<group string="Individual Files">
|
||||
<field name="txt_files" widget="many2many_binary" string="TXT Files"/>
|
||||
</group>
|
||||
<group string="Folder Upload (ZIP)">
|
||||
<field name="zip_file" filename="zip_filename"/>
|
||||
<field name="zip_filename" invisible="1"/>
|
||||
</group>
|
||||
<div class="alert alert-info" role="alert">
|
||||
<strong>Import Claim Submission Files</strong>
|
||||
<p class="mb-0">
|
||||
<b>Option 1:</b> Drag and drop individual .txt files above.<br/>
|
||||
<b>Option 2:</b> ZIP your folder(s) and upload. All subfolders
|
||||
will be scanned for .txt files automatically.<br/>
|
||||
You can use both options at once. Duplicates are skipped.
|
||||
</p>
|
||||
</div>
|
||||
</group>
|
||||
<group invisible="state != 'done'">
|
||||
<field name="result_message" widget="text" nolabel="1" colspan="2"/>
|
||||
</group>
|
||||
<field name="state" invisible="1"/>
|
||||
<footer>
|
||||
<button string="Import" name="action_import" type="object"
|
||||
class="btn-primary" invisible="state != 'draft'"/>
|
||||
<button string="Close" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_adp_import_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Import Submission Files</field>
|
||||
<field name="res_model">fusion_claims.adp.import.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -1,237 +0,0 @@
|
||||
# -*- 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 logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LoanerCheckoutWizard(models.TransientModel):
|
||||
"""Wizard to checkout loaner equipment."""
|
||||
_name = 'fusion.loaner.checkout.wizard'
|
||||
_description = 'Loaner Checkout Wizard'
|
||||
|
||||
# =========================================================================
|
||||
# CONTEXT FIELDS
|
||||
# =========================================================================
|
||||
|
||||
sale_order_id = fields.Many2one(
|
||||
'sale.order',
|
||||
string='Sale Order',
|
||||
readonly=True,
|
||||
)
|
||||
partner_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Client',
|
||||
required=True,
|
||||
)
|
||||
authorizer_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Authorizer',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# PRODUCT SELECTION
|
||||
# =========================================================================
|
||||
|
||||
product_id = fields.Many2one(
|
||||
'product.product',
|
||||
string='Product',
|
||||
domain="[('x_fc_can_be_loaned', '=', True)]",
|
||||
required=True,
|
||||
)
|
||||
lot_id = fields.Many2one(
|
||||
'stock.lot',
|
||||
string='Serial Number',
|
||||
domain="[('product_id', '=', product_id)]",
|
||||
)
|
||||
available_lot_ids = fields.Many2many(
|
||||
'stock.lot',
|
||||
compute='_compute_available_lots',
|
||||
string='Available Serial Numbers',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# DATES
|
||||
# =========================================================================
|
||||
|
||||
checkout_date = fields.Date(
|
||||
string='Checkout Date',
|
||||
required=True,
|
||||
default=fields.Date.context_today,
|
||||
)
|
||||
loaner_period_days = fields.Integer(
|
||||
string='Loaner Period (Days)',
|
||||
default=7,
|
||||
)
|
||||
expected_return_date = fields.Date(
|
||||
string='Expected Return Date',
|
||||
compute='_compute_expected_return',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# CONDITION
|
||||
# =========================================================================
|
||||
|
||||
checkout_condition = fields.Selection([
|
||||
('excellent', 'Excellent'),
|
||||
('good', 'Good'),
|
||||
('fair', 'Fair'),
|
||||
('needs_repair', 'Needs Repair'),
|
||||
], string='Condition', default='excellent', required=True)
|
||||
checkout_notes = fields.Text(
|
||||
string='Notes',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# PHOTOS
|
||||
# =========================================================================
|
||||
|
||||
checkout_photo_ids = fields.Many2many(
|
||||
'ir.attachment',
|
||||
'loaner_checkout_wizard_photo_rel',
|
||||
'wizard_id',
|
||||
'attachment_id',
|
||||
string='Photos',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# DELIVERY
|
||||
# =========================================================================
|
||||
|
||||
delivery_address = fields.Text(
|
||||
string='Delivery Address',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# COMPUTED
|
||||
# =========================================================================
|
||||
|
||||
@api.depends('product_id')
|
||||
def _compute_available_lots(self):
|
||||
"""Get available serial numbers for the selected product."""
|
||||
for wizard in self:
|
||||
if wizard.product_id:
|
||||
# Get loaner location
|
||||
loaner_location = self.env.ref('fusion_claims.stock_location_loaner', raise_if_not_found=False)
|
||||
if loaner_location:
|
||||
# Find lots with stock in loaner location
|
||||
quants = self.env['stock.quant'].search([
|
||||
('product_id', '=', wizard.product_id.id),
|
||||
('location_id', '=', loaner_location.id),
|
||||
('quantity', '>', 0),
|
||||
])
|
||||
wizard.available_lot_ids = quants.mapped('lot_id')
|
||||
else:
|
||||
# Fallback: all lots for product
|
||||
wizard.available_lot_ids = self.env['stock.lot'].search([
|
||||
('product_id', '=', wizard.product_id.id),
|
||||
])
|
||||
else:
|
||||
wizard.available_lot_ids = False
|
||||
|
||||
@api.depends('checkout_date', 'loaner_period_days')
|
||||
def _compute_expected_return(self):
|
||||
from datetime import timedelta
|
||||
for wizard in self:
|
||||
if wizard.checkout_date and wizard.loaner_period_days:
|
||||
wizard.expected_return_date = wizard.checkout_date + timedelta(days=wizard.loaner_period_days)
|
||||
else:
|
||||
wizard.expected_return_date = False
|
||||
|
||||
# =========================================================================
|
||||
# ONCHANGE
|
||||
# =========================================================================
|
||||
|
||||
@api.onchange('product_id')
|
||||
def _onchange_product_id(self):
|
||||
if self.product_id:
|
||||
self.loaner_period_days = self.product_id.x_fc_loaner_period_days or 7
|
||||
self.lot_id = False
|
||||
|
||||
# =========================================================================
|
||||
# DEFAULT GET
|
||||
# =========================================================================
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
res = super().default_get(fields_list)
|
||||
|
||||
# Get context
|
||||
active_model = self._context.get('active_model')
|
||||
active_id = self._context.get('active_id')
|
||||
|
||||
if active_model == 'sale.order' and active_id:
|
||||
order = self.env['sale.order'].browse(active_id)
|
||||
res['sale_order_id'] = order.id
|
||||
res['partner_id'] = order.partner_id.id
|
||||
res['authorizer_id'] = order.x_fc_authorizer_id.id if order.x_fc_authorizer_id else False
|
||||
if order.partner_shipping_id:
|
||||
res['delivery_address'] = order.partner_shipping_id.contact_address
|
||||
|
||||
# Get default loaner period from settings
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
default_period = int(ICP.get_param('fusion_claims.default_loaner_period_days', '7'))
|
||||
res['loaner_period_days'] = default_period
|
||||
|
||||
return res
|
||||
|
||||
# =========================================================================
|
||||
# ACTION
|
||||
# =========================================================================
|
||||
|
||||
def action_checkout(self):
|
||||
"""Create and confirm loaner checkout."""
|
||||
self.ensure_one()
|
||||
|
||||
if not self.product_id:
|
||||
raise UserError(_("Please select a product."))
|
||||
|
||||
# Create persistent attachments for photos
|
||||
photo_ids = []
|
||||
for photo in self.checkout_photo_ids:
|
||||
new_attachment = self.env['ir.attachment'].create({
|
||||
'name': photo.name,
|
||||
'datas': photo.datas,
|
||||
'res_model': 'fusion.loaner.checkout',
|
||||
'res_id': 0, # Will update after checkout creation
|
||||
})
|
||||
photo_ids.append(new_attachment.id)
|
||||
|
||||
# Create checkout record
|
||||
checkout_vals = {
|
||||
'sale_order_id': self.sale_order_id.id if self.sale_order_id else False,
|
||||
'partner_id': self.partner_id.id,
|
||||
'authorizer_id': self.authorizer_id.id if self.authorizer_id else False,
|
||||
'sales_rep_id': self.env.user.id,
|
||||
'product_id': self.product_id.id,
|
||||
'lot_id': self.lot_id.id if self.lot_id else False,
|
||||
'checkout_date': self.checkout_date,
|
||||
'loaner_period_days': self.loaner_period_days,
|
||||
'checkout_condition': self.checkout_condition,
|
||||
'checkout_notes': self.checkout_notes,
|
||||
'delivery_address': self.delivery_address,
|
||||
}
|
||||
|
||||
checkout = self.env['fusion.loaner.checkout'].create(checkout_vals)
|
||||
|
||||
# Update photo attachments
|
||||
if photo_ids:
|
||||
self.env['ir.attachment'].browse(photo_ids).write({'res_id': checkout.id})
|
||||
checkout.checkout_photo_ids = [(6, 0, photo_ids)]
|
||||
|
||||
# Confirm checkout
|
||||
checkout.action_checkout()
|
||||
|
||||
# Return to checkout record
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Loaner Checkout'),
|
||||
'res_model': 'fusion.loaner.checkout',
|
||||
'res_id': checkout.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'current',
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
# -*- 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 logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LoanerReturnWizard(models.TransientModel):
|
||||
"""Wizard to return loaner equipment."""
|
||||
_name = 'fusion.loaner.return.wizard'
|
||||
_description = 'Loaner Return Wizard'
|
||||
|
||||
# =========================================================================
|
||||
# CHECKOUT REFERENCE
|
||||
# =========================================================================
|
||||
|
||||
checkout_id = fields.Many2one(
|
||||
'fusion.loaner.checkout',
|
||||
string='Checkout Record',
|
||||
required=True,
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
# Display fields
|
||||
product_id = fields.Many2one(
|
||||
'product.product',
|
||||
string='Product',
|
||||
related='checkout_id.product_id',
|
||||
readonly=True,
|
||||
)
|
||||
lot_id = fields.Many2one(
|
||||
'stock.lot',
|
||||
string='Serial Number',
|
||||
related='checkout_id.lot_id',
|
||||
readonly=True,
|
||||
)
|
||||
partner_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Client',
|
||||
related='checkout_id.partner_id',
|
||||
readonly=True,
|
||||
)
|
||||
checkout_date = fields.Date(
|
||||
string='Checkout Date',
|
||||
related='checkout_id.checkout_date',
|
||||
readonly=True,
|
||||
)
|
||||
days_out = fields.Integer(
|
||||
string='Days Out',
|
||||
related='checkout_id.days_out',
|
||||
readonly=True,
|
||||
)
|
||||
checkout_condition = fields.Selection(
|
||||
related='checkout_id.checkout_condition',
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# RETURN DETAILS
|
||||
# =========================================================================
|
||||
|
||||
return_date = fields.Date(
|
||||
string='Return Date',
|
||||
required=True,
|
||||
default=fields.Date.context_today,
|
||||
)
|
||||
return_condition = fields.Selection([
|
||||
('excellent', 'Excellent'),
|
||||
('good', 'Good'),
|
||||
('fair', 'Fair'),
|
||||
('needs_repair', 'Needs Repair'),
|
||||
('damaged', 'Damaged'),
|
||||
], string='Condition', required=True)
|
||||
return_notes = fields.Text(
|
||||
string='Notes',
|
||||
help='Any notes about the return condition or issues',
|
||||
)
|
||||
return_photo_ids = fields.Many2many(
|
||||
'ir.attachment',
|
||||
'loaner_return_wizard_photo_rel',
|
||||
'wizard_id',
|
||||
'attachment_id',
|
||||
string='Photos',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# DEFAULT GET
|
||||
# =========================================================================
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
res = super().default_get(fields_list)
|
||||
|
||||
checkout_id = self._context.get('default_checkout_id')
|
||||
if checkout_id:
|
||||
checkout = self.env['fusion.loaner.checkout'].browse(checkout_id)
|
||||
res['checkout_id'] = checkout.id
|
||||
# Default return condition to checkout condition
|
||||
res['return_condition'] = checkout.checkout_condition
|
||||
|
||||
return res
|
||||
|
||||
# =========================================================================
|
||||
# ACTION
|
||||
# =========================================================================
|
||||
|
||||
def action_return(self):
|
||||
"""Process the loaner return."""
|
||||
self.ensure_one()
|
||||
|
||||
if not self.checkout_id:
|
||||
raise UserError(_("No checkout record found."))
|
||||
|
||||
if self.checkout_id.state not in ('checked_out', 'overdue', 'rental_pending'):
|
||||
raise UserError(_("This loaner has already been returned or is not in a returnable state."))
|
||||
|
||||
# Create persistent attachments for photos
|
||||
photo_ids = []
|
||||
for photo in self.return_photo_ids:
|
||||
new_attachment = self.env['ir.attachment'].create({
|
||||
'name': photo.name,
|
||||
'datas': photo.datas,
|
||||
'res_model': 'fusion.loaner.checkout',
|
||||
'res_id': self.checkout_id.id,
|
||||
})
|
||||
photo_ids.append(new_attachment.id)
|
||||
|
||||
# Process return
|
||||
self.checkout_id.action_process_return(
|
||||
return_condition=self.return_condition,
|
||||
return_notes=self.return_notes,
|
||||
return_photos=photo_ids if photo_ids else None,
|
||||
)
|
||||
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
Reference in New Issue
Block a user