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:
@@ -772,6 +772,30 @@ class WooInstance(models.Model):
|
|||||||
])
|
])
|
||||||
maps.action_push_price_to_odoo()
|
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)
|
# Product / Price Sync (Task 22)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|||||||
@@ -155,6 +155,46 @@ class WooProductMap(models.Model):
|
|||||||
'Sale price set to $%.2f' % price,
|
'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)
|
# Image Sync (Task 22)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|||||||
@@ -643,3 +643,8 @@ html[style*="color-scheme: dark"] {
|
|||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: 0 0 0 2px var(--woo-accent-glow);
|
box-shadow: 0 0 0 2px var(--woo-accent-glow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.woo-edit-input-text {
|
||||||
|
text-align: left;
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -546,12 +546,15 @@ export class ProductMapping extends Component {
|
|||||||
startEdit(mapId, field, currentValue) {
|
startEdit(mapId, field, currentValue) {
|
||||||
this.state.editingCell = { mapId, field };
|
this.state.editingCell = { mapId, field };
|
||||||
let val = currentValue !== null && currentValue !== undefined ? currentValue : '';
|
let val = currentValue !== null && currentValue !== undefined ? currentValue : '';
|
||||||
if (val !== '' && field === 'margin') {
|
const isSkuField = field === 'wc_sku' || field === 'odoo_sku';
|
||||||
val = String(Math.round(parseFloat(val)));
|
if (!isSkuField) {
|
||||||
} else if (val !== '') {
|
if (val !== '' && field === 'margin') {
|
||||||
val = String(parseFloat(parseFloat(val).toFixed(2)));
|
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
|
// Focus the input after OWL re-renders
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const input = document.querySelector('.woo-edit-input');
|
const input = document.querySelector('.woo-edit-input');
|
||||||
@@ -590,17 +593,48 @@ export class ProductMapping extends Component {
|
|||||||
const cell = this.state.editingCell;
|
const cell = this.state.editingCell;
|
||||||
if (!cell) return;
|
if (!cell) return;
|
||||||
|
|
||||||
const value = parseFloat(this.state.editValue);
|
const rawValue = this.state.editValue;
|
||||||
if (isNaN(value) || value < 0) {
|
const isSkuField = cell.field === 'wc_sku' || cell.field === 'odoo_sku';
|
||||||
this.notification.add("Invalid price value.", { type: "danger" });
|
|
||||||
this.cancelEdit();
|
if (!isSkuField) {
|
||||||
return;
|
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
|
// Cancel first so blur doesn't fire a second save after Enter
|
||||||
this.cancelEdit();
|
this.cancelEdit();
|
||||||
|
|
||||||
try {
|
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') {
|
if (cell.field === 'woo_regular') {
|
||||||
await rpc("/web/dataset/call_kw", {
|
await rpc("/web/dataset/call_kw", {
|
||||||
model: "woo.product.map",
|
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
|
// 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
|
// Top bar actions
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -121,6 +121,12 @@
|
|||||||
<button class="woo-btn woo-btn-secondary woo-btn-sm" t-on-click="bulkPriceWCToOdoo">
|
<button class="woo-btn woo-btn-secondary woo-btn-sm" t-on-click="bulkPriceWCToOdoo">
|
||||||
<i class="fa fa-arrow-left me-1"/> All Prices WC → Odoo
|
<i class="fa fa-arrow-left me-1"/> All Prices WC → Odoo
|
||||||
</button>
|
</button>
|
||||||
|
<button class="woo-btn woo-btn-secondary woo-btn-sm" t-on-click="bulkSkuOdooToWC">
|
||||||
|
<i class="fa fa-arrow-right me-1"/> All SKUs Odoo → WC
|
||||||
|
</button>
|
||||||
|
<button class="woo-btn woo-btn-secondary woo-btn-sm" t-on-click="bulkSkuWCToOdoo">
|
||||||
|
<i class="fa fa-arrow-left me-1"/> All SKUs WC → Odoo
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<t t-if="!state.mappedProducts.length">
|
<t t-if="!state.mappedProducts.length">
|
||||||
@@ -138,7 +144,9 @@
|
|||||||
<input type="checkbox" t-on-change="toggleSelectAllMapped"/>
|
<input type="checkbox" t-on-change="toggleSelectAllMapped"/>
|
||||||
</th>
|
</th>
|
||||||
<th>WooCommerce Product</th>
|
<th>WooCommerce Product</th>
|
||||||
<th>SKU</th>
|
<th>WC SKU</th>
|
||||||
|
<th class="text-center"><i class="fa fa-exchange" title="SKU Sync"/></th>
|
||||||
|
<th>Odoo SKU</th>
|
||||||
<th>Odoo Product</th>
|
<th>Odoo Product</th>
|
||||||
<th>WC Standard</th>
|
<th>WC Standard</th>
|
||||||
<th>WC Sale</th>
|
<th>WC Sale</th>
|
||||||
@@ -168,7 +176,38 @@
|
|||||||
</t>
|
</t>
|
||||||
<t t-else=""><t t-esc="p.woo_product_name"/></t>
|
<t t-else=""><t t-esc="p.woo_product_name"/></t>
|
||||||
</td>
|
</td>
|
||||||
<td><span class="woo-code"><t t-esc="p.woo_sku"/></span></td>
|
<td class="woo-editable-cell" t-on-click.stop="() => this.startEdit(p.id, 'wc_sku', p.woo_sku)">
|
||||||
|
<t t-if="this.isEditing(p.id, 'wc_sku')">
|
||||||
|
<input type="text" class="woo-edit-input woo-edit-input-text"
|
||||||
|
t-att-value="state.editValue"
|
||||||
|
t-on-input="onEditInput"
|
||||||
|
t-on-keydown="onEditKeydown"
|
||||||
|
t-on-blur="onEditBlur"
|
||||||
|
/>
|
||||||
|
</t>
|
||||||
|
<t t-else=""><span class="woo-code"><t t-esc="p.woo_sku"/></span></t>
|
||||||
|
</td>
|
||||||
|
<td class="text-center woo-price-sync-col">
|
||||||
|
<button class="woo-btn-icon" title="Push WC SKU to Odoo"
|
||||||
|
t-on-click.stop="() => this.pushSkuToOdoo(p.id)">
|
||||||
|
<i class="fa fa-arrow-right"/>
|
||||||
|
</button>
|
||||||
|
<button class="woo-btn-icon" title="Push Odoo SKU to WC"
|
||||||
|
t-on-click.stop="() => this.pushSkuToWC(p.id)">
|
||||||
|
<i class="fa fa-arrow-left"/>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td class="woo-editable-cell" t-on-click.stop="() => this.startEdit(p.id, 'odoo_sku', p.odoo_default_code)">
|
||||||
|
<t t-if="this.isEditing(p.id, 'odoo_sku')">
|
||||||
|
<input type="text" class="woo-edit-input woo-edit-input-text"
|
||||||
|
t-att-value="state.editValue"
|
||||||
|
t-on-input="onEditInput"
|
||||||
|
t-on-keydown="onEditKeydown"
|
||||||
|
t-on-blur="onEditBlur"
|
||||||
|
/>
|
||||||
|
</t>
|
||||||
|
<t t-else=""><span class="woo-code"><t t-esc="p.odoo_default_code"/></span></t>
|
||||||
|
</td>
|
||||||
<td><t t-esc="p.odoo_product_name"/></td>
|
<td><t t-esc="p.odoo_product_name"/></td>
|
||||||
<td class="text-end woo-editable-cell" t-on-click.stop="() => this.startEdit(p.id, 'woo_regular', p.woo_regular_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')">
|
<t t-if="this.isEditing(p.id, 'woo_regular')">
|
||||||
|
|||||||
Reference in New Issue
Block a user