This commit is contained in:
gsinghpal
2026-03-14 12:04:20 -04:00
parent fc3c966484
commit e9cf75ee48
75 changed files with 6991 additions and 873 deletions

View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<menuitem id="menu_fusion_inventory_root"
name="Fusion Inventory"
parent="stock.menu_stock_root"
sequence="90"/>
<!-- Brands section -->
<menuitem id="menu_fi_brands"
name="Brands / Vendors"
parent="menu_fusion_inventory_root"
action="action_product_brand"
sequence="5"/>
<!-- Sync section -->
<menuitem id="menu_fi_sync"
name="Inventory Sync"
parent="menu_fusion_inventory_root"
sequence="10"/>
<menuitem id="menu_fi_sync_config"
name="Remote Connections"
parent="menu_fi_sync"
action="action_fusion_sync_config"
sequence="10"/>
<menuitem id="menu_fi_product_mapping"
name="Product Mappings"
parent="menu_fi_sync"
action="action_fusion_product_mapping"
sequence="20"/>
<menuitem id="menu_fi_sync_log"
name="Sync Log"
parent="menu_fi_sync"
action="action_fusion_sync_log"
sequence="30"/>
<!-- Warehouse section -->
<menuitem id="menu_fi_warehouse"
name="Shared Warehouse"
parent="menu_fusion_inventory_root"
sequence="20"/>
<menuitem id="menu_fi_warehouse_inventory"
name="Warehouse Inventory"
parent="menu_fi_warehouse"
action="action_warehouse_inventory"
sequence="10"/>
<menuitem id="menu_fi_inter_company"
name="Inter-Company Transfers"
parent="menu_fi_warehouse"
action="action_inter_company_transfer"
sequence="20"/>
<!-- Reports section -->
<menuitem id="menu_fi_reports"
name="Reports"
parent="menu_fusion_inventory_root"
sequence="30"/>
<menuitem id="menu_fi_discrepancies"
name="Discrepancies"
parent="menu_fi_reports"
action="action_inventory_discrepancy"
sequence="10"/>
</data>
</odoo>

View File

@@ -0,0 +1,847 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ═══════════════════════════════════════════════════════════
Portal "My Inventory" link on portal home
═══════════════════════════════════════════════════════════ -->
<template id="portal_my_home_inventory" name="Show Inventory"
inherit_id="portal.portal_my_home"
customize_show="True" priority="60">
<xpath expr="//div[hasclass('o_portal_docs')]" position="before">
<t t-call="portal.portal_docs_entry">
<t t-set="icon" t-value="'/fusion_inventory/static/description/icon.png'"/>
<t t-set="title">Inventory Sheet</t>
<t t-set="url" t-value="'/my/inventory'"/>
<t t-set="text">View live inventory, search products, and book items</t>
<t t-set="placeholder_count" t-value="'fi_inventory'"/>
</t>
</xpath>
</template>
<!-- ═══════════════════════════════════════════════════════════
Portal Inventory Sheet - Main Page
═══════════════════════════════════════════════════════════ -->
<template id="portal_inventory_sheet" name="Inventory Sheet">
<t t-call="portal.portal_layout">
<t t-set="breadcrumbs_searchbar" t-value="True"/>
<div class="container-fluid px-2 px-md-4 py-3" id="fi_inventory_app">
<!-- ── Header ── -->
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
<h3 class="mb-0">Inventory Sheet</h3>
<small class="text-muted">
<span id="fi_last_update">Just now</span>
<span class="spinner-border spinner-border-sm ms-1 d-none"
id="fi_loading" role="status"/>
</small>
</div>
<!-- ── Search + Filters ── -->
<div class="row g-2 mb-3">
<div class="col-12 col-md-5">
<div class="input-group">
<input type="text" id="fi_search" class="form-control"
placeholder="Search by name, SKU, or serial..."
t-att-value="search"/>
<button class="btn btn-primary" id="fi_search_btn"
type="button">
<i class="fa fa-search"/>
</button>
</div>
</div>
<div class="col-12 col-md-4 position-relative">
<input type="text" id="fi_category_search" class="form-control"
placeholder="Search categories..." autocomplete="off"/>
<input type="hidden" id="fi_category_filter" value=""/>
<div id="fi_category_dropdown"
class="position-absolute w-100 bg-white border rounded-bottom shadow-sm d-none"
style="z-index:1050; max-height:250px; overflow-y:auto; top:100%;">
</div>
</div>
<div class="col-12 col-md-3" t-if="has_sync">
<select id="fi_warehouse_filter" class="form-select">
<option value="all"
t-att-selected="warehouse_filter == 'all'">All Locations</option>
<option value="local"
t-att-selected="warehouse_filter == 'local'">Local Only</option>
<option value="remote"
t-att-selected="warehouse_filter == 'remote'">Remote Only</option>
<t t-foreach="all_warehouses" t-as="wh">
<option t-att-value="wh['id']">
<t t-if="wh['type'] == 'remote'">&#x1F517; </t>
<t t-esc="wh['name']"/>
<t t-if="wh['company']"> (<t t-esc="wh['company']"/>)</t>
</option>
</t>
</select>
</div>
</div>
<!-- ── Compact table styles ── -->
<style>
#fi_inventory_table {
font-size: 0.8rem;
table-layout: fixed;
width: 100%;
}
#fi_inventory_table th,
#fi_inventory_table td {
padding: 0.3rem 0.4rem;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
}
#fi_inventory_table .fi-col-product {
width: auto;
white-space: normal;
word-wrap: break-word;
}
#fi_inventory_table .fi-col-sku {
width: 80px;
white-space: nowrap;
}
#fi_inventory_table .fi-col-cat {
width: 100px;
white-space: normal;
word-wrap: break-word;
}
#fi_inventory_table .fi-col-num {
width: 50px;
white-space: nowrap;
text-align: right;
}
#fi_inventory_table .fi-col-price {
width: 65px;
white-space: nowrap;
text-align: right;
}
#fi_inventory_table .fi-col-action {
width: 60px;
text-align: center;
white-space: nowrap;
}
#fi_inventory_table thead th {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.02em;
border-bottom: 2px solid #adb5bd;
}
#fi_inventory_table .badge {
font-size: 0.7rem;
padding: 0.15em 0.4em;
}
#fi_inventory_table .btn-sm {
font-size: 0.68rem;
padding: 0.1rem 0.35rem;
}
</style>
<!-- ── Legend ── -->
<div class="d-flex flex-wrap gap-3 mb-3 small">
<span><span class="badge bg-success">&#9679;</span> In Stock</span>
<span><span class="badge bg-warning text-dark">&#9679;</span> Booked</span>
<span><span class="badge bg-info">&#9679;</span> Incoming (PO)</span>
<span><span class="badge bg-danger">&#9679;</span> Out of Stock</span>
<span t-if="has_sync"><span class="badge bg-purple" style="background:#6f42c1!important;">&#9679;</span> Remote</span>
</div>
<!-- ══════════ Desktop Table ══════════ -->
<div class="table-responsive d-none d-md-block">
<table class="table table-hover table-sm table-bordered align-middle"
id="fi_inventory_table">
<thead class="table-light">
<tr>
<th class="fi-col-product">Product</th>
<th class="fi-col-sku">SKU</th>
<th class="fi-col-cat">Category</th>
<th class="fi-col-num">Local</th>
<th class="fi-col-num">Avail</th>
<th class="fi-col-num" t-if="has_sync">Remote</th>
<th class="fi-col-num" t-if="has_sync">Total</th>
<th class="fi-col-num">Booked</th>
<th class="fi-col-num">Incom.</th>
<th class="fi-col-price">Price</th>
<th class="fi-col-num">Margin</th>
<th class="fi-col-action">Action</th>
</tr>
</thead>
<tbody id="fi_table_body">
<t t-foreach="products" t-as="p">
<tr t-att-data-id="p['id']" t-att-data-tmpl="p.get('tmpl_id', 0)">
<td class="fi-col-product">
<strong t-esc="p['name']"/>
</td>
<td class="fi-col-sku text-muted">
<t t-esc="p['default_code']"/>
</td>
<td class="fi-col-cat"><t t-esc="p['category']"/></td>
<td class="fi-col-num"><t t-esc="p['qty_on_hand']"/></td>
<td class="fi-col-num">
<span t-attf-class="fw-bold #{'text-danger' if p['available_qty'] &lt;= 0 else 'text-success'}">
<t t-esc="p['available_qty']"/>
</span>
</td>
<td class="fi-col-num" t-if="has_sync">
<span t-if="p.get('remote_qty', 0) > 0"
class="badge text-white fi-remote-badge"
style="background:#6f42c1; cursor:pointer;"
t-att-title="', '.join([w['warehouse'] + ': ' + str(w['qty']) for w in p.get('remote_warehouses', [])])">
<t t-esc="p['remote_qty']"/>
</span>
<span t-if="p.get('remote_qty', 0) &lt;= 0"
class="text-muted">0</span>
</td>
<td class="fi-col-num" t-if="has_sync">
<strong><t t-esc="p.get('total_qty', p['qty_on_hand'])"/></strong>
</td>
<td class="fi-col-num">
<span t-if="p['booked_qty'] > 0"
class="badge bg-warning text-dark">
<t t-esc="p['booked_qty']"/>
</span>
</td>
<td class="fi-col-num">
<span t-if="p['shadow_qty'] > 0"
class="badge bg-info">
<t t-esc="p['shadow_qty']"/>
</span>
</td>
<td class="fi-col-price">
$<t t-esc="'%.2f' % p['sale_price']"/>
</td>
<td class="fi-col-num">
<span t-attf-class="badge #{'bg-success' if p['margin_pct'] > 0 else 'bg-secondary'}">
<t t-esc="'%.1f' % p['margin_pct']"/>%
</span>
</td>
<td class="fi-col-action">
<div class="btn-group btn-group-sm">
<button t-if="p['available_qty'] > 0"
class="btn btn-sm btn-outline-primary fi-book-btn"
t-att-data-id="p['id']"
t-att-data-name="p['name']">
Book
</button>
<button t-if="has_sync and p.get('remote_qty', 0) > 0 and p['id'] > 0"
class="btn btn-sm btn-outline-secondary fi-transfer-btn"
t-att-data-id="p['id']"
t-att-data-name="p['name']"
t-att-data-remote-qty="p['remote_qty']"
title="Transfer from remote">
<i class="fa fa-exchange"/>
</button>
</div>
<span t-if="p['available_qty'] &lt;= 0 and p.get('remote_qty', 0) &lt;= 0"
class="text-muted small">--</span>
</td>
</tr>
</t>
<!-- Remote-only products -->
<t t-foreach="remote_only_products" t-as="rp">
<tr class="table-light fst-italic" t-att-data-id="0">
<td class="fi-col-product">
<span class="badge bg-secondary me-1">Remote</span>
<t t-esc="rp['name']"/>
</td>
<td class="fi-col-sku text-muted"><t t-esc="rp['default_code']"/></td>
<td class="fi-col-cat"><t t-esc="rp['category']"/></td>
<td class="fi-col-num text-muted">0</td>
<td class="fi-col-num text-muted">0</td>
<td class="fi-col-num" t-if="has_sync">
<span class="badge text-white" style="background:#6f42c1;">
<t t-esc="rp['remote_qty']"/>
</span>
</td>
<td class="fi-col-num" t-if="has_sync">
<strong><t t-esc="rp['total_qty']"/></strong>
</td>
<td class="fi-col-num">-</td>
<td class="fi-col-num">-</td>
<td class="fi-col-price">$<t t-esc="'%.2f' % rp['sale_price']"/></td>
<td class="fi-col-num">-</td>
<td class="fi-col-action text-muted small">
<t t-esc="rp.get('config_name', '')"/>
</td>
</tr>
</t>
</tbody>
</table>
</div>
<!-- ══════════ Mobile Cards ══════════ -->
<div class="d-block d-md-none" id="fi_mobile_cards">
<t t-foreach="products" t-as="p">
<div class="card mb-2 fi-card" t-att-data-id="p['id']"
t-attf-style="border-left: 4px solid #{'#ffc107' if p['booked_qty'] > 0 else '#198754' if p['available_qty'] > 0 else '#dc3545'};">
<div class="card-body py-2 px-3">
<div class="d-flex justify-content-between align-items-start">
<div style="max-width:65%;">
<div class="fw-bold text-truncate">
<t t-esc="p['name']"/>
</div>
<small class="text-muted">
<t t-esc="p['default_code']"/>
<t t-if="p['category']"> | <t t-esc="p['category']"/></t>
</small>
</div>
<div class="text-end">
<div class="fw-bold">$<t t-esc="'%.2f' % p['sale_price']"/></div>
<span t-attf-class="badge #{'bg-success' if p['margin_pct'] > 0 else 'bg-secondary'} small">
<t t-esc="'%.1f' % p['margin_pct']"/>%
</span>
</div>
</div>
<div t-attf-class="row g-1 mt-2 text-center #{'row-cols-5' if has_sync else ''}" style="font-size:.85rem;">
<div t-attf-class="#{'col' if has_sync else 'col-3'}">
<div class="fw-bold"><t t-esc="p['qty_on_hand']"/></div>
<div class="text-muted" style="font-size:.7rem;">Local</div>
</div>
<div t-attf-class="#{'col' if has_sync else 'col-3'}">
<div t-attf-class="fw-bold #{'text-danger' if p['available_qty'] &lt;= 0 else ''}">
<t t-esc="p['available_qty']"/>
</div>
<div class="text-muted" style="font-size:.7rem;">Avail</div>
</div>
<div t-attf-class="#{'col' if has_sync else 'col-3'}" t-if="has_sync">
<div class="fw-bold" style="color:#6f42c1;">
<t t-esc="p.get('remote_qty', 0)"/>
</div>
<div class="text-muted" style="font-size:.7rem;">Remote</div>
</div>
<div t-attf-class="#{'col' if has_sync else 'col-3'}">
<div class="fw-bold"><t t-esc="p['booked_qty']"/></div>
<div class="text-muted" style="font-size:.7rem;">Booked</div>
</div>
<div t-attf-class="#{'col' if has_sync else 'col-3'}">
<div class="fw-bold"><t t-esc="p['shadow_qty']"/></div>
<div class="text-muted" style="font-size:.7rem;">Incoming</div>
</div>
</div>
<div class="mt-2 text-end">
<div class="btn-group btn-group-sm">
<button t-if="p['available_qty'] > 0"
class="btn btn-sm btn-outline-primary fi-book-btn"
t-att-data-id="p['id']"
t-att-data-name="p['name']">
Book
</button>
<button t-if="has_sync and p.get('remote_qty', 0) > 0 and p['id'] > 0"
class="btn btn-sm btn-outline-secondary fi-transfer-btn"
t-att-data-id="p['id']"
t-att-data-name="p['name']"
t-att-data-remote-qty="p['remote_qty']">
<i class="fa fa-exchange"/> Transfer
</button>
</div>
</div>
</div>
</div>
</t>
</div>
<!-- ══════════ Pagination ══════════ -->
<nav class="mt-3" id="fi_pagination">
</nav>
<div class="text-muted small text-center mt-2">
<span id="fi_count_text">Showing <t t-esc="len(products)"/> of <t t-esc="total_products"/> products</span>
| Auto-refreshes every 30 seconds
</div>
</div>
<!-- ══════════ Transfer Modal ══════════ -->
<div class="modal fade" id="fi_transfer_modal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Transfer from Remote</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"/>
</div>
<div class="modal-body">
<p>
Transfer <strong id="fi_transfer_product_name"></strong>
from remote inventory.
</p>
<div class="mb-3">
<label class="form-label">Available at remote:</label>
<span id="fi_transfer_remote_qty" class="fw-bold"></span>
</div>
<div class="mb-3">
<label for="fi_transfer_qty" class="form-label">Quantity to transfer:</label>
<input type="number" id="fi_transfer_qty" class="form-control"
min="1" value="1"/>
</div>
<div id="fi_transfer_result" class="d-none">
<div id="fi_transfer_success" class="alert alert-success d-none"></div>
<div id="fi_transfer_error" class="alert alert-danger d-none"></div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="fi_transfer_confirm_btn">
<span class="spinner-border spinner-border-sm d-none me-1" id="fi_transfer_spinner"/>
Confirm Transfer
</button>
</div>
</div>
</div>
</div>
<!-- ══════════ JavaScript: Polling + Booking + Transfer ══════════ -->
<script type="text/javascript">
(function () {
'use strict';
var refreshInterval = 30000;
var timer = null;
var searchTimer = null;
var currentPage = <t t-esc="page"/>;
var hasSync = <t t-esc="'true' if has_sync else 'false'"/>;
function updateTimestamp() {
var el = document.getElementById('fi_last_update');
if (el) el.textContent = new Date().toLocaleTimeString();
}
function showLoading(show) {
var el = document.getElementById('fi_loading');
if (el) el.classList.toggle('d-none', !show);
}
function refreshData(resetPage) {
if (resetPage) currentPage = 1;
var searchEl = document.getElementById('fi_search');
var catEl = document.getElementById('fi_category_filter');
var whEl = document.getElementById('fi_warehouse_filter');
var search = searchEl ? searchEl.value.trim() : '';
var catId = catEl ? catEl.value.trim() : '';
var warehouse = whEl ? whEl.value : 'all';
showLoading(true);
fetch('/my/inventory/data', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'call',
params: {
search: search,
category_ids: catId ? [parseInt(catId)] : null,
page: currentPage,
warehouse: warehouse
}
})
})
.then(function(r) { return r.json(); })
.then(function(data) {
showLoading(false);
updateTimestamp();
if (data.result &amp;&amp; data.result.products) {
rebuildTable(data.result.products);
rebuildMobileCards(data.result.products);
updateCount(data.result.products.length, data.result.total);
rebuildPagination(data.result.page, data.result.total_pages);
}
})
.catch(function() { showLoading(false); });
}
function debouncedSearch() {
if (searchTimer) clearTimeout(searchTimer);
currentPage = 1;
searchTimer = setTimeout(refreshData, 300);
}
function goToPage(pg) {
currentPage = pg;
refreshData();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function rebuildPagination(page, totalPages) {
var nav = document.getElementById('fi_pagination');
if (!nav) return;
if (totalPages &lt;= 1) { nav.innerHTML = ''; return; }
var html = '&lt;ul class="pagination pagination-sm justify-content-center">';
html += '&lt;li class="page-item' + (page &lt;= 1 ? ' disabled' : '') + '">'
+ '&lt;a class="page-link" href="#" data-page="' + (page - 1) + '">&amp;lt;&lt;/a>&lt;/li>';
for (var pg = 1; pg &lt;= totalPages; pg++) {
if (pg &lt;= 3 || Math.abs(pg - page) &lt;= 1 || pg === totalPages) {
html += '&lt;li class="page-item' + (pg === page ? ' active' : '') + '">'
+ '&lt;a class="page-link" href="#" data-page="' + pg + '">' + pg + '&lt;/a>&lt;/li>';
} else if (pg === 4 &amp;&amp; page > 5) {
html += '&lt;li class="page-item disabled">&lt;span class="page-link">...&lt;/span>&lt;/li>';
} else if (pg === totalPages - 1 &amp;&amp; page &lt; totalPages - 3) {
html += '&lt;li class="page-item disabled">&lt;span class="page-link">...&lt;/span>&lt;/li>';
}
}
html += '&lt;li class="page-item' + (page >= totalPages ? ' disabled' : '') + '">'
+ '&lt;a class="page-link" href="#" data-page="' + (page + 1) + '">&amp;gt;&lt;/a>&lt;/li>';
html += '&lt;/ul>';
nav.innerHTML = html;
nav.querySelectorAll('a.page-link[data-page]').forEach(function(a) {
a.addEventListener('click', function(e) {
e.preventDefault();
var pg = parseInt(this.getAttribute('data-page'));
if (pg >= 1 &amp;&amp; pg &lt;= totalPages) goToPage(pg);
});
});
}
function updateCount(shown, total) {
var el = document.getElementById('fi_count_text');
if (el) el.textContent = 'Showing ' + shown + ' of ' + total + ' products';
}
function rebuildTable(products) {
var tbody = document.getElementById('fi_table_body');
if (!tbody) return;
var html = '';
products.forEach(function(p) {
var isRemoteOnly = p.remote_only || false;
var availClass = p.available_qty &lt;= 0 ? 'text-danger' : 'text-success';
var marginClass = p.margin_pct > 0 ? 'bg-success' : 'bg-secondary';
var remoteQty = p.remote_qty || 0;
var totalQty = p.total_qty || p.qty_on_hand;
var actionHtml = '';
if (!isRemoteOnly) {
if (p.available_qty > 0) {
actionHtml += '&lt;button class="btn btn-sm btn-outline-primary fi-book-btn" data-id="' + p.id + '" data-name="' + escHtml(p.name) + '">Book&lt;/button>';
}
if (hasSync &amp;&amp; remoteQty > 0 &amp;&amp; p.id > 0) {
actionHtml += '&lt;button class="btn btn-sm btn-outline-secondary fi-transfer-btn" data-id="' + p.id + '" data-name="' + escHtml(p.name) + '" data-remote-qty="' + remoteQty + '">&lt;i class="fa fa-exchange">&lt;/i>&lt;/button>';
}
if (!actionHtml) actionHtml = '&lt;span class="text-muted small">--&lt;/span>';
actionHtml = '&lt;div class="btn-group btn-group-sm">' + actionHtml + '&lt;/div>';
} else {
actionHtml = '&lt;span class="text-muted small">' + escHtml(p.config_name || '') + '&lt;/span>';
}
var remoteCol = '';
var totalCol = '';
if (hasSync) {
var whTooltip = (p.remote_warehouses || []).map(function(w) { return w.warehouse + ': ' + w.qty; }).join(', ');
remoteCol = remoteQty > 0
? '&lt;span class="badge text-white fi-remote-badge" style="background:#6f42c1; cursor:pointer;" title="' + escHtml(whTooltip) + '">' + remoteQty + '&lt;/span>'
: '&lt;span class="text-muted">0&lt;/span>';
totalCol = '&lt;strong>' + totalQty + '&lt;/strong>';
}
var trClass = isRemoteOnly ? ' class="table-light fst-italic"' : '';
var nameCell = isRemoteOnly
? '&lt;span class="badge bg-secondary me-1">Remote&lt;/span>' + escHtml(p.name)
: '&lt;strong>' + escHtml(p.name) + '&lt;/strong>';
html += '&lt;tr' + trClass + ' data-id="' + p.id + '">'
+ '&lt;td class="fi-col-product">' + nameCell + '&lt;/td>'
+ '&lt;td class="fi-col-sku text-muted">' + escHtml(p.default_code) + '&lt;/td>'
+ '&lt;td class="fi-col-cat">' + escHtml(p.category) + '&lt;/td>'
+ '&lt;td class="fi-col-num">' + p.qty_on_hand + '&lt;/td>'
+ '&lt;td class="fi-col-num">&lt;span class="fw-bold ' + availClass + '">' + p.available_qty + '&lt;/span>&lt;/td>'
+ (hasSync ? '&lt;td class="fi-col-num">' + remoteCol + '&lt;/td>' : '')
+ (hasSync ? '&lt;td class="fi-col-num">' + totalCol + '&lt;/td>' : '')
+ '&lt;td class="fi-col-num">' + (p.booked_qty > 0 ? '&lt;span class="badge bg-warning text-dark">' + p.booked_qty + '&lt;/span>' : '') + '&lt;/td>'
+ '&lt;td class="fi-col-num">' + (p.shadow_qty > 0 ? '&lt;span class="badge bg-info">' + p.shadow_qty + '&lt;/span>' : '') + '&lt;/td>'
+ '&lt;td class="fi-col-price">$' + p.sale_price.toFixed(2) + '&lt;/td>'
+ '&lt;td class="fi-col-num">&lt;span class="badge ' + marginClass + '">' + p.margin_pct.toFixed(1) + '%&lt;/span>&lt;/td>'
+ '&lt;td class="fi-col-action">' + actionHtml + '&lt;/td>'
+ '&lt;/tr>';
});
tbody.innerHTML = html;
bindBookButtons();
bindTransferButtons();
}
function rebuildMobileCards(products) {
var container = document.getElementById('fi_mobile_cards');
if (!container) return;
var html = '';
products.forEach(function(p) {
if (p.remote_only) return;
var borderColor = p.booked_qty > 0 ? '#ffc107' : (p.available_qty > 0 ? '#198754' : '#dc3545');
var marginClass = p.margin_pct > 0 ? 'bg-success' : 'bg-secondary';
var remoteQty = p.remote_qty || 0;
var availClass = p.available_qty &lt;= 0 ? 'text-danger' : '';
var remoteMobile = '';
if (hasSync) {
remoteMobile = '&lt;div class="col">'
+ '&lt;div class="fw-bold" style="color:#6f42c1;">' + remoteQty + '&lt;/div>'
+ '&lt;div class="text-muted" style="font-size:.7rem;">Remote&lt;/div>'
+ '&lt;/div>';
}
var btns = '';
if (p.available_qty > 0) {
btns += '&lt;button class="btn btn-sm btn-outline-primary fi-book-btn" data-id="' + p.id + '" data-name="' + escHtml(p.name) + '">Book&lt;/button>';
}
if (hasSync &amp;&amp; remoteQty > 0 &amp;&amp; p.id > 0) {
btns += '&lt;button class="btn btn-sm btn-outline-secondary fi-transfer-btn" data-id="' + p.id + '" data-name="' + escHtml(p.name) + '" data-remote-qty="' + remoteQty + '">&lt;i class="fa fa-exchange">&lt;/i> Transfer&lt;/button>';
}
var colClass = hasSync ? 'col' : 'col-3';
var rowClass = hasSync ? 'row g-1 mt-2 text-center row-cols-5' : 'row g-1 mt-2 text-center';
html += '&lt;div class="card mb-2 fi-card" data-id="' + p.id + '" style="border-left: 4px solid ' + borderColor + ';">'
+ '&lt;div class="card-body py-2 px-3">'
+ '&lt;div class="d-flex justify-content-between align-items-start">'
+ '&lt;div style="max-width:65%;">'
+ '&lt;div class="fw-bold text-truncate">' + escHtml(p.name) + '&lt;/div>'
+ '&lt;small class="text-muted">' + escHtml(p.default_code) + (p.category ? ' | ' + escHtml(p.category) : '') + '&lt;/small>'
+ '&lt;/div>'
+ '&lt;div class="text-end">'
+ '&lt;div class="fw-bold">$' + p.sale_price.toFixed(2) + '&lt;/div>'
+ '&lt;span class="badge ' + marginClass + ' small">' + p.margin_pct.toFixed(1) + '%&lt;/span>'
+ '&lt;/div>'
+ '&lt;/div>'
+ '&lt;div class="' + rowClass + '" style="font-size:.85rem;">'
+ '&lt;div class="' + colClass + '">&lt;div class="fw-bold">' + p.qty_on_hand + '&lt;/div>&lt;div class="text-muted" style="font-size:.7rem;">Local&lt;/div>&lt;/div>'
+ '&lt;div class="' + colClass + '">&lt;div class="fw-bold ' + availClass + '">' + p.available_qty + '&lt;/div>&lt;div class="text-muted" style="font-size:.7rem;">Avail&lt;/div>&lt;/div>'
+ remoteMobile
+ '&lt;div class="' + colClass + '">&lt;div class="fw-bold">' + p.booked_qty + '&lt;/div>&lt;div class="text-muted" style="font-size:.7rem;">Booked&lt;/div>&lt;/div>'
+ '&lt;div class="' + colClass + '">&lt;div class="fw-bold">' + p.shadow_qty + '&lt;/div>&lt;div class="text-muted" style="font-size:.7rem;">Incoming&lt;/div>&lt;/div>'
+ '&lt;/div>'
+ (btns ? '&lt;div class="mt-2 text-end">&lt;div class="btn-group btn-group-sm">' + btns + '&lt;/div>&lt;/div>' : '')
+ '&lt;/div>&lt;/div>';
});
container.innerHTML = html;
bindBookButtons();
bindTransferButtons();
}
function escHtml(s) {
if (!s) return '';
return s.replace(/&amp;/g, '&amp;amp;').replace(/&lt;/g, '&amp;lt;').replace(/>/g, '&amp;gt;').replace(/"/g, '&amp;quot;');
}
/* ── Booking ── */
function bookProduct(productId, productName) {
if (!confirm('Book "' + productName + '" for 24 hours?')) return;
fetch('/my/inventory/book', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'call',
params: {product_id: productId, quantity: 1}
})
})
.then(function(r) { return r.json(); })
.then(function(data) {
var res = data.result || {};
if (res.success) {
alert('Product booked! Expires at ' + res.expires_at);
refreshData();
} else {
alert(res.error || 'Booking failed');
}
})
.catch(function() { alert('Network error'); });
}
function bindBookButtons() {
document.querySelectorAll('.fi-book-btn').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.preventDefault();
bookProduct(
parseInt(this.getAttribute('data-id')),
this.getAttribute('data-name') || ''
);
});
});
}
/* ── Transfer ── */
var currentTransferProductId = null;
function bindTransferButtons() {
document.querySelectorAll('.fi-transfer-btn').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.preventDefault();
var pid = parseInt(this.getAttribute('data-id'));
var pname = this.getAttribute('data-name') || '';
var remoteQty = parseFloat(this.getAttribute('data-remote-qty') || '0');
openTransferModal(pid, pname, remoteQty);
});
});
}
function openTransferModal(productId, productName, remoteQty) {
currentTransferProductId = productId;
document.getElementById('fi_transfer_product_name').textContent = productName;
document.getElementById('fi_transfer_remote_qty').textContent = remoteQty;
document.getElementById('fi_transfer_qty').value = 1;
document.getElementById('fi_transfer_qty').max = remoteQty;
document.getElementById('fi_transfer_result').classList.add('d-none');
document.getElementById('fi_transfer_success').classList.add('d-none');
document.getElementById('fi_transfer_error').classList.add('d-none');
document.getElementById('fi_transfer_confirm_btn').disabled = false;
var modal = new bootstrap.Modal(document.getElementById('fi_transfer_modal'));
modal.show();
}
function executeTransfer() {
if (!currentTransferProductId) return;
var qty = parseFloat(document.getElementById('fi_transfer_qty').value) || 1;
var btn = document.getElementById('fi_transfer_confirm_btn');
var spinner = document.getElementById('fi_transfer_spinner');
btn.disabled = true;
spinner.classList.remove('d-none');
fetch('/my/inventory/transfer', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'call',
params: {
product_id: currentTransferProductId,
quantity: qty
}
})
})
.then(function(r) { return r.json(); })
.then(function(data) {
spinner.classList.add('d-none');
document.getElementById('fi_transfer_result').classList.remove('d-none');
var res = data.result || {};
if (res.success) {
var msg = 'Transfer initiated!';
if (res.local_po) msg += ' Local PO: ' + res.local_po;
if (res.remote_so) msg += ' | Remote SO: ' + res.remote_so;
var successEl = document.getElementById('fi_transfer_success');
successEl.textContent = msg;
successEl.classList.remove('d-none');
refreshData();
} else {
var errorEl = document.getElementById('fi_transfer_error');
errorEl.textContent = res.error || 'Transfer failed';
errorEl.classList.remove('d-none');
btn.disabled = false;
}
})
.catch(function() {
spinner.classList.add('d-none');
btn.disabled = false;
alert('Network error');
});
}
/* ── Category Search ── */
var catSearchTimer = null;
function setupCategorySearch() {
var input = document.getElementById('fi_category_search');
var hidden = document.getElementById('fi_category_filter');
var dropdown = document.getElementById('fi_category_dropdown');
if (!input || !dropdown) return;
input.addEventListener('input', function() {
if (catSearchTimer) clearTimeout(catSearchTimer);
var q = input.value.trim();
if (q.length === 0) {
hidden.value = '';
dropdown.classList.add('d-none');
refreshData(true);
return;
}
catSearchTimer = setTimeout(function() {
fetch('/my/inventory/categories', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
jsonrpc: '2.0', method: 'call',
params: { search: q }
})
})
.then(function(r) { return r.json(); })
.then(function(data) {
var cats = data.result || [];
var html = '';
if (cats.length === 0) {
html = '&lt;div class="px-3 py-2 text-muted small">No categories found&lt;/div>';
}
cats.forEach(function(c) {
html += '&lt;div class="px-3 py-2 fi-cat-option" style="cursor:pointer;" data-id="' + c.id + '" data-name="' + escHtml(c.name) + '">'
+ escHtml(c.name) + '&lt;/div>';
});
dropdown.innerHTML = html;
dropdown.classList.remove('d-none');
dropdown.querySelectorAll('.fi-cat-option').forEach(function(opt) {
opt.addEventListener('click', function() {
hidden.value = this.getAttribute('data-id');
input.value = this.getAttribute('data-name');
dropdown.classList.add('d-none');
refreshData(true);
});
opt.addEventListener('mouseenter', function() {
this.style.background = '#e9ecef';
});
opt.addEventListener('mouseleave', function() {
this.style.background = '';
});
});
});
}, 250);
});
input.addEventListener('focus', function() {
if (input.value.trim()) input.dispatchEvent(new Event('input'));
});
document.addEventListener('click', function(e) {
if (!input.contains(e.target) &amp;&amp; !dropdown.contains(e.target)) {
dropdown.classList.add('d-none');
}
});
}
/* ── Init ── */
document.addEventListener('DOMContentLoaded', function() {
bindBookButtons();
bindTransferButtons();
setupCategorySearch();
var searchBtn = document.getElementById('fi_search_btn');
var searchInput = document.getElementById('fi_search');
var whFilter = document.getElementById('fi_warehouse_filter');
var transferBtn = document.getElementById('fi_transfer_confirm_btn');
if (searchBtn) searchBtn.addEventListener('click', function() { refreshData(true); });
if (searchInput) {
searchInput.addEventListener('input', debouncedSearch);
searchInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') { e.preventDefault(); refreshData(true); }
});
}
if (whFilter) whFilter.addEventListener('change', function() { refreshData(true); });
if (transferBtn) transferBtn.addEventListener('click', executeTransfer);
rebuildPagination(<t t-esc="page"/>, <t t-esc="total_pages"/>);
timer = setInterval(refreshData, refreshInterval);
updateTimestamp();
});
})();
</script>
</t>
</template>
</odoo>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Add Extra Cost + Price Impact to PTAV list view (Configure Variants grid) -->
<record id="view_ptav_list_inherit_fi" model="ir.ui.view">
<field name="name">product.template.attribute.value.list.fi</field>
<field name="model">product.template.attribute.value</field>
<field name="inherit_id" ref="product.product_template_attribute_value_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='price_extra']" position="after">
<field name="x_fi_extra_cost" widget="monetary"
string="Extra Cost"
options="{'field_digits': True}"/>
<field name="x_fi_extra_price_impact" widget="monetary"
string="+ Price (Cost)"
readonly="1"
options="{'field_digits': True}"/>
</xpath>
</field>
</record>
<!-- Add Extra Cost + Price Impact to PTAV form view -->
<record id="view_ptav_form_inherit_fi" model="ir.ui.view">
<field name="name">product.template.attribute.value.form.fi</field>
<field name="model">product.template.attribute.value</field>
<field name="inherit_id" ref="product.product_template_attribute_value_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='price_extra']" position="after">
<field name="x_fi_extra_cost" widget="monetary"
options="{'field_digits': True}"/>
<field name="x_fi_extra_price_impact" widget="monetary"
readonly="1"
options="{'field_digits': True}"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,175 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Brand Form View -->
<record id="view_product_brand_form" model="ir.ui.view">
<field name="name">product.brand.form</field>
<field name="model">product.brand</field>
<field name="arch" type="xml">
<form string="Brand">
<sheet>
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger"
invisible="active"/>
<field name="active" invisible="1"/>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" name="action_view_products"
type="object" icon="fa-cube">
<field string="Products" name="product_count" widget="statinfo"/>
</button>
<button class="oe_stat_button" name="action_view_sub_brands"
type="object" icon="fa-sitemap"
invisible="child_count == 0">
<field string="Sub-Brands" name="child_count" widget="statinfo"/>
</button>
</div>
<field name="logo" widget="image" class="oe_avatar"
options="{'convert_to_webp': True, 'preview_image': 'logo'}"/>
<div class="oe_title">
<label for="name" string="Brand Name"/>
<h1><field name="name" placeholder="e.g. Invacare"/></h1>
</div>
<group>
<group string="Manufacturer / Vendor">
<field name="partner_id"/>
<field name="parent_id"
options="{'no_create': True}"
domain="[('id', '!=', id)]"/>
</group>
<group string="Default Pricing">
<label for="primary_discount_pct" string="Primary Discount"/>
<div class="d-flex align-items-center">
<field name="primary_discount_pct" class="oe_inline"
style="max-width: 80px;"/>
<span class="ms-1 text-muted">%</span>
</div>
<label for="secondary_discount_pct" string="Secondary Discount"/>
<div class="d-flex align-items-center">
<field name="secondary_discount_pct" class="oe_inline"
style="max-width: 80px;"/>
<span class="ms-1 text-muted">%</span>
</div>
<label for="net_discount_pct" string="Effective Discount"/>
<div class="d-flex align-items-center">
<field name="net_discount_pct" class="oe_inline fw-bold"
style="max-width: 80px;"/>
<span class="ms-1 text-muted">%</span>
</div>
</group>
</group>
<notebook>
<page string="Pricing Rules" name="pricing_rules">
<field name="pricing_rule_ids">
<list editable="bottom">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="apply_on"/>
<field name="categ_id"
column_invisible="True"
invisible="apply_on != 'category'"/>
<field name="product_tmpl_id"
column_invisible="True"
invisible="apply_on != 'product'"/>
<field name="pricing_method"/>
<field name="primary_discount_pct" string="Primary %"
invisible="pricing_method != 'tiered_pct'"/>
<field name="secondary_discount_pct" string="Secondary %"
invisible="pricing_method != 'tiered_pct'"/>
<field name="flat_discount_pct" string="Discount %"
invisible="pricing_method != 'flat_pct'"/>
<field name="fixed_rebate_amount" string="Rebate $"
invisible="pricing_method != 'fixed_rebate'"/>
<field name="fixed_cost_price" string="Cost $"
invisible="pricing_method != 'fixed_cost'"/>
<field name="net_discount_pct" string="Effective %"/>
</list>
</field>
</page>
<page string="Sub-Brands" name="sub_brands">
<field name="child_ids">
<list>
<field name="logo" widget="image"
options="{'size': [32, 32]}" class="p-0"/>
<field name="name"/>
<field name="partner_id"/>
<field name="primary_discount_pct" string="Primary %"/>
<field name="secondary_discount_pct" string="Secondary %"/>
<field name="net_discount_pct" string="Effective %"/>
<field name="product_count" string="Products"/>
</list>
</field>
</page>
<page string="Notes" name="notes">
<field name="notes" nolabel="1"
placeholder="Internal notes about the pricing arrangement with this brand..."/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Brand List View -->
<record id="view_product_brand_list" model="ir.ui.view">
<field name="name">product.brand.list</field>
<field name="model">product.brand</field>
<field name="arch" type="xml">
<list string="Brands">
<field name="logo" widget="image" options="{'size': [32, 32]}" class="p-0"/>
<field name="name"/>
<field name="parent_id" optional="show"/>
<field name="partner_id"/>
<field name="primary_discount_pct" string="Primary %"/>
<field name="secondary_discount_pct" string="Secondary %"/>
<field name="net_discount_pct" string="Effective %"/>
<field name="product_count" string="Products"/>
</list>
</field>
</record>
<!-- Brand Search View -->
<record id="view_product_brand_search" model="ir.ui.view">
<field name="name">product.brand.search</field>
<field name="model">product.brand</field>
<field name="arch" type="xml">
<search string="Brands">
<field name="name" string="Brand Name"/>
<field name="partner_id" string="Vendor"/>
<field name="parent_id" string="Parent Brand"/>
<separator/>
<filter string="Top-Level Brands" name="top_level"
domain="[('parent_id', '=', False)]"/>
<filter string="Sub-Brands" name="sub_brands"
domain="[('parent_id', '!=', False)]"/>
<filter string="Has Pricing Rules" name="has_rules"
domain="[('pricing_rule_ids', '!=', False)]"/>
<filter string="Has Pricing" name="has_pricing"
domain="[('primary_discount_pct', '&gt;', 0)]"/>
<filter string="Archived" name="archived"
domain="[('active', '=', False)]"/>
<separator/>
<filter string="Parent Brand" name="group_parent"
context="{'group_by': 'parent_id'}"/>
<filter string="Vendor" name="group_vendor"
context="{'group_by': 'partner_id'}"/>
</search>
</field>
</record>
<!-- Brand Action -->
<record id="action_product_brand" model="ir.actions.act_window">
<field name="name">Brands / Vendors</field>
<field name="res_model">product.brand</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first brand
</p>
<p>
Brands link products to manufacturers and define tiered pricing
structures. Use pricing rules for category-specific or
product-specific overrides.
</p>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,174 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ═══════════════════════════════════════════════════════════
Product Form: Two-column layout
LEFT = Brand/Classification + Taxes (moved fields)
RIGHT = Pricing only (Sales Price, Margin, Profit, Cost)
═══════════════════════════════════════════════════════════ -->
<record id="view_product_template_form_inherit_fi" model="ir.ui.view">
<field name="name">product.template.form.fusion.inventory</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_only_form_view"/>
<field name="priority">50</field>
<field name="arch" type="xml">
<!-- ── Make fields required ── -->
<xpath expr="//field[@name='type']" position="attributes">
<attribute name="required">1</attribute>
</xpath>
<xpath expr="//field[@name='list_price']" position="attributes">
<attribute name="required">1</attribute>
</xpath>
<xpath expr="//field[@name='standard_price']" position="attributes">
<attribute name="required">1</attribute>
</xpath>
<xpath expr="//field[@name='categ_id']" position="attributes">
<attribute name="required">1</attribute>
</xpath>
<xpath expr="//field[@name='default_code']" position="attributes">
<attribute name="required">1</attribute>
</xpath>
<!-- ═════════════════════════════════════════════════════
RIGHT COLUMN: Margin + Profit above Cost
(everything else pricing-related stays here)
═════════════════════════════════════════════════════ -->
<xpath expr="//label[@id='standard_price_label']" position="before">
<label for="x_fi_margin_pct" string="Margin"/>
<div name="margin_pct_uom" class="d-flex align-items-center">
<field name="x_fi_margin_pct" class="oe_inline"
style="max-width: 80px;"/>
<span class="ms-1 text-muted">%</span>
</div>
<label for="x_fi_profit_amount" string="Profit"/>
<div name="profit_amount_wrapper">
<field name="x_fi_profit_amount" class="oe_inline"
widget="monetary" readonly="1"
options="{'currency_field': 'currency_id'}"/>
</div>
</xpath>
<!-- ═════════════════════════════════════════════════════
LEFT COLUMN: move fields from right, add sections
═════════════════════════════════════════════════════ -->
<xpath expr="//group[@name='group_general']" position="inside">
<!-- ── Brand &amp; Classification ── -->
<separator string="Brand &amp; Classification"/>
<field name="x_fi_brand_ids" widget="many2many_tags"
options="{'color_field': 'id'}"
placeholder="Select brand(s)..."/>
<label for="x_fi_expected_cost" string="Expected Cost"
invisible="not x_fi_brand_ids"/>
<div class="d-flex align-items-center"
invisible="not x_fi_brand_ids">
<field name="x_fi_expected_cost" class="oe_inline"
widget="monetary" readonly="1"
options="{'currency_field': 'currency_id'}"/>
<span class="ms-2 text-muted fst-italic">(from brand discount)</span>
</div>
<xpath expr="//field[@name='categ_id']" position="move"/>
<xpath expr="//field[@name='default_code']" position="move"/>
<xpath expr="//field[@name='barcode']" position="move"/>
<xpath expr="//label[@for='base_unit_count']" position="move"/>
<xpath expr="//div[@name='base_unit_price']" position="move"/>
<xpath expr="//group[@name='group_standard_price']/field[@name='company_id']" position="move"/>
<!-- ── Taxes ── -->
<separator string="Taxes"/>
<xpath expr="//label[@for='taxes_id']" position="move"/>
<xpath expr="//div[@name='taxes_div']" position="move"/>
<xpath expr="//field[@name='supplier_taxes_id']" position="move"/>
</xpath>
<!-- ── Shipping Cost below Cost ── -->
<xpath expr="//div[@name='standard_price_uom']" position="after">
<label for="x_fi_shipping_cost" string="Shipping Cost"/>
<div name="shipping_cost_wrapper">
<field name="x_fi_shipping_cost" class="oe_inline"
widget="monetary"
options="{'currency_field': 'currency_id'}"/>
</div>
</xpath>
<!-- ── Apply Margin to Variants button in right column ── -->
<xpath expr="//group[@name='group_standard_price']" position="inside">
<div colspan="2" class="mt-2"
invisible="product_variant_count &lt;= 1">
<button name="action_apply_margin_to_all_variants"
type="object"
string="Apply Margin to All Variants"
class="btn-secondary btn-sm"
icon="fa-refresh"
confirm="Apply the template margin to all non-overridden variants? Overridden variants will be skipped."/>
</div>
</xpath>
<!-- ── Fusion Inventory tab ── -->
<xpath expr="//notebook" position="inside">
<page string="Fusion Inventory" name="fusion_inventory_tab">
<group string="Name Case Conversion">
<field name="x_fi_case_conversion" widget="radio"/>
</group>
<group string="Remote Inventory" invisible="not has_remote_mapping">
<group>
<field name="remote_qty_available"/>
<field name="remote_qty_forecast"/>
</group>
</group>
</page>
</xpath>
<!-- ── Purchase History tab ── -->
<xpath expr="//notebook" position="inside">
<page string="Purchase History" name="purchase_history_tab">
<div class="d-flex align-items-center mb-3">
<h3 class="mb-0">Vendor Bill History</h3>
<button name="action_view_purchase_history" type="object"
string="Open Full History" class="btn-link ms-3"
icon="fa-external-link"/>
<button name="action_refresh_cost_from_bills" type="object"
string="Refresh Cost from Latest Bill"
class="btn-secondary ms-3"
icon="fa-refresh"
confirm="Update this product's cost to the latest vendor bill price?"/>
</div>
<field name="x_fi_purchase_history_ids" readonly="1" nolabel="1">
<list limit="50" default_order="date desc">
<field name="move_id" string="Bill Number"/>
<field name="date" string="Date"/>
<field name="partner_id" string="Vendor"/>
<field name="price_unit" string="Unit Cost"/>
<field name="quantity"/>
<field name="x_fi_suggested_price" string="Suggested Price"/>
</list>
</field>
</page>
</xpath>
</field>
</record>
<!-- ═══════════════════════════════════════════════════════════
Taxes: make required
═══════════════════════════════════════════════════════════ -->
<record id="view_product_template_form_inherit_fi_taxes" model="ir.ui.view">
<field name="name">product.template.form.fusion.inventory.taxes</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="account.product_template_form_view"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='taxes_id']" position="attributes">
<attribute name="required">1</attribute>
</xpath>
<xpath expr="//field[@name='supplier_taxes_id']" position="attributes">
<attribute name="required">1</attribute>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Add remote stock + brand columns to product list view -->
<record id="view_product_template_list_inherit_sync" model="ir.ui.view">
<field name="name">product.template.list.sync</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_tree_view"/>
<field name="arch" type="xml">
<xpath expr="//list" position="inside">
<field name="x_fi_brand_ids" widget="many2many_tags" string="Brand(s)" optional="show"/>
<field name="x_fi_margin_pct" string="Margin %" optional="show"/>
<field name="remote_qty_available" string="Remote Stock" optional="show"
decoration-danger="remote_qty_available == 0 and has_remote_mapping"
decoration-success="remote_qty_available > 0"/>
<field name="has_remote_mapping" column_invisible="1"/>
</xpath>
</field>
</record>
<!-- Add brand search/filter/group-by to product search view -->
<record id="view_product_template_search_inherit_fi" model="ir.ui.view">
<field name="name">product.template.search.fusion.inventory</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_search_view"/>
<field name="arch" type="xml">
<xpath expr="//search" position="inside">
<field name="x_fi_brand_ids" string="Brand"/>
<separator/>
<filter string="Has Brand" name="has_brand"
domain="[('x_fi_brand_ids', '!=', False)]"/>
<filter string="No Brand" name="no_brand"
domain="[('x_fi_brand_ids', '=', False)]"/>
<separator/>
<filter string="Brand" name="group_brand"
context="{'group_by': 'x_fi_brand_ids'}"/>
</xpath>
</field>
</record>
<!-- ═══════════════════════════════════════════════════════════
Variant Form: margin, profit, override
═══════════════════════════════════════════════════════════ -->
<record id="view_product_product_form_fi" model="ir.ui.view">
<field name="name">product.product.form.fusion.inventory</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="product.product_variant_easy_edit_view"/>
<field name="arch" type="xml">
<!-- Make Sale Price always read-only on variant form -->
<xpath expr="//group[@name='pricing']//field[@name='lst_price']" position="attributes">
<attribute name="readonly">1</attribute>
</xpath>
<xpath expr="//group[@name='pricing']//field[@name='cost_currency_id']" position="after">
<label for="x_fi_shipping_cost" string="Shipping Cost"/>
<div>
<field name="x_fi_shipping_cost" class="oe_inline"
widget="monetary"
options="{'currency_field': 'currency_id'}"/>
</div>
<label for="x_fi_variant_margin_pct" string="Margin"/>
<div class="d-flex align-items-center">
<field name="x_fi_variant_margin_pct" class="oe_inline"
style="max-width: 80px;"/>
<span class="ms-1 text-muted">%</span>
<field name="x_fi_margin_override" class="ms-3"/>
<label for="x_fi_margin_override" string="Override"
class="ms-1 text-muted"/>
</div>
<label for="x_fi_variant_profit" string="Profit"/>
<div>
<field name="x_fi_variant_profit" class="oe_inline"
widget="monetary" readonly="1"
options="{'currency_field': 'currency_id'}"/>
</div>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_settings_view_form_fusion_inventory" model="ir.ui.view">
<field name="name">res.config.settings.view.form.fusion.inventory</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//form" position="inside">
<app data-string="Fusion Inventory" string="Fusion Inventory"
name="fusion_inventory"
groups="stock.group_stock_manager">
<!-- GENERAL SETTINGS -->
<h2>General</h2>
<div class="row mt-4 o_settings_container">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="fi_auto_update_cost"/>
</div>
<div class="o_setting_right_pane">
<label for="fi_auto_update_cost"/>
<div class="text-muted">
Automatically update product cost when a vendor bill is confirmed.
Uses the latest bill line price (zero-price lines are skipped).
</div>
<div class="mt-3">
<button name="action_sync_all_costs_from_bills"
type="object"
string="Sync All Product Costs Now"
class="btn-secondary"
icon="fa-refresh"
confirm="This will update every product's cost to its latest vendor bill price. Continue?"/>
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<span class="o_form_label">Default Margin (%)</span>
<div class="text-muted">
Default margin percentage applied to newly created products.
</div>
<div class="mt-2">
<field name="fi_default_margin" style="max-width: 100px;"/> %
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<span class="o_form_label">Booking Hold Duration</span>
<div class="text-muted">
How many hours a product booking holds in the inventory sheet
before automatically expiring.
</div>
<div class="mt-2">
<field name="fi_booking_hold_hours" style="max-width: 80px;"/> hours
</div>
</div>
</div>
</div>
<!-- CASE CONVERSION -->
<h2>Product Name Case Conversion</h2>
<div class="row mt-4 o_settings_container">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<span class="o_form_label">Global Case Conversion</span>
<div class="text-muted">
Automatically convert ALL product names to the selected case.
This overrides individual product settings and applies to new products.
Set to "No Conversion" to let individual products control their own case.
</div>
<div class="mt-3">
<field name="fi_case_conversion" widget="radio"/>
</div>
<div class="mt-3">
<button name="action_apply_case_conversion_all"
type="object" string="Apply to All Existing Products"
class="btn-secondary"
confirm="This will convert all existing product names. Continue?"/>
</div>
</div>
</div>
</div>
<!-- OPENAI / AI -->
<h2>AI Configuration</h2>
<div class="row mt-4 o_settings_container">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<span class="o_form_label">OpenAI API Key</span>
<div class="text-muted">
Used for discrepancy analysis and notes parsing.
If empty, falls back to the Fusion Digitize API key.
</div>
<div class="mt-2">
<field name="fi_openai_api_key" password="True"
placeholder="Leave empty to use Fusion Digitize key"/>
</div>
</div>
</div>
</div>
</app>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Brand smart button + Create Brand on vendor contacts -->
<record id="view_partner_form_fi_brand" model="ir.ui.view">
<field name="name">res.partner.form.fusion.inventory.brand</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<button class="oe_stat_button" name="action_view_brands"
type="object" icon="fa-tags"
invisible="brand_count == 0">
<field string="Brands" name="brand_count" widget="statinfo"/>
</button>
<button class="oe_stat_button" name="action_create_brand"
type="object" icon="fa-plus-circle"
string="Create Brand"
invisible="supplier_rank == 0 or brand_count &gt; 0"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,137 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ═══════════════════════════════════════════════════════════
Stock Picking Form: SO/Invoice tracking + Serial Scanner
═══════════════════════════════════════════════════════════ -->
<record id="view_picking_form_inherit_fi" model="ir.ui.view">
<field name="name">stock.picking.form.fusion.inventory</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_form"/>
<field name="arch" type="xml">
<!-- ── Smart buttons: Sale Order ── -->
<xpath expr="//div[@name='button_box']" position="inside">
<button name="action_view_sale_order" type="object"
class="oe_stat_button" icon="fa-shopping-cart"
invisible="not x_fi_sale_order_id">
<field name="x_fi_sale_order_id" widget="statinfo"
string="Sale Order"/>
</button>
<button name="action_view_invoices" type="object"
class="oe_stat_button" icon="fa-file-text-o"
invisible="x_fi_invoice_count == 0">
<field name="x_fi_invoice_count" widget="statinfo"
string="Invoices"/>
</button>
<button name="action_view_purchase_order" type="object"
class="oe_stat_button" icon="fa-truck"
invisible="not x_fi_purchase_order_id">
<field name="x_fi_purchase_order_id" widget="statinfo"
string="Purchase Order"/>
</button>
<button name="action_view_bills" type="object"
class="oe_stat_button" icon="fa-credit-card"
invisible="x_fi_bill_count == 0">
<field name="x_fi_bill_count" widget="statinfo"
string="Bills"/>
</button>
</xpath>
<!-- ── Status pills below header: Sale Order ── -->
<xpath expr="//header" position="after">
<div class="d-flex gap-2 px-3 pb-2"
invisible="not x_fi_sale_order_id">
<span class="text-muted">SO Status:</span>
<field name="x_fi_sale_order_state" widget="badge"
decoration-info="x_fi_sale_order_state == 'draft'"
decoration-primary="x_fi_sale_order_state == 'sale'"
decoration-success="x_fi_sale_order_state == 'done'"
decoration-danger="x_fi_sale_order_state == 'cancel'"/>
<span class="text-muted ms-3">Invoice:</span>
<field name="x_fi_invoice_status" widget="badge"
decoration-danger="x_fi_invoice_status == 'no'"
decoration-warning="x_fi_invoice_status == 'invoiced'"
decoration-success="x_fi_invoice_status == 'paid'"/>
</div>
<div class="d-flex gap-2 px-3 pb-2"
invisible="not x_fi_purchase_order_id">
<span class="text-muted">PO Status:</span>
<field name="x_fi_purchase_order_state" widget="badge"
decoration-info="x_fi_purchase_order_state == 'draft'"
decoration-primary="x_fi_purchase_order_state == 'purchase'"
decoration-success="x_fi_purchase_order_state == 'done'"
decoration-danger="x_fi_purchase_order_state == 'cancel'"/>
<span class="text-muted ms-3">Bill:</span>
<field name="x_fi_bill_status" widget="badge"
decoration-danger="x_fi_bill_status == 'no'"
decoration-warning="x_fi_bill_status == 'billed'"
decoration-success="x_fi_bill_status == 'paid'"/>
</div>
</xpath>
<!-- ── Serial Number Scan button ── -->
<xpath expr="//header" position="inside">
<button name="action_scan_serial_numbers" type="object"
string="Scan Serial Numbers"
class="btn-secondary"
invisible="picking_type_code != 'outgoing'"/>
</xpath>
</field>
</record>
<!-- ═══════════════════════════════════════════════════════════
Stock Picking List: Invoice status column + filters
═══════════════════════════════════════════════════════════ -->
<record id="view_picking_list_inherit_fi" model="ir.ui.view">
<field name="name">stock.picking.list.fusion.inventory</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.vpicktree"/>
<field name="arch" type="xml">
<xpath expr="//list" position="inside">
<field name="x_fi_invoice_status" string="Invoice Status"
widget="badge" optional="show"
decoration-danger="x_fi_invoice_status == 'no'"
decoration-warning="x_fi_invoice_status == 'invoiced'"
decoration-success="x_fi_invoice_status == 'paid'"/>
<field name="x_fi_bill_status" string="Bill Status"
widget="badge" optional="show"
decoration-danger="x_fi_bill_status == 'no'"
decoration-warning="x_fi_bill_status == 'billed'"
decoration-success="x_fi_bill_status == 'paid'"/>
</xpath>
</field>
</record>
<!-- ═══════════════════════════════════════════════════════════
Stock Picking Search: Filters for invoice status
═══════════════════════════════════════════════════════════ -->
<record id="view_picking_search_inherit_fi" model="ir.ui.view">
<field name="name">stock.picking.search.fusion.inventory</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_internal_search"/>
<field name="arch" type="xml">
<xpath expr="//search" position="inside">
<separator/>
<filter name="fi_paid_not_delivered" string="Paid - Ready to Deliver"
domain="[('x_fi_invoice_status', '=', 'paid'), ('state', 'not in', ('done', 'cancel'))]"/>
<filter name="fi_invoiced_not_delivered" string="Invoiced - Not Delivered"
domain="[('x_fi_invoice_status', '=', 'invoiced'), ('state', 'not in', ('done', 'cancel'))]"/>
<filter name="fi_not_invoiced" string="Not Invoiced"
domain="[('x_fi_invoice_status', '=', 'no'), ('picking_type_code', '=', 'outgoing')]"/>
<separator/>
<filter name="fi_bill_paid" string="Bill Paid"
domain="[('x_fi_bill_status', '=', 'paid'), ('picking_type_code', '=', 'incoming')]"/>
<filter name="fi_billed_not_paid" string="Billed - Not Paid"
domain="[('x_fi_bill_status', '=', 'billed'), ('picking_type_code', '=', 'incoming')]"/>
<filter name="fi_not_billed" string="Not Billed"
domain="[('x_fi_bill_status', '=', 'no'), ('picking_type_code', '=', 'incoming')]"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,404 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Sync Config Form View -->
<record id="view_fusion_sync_config_form" model="ir.ui.view">
<field name="name">fusion.sync.config.form</field>
<field name="model">fusion.sync.config</field>
<field name="arch" type="xml">
<form>
<header>
<button name="action_test_connection" type="object"
string="Test Connection" class="btn-primary"
invisible="state == 'connected'"/>
<button name="action_test_connection" type="object"
string="Re-Test Connection"
invisible="state != 'connected'"/>
<button name="action_sync_now" type="object"
string="Sync Now" class="btn-primary"
invisible="state != 'connected'"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,connected"/>
</header>
<sheet>
<div class="oe_title">
<h1><field name="name" placeholder="e.g., Mobility Specialties"/></h1>
</div>
<group>
<group string="Connection">
<field name="url" placeholder="https://erp.mobilityspecialties.com"/>
<field name="db_name" placeholder="mobility-prod"/>
<field name="username" placeholder="admin"/>
<field name="api_key" password="True"/>
</group>
<group string="Sync Settings">
<field name="sync_products"/>
<field name="sync_stock"/>
<field name="sync_interval"/>
<field name="remote_warehouse_name"
placeholder="Leave empty for all warehouses"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
</group>
<group>
<group string="Inter-Company">
<field name="remote_partner_id"
placeholder="Select the partner representing the remote company"/>
<field name="local_company_name"
placeholder="e.g., Mobility Specialties Inc"/>
</group>
<group string="Shared Warehouse">
<field name="is_shared_warehouse"/>
<field name="warehouse_location_id"
invisible="not is_shared_warehouse"/>
</group>
</group>
<group string="Status">
<field name="last_sync"/>
<field name="last_sync_status"/>
<field name="remote_uid" invisible="state != 'connected'"/>
</group>
<notebook>
<page string="Product Mappings" name="mappings">
<field name="active" invisible="1"/>
<p class="text-muted" invisible="state == 'connected'">
Connect and sync to see product mappings here.
</p>
</page>
<page string="Remote Warehouses" name="warehouses">
<field name="sync_warehouse_ids" readonly="1">
<list>
<field name="name"/>
<field name="code"/>
<field name="company_name"/>
<field name="remote_warehouse_id"/>
<field name="remote_lot_stock_id"/>
<field name="active" widget="boolean"/>
</list>
</field>
<p class="text-muted" invisible="sync_warehouse_count > 0">
Run "Sync Now" to discover remote warehouses.
</p>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Sync Config List View -->
<record id="view_fusion_sync_config_list" model="ir.ui.view">
<field name="name">fusion.sync.config.list</field>
<field name="model">fusion.sync.config</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="url"/>
<field name="is_shared_warehouse" widget="boolean"/>
<field name="state" widget="badge"
decoration-success="state == 'connected'"
decoration-danger="state == 'error'"
decoration-info="state == 'draft'"/>
<field name="last_sync"/>
<field name="last_sync_status"/>
</list>
</field>
</record>
<!-- Product Mapping List View -->
<record id="view_fusion_product_sync_mapping_list" model="ir.ui.view">
<field name="name">fusion.product.sync.mapping.list</field>
<field name="model">fusion.product.sync.mapping</field>
<field name="arch" type="xml">
<list editable="bottom">
<field name="remote_product_name"/>
<field name="remote_default_code"/>
<field name="remote_barcode" optional="hide"/>
<field name="local_product_id"/>
<field name="auto_matched" widget="boolean"/>
<field name="remote_qty_available"
decoration-danger="remote_qty_available == 0"
decoration-success="remote_qty_available > 0"/>
<field name="remote_qty_forecast"/>
<field name="owner_config_id" optional="show"/>
<field name="last_stock_sync"/>
<field name="config_id" column_invisible="1"/>
</list>
</field>
</record>
<!-- Product Mapping Search View -->
<record id="view_fusion_product_sync_mapping_search" model="ir.ui.view">
<field name="name">fusion.product.sync.mapping.search</field>
<field name="model">fusion.product.sync.mapping</field>
<field name="arch" type="xml">
<search>
<field name="remote_product_name"/>
<field name="remote_default_code"/>
<field name="local_product_id"/>
<separator/>
<filter name="mapped" string="Mapped"
domain="[('local_product_id', '!=', False)]"/>
<filter name="unmapped" string="Unmapped"
domain="[('local_product_id', '=', False)]"/>
<filter name="in_stock" string="Remote In Stock"
domain="[('remote_qty_available', '&gt;', 0)]"/>
</search>
</field>
</record>
<!-- Sync Log List View -->
<record id="view_fusion_sync_log_list" model="ir.ui.view">
<field name="name">fusion.sync.log.list</field>
<field name="model">fusion.sync.log</field>
<field name="arch" type="xml">
<list>
<field name="create_date" string="Date"/>
<field name="config_id"/>
<field name="direction"/>
<field name="sync_type"/>
<field name="status" widget="badge"
decoration-success="status == 'success'"
decoration-warning="status == 'partial'"
decoration-danger="status == 'error'"/>
<field name="summary"/>
<field name="product_count"/>
</list>
</field>
</record>
<!-- Sync Config Action -->
<record id="action_fusion_sync_config" model="ir.actions.act_window">
<field name="name">Remote Connections</field>
<field name="res_model">fusion.sync.config</field>
<field name="view_mode">list,form</field>
</record>
<!-- Product Mapping Action -->
<record id="action_fusion_product_mapping" model="ir.actions.act_window">
<field name="name">Product Mappings</field>
<field name="res_model">fusion.product.sync.mapping</field>
<field name="view_mode">list</field>
<field name="context">{'search_default_mapped': 1}</field>
</record>
<!-- Sync Log Action -->
<record id="action_fusion_sync_log" model="ir.actions.act_window">
<field name="name">Sync Log</field>
<field name="res_model">fusion.sync.log</field>
<field name="view_mode">list</field>
</record>
<!-- Inter-Company Transfer Views -->
<record id="view_inter_company_transfer_form" model="ir.ui.view">
<field name="name">fusion.inter.company.transfer.form</field>
<field name="model">fusion.inter.company.transfer</field>
<field name="arch" type="xml">
<form>
<header>
<button name="action_execute_transfer" type="object"
string="Execute Transfer (One-Click)" class="btn-primary"
invisible="state not in ('draft', 'requested')"
confirm="This will create remote SO, remote Invoice, local PO, and local Vendor Bill automatically. Continue?"/>
<button name="action_request" type="object"
string="Request (Manual)" class="btn-secondary"
invisible="state != 'draft'"/>
<button name="action_create_remote_so" type="object"
string="Create Remote SO" class="btn-primary"
invisible="state != 'requested'"/>
<button name="action_create_local_po" type="object"
string="Create Local PO" class="btn-primary"
invisible="state != 'so_created'"/>
<button name="action_create_invoice" type="object"
string="Create Invoice" class="btn-primary"
invisible="state != 'po_created'"/>
<button name="action_create_vendor_bill" type="object"
string="Create Vendor Bill" class="btn-secondary"
invisible="state != 'invoiced'"/>
<button name="action_create_delivery_task" type="object"
string="Create Delivery Task" class="btn-secondary"
invisible="state not in ('po_created', 'invoiced')"/>
<button name="action_mark_transferred" type="object"
string="Mark Transferred" class="btn-primary"
invisible="state != 'invoiced'"/>
<button name="action_complete" type="object"
string="Complete" class="btn-success"
invisible="state != 'transferred'"/>
<button name="action_retry" type="object"
string="Retry" class="btn-warning"
invisible="state != 'error'"/>
<button name="action_cancel" type="object"
string="Cancel" class="btn-secondary"
invisible="state in ('done', 'cancelled')"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,requested,so_created,po_created,invoiced,transferred,done"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="%(action_inter_company_transfer)d"
type="action" class="oe_stat_button"
icon="fa-shopping-cart"
invisible="not local_po_id">
<field name="local_po_id" widget="statinfo" string="Local PO"/>
</button>
</div>
<div class="alert alert-danger" role="alert"
invisible="state != 'error'">
<strong>Transfer failed at step: </strong>
<field name="error_step" readonly="1"/>
</div>
<group>
<group string="Transfer Details">
<field name="config_id"/>
<field name="product_id"/>
<field name="product_mapping_id" readonly="1"/>
<field name="quantity"/>
<field name="requested_by" readonly="1"/>
</group>
<group string="References">
<field name="remote_so_id" readonly="1"/>
<field name="remote_so_name" readonly="1"/>
<field name="local_po_id" readonly="1"/>
<field name="remote_invoice_id" readonly="1"/>
<field name="local_bill_id" readonly="1"/>
<field name="task_id" readonly="1"/>
</group>
</group>
<field name="notes" placeholder="Notes about this transfer..."/>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="view_inter_company_transfer_list" model="ir.ui.view">
<field name="name">fusion.inter.company.transfer.list</field>
<field name="model">fusion.inter.company.transfer</field>
<field name="arch" type="xml">
<list>
<field name="product_id"/>
<field name="config_id"/>
<field name="quantity"/>
<field name="state" widget="badge"
decoration-info="state in ('draft', 'requested')"
decoration-primary="state in ('so_created', 'po_created')"
decoration-warning="state == 'invoiced'"
decoration-success="state in ('transferred', 'done')"
decoration-danger="state == 'cancelled'"/>
<field name="local_po_id"/>
<field name="requested_by"/>
<field name="create_date"/>
</list>
</field>
</record>
<record id="action_inter_company_transfer" model="ir.actions.act_window">
<field name="name">Inter-Company Transfers</field>
<field name="res_model">fusion.inter.company.transfer</field>
<field name="view_mode">list,form</field>
</record>
<!-- Warehouse Inventory Views -->
<record id="view_warehouse_inventory_list" model="ir.ui.view">
<field name="name">fusion.warehouse.inventory.list</field>
<field name="model">fusion.warehouse.inventory</field>
<field name="arch" type="xml">
<list editable="bottom">
<field name="product_id"/>
<field name="lot_id"/>
<field name="owner_config_id"/>
<field name="quantity"/>
<field name="location_bin"/>
<field name="state" widget="badge"
decoration-success="state == 'available'"
decoration-warning="state == 'reserved'"
decoration-info="state == 'in_transit'"
decoration-muted="state == 'transferred'"/>
</list>
</field>
</record>
<record id="action_warehouse_inventory" model="ir.actions.act_window">
<field name="name">Shared Warehouse Inventory</field>
<field name="res_model">fusion.warehouse.inventory</field>
<field name="view_mode">list</field>
</record>
<!-- Discrepancy Views -->
<record id="view_inventory_discrepancy_list" model="ir.ui.view">
<field name="name">fusion.inventory.discrepancy.list</field>
<field name="model">fusion.inventory.discrepancy</field>
<field name="arch" type="xml">
<list>
<field name="scan_date"/>
<field name="product_id"/>
<field name="discrepancy_type"/>
<field name="missing_serials"/>
<field name="expected_qty"/>
<field name="actual_qty"/>
<field name="difference"
decoration-danger="difference != 0"/>
<field name="source"/>
<field name="state" widget="badge"
decoration-danger="state == 'detected'"
decoration-warning="state == 'reviewed'"
decoration-success="state == 'resolved'"
decoration-muted="state == 'ignored'"/>
</list>
</field>
</record>
<record id="view_inventory_discrepancy_form" model="ir.ui.view">
<field name="name">fusion.inventory.discrepancy.form</field>
<field name="model">fusion.inventory.discrepancy</field>
<field name="arch" type="xml">
<form>
<header>
<button name="action_mark_reviewed" type="object"
string="Mark Reviewed" class="btn-primary"
invisible="state != 'detected'"/>
<button name="action_mark_resolved" type="object"
string="Resolve" class="btn-success"
invisible="state != 'reviewed'"/>
<button name="action_ignore" type="object"
string="Ignore" class="btn-secondary"
invisible="state in ('resolved', 'ignored')"/>
<field name="state" widget="statusbar"
statusbar_visible="detected,reviewed,resolved"/>
</header>
<sheet>
<group>
<group>
<field name="product_id"/>
<field name="discrepancy_type"/>
<field name="scan_date"/>
<field name="reviewed_by"/>
</group>
<group>
<field name="expected_qty"/>
<field name="actual_qty"/>
<field name="difference"/>
<field name="source"/>
</group>
</group>
<group string="Serial Numbers">
<field name="missing_serials" nolabel="1"/>
</group>
<group string="Resolution">
<field name="resolution_notes" nolabel="1"
placeholder="Describe how this was resolved..."/>
</group>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="action_inventory_discrepancy" model="ir.actions.act_window">
<field name="name">Discrepancies</field>
<field name="res_model">fusion.inventory.discrepancy</field>
<field name="view_mode">list,form</field>
<field name="context">{'search_default_state': 'detected'}</field>
</record>
</odoo>