This commit is contained in:
gsinghpal
2026-02-25 09:40:41 -05:00
parent 0e1aebe60b
commit e71bc503f9
69 changed files with 7537 additions and 82 deletions

View File

@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Cancellation Request List View -->
<record id="rental_cancellation_request_view_list" model="ir.ui.view">
<field name="name">rental.cancellation.request.list</field>
<field name="model">rental.cancellation.request</field>
<field name="arch" type="xml">
<list string="Cancellation Requests">
<field name="order_id"/>
<field name="partner_id"/>
<field name="request_date"/>
<field name="reason"/>
<field name="assigned_user_id"/>
<field name="state"
decoration-info="state == 'new'"
decoration-success="state in ('confirmed', 'completed')"
decoration-warning="state == 'pickup_scheduled'"
decoration-danger="state == 'rejected'"
widget="badge"/>
</list>
</field>
</record>
<!-- Cancellation Request Form View -->
<record id="rental_cancellation_request_view_form" model="ir.ui.view">
<field name="name">rental.cancellation.request.form</field>
<field name="model">rental.cancellation.request</field>
<field name="arch" type="xml">
<form string="Cancellation Request">
<header>
<button name="action_confirm"
type="object"
string="Confirm Cancellation"
class="btn-primary"
invisible="state != 'new'"/>
<button name="action_schedule_pickup"
type="object"
string="Schedule Pickup"
class="btn-primary"
invisible="state != 'confirmed'"/>
<button name="action_complete"
type="object"
string="Mark Completed"
class="btn-success"
invisible="state not in ('confirmed', 'pickup_scheduled')"/>
<button name="action_reject"
type="object"
string="Reject"
class="btn-danger"
invisible="state not in ('new', 'confirmed')"
confirm="Are you sure you want to reject this cancellation request?"/>
<field name="state" widget="statusbar"
statusbar_visible="new,confirmed,pickup_scheduled,completed"/>
</header>
<sheet>
<div class="oe_title">
<h1>
<field name="order_id" readonly="1"/>
</h1>
</div>
<group>
<group string="Request Details">
<field name="partner_id"/>
<field name="request_date"/>
<field name="requested_pickup_date"/>
</group>
<group string="Assignment">
<field name="assigned_user_id"/>
<field name="pickup_activity_id" readonly="1"/>
</group>
</group>
<group string="Reason">
<field name="reason" nolabel="1"/>
</group>
</sheet>
<chatter/>
</form>
</field>
</record>
<!-- Cancellation Request Action -->
<record id="action_rental_cancellation_request" model="ir.actions.act_window">
<field name="name">Cancellation Requests</field>
<field name="res_model">rental.cancellation.request</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!--
Override the core rental product action to add a hard domain filter.
The core action relies on search_default_filter_to_rent which can be
removed by the user. Adding a domain ensures only rental products
are ever shown when accessed from the Rental app.
-->
<record id="sale_renting.rental_product_template_action" model="ir.actions.act_window">
<field name="domain">[('rent_ok', '=', True)]</field>
</record>
<!-- Top-level menu under Rental app -->
<menuitem id="menu_rental_enhancement_root"
name="Rental Enhancement"
parent="sale_renting.rental_menu_root"
sequence="30"/>
<!-- Renewal History submenu -->
<menuitem id="menu_rental_renewal_log"
name="Renewal History"
parent="menu_rental_enhancement_root"
action="action_rental_renewal_log"
sequence="10"/>
<!-- Cancellation Requests submenu -->
<menuitem id="menu_rental_cancellation_request"
name="Cancellation Requests"
parent="menu_rental_enhancement_root"
action="action_rental_cancellation_request"
sequence="20"/>
</odoo>

View File

@@ -0,0 +1,146 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="portal_rental_inspection" model="ir.ui.view">
<field name="name">Rental Pickup Inspection</field>
<field name="type">qweb</field>
<field name="key">fusion_rental.portal_rental_inspection</field>
<field name="arch" type="xml">
<t t-call="web.frontend_layout">
<div class="container py-4">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h3 class="mb-0">Rental Pickup Inspection</h3>
</div>
<div class="card-body">
<div class="mb-3">
<strong>Order:</strong> <t t-out="order.name or ''">SO001</t><br/>
<strong>Customer:</strong> <t t-out="order.partner_id.name or ''">Customer</t><br/>
<strong>Task:</strong> <t t-out="task.name or ''">Task</t>
</div>
<h5>Equipment Condition</h5>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="condition" id="cond_excellent" value="excellent"/>
<label class="form-check-label" for="cond_excellent">Excellent - No issues</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="condition" id="cond_good" value="good"/>
<label class="form-check-label" for="cond_good">Good - Minor wear</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="condition" id="cond_fair" value="fair"/>
<label class="form-check-label" for="cond_fair">Fair - Some issues noted</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="condition" id="cond_damaged" value="damaged"/>
<label class="form-check-label" for="cond_damaged">Damaged - Requires review</label>
</div>
</div>
<div class="mb-3">
<label for="inspection_notes" class="form-label">Notes / Damage Description</label>
<textarea id="inspection_notes" class="form-control" rows="4"
placeholder="Describe the condition, any damage, missing parts..."/>
</div>
<div class="mb-3">
<label class="form-label">Photos</label>
<input type="file" id="inspection_photos" class="form-control"
accept="image/*" multiple="multiple"/>
<div class="text-muted small mt-1">Upload photos of the equipment condition.</div>
</div>
<div id="inspection_error" class="alert alert-danger d-none"></div>
<div id="inspection_success" class="alert alert-success d-none"></div>
<button type="button" id="btn_submit_inspection" class="btn btn-primary btn-lg w-100"
t-att-data-task-id="task.id">
Submit Inspection
</button>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('btn_submit_inspection').addEventListener('click', async function() {
var btn = this;
btn.disabled = true;
btn.textContent = 'Submitting...';
var errDiv = document.getElementById('inspection_error');
var successDiv = document.getElementById('inspection_success');
errDiv.classList.add('d-none');
successDiv.classList.add('d-none');
var condition = document.querySelector('input[name="condition"]:checked');
if (!condition) {
errDiv.textContent = 'Please select an equipment condition.';
errDiv.classList.remove('d-none');
btn.disabled = false;
btn.textContent = 'Submit Inspection';
return;
}
var photoIds = [];
var files = document.getElementById('inspection_photos').files;
for (var i = 0; i &lt; files.length; i++) {
var reader = new FileReader();
var data = await new Promise(function(resolve) {
reader.onload = function(e) { resolve(e.target.result.split(',')[1]); };
reader.readAsDataURL(files[i]);
});
var resp = await fetch('/web/dataset/call_kw', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
jsonrpc: '2.0', method: 'call', id: i + 1,
params: {
model: 'ir.attachment', method: 'create',
args: [{'name': files[i].name, 'type': 'binary', 'datas': data}],
kwargs: {},
}
})
});
var result = await resp.json();
if (result.result) photoIds.push(result.result);
}
var taskId = btn.dataset.taskId;
fetch('/my/technician/rental-inspection/' + taskId + '/submit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
jsonrpc: '2.0', method: 'call', id: 99,
params: {
task_id: parseInt(taskId),
condition: condition.value,
notes: document.getElementById('inspection_notes').value,
photo_ids: photoIds,
}
})
}).then(function(r) { return r.json(); }).then(function(data) {
var res = data.result || data;
if (res.success) {
successDiv.textContent = res.message || 'Inspection saved!';
successDiv.classList.remove('d-none');
btn.textContent = 'Submitted!';
} else {
errDiv.textContent = res.error || 'An error occurred.';
errDiv.classList.remove('d-none');
btn.disabled = false;
btn.textContent = 'Submit Inspection';
}
});
});
});
</script>
</t>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!--
Add Security Deposit fields to the Rental prices tab.
These fields are defined on product.template in fusion_claims but
only shown inside the Loaner Settings tab (invisible unless
x_fc_can_be_loaned). This view makes them accessible for ALL
rental products via the standard Rental prices page.
-->
<record id="product_template_form_rental_deposit" model="ir.ui.view">
<field name="name">product.template.form.rental.deposit</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="sale_renting.product_template_form_view_rental"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='pricing']//group[@name='extra_rental']" position="after">
<group string="Security Deposit" name="security_deposit">
<group>
<field name="x_fc_security_deposit_type"/>
<field name="x_fc_security_deposit_amount"
widget="monetary"
invisible="x_fc_security_deposit_type != 'fixed'"/>
<field name="x_fc_security_deposit_percent"
invisible="x_fc_security_deposit_type != 'percentage'"/>
</group>
<group>
<div class="text-muted" style="padding-top:8px;">
<p class="mb-1"><strong>Fixed:</strong> A flat dollar amount charged as deposit.</p>
<p class="mb-0"><strong>Percentage:</strong> Calculated from the rental line price.</p>
</div>
</group>
</group>
</xpath>
</field>
</record>
<!--
Show security deposit badge on sale order lines so users can
identify which line is the auto-generated deposit.
-->
<record id="sale_order_line_deposit_badge" model="ir.ui.view">
<field name="name">sale.order.line.deposit.badge</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale_renting.rental_order_form_view"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='order_line']//list//field[@name='qty_returned']" position="before">
<field name="is_security_deposit" column_invisible="not parent.is_rental_order"
widget="boolean_toggle" readonly="1" optional="show"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Renewal Log List View -->
<record id="rental_renewal_log_view_list" model="ir.ui.view">
<field name="name">rental.renewal.log.list</field>
<field name="model">rental.renewal.log</field>
<field name="arch" type="xml">
<list string="Renewal History" create="false">
<field name="order_id"/>
<field name="partner_id"/>
<field name="renewal_number"/>
<field name="renewal_type"/>
<field name="previous_start_date"/>
<field name="previous_return_date"/>
<field name="new_start_date"/>
<field name="new_return_date"/>
<field name="invoice_id"/>
<field name="payment_status"
decoration-success="payment_status == 'paid'"
decoration-danger="payment_status == 'failed'"
decoration-warning="payment_status == 'pending'"
widget="badge"/>
<field name="state"
decoration-success="state == 'done'"
decoration-danger="state == 'failed'"
decoration-info="state == 'draft'"
widget="badge"/>
</list>
</field>
</record>
<!-- Renewal Log Form View -->
<record id="rental_renewal_log_view_form" model="ir.ui.view">
<field name="name">rental.renewal.log.form</field>
<field name="model">rental.renewal.log</field>
<field name="arch" type="xml">
<form string="Renewal Log" create="false">
<sheet>
<div class="oe_title">
<h1>
<field name="order_id" readonly="1"/>
<span> - Renewal #</span>
<field name="renewal_number" readonly="1" class="oe_inline"/>
</h1>
</div>
<group>
<group string="Renewal Details">
<field name="renewal_type"/>
<field name="state"/>
<field name="partner_id"/>
</group>
<group string="Payment">
<field name="invoice_id"/>
<field name="payment_status"/>
<field name="payment_transaction_id"/>
</group>
</group>
<group>
<group string="Previous Period">
<field name="previous_start_date"/>
<field name="previous_return_date"/>
</group>
<group string="New Period">
<field name="new_start_date"/>
<field name="new_return_date"/>
</group>
</group>
<group string="Notes">
<field name="notes" nolabel="1"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- Renewal Log Action -->
<record id="action_rental_renewal_log" model="ir.actions.act_window">
<field name="name">Renewal History</field>
<field name="res_model">rental.renewal.log</field>
<field name="view_mode">list,form</field>
<field name="context">{'create': False}</field>
</record>
</odoo>

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_settings_view_form_inherit_fusion_rental" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.fusion.rental</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="sale_renting.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//app[@name='sale_renting']" position="inside">
<block title="Rental Enhancement" name="rental_enhancement_settings">
<setting string="Google Review URL"
help="Google Review link shown in thank-you emails after rental close.">
<div class="content-group">
<div class="mt-2">
<field name="rental_google_review_url"
placeholder="https://g.page/r/YOUR_REVIEW_LINK/review"/>
</div>
<div class="text-muted mt-1">
For multiple locations, set the URL per warehouse in
Inventory &gt; Configuration &gt; Warehouses.
</div>
</div>
</setting>
<setting string="Security Deposit Hold Period"
help="How many days to hold the security deposit after product pickup before processing the refund.">
<div class="content-group">
<div class="row mt-2">
<label class="col-lg-3 o_light_label" for="rental_deposit_hold_days"/>
<field name="rental_deposit_hold_days" class="col-lg-2"/>
<span class="col-lg-4 text-muted">days after pickup</span>
</div>
</div>
</setting>
</block>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,202 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="sale_order_form_inherit_fusion_rental" model="ir.ui.view">
<field name="name">sale.order.form.inherit.fusion.rental</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale_renting.rental_order_form_view"/>
<field name="arch" type="xml">
<!-- Header buttons -->
<button name="action_open_pickup" position="before">
<button name="action_send_rental_agreement"
type="object"
class="btn-secondary"
string="Send Agreement"
data-hotkey="a"
invisible="not is_rental_order or state != 'sale' or rental_agreement_signed"
icon="fa-file-text-o"/>
<button name="action_manual_renewal"
type="object"
class="btn-secondary"
string="Renew Rental"
data-hotkey="r"
invisible="not is_rental_order or state != 'sale' or rental_status not in ('pickup', 'return')"
icon="fa-refresh"/>
<!-- Deposit buttons -->
<button name="action_create_deposit_invoice"
type="object"
class="btn-secondary"
string="Create Deposit Invoice"
invisible="not is_rental_order or state != 'sale' or rental_deposit_invoice_id"
icon="fa-file-text"/>
<button name="action_mark_deposit_collected"
type="object"
class="btn-secondary"
string="Mark Deposit Collected"
invisible="not is_rental_order or rental_deposit_status != 'pending'"
icon="fa-check-circle"/>
<button name="action_refund_deposit"
type="object"
class="btn-secondary"
string="Refund Deposit"
invisible="not is_rental_order or rental_deposit_status != 'collected'"
confirm="This will initiate the deposit refund hold period. Continue?"
icon="fa-undo"/>
<button name="action_force_refund_deposit"
type="object"
class="btn-secondary"
string="Process Refund Now"
invisible="not is_rental_order or rental_deposit_status != 'refund_hold'"
confirm="Skip the hold period and process the refund immediately?"
icon="fa-bolt"/>
<button name="action_deduct_deposit"
type="object"
class="btn-danger"
string="Deduct Deposit"
invisible="not is_rental_order or rental_deposit_status != 'collected'"
icon="fa-minus-circle"/>
<button name="action_close_rental"
type="object"
class="btn-warning"
string="Close Rental"
data-hotkey="x"
invisible="not is_rental_order or state != 'sale' or rental_closed or rental_deposit_status not in ('refunded', 'deducted', False)"
confirm="This will delete the stored card and send a thank-you email. Continue?"
icon="fa-power-off"/>
</button>
<!-- Rental fields -->
<field name="duration_days" position="after">
<!-- Renewal settings -->
<field name="rental_auto_renew" invisible="not is_rental_order"/>
<field name="rental_renewal_count" invisible="not is_rental_order or rental_renewal_count == 0"/>
<field name="rental_max_renewals" invisible="not is_rental_order or not rental_auto_renew"/>
<field name="rental_payment_token_id" invisible="not is_rental_order"/>
<field name="rental_next_renewal_date" invisible="not is_rental_order or not rental_auto_renew"/>
<field name="rental_reminder_sent" invisible="1"/>
<field name="rental_original_duration" invisible="1"/>
<field name="rental_agreement_token" invisible="1"/>
<!-- Agreement status -->
<field name="rental_agreement_signed" invisible="not is_rental_order"
widget="boolean_toggle" readonly="1"/>
<field name="rental_agreement_signer_name"
invisible="not is_rental_order or not rental_agreement_signed" readonly="1"/>
<field name="rental_agreement_signed_date"
invisible="not is_rental_order or not rental_agreement_signed" readonly="1"/>
<!-- Rental charges invoice -->
<field name="rental_charges_invoice_id"
invisible="not is_rental_order or not rental_charges_invoice_id" readonly="1"/>
<!-- Deposit status -->
<field name="rental_deposit_status" invisible="not is_rental_order or not rental_deposit_status"
decoration-success="rental_deposit_status == 'refunded'"
decoration-warning="rental_deposit_status in ('pending', 'refund_hold', 'collected')"
decoration-danger="rental_deposit_status == 'deducted'"
widget="badge"/>
<field name="rental_deposit_invoice_id"
invisible="not is_rental_order or not rental_deposit_invoice_id" readonly="1"/>
<!-- Inspection status -->
<field name="rental_inspection_status"
invisible="not is_rental_order or not rental_inspection_status"
decoration-success="rental_inspection_status == 'passed'"
decoration-danger="rental_inspection_status == 'flagged'"
widget="badge"/>
<!-- Purchase interest -->
<field name="rental_purchase_interest"
invisible="not is_rental_order or not rental_purchase_interest"
widget="boolean_toggle" readonly="1"/>
<field name="rental_purchase_coupon_id"
invisible="not is_rental_order or not rental_purchase_coupon_id" readonly="1"/>
<!-- Close status -->
<field name="rental_closed"
invisible="not is_rental_order or not rental_closed" readonly="1"/>
<field name="rental_marketing_email_sent" invisible="1"/>
</field>
<!-- Notebook pages -->
<xpath expr="//notebook" position="inside">
<page string="Renewal History"
name="renewal_history"
invisible="not is_rental_order or rental_renewal_count == 0">
<field name="rental_renewal_log_ids" readonly="1">
<list>
<field name="renewal_number"/>
<field name="renewal_type"/>
<field name="previous_start_date"/>
<field name="previous_return_date"/>
<field name="new_start_date"/>
<field name="new_return_date"/>
<field name="invoice_id"/>
<field name="payment_status"
decoration-success="payment_status == 'paid'"
decoration-danger="payment_status == 'failed'"
decoration-warning="payment_status == 'pending'"
widget="badge"/>
<field name="state"
decoration-success="state == 'done'"
decoration-danger="state == 'failed'"
widget="badge"/>
</list>
</field>
</page>
<page string="Cancellation Requests"
name="cancellation_requests"
invisible="not is_rental_order"
badge="rental_cancellation_request_ids">
<field name="rental_cancellation_request_ids">
<list>
<field name="request_date"/>
<field name="partner_id"/>
<field name="reason"/>
<field name="assigned_user_id"/>
<field name="state"
decoration-info="state == 'new'"
decoration-success="state in ('confirmed', 'completed')"
decoration-warning="state == 'pickup_scheduled'"
decoration-danger="state == 'rejected'"
widget="badge"/>
</list>
</field>
</page>
<page string="Inspection"
name="inspection"
invisible="not is_rental_order or not rental_inspection_status">
<group>
<group>
<field name="rental_inspection_status"/>
</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>
<page string="Agreement"
name="agreement_tab"
invisible="not is_rental_order or not rental_agreement_signed">
<group>
<group string="Signature Details">
<field name="rental_agreement_signer_name" readonly="1"/>
<field name="rental_agreement_signed_date" readonly="1"/>
</group>
<group string="Signature">
<field name="rental_agreement_signature" widget="image" readonly="1"/>
</group>
</group>
</page>
</xpath>
</field>
</record>
</odoo>