feat: inline-editable prices in product mapping UI
Click any price cell (WC Standard, WC Sale, Odoo Price) to edit inline. Enter or click away saves and syncs to the source. Escape cancels. Validation: sale price cannot exceed standard price. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -135,6 +135,26 @@ class WooProductMap(models.Model):
|
||||
'Standard price set to $%.2f' % price,
|
||||
)
|
||||
|
||||
def action_set_sale_price(self, price):
|
||||
"""Set the WC sale price directly."""
|
||||
self.ensure_one()
|
||||
if not self.instance_id:
|
||||
return
|
||||
client = self.instance_id._get_client()
|
||||
# Sale price cannot exceed regular price
|
||||
if self.woo_regular_price and price > self.woo_regular_price + 0.01:
|
||||
raise UserError(
|
||||
'Sale price ($%.2f) cannot exceed the standard price ($%.2f).'
|
||||
% (price, self.woo_regular_price)
|
||||
)
|
||||
update_data = {'sale_price': str(price) if price > 0 else ''}
|
||||
client.update_product(self.woo_product_id, update_data)
|
||||
self.woo_sale_price = price
|
||||
self.instance_id._log_sync(
|
||||
'product', 'odoo_to_woo', self.woo_product_name, 'success',
|
||||
'Sale price set to $%.2f' % price,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Image Sync (Task 22)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@@ -615,3 +615,26 @@ html[style*="color-scheme: dark"] {
|
||||
width: 60px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------
|
||||
Editable price cells
|
||||
---------------------------------------------------------- */
|
||||
.woo-editable-cell {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
.woo-editable-cell:hover {
|
||||
background: var(--woo-bg-hover) !important;
|
||||
}
|
||||
.woo-edit-input {
|
||||
width: 90px;
|
||||
padding: 2px 6px;
|
||||
border: 1px solid var(--woo-accent);
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
text-align: right;
|
||||
background: var(--woo-input-bg);
|
||||
color: var(--woo-text-primary);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px var(--woo-accent-glow);
|
||||
}
|
||||
|
||||
@@ -43,6 +43,10 @@ export class ProductMapping extends Component {
|
||||
mappedPage: 1,
|
||||
mappedTotal: 0,
|
||||
|
||||
// Inline price editing
|
||||
editingCell: null, // { mapId: int, field: 'woo_regular'|'woo_sale'|'odoo_price' }
|
||||
editValue: '',
|
||||
|
||||
// Unmatched tab
|
||||
odooProducts: [],
|
||||
wooProducts: [],
|
||||
@@ -522,6 +526,96 @@ export class ProductMapping extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Inline price editing
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
startEdit(mapId, field, currentValue) {
|
||||
this.state.editingCell = { mapId, field };
|
||||
this.state.editValue = currentValue !== null && currentValue !== undefined ? String(currentValue) : '';
|
||||
// Focus the input after OWL re-renders
|
||||
setTimeout(() => {
|
||||
const input = document.querySelector('.woo-edit-input');
|
||||
if (input) { input.focus(); input.select(); }
|
||||
}, 50);
|
||||
}
|
||||
|
||||
cancelEdit() {
|
||||
this.state.editingCell = null;
|
||||
this.state.editValue = '';
|
||||
}
|
||||
|
||||
onEditInput(ev) {
|
||||
this.state.editValue = ev.target.value;
|
||||
}
|
||||
|
||||
async onEditKeydown(ev) {
|
||||
if (ev.key === 'Enter') {
|
||||
await this.saveEdit();
|
||||
} else if (ev.key === 'Escape') {
|
||||
this.cancelEdit();
|
||||
}
|
||||
}
|
||||
|
||||
async onEditBlur() {
|
||||
await this.saveEdit();
|
||||
}
|
||||
|
||||
isEditing(mapId, field) {
|
||||
return this.state.editingCell &&
|
||||
this.state.editingCell.mapId === mapId &&
|
||||
this.state.editingCell.field === field;
|
||||
}
|
||||
|
||||
async saveEdit() {
|
||||
const cell = this.state.editingCell;
|
||||
if (!cell) return;
|
||||
|
||||
const value = parseFloat(this.state.editValue);
|
||||
if (isNaN(value) || value < 0) {
|
||||
this.notification.add("Invalid price value.", { type: "danger" });
|
||||
this.cancelEdit();
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel first so blur doesn't fire a second save after Enter
|
||||
this.cancelEdit();
|
||||
|
||||
try {
|
||||
if (cell.field === 'woo_regular') {
|
||||
await rpc("/web/dataset/call_kw", {
|
||||
model: "woo.product.map",
|
||||
method: "action_set_regular_price",
|
||||
args: [[cell.mapId], value],
|
||||
kwargs: {},
|
||||
});
|
||||
} else if (cell.field === 'woo_sale') {
|
||||
await rpc("/web/dataset/call_kw", {
|
||||
model: "woo.product.map",
|
||||
method: "action_set_sale_price",
|
||||
args: [[cell.mapId], value],
|
||||
kwargs: {},
|
||||
});
|
||||
} else if (cell.field === 'odoo_price') {
|
||||
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], { list_price: value }],
|
||||
kwargs: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
this.notification.add("Price updated.", { type: "success" });
|
||||
} catch (err) {
|
||||
console.error("[ProductMapping] saveEdit error:", err);
|
||||
this.notification.add(err.message || "Failed to update price.", { type: "danger" });
|
||||
}
|
||||
|
||||
await this._loadMapped();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Individual price sync
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -168,12 +168,32 @@
|
||||
</td>
|
||||
<td><span class="woo-code"><t t-esc="p.woo_sku"/></span></td>
|
||||
<td><t t-esc="p.odoo_product_name"/></td>
|
||||
<td class="text-end" t-esc="this.formatPrice(p.woo_regular_price)"/>
|
||||
<td class="text-end">
|
||||
<t t-if="p.woo_sale_price">
|
||||
<span class="woo-sale-price" t-esc="this.formatPrice(p.woo_sale_price)"/>
|
||||
<td class="text-end woo-editable-cell" t-on-click.stop="() => this.startEdit(p.id, 'woo_regular', p.woo_regular_price)">
|
||||
<t t-if="this.isEditing(p.id, 'woo_regular')">
|
||||
<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.woo_regular_price)"/>
|
||||
</td>
|
||||
<td class="text-end woo-editable-cell" t-on-click.stop="() => this.startEdit(p.id, 'woo_sale', p.woo_sale_price)">
|
||||
<t t-if="this.isEditing(p.id, 'woo_sale')">
|
||||
<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 t-if="p.woo_sale_price">
|
||||
<span class="woo-sale-price" t-esc="this.formatPrice(p.woo_sale_price)"/>
|
||||
</t>
|
||||
<t t-else=""><span class="woo-text-muted">—</span></t>
|
||||
</t>
|
||||
<t t-else=""><span class="woo-text-muted">—</span></t>
|
||||
</td>
|
||||
<td class="text-center woo-price-sync-col">
|
||||
<button class="woo-btn-icon" title="Push WC price to Odoo"
|
||||
@@ -185,7 +205,17 @@
|
||||
<i class="fa fa-arrow-left"/>
|
||||
</button>
|
||||
</td>
|
||||
<td class="text-end" t-esc="this.formatPrice(p.odoo_price)"/>
|
||||
<td class="text-end woo-editable-cell" t-on-click.stop="() => this.startEdit(p.id, 'odoo_price', p.odoo_price)">
|
||||
<t t-if="this.isEditing(p.id, 'odoo_price')">
|
||||
<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_price)"/>
|
||||
</td>
|
||||
<td><t t-esc="p.instance_name"/></td>
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
|
||||
Reference in New Issue
Block a user