feat: add editable WC SKU and Odoo SKU columns with bidirectional sync
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user