diff --git a/fusion-woo-odoo/fusion_woocommerce/models/woo_product_map.py b/fusion-woo-odoo/fusion_woocommerce/models/woo_product_map.py index 0c379943..cd5407fb 100644 --- a/fusion-woo-odoo/fusion_woocommerce/models/woo_product_map.py +++ b/fusion-woo-odoo/fusion_woocommerce/models/woo_product_map.py @@ -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) # ------------------------------------------------------------------ diff --git a/fusion-woo-odoo/fusion_woocommerce/static/src/css/woo_styles.css b/fusion-woo-odoo/fusion_woocommerce/static/src/css/woo_styles.css index 3ed6f689..90fcaf67 100644 --- a/fusion-woo-odoo/fusion_woocommerce/static/src/css/woo_styles.css +++ b/fusion-woo-odoo/fusion_woocommerce/static/src/css/woo_styles.css @@ -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); +} diff --git a/fusion-woo-odoo/fusion_woocommerce/static/src/js/product_mapping.js b/fusion-woo-odoo/fusion_woocommerce/static/src/js/product_mapping.js index 1f247630..b1256ca4 100644 --- a/fusion-woo-odoo/fusion_woocommerce/static/src/js/product_mapping.js +++ b/fusion-woo-odoo/fusion_woocommerce/static/src/js/product_mapping.js @@ -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 // ------------------------------------------------------------------------- diff --git a/fusion-woo-odoo/fusion_woocommerce/static/src/xml/product_mapping.xml b/fusion-woo-odoo/fusion_woocommerce/static/src/xml/product_mapping.xml index 3ca68364..4f341b78 100644 --- a/fusion-woo-odoo/fusion_woocommerce/static/src/xml/product_mapping.xml +++ b/fusion-woo-odoo/fusion_woocommerce/static/src/xml/product_mapping.xml @@ -168,12 +168,32 @@