From 3179cc1f7b43a948e12630c338a7e33fd768e346 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Wed, 1 Apr 2026 13:32:26 -0400 Subject: [PATCH] feat: add editable WC SKU and Odoo SKU columns with bidirectional sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both SKU fields are inline-editable. Arrow buttons sync individual SKUs. Bulk buttons sync all SKUs Odoo→WC or WC→Odoo. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../fusion_woocommerce/models/woo_instance.py | 24 ++++ .../models/woo_product_map.py | 40 ++++++ .../static/src/css/woo_styles.css | 5 + .../static/src/js/product_mapping.js | 130 ++++++++++++++++-- .../static/src/xml/product_mapping.xml | 43 +++++- 5 files changed, 230 insertions(+), 12 deletions(-) diff --git a/fusion-woo-odoo/fusion_woocommerce/models/woo_instance.py b/fusion-woo-odoo/fusion_woocommerce/models/woo_instance.py index a725ef58..08fb9480 100644 --- a/fusion-woo-odoo/fusion_woocommerce/models/woo_instance.py +++ b/fusion-woo-odoo/fusion_woocommerce/models/woo_instance.py @@ -772,6 +772,30 @@ class WooInstance(models.Model): ]) maps.action_push_price_to_odoo() + # ------------------------------------------------------------------ + # Bulk SKU Sync (UI actions) + # ------------------------------------------------------------------ + + def action_bulk_sku_odoo_to_wc(self): + """Push all Odoo SKUs to WooCommerce.""" + self.ensure_one() + maps = self.env['woo.product.map'].search([ + ('instance_id', '=', self.id), + ('state', '=', 'mapped'), + ('product_id', '!=', False), + ]) + maps.action_push_sku_to_wc() + + def action_bulk_sku_wc_to_odoo(self): + """Pull all WC SKUs to Odoo.""" + self.ensure_one() + maps = self.env['woo.product.map'].search([ + ('instance_id', '=', self.id), + ('state', '=', 'mapped'), + ('product_id', '!=', False), + ]) + maps.action_push_sku_to_odoo() + # ------------------------------------------------------------------ # Product / Price Sync (Task 22) # ------------------------------------------------------------------ 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 cd5407fb..e6a1a577 100644 --- a/fusion-woo-odoo/fusion_woocommerce/models/woo_product_map.py +++ b/fusion-woo-odoo/fusion_woocommerce/models/woo_product_map.py @@ -155,6 +155,46 @@ class WooProductMap(models.Model): 'Sale price set to $%.2f' % price, ) + # ------------------------------------------------------------------ + # SKU Sync + # ------------------------------------------------------------------ + + def action_set_wc_sku(self, sku): + """Set WC product SKU.""" + self.ensure_one() + if not self.instance_id: + return + client = self.instance_id._get_client() + client.update_product(self.woo_product_id, {'sku': sku}) + self.woo_sku = sku + self.instance_id._log_sync( + 'product', 'odoo_to_woo', self.woo_product_name, 'success', + 'WC SKU set to %s' % sku, + ) + + def action_push_sku_to_odoo(self): + """Copy WC SKU to Odoo internal reference.""" + for rec in self: + if rec.product_id and rec.woo_sku: + rec.product_id.default_code = rec.woo_sku + rec.instance_id._log_sync( + 'product', 'woo_to_odoo', rec.product_id.name, 'success', + 'Odoo SKU set from WC: %s' % rec.woo_sku, + ) + + def action_push_sku_to_wc(self): + """Copy Odoo internal reference to WC SKU.""" + for rec in self: + if rec.product_id and rec.instance_id: + sku = rec.product_id.default_code or '' + client = rec.instance_id._get_client() + client.update_product(rec.woo_product_id, {'sku': sku}) + rec.woo_sku = sku + rec.instance_id._log_sync( + 'product', 'odoo_to_woo', rec.product_id.name, 'success', + 'WC SKU set from Odoo: %s' % sku, + ) + # ------------------------------------------------------------------ # 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 30c2a387..2aa8caf8 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 @@ -643,3 +643,8 @@ html[style*="color-scheme: dark"] { outline: none; box-shadow: 0 0 0 2px var(--woo-accent-glow); } + +.woo-edit-input-text { + text-align: left; + width: 120px; +} 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 eb6b0aa9..1593117e 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 @@ -546,12 +546,15 @@ export class ProductMapping extends Component { startEdit(mapId, field, currentValue) { this.state.editingCell = { mapId, field }; let val = currentValue !== null && currentValue !== undefined ? currentValue : ''; - if (val !== '' && field === 'margin') { - val = String(Math.round(parseFloat(val))); - } else if (val !== '') { - val = String(parseFloat(parseFloat(val).toFixed(2))); + const isSkuField = field === 'wc_sku' || field === 'odoo_sku'; + if (!isSkuField) { + if (val !== '' && field === 'margin') { + val = String(Math.round(parseFloat(val))); + } else if (val !== '') { + val = String(parseFloat(parseFloat(val).toFixed(2))); + } } - this.state.editValue = val; + this.state.editValue = String(val); // Focus the input after OWL re-renders setTimeout(() => { const input = document.querySelector('.woo-edit-input'); @@ -590,17 +593,48 @@ export class ProductMapping extends Component { 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; + const rawValue = this.state.editValue; + const isSkuField = cell.field === 'wc_sku' || cell.field === 'odoo_sku'; + + if (!isSkuField) { + const value = parseFloat(rawValue); + 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 (isSkuField) { + const skuValue = String(rawValue || ''); + if (cell.field === 'wc_sku') { + await rpc("/web/dataset/call_kw", { + model: "woo.product.map", + method: "action_set_wc_sku", + args: [[cell.mapId], skuValue], + kwargs: {}, + }); + } else if (cell.field === 'odoo_sku') { + 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], { default_code: skuValue }], + kwargs: {}, + }); + } + } + this.notification.add("SKU updated.", { type: "success" }); + await this._loadMapped(); + return; + } + + const value = parseFloat(rawValue); if (cell.field === 'woo_regular') { await rpc("/web/dataset/call_kw", { model: "woo.product.map", @@ -709,6 +743,40 @@ export class ProductMapping extends Component { } } + // ------------------------------------------------------------------------- + // Individual SKU sync + // ------------------------------------------------------------------------- + + async pushSkuToOdoo(mapId) { + try { + await rpc("/web/dataset/call_kw", { + model: "woo.product.map", + method: "action_push_sku_to_odoo", + args: [[mapId]], + kwargs: {}, + }); + this.notification.add("WC SKU pushed to Odoo.", { type: "success" }); + await this._loadMapped(); + } catch (err) { + this.notification.add(err.message || "Failed.", { type: "danger" }); + } + } + + async pushSkuToWC(mapId) { + try { + await rpc("/web/dataset/call_kw", { + model: "woo.product.map", + method: "action_push_sku_to_wc", + args: [[mapId]], + kwargs: {}, + }); + this.notification.add("Odoo SKU pushed to WC.", { type: "success" }); + await this._loadMapped(); + } catch (err) { + this.notification.add(err.message || "Failed.", { type: "danger" }); + } + } + // ------------------------------------------------------------------------- // Bulk price sync // ------------------------------------------------------------------------- @@ -759,6 +827,48 @@ export class ProductMapping extends Component { } } + // ------------------------------------------------------------------------- + // Bulk SKU sync + // ------------------------------------------------------------------------- + + async bulkSkuOdooToWC() { + if (!this.state.instanceId) { this.notification.add("Select an instance.", { type: "warning" }); return; } + this.state.loading = true; + try { + await rpc("/web/dataset/call_kw", { + model: "woo.instance", + method: "action_bulk_sku_odoo_to_wc", + args: [[this.state.instanceId]], + kwargs: {}, + }); + this.notification.add("All Odoo SKUs pushed to WooCommerce.", { type: "success" }); + await this._refreshAll(); + } catch (err) { + this.notification.add("Bulk SKU sync failed.", { type: "danger" }); + } finally { + this.state.loading = false; + } + } + + async bulkSkuWCToOdoo() { + if (!this.state.instanceId) { this.notification.add("Select an instance.", { type: "warning" }); return; } + this.state.loading = true; + try { + await rpc("/web/dataset/call_kw", { + model: "woo.instance", + method: "action_bulk_sku_wc_to_odoo", + args: [[this.state.instanceId]], + kwargs: {}, + }); + this.notification.add("All WC SKUs pulled to Odoo.", { type: "success" }); + await this._refreshAll(); + } catch (err) { + this.notification.add("Bulk SKU sync failed.", { type: "danger" }); + } finally { + this.state.loading = false; + } + } + // ------------------------------------------------------------------------- // Top bar actions // ------------------------------------------------------------------------- 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 5734db4d..af80bc82 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 @@ -121,6 +121,12 @@ + + @@ -138,7 +144,9 @@ WooCommerce Product - SKU + WC SKU + + Odoo SKU Odoo Product WC Standard WC Sale @@ -168,7 +176,38 @@ - + + + + + + + + + + + + + + + +