feat: add Cost and Margin % columns with inline editing
Cost column shows Odoo standard_price (editable). Margin % is calculated from cost and sale price. Editing margin auto-calculates the sale price using: price = cost / (1 - margin/100). All cells are inline-editable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -158,6 +158,7 @@ class WooProductSearchController(http.Controller):
|
|||||||
'odoo_product_name': m.product_id.name if m.product_id else '',
|
'odoo_product_name': m.product_id.name if m.product_id else '',
|
||||||
'odoo_default_code': m.product_id.default_code or '' if m.product_id else '',
|
'odoo_default_code': m.product_id.default_code or '' if m.product_id else '',
|
||||||
'odoo_price': m.product_id.list_price if m.product_id else 0.0,
|
'odoo_price': m.product_id.list_price if m.product_id else 0.0,
|
||||||
|
'odoo_cost': m.product_id.standard_price if m.product_id else 0.0,
|
||||||
'woo_regular_price': m.woo_regular_price or 0.0,
|
'woo_regular_price': m.woo_regular_price or 0.0,
|
||||||
'woo_sale_price': m.woo_sale_price or 0.0,
|
'woo_sale_price': m.woo_sale_price or 0.0,
|
||||||
'sync_price': m.sync_price,
|
'sync_price': m.sync_price,
|
||||||
|
|||||||
@@ -626,6 +626,11 @@ html[style*="color-scheme: dark"] {
|
|||||||
.woo-editable-cell:hover {
|
.woo-editable-cell:hover {
|
||||||
background: var(--woo-bg-hover) !important;
|
background: var(--woo-bg-hover) !important;
|
||||||
}
|
}
|
||||||
|
.woo-margin-cell {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--woo-success);
|
||||||
|
}
|
||||||
|
|
||||||
.woo-edit-input {
|
.woo-edit-input {
|
||||||
width: 90px;
|
width: 90px;
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
|
|||||||
@@ -226,6 +226,19 @@ export class ProductMapping extends Component {
|
|||||||
return "$" + n.toFixed(2);
|
return "$" + n.toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
calcMargin(cost, price) {
|
||||||
|
const c = parseFloat(cost) || 0;
|
||||||
|
const p = parseFloat(price) || 0;
|
||||||
|
if (p <= 0 || c <= 0) return null;
|
||||||
|
return ((p - c) / p) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatMargin(cost, price) {
|
||||||
|
const m = this.calcMargin(cost, price);
|
||||||
|
if (m === null) return "—";
|
||||||
|
return m.toFixed(1) + "%";
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Tab handlers
|
// Tab handlers
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@@ -606,6 +619,44 @@ export class ProductMapping extends Component {
|
|||||||
kwargs: {},
|
kwargs: {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else if (cell.field === 'odoo_cost') {
|
||||||
|
const product = this.state.mappedProducts.find(p => p.id === cell.mapId);
|
||||||
|
if (product && product.odoo_product_id) {
|
||||||
|
await rpc("/web/dataset/call_kw", {
|
||||||
|
model: "product.product",
|
||||||
|
method: "write",
|
||||||
|
args: [[product.odoo_product_id], { standard_price: value }],
|
||||||
|
kwargs: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (cell.field === 'margin') {
|
||||||
|
// Margin edit: calculate new sale price from cost and desired margin
|
||||||
|
const product = this.state.mappedProducts.find(p => p.id === cell.mapId);
|
||||||
|
if (product && product.odoo_product_id) {
|
||||||
|
const cost = parseFloat(product.odoo_cost) || 0;
|
||||||
|
if (cost <= 0) {
|
||||||
|
this.notification.add("Cannot calculate price: cost is zero.", { type: "danger" });
|
||||||
|
await this._loadMapped();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value >= 100) {
|
||||||
|
this.notification.add("Margin cannot be 100% or more.", { type: "danger" });
|
||||||
|
await this._loadMapped();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// price = cost / (1 - margin/100)
|
||||||
|
const newPrice = cost / (1 - value / 100);
|
||||||
|
const rounded = Math.round(newPrice * 100) / 100;
|
||||||
|
await rpc("/web/dataset/call_kw", {
|
||||||
|
model: "product.product",
|
||||||
|
method: "write",
|
||||||
|
args: [[product.odoo_product_id], { list_price: rounded }],
|
||||||
|
kwargs: {},
|
||||||
|
});
|
||||||
|
this.notification.add("Sale price set to $" + rounded.toFixed(2) + " (" + value.toFixed(1) + "% margin).", { type: "success" });
|
||||||
|
await this._loadMapped();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.notification.add("Price updated.", { type: "success" });
|
this.notification.add("Price updated.", { type: "success" });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -144,6 +144,8 @@
|
|||||||
<th>WC Sale</th>
|
<th>WC Sale</th>
|
||||||
<th class="text-center"><i class="fa fa-exchange" title="Price Sync"/></th>
|
<th class="text-center"><i class="fa fa-exchange" title="Price Sync"/></th>
|
||||||
<th>Odoo Price</th>
|
<th>Odoo Price</th>
|
||||||
|
<th>Cost</th>
|
||||||
|
<th>Margin %</th>
|
||||||
<th>Instance</th>
|
<th>Instance</th>
|
||||||
<th>Price Sync</th>
|
<th>Price Sync</th>
|
||||||
<th>Inventory Sync</th>
|
<th>Inventory Sync</th>
|
||||||
@@ -216,6 +218,28 @@
|
|||||||
</t>
|
</t>
|
||||||
<t t-else="" t-esc="this.formatPrice(p.odoo_price)"/>
|
<t t-else="" t-esc="this.formatPrice(p.odoo_price)"/>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="text-end woo-editable-cell" t-on-click.stop="() => this.startEdit(p.id, 'odoo_cost', p.odoo_cost)">
|
||||||
|
<t t-if="this.isEditing(p.id, 'odoo_cost')">
|
||||||
|
<input type="number" step="0.01" min="0" class="woo-edit-input"
|
||||||
|
t-att-value="state.editValue"
|
||||||
|
t-on-input="onEditInput"
|
||||||
|
t-on-keydown="onEditKeydown"
|
||||||
|
t-on-blur="onEditBlur"
|
||||||
|
/>
|
||||||
|
</t>
|
||||||
|
<t t-else="" t-esc="this.formatPrice(p.odoo_cost)"/>
|
||||||
|
</td>
|
||||||
|
<td class="text-end woo-editable-cell woo-margin-cell" t-on-click.stop="() => this.startEdit(p.id, 'margin', this.calcMargin(p.odoo_cost, p.odoo_price))">
|
||||||
|
<t t-if="this.isEditing(p.id, 'margin')">
|
||||||
|
<input type="number" step="0.1" min="0" max="99.9" class="woo-edit-input"
|
||||||
|
t-att-value="state.editValue"
|
||||||
|
t-on-input="onEditInput"
|
||||||
|
t-on-keydown="onEditKeydown"
|
||||||
|
t-on-blur="onEditBlur"
|
||||||
|
/>
|
||||||
|
</t>
|
||||||
|
<t t-else="" t-esc="this.formatMargin(p.odoo_cost, p.odoo_price)"/>
|
||||||
|
</td>
|
||||||
<td><t t-esc="p.instance_name"/></td>
|
<td><t t-esc="p.instance_name"/></td>
|
||||||
<td>
|
<td>
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
|
|||||||
Reference in New Issue
Block a user