changes
This commit is contained in:
72
fusion_inventory/views/menus.xml
Normal file
72
fusion_inventory/views/menus.xml
Normal 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>
|
||||
847
fusion_inventory/views/portal_inventory_templates.xml
Normal file
847
fusion_inventory/views/portal_inventory_templates.xml
Normal 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'">🔗 </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">●</span> In Stock</span>
|
||||
<span><span class="badge bg-warning text-dark">●</span> Booked</span>
|
||||
<span><span class="badge bg-info">●</span> Incoming (PO)</span>
|
||||
<span><span class="badge bg-danger">●</span> Out of Stock</span>
|
||||
<span t-if="has_sync"><span class="badge bg-purple" style="background:#6f42c1!important;">●</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'] <= 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) <= 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'] <= 0 and p.get('remote_qty', 0) <= 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'] <= 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 && 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 <= 1) { nav.innerHTML = ''; return; }
|
||||
|
||||
var html = '<ul class="pagination pagination-sm justify-content-center">';
|
||||
html += '<li class="page-item' + (page <= 1 ? ' disabled' : '') + '">'
|
||||
+ '<a class="page-link" href="#" data-page="' + (page - 1) + '">&lt;</a></li>';
|
||||
|
||||
for (var pg = 1; pg <= totalPages; pg++) {
|
||||
if (pg <= 3 || Math.abs(pg - page) <= 1 || pg === totalPages) {
|
||||
html += '<li class="page-item' + (pg === page ? ' active' : '') + '">'
|
||||
+ '<a class="page-link" href="#" data-page="' + pg + '">' + pg + '</a></li>';
|
||||
} else if (pg === 4 && page > 5) {
|
||||
html += '<li class="page-item disabled"><span class="page-link">...</span></li>';
|
||||
} else if (pg === totalPages - 1 && page < totalPages - 3) {
|
||||
html += '<li class="page-item disabled"><span class="page-link">...</span></li>';
|
||||
}
|
||||
}
|
||||
|
||||
html += '<li class="page-item' + (page >= totalPages ? ' disabled' : '') + '">'
|
||||
+ '<a class="page-link" href="#" data-page="' + (page + 1) + '">&gt;</a></li>';
|
||||
html += '</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 && pg <= 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 <= 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 += '<button class="btn btn-sm btn-outline-primary fi-book-btn" data-id="' + p.id + '" data-name="' + escHtml(p.name) + '">Book</button>';
|
||||
}
|
||||
if (hasSync && remoteQty > 0 && p.id > 0) {
|
||||
actionHtml += '<button class="btn btn-sm btn-outline-secondary fi-transfer-btn" data-id="' + p.id + '" data-name="' + escHtml(p.name) + '" data-remote-qty="' + remoteQty + '"><i class="fa fa-exchange"></i></button>';
|
||||
}
|
||||
if (!actionHtml) actionHtml = '<span class="text-muted small">--</span>';
|
||||
actionHtml = '<div class="btn-group btn-group-sm">' + actionHtml + '</div>';
|
||||
} else {
|
||||
actionHtml = '<span class="text-muted small">' + escHtml(p.config_name || '') + '</span>';
|
||||
}
|
||||
|
||||
var remoteCol = '';
|
||||
var totalCol = '';
|
||||
if (hasSync) {
|
||||
var whTooltip = (p.remote_warehouses || []).map(function(w) { return w.warehouse + ': ' + w.qty; }).join(', ');
|
||||
remoteCol = remoteQty > 0
|
||||
? '<span class="badge text-white fi-remote-badge" style="background:#6f42c1; cursor:pointer;" title="' + escHtml(whTooltip) + '">' + remoteQty + '</span>'
|
||||
: '<span class="text-muted">0</span>';
|
||||
totalCol = '<strong>' + totalQty + '</strong>';
|
||||
}
|
||||
|
||||
var trClass = isRemoteOnly ? ' class="table-light fst-italic"' : '';
|
||||
var nameCell = isRemoteOnly
|
||||
? '<span class="badge bg-secondary me-1">Remote</span>' + escHtml(p.name)
|
||||
: '<strong>' + escHtml(p.name) + '</strong>';
|
||||
|
||||
html += '<tr' + trClass + ' data-id="' + p.id + '">'
|
||||
+ '<td class="fi-col-product">' + nameCell + '</td>'
|
||||
+ '<td class="fi-col-sku text-muted">' + escHtml(p.default_code) + '</td>'
|
||||
+ '<td class="fi-col-cat">' + escHtml(p.category) + '</td>'
|
||||
+ '<td class="fi-col-num">' + p.qty_on_hand + '</td>'
|
||||
+ '<td class="fi-col-num"><span class="fw-bold ' + availClass + '">' + p.available_qty + '</span></td>'
|
||||
+ (hasSync ? '<td class="fi-col-num">' + remoteCol + '</td>' : '')
|
||||
+ (hasSync ? '<td class="fi-col-num">' + totalCol + '</td>' : '')
|
||||
+ '<td class="fi-col-num">' + (p.booked_qty > 0 ? '<span class="badge bg-warning text-dark">' + p.booked_qty + '</span>' : '') + '</td>'
|
||||
+ '<td class="fi-col-num">' + (p.shadow_qty > 0 ? '<span class="badge bg-info">' + p.shadow_qty + '</span>' : '') + '</td>'
|
||||
+ '<td class="fi-col-price">$' + p.sale_price.toFixed(2) + '</td>'
|
||||
+ '<td class="fi-col-num"><span class="badge ' + marginClass + '">' + p.margin_pct.toFixed(1) + '%</span></td>'
|
||||
+ '<td class="fi-col-action">' + actionHtml + '</td>'
|
||||
+ '</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 <= 0 ? 'text-danger' : '';
|
||||
|
||||
var remoteMobile = '';
|
||||
if (hasSync) {
|
||||
remoteMobile = '<div class="col">'
|
||||
+ '<div class="fw-bold" style="color:#6f42c1;">' + remoteQty + '</div>'
|
||||
+ '<div class="text-muted" style="font-size:.7rem;">Remote</div>'
|
||||
+ '</div>';
|
||||
}
|
||||
|
||||
var btns = '';
|
||||
if (p.available_qty > 0) {
|
||||
btns += '<button class="btn btn-sm btn-outline-primary fi-book-btn" data-id="' + p.id + '" data-name="' + escHtml(p.name) + '">Book</button>';
|
||||
}
|
||||
if (hasSync && remoteQty > 0 && p.id > 0) {
|
||||
btns += '<button class="btn btn-sm btn-outline-secondary fi-transfer-btn" data-id="' + p.id + '" data-name="' + escHtml(p.name) + '" data-remote-qty="' + remoteQty + '"><i class="fa fa-exchange"></i> Transfer</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 += '<div class="card mb-2 fi-card" data-id="' + p.id + '" style="border-left: 4px solid ' + borderColor + ';">'
|
||||
+ '<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">' + escHtml(p.name) + '</div>'
|
||||
+ '<small class="text-muted">' + escHtml(p.default_code) + (p.category ? ' | ' + escHtml(p.category) : '') + '</small>'
|
||||
+ '</div>'
|
||||
+ '<div class="text-end">'
|
||||
+ '<div class="fw-bold">$' + p.sale_price.toFixed(2) + '</div>'
|
||||
+ '<span class="badge ' + marginClass + ' small">' + p.margin_pct.toFixed(1) + '%</span>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
+ '<div class="' + rowClass + '" style="font-size:.85rem;">'
|
||||
+ '<div class="' + colClass + '"><div class="fw-bold">' + p.qty_on_hand + '</div><div class="text-muted" style="font-size:.7rem;">Local</div></div>'
|
||||
+ '<div class="' + colClass + '"><div class="fw-bold ' + availClass + '">' + p.available_qty + '</div><div class="text-muted" style="font-size:.7rem;">Avail</div></div>'
|
||||
+ remoteMobile
|
||||
+ '<div class="' + colClass + '"><div class="fw-bold">' + p.booked_qty + '</div><div class="text-muted" style="font-size:.7rem;">Booked</div></div>'
|
||||
+ '<div class="' + colClass + '"><div class="fw-bold">' + p.shadow_qty + '</div><div class="text-muted" style="font-size:.7rem;">Incoming</div></div>'
|
||||
+ '</div>'
|
||||
+ (btns ? '<div class="mt-2 text-end"><div class="btn-group btn-group-sm">' + btns + '</div></div>' : '')
|
||||
+ '</div></div>';
|
||||
});
|
||||
container.innerHTML = html;
|
||||
bindBookButtons();
|
||||
bindTransferButtons();
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
if (!s) return '';
|
||||
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&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 = '<div class="px-3 py-2 text-muted small">No categories found</div>';
|
||||
}
|
||||
cats.forEach(function(c) {
|
||||
html += '<div class="px-3 py-2 fi-cat-option" style="cursor:pointer;" data-id="' + c.id + '" data-name="' + escHtml(c.name) + '">'
|
||||
+ escHtml(c.name) + '</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) && !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>
|
||||
38
fusion_inventory/views/product_attribute_views.xml
Normal file
38
fusion_inventory/views/product_attribute_views.xml
Normal 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>
|
||||
175
fusion_inventory/views/product_brand_views.xml
Normal file
175
fusion_inventory/views/product_brand_views.xml
Normal 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', '>', 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>
|
||||
174
fusion_inventory/views/product_template_views.xml
Normal file
174
fusion_inventory/views/product_template_views.xml
Normal 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 & Classification ── -->
|
||||
<separator string="Brand & 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 <= 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>
|
||||
81
fusion_inventory/views/product_views.xml
Normal file
81
fusion_inventory/views/product_views.xml
Normal 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>
|
||||
117
fusion_inventory/views/res_config_settings_views.xml
Normal file
117
fusion_inventory/views/res_config_settings_views.xml
Normal 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>
|
||||
24
fusion_inventory/views/res_partner_views.xml
Normal file
24
fusion_inventory/views/res_partner_views.xml
Normal 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 > 0"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
137
fusion_inventory/views/stock_picking_views.xml
Normal file
137
fusion_inventory/views/stock_picking_views.xml
Normal 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>
|
||||
404
fusion_inventory/views/sync_config_views.xml
Normal file
404
fusion_inventory/views/sync_config_views.xml
Normal 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', '>', 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>
|
||||
Reference in New Issue
Block a user