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 @@
+
+
WooCommerce Product
- SKU
+ WC SKU
+
+ Odoo SKU
Odoo Product
WC Standard
WC Sale
@@ -168,7 +176,38 @@