changes
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Claims',
|
||||
'version': '19.0.6.0.0',
|
||||
'version': '19.0.7.0.0',
|
||||
'category': 'Sales',
|
||||
'summary': 'Complete ADP Claims Management with Dashboard, Sales Integration, Billing Automation, and Two-Stage Verification.',
|
||||
'description': """
|
||||
|
||||
@@ -61,6 +61,61 @@ class ProductTemplate(models.Model):
|
||||
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')
|
||||
|
||||
# ==========================================================================
|
||||
# COMPUTED FIELDS
|
||||
# ==========================================================================
|
||||
@@ -107,3 +162,25 @@ class ProductTemplate(models.Model):
|
||||
|
||||
return self.default_code or ''
|
||||
|
||||
# ==========================================================================
|
||||
# SECURITY DEPOSIT (added by fusion_rental)
|
||||
# ==========================================================================
|
||||
|
||||
x_fc_security_deposit_type = fields.Selection(
|
||||
[
|
||||
('fixed', 'Fixed Amount'),
|
||||
('percentage', 'Percentage of Rental Price'),
|
||||
],
|
||||
string='Security Deposit Type',
|
||||
help='How the security deposit is calculated for this rental product.',
|
||||
)
|
||||
x_fc_security_deposit_amount = fields.Float(
|
||||
string='Security Deposit Amount',
|
||||
digits='Product Price',
|
||||
help='Fixed dollar amount for the security deposit.',
|
||||
)
|
||||
x_fc_security_deposit_percent = fields.Float(
|
||||
string='Security Deposit (%)',
|
||||
help='Percentage of the rental line price to charge as deposit.',
|
||||
)
|
||||
|
||||
|
||||
@@ -388,6 +388,30 @@ class FusionTechnicianTask(models.Model):
|
||||
string='Notified At',
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# RENTAL INSPECTION (added by fusion_rental)
|
||||
# ------------------------------------------------------------------
|
||||
rental_inspection_condition = fields.Selection([
|
||||
('excellent', 'Excellent'),
|
||||
('good', 'Good'),
|
||||
('fair', 'Fair'),
|
||||
('damaged', 'Damaged'),
|
||||
], string='Inspection Condition')
|
||||
rental_inspection_notes = fields.Text(
|
||||
string='Inspection Notes',
|
||||
)
|
||||
rental_inspection_photo_ids = fields.Many2many(
|
||||
'ir.attachment',
|
||||
'technician_task_inspection_photo_rel',
|
||||
'task_id',
|
||||
'attachment_id',
|
||||
string='Inspection Photos',
|
||||
)
|
||||
rental_inspection_completed = fields.Boolean(
|
||||
string='Inspection Completed',
|
||||
default=False,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# COMPUTED FIELDS
|
||||
# ------------------------------------------------------------------
|
||||
@@ -1608,22 +1632,37 @@ class FusionTechnicianTask(models.Model):
|
||||
'res_id': self.purchase_order_id.id,
|
||||
}
|
||||
|
||||
def _is_rental_pickup_task(self):
|
||||
"""Check if this is a pickup task for a rental order."""
|
||||
self.ensure_one()
|
||||
return (
|
||||
self.task_type == 'pickup'
|
||||
and self.sale_order_id
|
||||
and self.sale_order_id.is_rental_order
|
||||
)
|
||||
|
||||
def action_complete_task(self):
|
||||
"""Mark task as Completed."""
|
||||
for task in self:
|
||||
if task.status not in ('in_progress', 'en_route', 'scheduled'):
|
||||
raise UserError(_("Task must be in progress to complete."))
|
||||
|
||||
if task._is_rental_pickup_task() and not task.rental_inspection_completed:
|
||||
raise UserError(_(
|
||||
"Rental pickup tasks require a security inspection before "
|
||||
"completion. Please complete the inspection from the "
|
||||
"technician portal first."
|
||||
))
|
||||
|
||||
task.with_context(skip_travel_recalc=True).write({
|
||||
'status': 'completed',
|
||||
'completion_datetime': fields.Datetime.now(),
|
||||
})
|
||||
task._post_status_message('completed')
|
||||
# Post completion notes to linked order chatter
|
||||
if task.completion_notes and (task.sale_order_id or task.purchase_order_id):
|
||||
task._post_completion_to_linked_order()
|
||||
# Notify the person who scheduled the task
|
||||
task._notify_scheduler_on_completion()
|
||||
# Auto-advance ODSP status for delivery tasks
|
||||
|
||||
if (task.task_type == 'delivery'
|
||||
and task.sale_order_id
|
||||
and task.sale_order_id.x_fc_is_odsp_sale
|
||||
@@ -1633,6 +1672,44 @@ class FusionTechnicianTask(models.Model):
|
||||
"Delivery task completed by technician. Order marked as delivered.",
|
||||
)
|
||||
|
||||
if task._is_rental_pickup_task():
|
||||
task._apply_rental_inspection_results()
|
||||
|
||||
def _apply_rental_inspection_results(self):
|
||||
"""Write inspection results from the task back to the rental order."""
|
||||
self.ensure_one()
|
||||
order = self.sale_order_id
|
||||
if not order or not order.is_rental_order:
|
||||
return
|
||||
|
||||
inspection_status = 'passed'
|
||||
if self.rental_inspection_condition in ('fair', 'damaged'):
|
||||
inspection_status = 'flagged'
|
||||
|
||||
vals = {
|
||||
'rental_inspection_status': inspection_status,
|
||||
'rental_inspection_notes': self.rental_inspection_notes or '',
|
||||
}
|
||||
if self.rental_inspection_photo_ids:
|
||||
vals['rental_inspection_photo_ids'] = [(6, 0, self.rental_inspection_photo_ids.ids)]
|
||||
order.write(vals)
|
||||
|
||||
if inspection_status == 'passed':
|
||||
order._refund_security_deposit()
|
||||
elif inspection_status == 'flagged':
|
||||
order.activity_schedule(
|
||||
'mail.mail_activity_data_todo',
|
||||
date_deadline=fields.Date.today(),
|
||||
summary=_("Review rental inspection: %s", order.name),
|
||||
note=_(
|
||||
"Technician flagged rental pickup for %s. "
|
||||
"Condition: %s. Please review inspection photos and notes.",
|
||||
order.partner_id.name,
|
||||
self.rental_inspection_condition or 'Unknown',
|
||||
),
|
||||
user_id=order.user_id.id or self.env.uid,
|
||||
)
|
||||
|
||||
def action_cancel_task(self):
|
||||
"""Cancel the task. Sends cancellation email and reverts sale order if delivery."""
|
||||
for task in self:
|
||||
|
||||
@@ -270,13 +270,18 @@
|
||||
<tr>
|
||||
<td style="width: 20%; padding: 5px 4px; border: none;"><strong>Card #:</strong></td>
|
||||
<td style="padding: 5px 4px; border: none;">
|
||||
<span class="cc-box"></span><span class="cc-box"></span><span class="cc-box"></span><span class="cc-box"></span>
|
||||
<span style="margin: 0 3px;">-</span>
|
||||
<span class="cc-box"></span><span class="cc-box"></span><span class="cc-box"></span><span class="cc-box"></span>
|
||||
<span style="margin: 0 3px;">-</span>
|
||||
<span class="cc-box"></span><span class="cc-box"></span><span class="cc-box"></span><span class="cc-box"></span>
|
||||
<span style="margin: 0 3px;">-</span>
|
||||
<span class="cc-box"></span><span class="cc-box"></span><span class="cc-box"></span><span class="cc-box"></span>
|
||||
<t t-if="doc.rental_payment_token_id">
|
||||
<span style="font-size: 14px;">**** **** **** <t t-out="doc._get_card_last_four() or '****'">1234</t></span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="cc-box"></span><span class="cc-box"></span><span class="cc-box"></span><span class="cc-box"></span>
|
||||
<span style="margin: 0 3px;">-</span>
|
||||
<span class="cc-box"></span><span class="cc-box"></span><span class="cc-box"></span><span class="cc-box"></span>
|
||||
<span style="margin: 0 3px;">-</span>
|
||||
<span class="cc-box"></span><span class="cc-box"></span><span class="cc-box"></span><span class="cc-box"></span>
|
||||
<span style="margin: 0 3px;">-</span>
|
||||
<span class="cc-box"></span><span class="cc-box"></span><span class="cc-box"></span><span class="cc-box"></span>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -286,14 +291,25 @@
|
||||
<span style="margin: 0 2px;">/</span>
|
||||
<span class="cc-box"></span><span class="cc-box"></span>
|
||||
<span style="margin-left: 20px;"><strong>CVV:</strong></span>
|
||||
<span class="cc-box"></span><span class="cc-box"></span><span class="cc-box"></span>
|
||||
<span style="margin-left: 20px;"><strong>Security Deposit:</strong> $___________</span>
|
||||
<span>***</span>
|
||||
<t t-set="deposit_lines" t-value="doc.order_line.filtered(lambda l: l.is_security_deposit)"/>
|
||||
<span style="margin-left: 20px;"><strong>Security Deposit:</strong>
|
||||
<t t-if="deposit_lines">
|
||||
$<t t-out="'%.2f' % sum(deposit_lines.mapped('price_unit'))">0.00</t>
|
||||
</t>
|
||||
<t t-else="">$___________</t>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 5px 4px; border: none;"><strong>Cardholder:</strong></td>
|
||||
<td style="padding: 5px 4px; border: none;">
|
||||
<div style="border-bottom: 1px solid #000; min-height: 18px; width: 100%;"></div>
|
||||
<t t-if="doc.rental_agreement_signer_name">
|
||||
<span t-out="doc.rental_agreement_signer_name">Name</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div style="border-bottom: 1px solid #000; min-height: 18px; width: 100%;"></div>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -316,15 +332,24 @@
|
||||
<tr>
|
||||
<td style="width: 40%; padding: 5px; border: none;">
|
||||
<div class="signature-label">FULL NAME (PRINT)</div>
|
||||
<div class="signature-line"></div>
|
||||
<t t-if="doc.rental_agreement_signer_name">
|
||||
<div style="min-height: 18px; font-size: 14px;" t-out="doc.rental_agreement_signer_name">Name</div>
|
||||
</t>
|
||||
<t t-else=""><div class="signature-line"></div></t>
|
||||
</td>
|
||||
<td style="width: 40%; padding: 5px; border: none;">
|
||||
<div class="signature-label">SIGNATURE</div>
|
||||
<div class="signature-line"></div>
|
||||
<t t-if="doc.rental_agreement_signature">
|
||||
<img t-att-src="'data:image/png;base64,' + doc.rental_agreement_signature.decode('utf-8') if doc.rental_agreement_signature else ''" style="max-height: 50px; max-width: 100%;"/>
|
||||
</t>
|
||||
<t t-else=""><div class="signature-line"></div></t>
|
||||
</td>
|
||||
<td style="width: 20%; padding: 5px; border: none;">
|
||||
<div class="signature-label">DATE</div>
|
||||
<div class="signature-line"></div>
|
||||
<t t-if="doc.rental_agreement_signed_date">
|
||||
<div style="min-height: 18px; font-size: 14px;" t-out="doc.rental_agreement_signed_date.strftime('%m/%d/%Y')">Date</div>
|
||||
</t>
|
||||
<t t-else=""><div class="signature-line"></div></t>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
191
fusion_claims/scripts/cleanup_demo_pool.py
Normal file
191
fusion_claims/scripts/cleanup_demo_pool.py
Normal file
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Demo Pool Cleanup Script
|
||||
|
||||
Removes the Studio-created x_demo_pool_tracking model and all related
|
||||
database artifacts (tables, ir.model entries, fields, views, menus, etc.).
|
||||
|
||||
WARNING: Only run this AFTER verifying the import_demo_pool.py migration
|
||||
was successful and all data is correct in Loaner Products.
|
||||
|
||||
Run via Odoo shell:
|
||||
docker exec -i odoo-mobility-app odoo shell -d mobility < cleanup_demo_pool.py
|
||||
|
||||
Copyright 2024-2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
DEMO_POOL_MODELS = [
|
||||
'x_demo_pool_tracking',
|
||||
'x_demo_pool_tracking_line_4a032',
|
||||
'x_demo_pool_tracking_line_629cc',
|
||||
'x_demo_pool_tracking_line_b4ec9',
|
||||
'x_demo_pool_tracking_stage',
|
||||
'x_demo_pool_tracking_tag',
|
||||
]
|
||||
|
||||
DEMO_POOL_TABLES = DEMO_POOL_MODELS + [
|
||||
'x_demo_pool_tracking_tag_rel',
|
||||
]
|
||||
|
||||
PRODUCT_TEMPLATE_STUDIO_FIELDS_TO_REMOVE = [
|
||||
'x_studio_many2one_field_V8rnk',
|
||||
]
|
||||
|
||||
|
||||
def run_cleanup(env):
|
||||
cr = env.cr
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print("Demo Pool Cleanup")
|
||||
print(f"{'='*60}")
|
||||
print()
|
||||
|
||||
# =====================================================================
|
||||
# Step 1: Remove ir.ui.menu entries referencing demo pool actions
|
||||
# =====================================================================
|
||||
print("[1/7] Removing menu items...")
|
||||
cr.execute("""
|
||||
DELETE FROM ir_ui_menu
|
||||
WHERE action IN (
|
||||
SELECT CONCAT('ir.actions.act_window,', id)
|
||||
FROM ir_act_window
|
||||
WHERE res_model IN %s
|
||||
)
|
||||
""", (tuple(DEMO_POOL_MODELS),))
|
||||
print(f" Removed {cr.rowcount} menu items")
|
||||
|
||||
# =====================================================================
|
||||
# Step 2: Remove ir.actions.act_window entries
|
||||
# =====================================================================
|
||||
print("[2/7] Removing window actions...")
|
||||
cr.execute("""
|
||||
DELETE FROM ir_act_window WHERE res_model IN %s
|
||||
""", (tuple(DEMO_POOL_MODELS),))
|
||||
print(f" Removed {cr.rowcount} window actions")
|
||||
|
||||
# =====================================================================
|
||||
# Step 3: Remove ir.ui.view entries
|
||||
# =====================================================================
|
||||
print("[3/7] Removing views...")
|
||||
cr.execute("""
|
||||
DELETE FROM ir_ui_view WHERE model IN %s
|
||||
""", (tuple(DEMO_POOL_MODELS),))
|
||||
print(f" Removed {cr.rowcount} views")
|
||||
|
||||
# =====================================================================
|
||||
# Step 4: Remove ir.model.access entries
|
||||
# =====================================================================
|
||||
print("[4/7] Removing access rules...")
|
||||
cr.execute("""
|
||||
SELECT id FROM ir_model WHERE model IN %s
|
||||
""", (tuple(DEMO_POOL_MODELS),))
|
||||
model_ids = [row[0] for row in cr.fetchall()]
|
||||
|
||||
if model_ids:
|
||||
cr.execute("""
|
||||
DELETE FROM ir_model_access WHERE model_id IN %s
|
||||
""", (tuple(model_ids),))
|
||||
print(f" Removed {cr.rowcount} access rules")
|
||||
else:
|
||||
print(" No model IDs found (already cleaned?)")
|
||||
|
||||
# =====================================================================
|
||||
# Step 5: Remove ir.model.fields and ir.model entries
|
||||
# =====================================================================
|
||||
print("[5/7] Removing field definitions and model registry entries...")
|
||||
|
||||
if model_ids:
|
||||
cr.execute("""
|
||||
DELETE FROM ir_model_fields_selection
|
||||
WHERE field_id IN (
|
||||
SELECT id FROM ir_model_fields WHERE model_id IN %s
|
||||
)
|
||||
""", (tuple(model_ids),))
|
||||
print(f" Removed {cr.rowcount} field selection values")
|
||||
|
||||
cr.execute("""
|
||||
DELETE FROM ir_model_fields WHERE model_id IN %s
|
||||
""", (tuple(model_ids),))
|
||||
print(f" Removed {cr.rowcount} field definitions")
|
||||
|
||||
cr.execute("""
|
||||
DELETE FROM ir_model_constraint WHERE model IN %s
|
||||
""", (tuple(model_ids),))
|
||||
print(f" Removed {cr.rowcount} constraints")
|
||||
|
||||
cr.execute("""
|
||||
DELETE FROM ir_model_relation WHERE model IN %s
|
||||
""", (tuple(model_ids),))
|
||||
print(f" Removed {cr.rowcount} relations")
|
||||
|
||||
cr.execute("""
|
||||
DELETE FROM ir_model WHERE id IN %s
|
||||
""", (tuple(model_ids),))
|
||||
print(f" Removed {cr.rowcount} model registry entries")
|
||||
|
||||
# =====================================================================
|
||||
# Step 6: Remove Studio field on product.template that linked to demo pool
|
||||
# =====================================================================
|
||||
print("[6/7] Removing demo pool Studio fields from product.template...")
|
||||
for field_name in PRODUCT_TEMPLATE_STUDIO_FIELDS_TO_REMOVE:
|
||||
cr.execute("""
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'product_template' AND column_name = %s
|
||||
)
|
||||
""", (field_name,))
|
||||
exists = cr.fetchone()[0]
|
||||
if exists:
|
||||
cr.execute("""
|
||||
DELETE FROM ir_model_fields_selection
|
||||
WHERE field_id IN (
|
||||
SELECT id FROM ir_model_fields
|
||||
WHERE model = 'product.template' AND name = %s
|
||||
)
|
||||
""", (field_name,))
|
||||
|
||||
cr.execute("""
|
||||
DELETE FROM ir_model_fields
|
||||
WHERE model = 'product.template' AND name = %s
|
||||
""", (field_name,))
|
||||
|
||||
cr.execute(f'ALTER TABLE product_template DROP COLUMN IF EXISTS "{field_name}"')
|
||||
print(f" Removed {field_name} from product.template")
|
||||
else:
|
||||
print(f" {field_name} not found on product.template (already removed)")
|
||||
|
||||
# =====================================================================
|
||||
# Step 7: Drop the actual database tables
|
||||
# =====================================================================
|
||||
print("[7/7] Dropping demo pool tables...")
|
||||
for table in DEMO_POOL_TABLES:
|
||||
cr.execute(f"DROP TABLE IF EXISTS \"{table}\" CASCADE")
|
||||
print(f" Dropped {table}")
|
||||
|
||||
cr.commit()
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print("Cleanup Complete")
|
||||
print(f"{'='*60}")
|
||||
print()
|
||||
print("The following have been removed:")
|
||||
print(" - x_demo_pool_tracking model and all line/stage/tag tables")
|
||||
print(" - All related views, menus, actions, and access rules")
|
||||
print(" - x_studio_many2one_field_V8rnk from product.template")
|
||||
print()
|
||||
print("NOT touched:")
|
||||
print(" - x_studio_adp_price, x_studio_adp_sku on product.template")
|
||||
print(" - x_studio_msrp, x_studio_product_description_long on product.template")
|
||||
print(" - x_studio_product_details on product.template")
|
||||
print()
|
||||
print("Restart the Odoo server to clear the model registry cache.")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
|
||||
run_cleanup(env)
|
||||
277
fusion_claims/scripts/import_demo_pool.py
Normal file
277
fusion_claims/scripts/import_demo_pool.py
Normal file
@@ -0,0 +1,277 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Demo Pool to Loaner Products Migration Script
|
||||
|
||||
Reads all records from the Studio-created x_demo_pool_tracking table
|
||||
and creates proper product.template records with x_fc_can_be_loaned=True,
|
||||
plus stock.lot serial numbers where applicable.
|
||||
|
||||
Run via Odoo shell:
|
||||
docker exec -i odoo-mobility-app odoo shell -d mobility < import_demo_pool.py
|
||||
|
||||
Or from the host with the script mounted:
|
||||
docker exec -i odoo-mobility-app odoo shell -d mobility \
|
||||
< /mnt/extra-addons/fusion_claims/scripts/import_demo_pool.py
|
||||
|
||||
Copyright 2024-2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
EQUIPMENT_TYPE_MAP = {
|
||||
'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',
|
||||
'Wheelchair': 'standard_wheelchair',
|
||||
'Standard Wheelchair': 'standard_wheelchair',
|
||||
'Power Wheelchair': 'power_wheelchair',
|
||||
'Cushion': 'cushion',
|
||||
'Backrest': 'backrest',
|
||||
'Stairlift': 'stairlift',
|
||||
'Others': 'others',
|
||||
}
|
||||
|
||||
WHEELCHAIR_CATEGORY_MAP = {
|
||||
'Type 1': 'type_1',
|
||||
'Type 2': 'type_2',
|
||||
'Type 3': 'type_3',
|
||||
'Type 4': 'type_4',
|
||||
'Type 5': 'type_5',
|
||||
}
|
||||
|
||||
LOCATION_MAP = {
|
||||
'Warehouse': 'warehouse',
|
||||
'Westin Brampton': 'westin_brampton',
|
||||
'Mobility Etobicoke': 'mobility_etobicoke',
|
||||
'Scarborough Storage': 'scarborough_storage',
|
||||
'Client/Loaned': 'client_loaned',
|
||||
'Rented Out': 'rented_out',
|
||||
}
|
||||
|
||||
LISTING_TYPE_MAP = {
|
||||
'Owned': 'owned',
|
||||
'Borrowed': 'borrowed',
|
||||
}
|
||||
|
||||
SKIP_SERIALS = {'na', 'n/a', 'update', 'updated', ''}
|
||||
|
||||
|
||||
def extract_name(json_val):
|
||||
"""Extract English name from Odoo JSONB field."""
|
||||
if not json_val:
|
||||
return ''
|
||||
if isinstance(json_val, dict):
|
||||
return json_val.get('en_US', '') or ''
|
||||
if isinstance(json_val, str):
|
||||
try:
|
||||
parsed = json.loads(json_val)
|
||||
if isinstance(parsed, dict):
|
||||
return parsed.get('en_US', '') or ''
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return json_val
|
||||
return str(json_val)
|
||||
|
||||
|
||||
def get_category_id(env, equipment_type_key):
|
||||
"""Map equipment type to an appropriate product category."""
|
||||
loaner_cat = env.ref('fusion_claims.product_category_loaner', raise_if_not_found=False)
|
||||
wheelchair_cat = env.ref('fusion_claims.product_category_loaner_wheelchair', raise_if_not_found=False)
|
||||
powerchair_cat = env.ref('fusion_claims.product_category_loaner_powerchair', raise_if_not_found=False)
|
||||
rollator_cat = env.ref('fusion_claims.product_category_loaner_rollator', raise_if_not_found=False)
|
||||
|
||||
if not loaner_cat:
|
||||
return env['product.category'].search([], limit=1).id
|
||||
|
||||
wheelchair_types = {
|
||||
'type_2_mw', 'type_3_mw', 'type_4_mw', 'type_5_mw',
|
||||
'type_2_walker', 'type_3_walker', 'type_1_walker',
|
||||
'standard_wheelchair', 'transport_wheelchair',
|
||||
}
|
||||
powerchair_types = {'type_2_pw', 'type_3_pw', 'power_wheelchair'}
|
||||
rollator_types = set()
|
||||
|
||||
if equipment_type_key in powerchair_types and powerchair_cat:
|
||||
return powerchair_cat.id
|
||||
if equipment_type_key in wheelchair_types and wheelchair_cat:
|
||||
return wheelchair_cat.id
|
||||
if equipment_type_key in rollator_types and rollator_cat:
|
||||
return rollator_cat.id
|
||||
return loaner_cat.id
|
||||
|
||||
|
||||
def fetch_accessories(cr, demo_pool_id):
|
||||
"""Fetch accessory lines from x_demo_pool_tracking_line_b4ec9."""
|
||||
cr.execute("""
|
||||
SELECT x_name FROM x_demo_pool_tracking_line_b4ec9
|
||||
WHERE x_demo_pool_tracking_id = %s
|
||||
ORDER BY x_studio_sequence, id
|
||||
""", (demo_pool_id,))
|
||||
rows = cr.fetchall()
|
||||
accessories = []
|
||||
for row in rows:
|
||||
name = extract_name(row[0])
|
||||
if name:
|
||||
accessories.append(name)
|
||||
return accessories
|
||||
|
||||
|
||||
def run_import(env):
|
||||
cr = env.cr
|
||||
ProductTemplate = env['product.template']
|
||||
StockLot = env['stock.lot']
|
||||
|
||||
company = env.company
|
||||
|
||||
cr.execute("""
|
||||
SELECT
|
||||
id, x_name, x_studio_equipment_type, x_studio_wheelchair_categorytype,
|
||||
x_studio_serial_number, x_studio_seat_width, x_studio_seat_depth,
|
||||
x_studio_seat_height, x_studio_where_is_it_located, x_studio_listing_type,
|
||||
x_studio_asset_, x_studio_package_information, x_active,
|
||||
x_studio_value, x_studio_notes
|
||||
FROM x_demo_pool_tracking
|
||||
ORDER BY id
|
||||
""")
|
||||
rows = cr.fetchall()
|
||||
columns = [
|
||||
'id', 'x_name', 'equipment_type', 'wheelchair_category',
|
||||
'serial_number', 'seat_width', 'seat_depth',
|
||||
'seat_height', 'location', 'listing_type',
|
||||
'asset_number', 'package_info', 'active',
|
||||
'value', 'notes',
|
||||
]
|
||||
|
||||
created_count = 0
|
||||
serial_count = 0
|
||||
skipped_serials = 0
|
||||
errors = []
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print("Demo Pool to Loaner Products Migration")
|
||||
print(f"{'='*60}")
|
||||
print(f"Records found: {len(rows)}")
|
||||
print()
|
||||
|
||||
for row in rows:
|
||||
record = dict(zip(columns, row))
|
||||
demo_id = record['id']
|
||||
|
||||
try:
|
||||
name = extract_name(record['x_name'])
|
||||
if not name:
|
||||
errors.append(f"ID {demo_id}: empty name, skipped")
|
||||
continue
|
||||
|
||||
equipment_type_raw = record['equipment_type'] or ''
|
||||
equipment_type_key = EQUIPMENT_TYPE_MAP.get(equipment_type_raw, '')
|
||||
|
||||
wheelchair_cat_raw = record['wheelchair_category'] or ''
|
||||
wheelchair_cat_key = WHEELCHAIR_CATEGORY_MAP.get(wheelchair_cat_raw, '')
|
||||
|
||||
location_raw = record['location'] or ''
|
||||
location_key = LOCATION_MAP.get(location_raw, '')
|
||||
|
||||
listing_raw = record['listing_type'] or ''
|
||||
listing_key = LISTING_TYPE_MAP.get(listing_raw, '')
|
||||
|
||||
seat_width = (record['seat_width'] or '').strip()
|
||||
seat_depth = (record['seat_depth'] or '').strip()
|
||||
seat_height = (record['seat_height'] or '').strip()
|
||||
asset_number = (record['asset_number'] or '').strip()
|
||||
package_info = (record['package_info'] or '').strip()
|
||||
|
||||
accessories = fetch_accessories(cr, demo_id)
|
||||
if accessories:
|
||||
acc_text = '\n'.join(f'- {a}' for a in accessories)
|
||||
if package_info:
|
||||
package_info = f"{package_info}\n\nAccessories:\n{acc_text}"
|
||||
else:
|
||||
package_info = f"Accessories:\n{acc_text}"
|
||||
|
||||
is_active = bool(record['active'])
|
||||
categ_id = get_category_id(env, equipment_type_key)
|
||||
|
||||
product_vals = {
|
||||
'name': name,
|
||||
'type': 'consu',
|
||||
'tracking': 'serial',
|
||||
'sale_ok': False,
|
||||
'purchase_ok': False,
|
||||
'x_fc_can_be_loaned': True,
|
||||
'x_fc_loaner_period_days': 7,
|
||||
'x_fc_equipment_type': equipment_type_key or False,
|
||||
'x_fc_wheelchair_category': wheelchair_cat_key or False,
|
||||
'x_fc_seat_width': seat_width or False,
|
||||
'x_fc_seat_depth': seat_depth or False,
|
||||
'x_fc_seat_height': seat_height or False,
|
||||
'x_fc_storage_location': location_key or False,
|
||||
'x_fc_listing_type': listing_key or False,
|
||||
'x_fc_asset_number': asset_number or False,
|
||||
'x_fc_package_info': package_info or False,
|
||||
'categ_id': categ_id,
|
||||
'active': is_active,
|
||||
}
|
||||
|
||||
product = ProductTemplate.with_context(active_test=False).create(product_vals)
|
||||
created_count += 1
|
||||
|
||||
serial_raw = (record['serial_number'] or '').strip()
|
||||
if serial_raw.lower() not in SKIP_SERIALS:
|
||||
try:
|
||||
product_product = product.product_variant_id
|
||||
if product_product:
|
||||
lot = StockLot.create({
|
||||
'name': serial_raw,
|
||||
'product_id': product_product.id,
|
||||
'company_id': company.id,
|
||||
})
|
||||
serial_count += 1
|
||||
except Exception as e:
|
||||
skipped_serials += 1
|
||||
errors.append(f"ID {demo_id} ({name}): serial '{serial_raw}' failed: {e}")
|
||||
else:
|
||||
skipped_serials += 1
|
||||
|
||||
status = "ACTIVE" if is_active else "ARCHIVED"
|
||||
print(f" [{status}] {name} (demo #{demo_id}) -> product #{product.id}"
|
||||
f"{f' serial={serial_raw}' if serial_raw.lower() not in SKIP_SERIALS else ''}")
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"ID {demo_id}: {e}")
|
||||
print(f" ERROR: ID {demo_id}: {e}")
|
||||
|
||||
env.cr.commit()
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print("Migration Summary")
|
||||
print(f"{'='*60}")
|
||||
print(f"Products created: {created_count}")
|
||||
print(f"Serials created: {serial_count}")
|
||||
print(f"Serials skipped: {skipped_serials}")
|
||||
print(f"Errors: {len(errors)}")
|
||||
|
||||
if errors:
|
||||
print(f"\nErrors:")
|
||||
for err in errors:
|
||||
print(f" - {err}")
|
||||
|
||||
print(f"\nDone. Verify in Fusion Claims > Loaner Management > Loaner Products")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
|
||||
run_import(env)
|
||||
@@ -154,14 +154,12 @@
|
||||
<button class="btn btn-sm d-flex align-items-center gap-1"
|
||||
t-att-class="state.showTasks ? 'btn-primary' : 'btn-outline-secondary'"
|
||||
t-on-click="toggleTasks">
|
||||
<i class="fa fa-map-marker"/>Tasks
|
||||
<span class="badge text-bg-secondary ms-1" t-esc="state.taskCount"/>
|
||||
<i class="fa fa-map-marker"/>Tasks <t t-esc="state.taskCount"/>
|
||||
</button>
|
||||
<button class="btn btn-sm d-flex align-items-center gap-1"
|
||||
t-att-class="state.showTechnicians ? 'btn-primary' : 'btn-outline-secondary'"
|
||||
t-on-click="toggleTechnicians">
|
||||
<i class="fa fa-user"/>Techs
|
||||
<span class="badge text-bg-secondary ms-1" t-esc="state.techCount"/>
|
||||
<i class="fa fa-user"/>Techs <t t-esc="state.techCount"/>
|
||||
</button>
|
||||
<span class="border-start mx-1" style="height:20px;"/>
|
||||
<span class="text-muted fw-bold" style="font-size:11px;">Pins:</span>
|
||||
|
||||
@@ -282,6 +282,60 @@
|
||||
</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 -->
|
||||
<!-- ===================================================================== -->
|
||||
@@ -336,6 +390,8 @@
|
||||
<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">
|
||||
@@ -430,9 +486,40 @@
|
||||
<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>
|
||||
|
||||
@@ -232,6 +232,22 @@
|
||||
<field name="voice_note_transcription"/>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Rental Inspection" name="rental_inspection"
|
||||
invisible="task_type != 'pickup'">
|
||||
<group>
|
||||
<group string="Condition">
|
||||
<field name="rental_inspection_condition"/>
|
||||
<field name="rental_inspection_completed"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Inspection Notes">
|
||||
<field name="rental_inspection_notes" nolabel="1"/>
|
||||
</group>
|
||||
<group string="Inspection Photos">
|
||||
<field name="rental_inspection_photo_ids"
|
||||
widget="many2many_binary" nolabel="1"/>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
|
||||
Reference in New Issue
Block a user