feat: add pagination, individual price sync arrows, and bulk price sync
- Pagination with page nav for mapped, unmatched Odoo, and unmatched WC tabs - Per-product arrow buttons to push price in either direction - Bulk price sync buttons: All Prices Odoo→WC and All Prices WC→Odoo - Server-side offset/limit with total count in search endpoints Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,7 @@ class WooProductSearchController(http.Controller):
|
|||||||
'/woo/search/odoo_products',
|
'/woo/search/odoo_products',
|
||||||
type='jsonrpc', auth='user', methods=['POST'],
|
type='jsonrpc', auth='user', methods=['POST'],
|
||||||
)
|
)
|
||||||
def search_odoo_products(self, query='', instance_id=None, limit=20, **kw):
|
def search_odoo_products(self, query='', instance_id=None, limit=20, offset=0, **kw):
|
||||||
"""
|
"""
|
||||||
Search Odoo products by name or internal reference (SKU).
|
Search Odoo products by name or internal reference (SKU).
|
||||||
|
|
||||||
@@ -29,11 +29,13 @@ class WooProductSearchController(http.Controller):
|
|||||||
query (str): Search string matched against name and default_code.
|
query (str): Search string matched against name and default_code.
|
||||||
instance_id (int): woo.instance ID (used for future per-instance filtering).
|
instance_id (int): woo.instance ID (used for future per-instance filtering).
|
||||||
limit (int): Max results to return (default 20).
|
limit (int): Max results to return (default 20).
|
||||||
|
offset (int): Offset for pagination (default 0).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list of {id, name, default_code, list_price, qty_available}
|
dict with 'results' list and 'total' count
|
||||||
"""
|
"""
|
||||||
limit = min(int(limit or 20), 100)
|
limit = min(int(limit or 20), 100)
|
||||||
|
offset = int(offset or 0)
|
||||||
domain = []
|
domain = []
|
||||||
|
|
||||||
if query:
|
if query:
|
||||||
@@ -43,24 +45,28 @@ class WooProductSearchController(http.Controller):
|
|||||||
('default_code', 'ilike', query),
|
('default_code', 'ilike', query),
|
||||||
]
|
]
|
||||||
|
|
||||||
products = request.env['product.product'].search(domain, limit=limit)
|
total = request.env['product.product'].search_count(domain)
|
||||||
|
products = request.env['product.product'].search(domain, limit=limit, offset=offset)
|
||||||
|
|
||||||
return [
|
return {
|
||||||
{
|
'results': [
|
||||||
'id': p.id,
|
{
|
||||||
'name': p.name,
|
'id': p.id,
|
||||||
'default_code': p.default_code or '',
|
'name': p.name,
|
||||||
'list_price': p.list_price,
|
'default_code': p.default_code or '',
|
||||||
'qty_available': p.qty_available,
|
'list_price': p.list_price,
|
||||||
}
|
'qty_available': p.qty_available,
|
||||||
for p in products
|
}
|
||||||
]
|
for p in products
|
||||||
|
],
|
||||||
|
'total': total,
|
||||||
|
}
|
||||||
|
|
||||||
@http.route(
|
@http.route(
|
||||||
'/woo/search/woo_products',
|
'/woo/search/woo_products',
|
||||||
type='jsonrpc', auth='user', methods=['POST'],
|
type='jsonrpc', auth='user', methods=['POST'],
|
||||||
)
|
)
|
||||||
def search_woo_products(self, query='', instance_id=None, limit=20, **kw):
|
def search_woo_products(self, query='', instance_id=None, limit=20, offset=0, **kw):
|
||||||
"""
|
"""
|
||||||
Search unmapped WooCommerce products from the woo.product.map model.
|
Search unmapped WooCommerce products from the woo.product.map model.
|
||||||
|
|
||||||
@@ -68,11 +74,13 @@ class WooProductSearchController(http.Controller):
|
|||||||
query (str): Search string matched against woo_product_name and woo_sku.
|
query (str): Search string matched against woo_product_name and woo_sku.
|
||||||
instance_id (int): woo.instance ID — filters results to this instance.
|
instance_id (int): woo.instance ID — filters results to this instance.
|
||||||
limit (int): Max results to return (default 20).
|
limit (int): Max results to return (default 20).
|
||||||
|
offset (int): Offset for pagination (default 0).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list of {id, woo_product_id, woo_product_name, woo_sku, woo_product_type}
|
dict with 'results' list and 'total' count
|
||||||
"""
|
"""
|
||||||
limit = min(int(limit or 20), 100)
|
limit = min(int(limit or 20), 100)
|
||||||
|
offset = int(offset or 0)
|
||||||
domain = [('state', '=', 'unmapped')]
|
domain = [('state', '=', 'unmapped')]
|
||||||
|
|
||||||
if instance_id:
|
if instance_id:
|
||||||
@@ -85,24 +93,28 @@ class WooProductSearchController(http.Controller):
|
|||||||
('woo_sku', 'ilike', query),
|
('woo_sku', 'ilike', query),
|
||||||
]
|
]
|
||||||
|
|
||||||
maps = request.env['woo.product.map'].search(domain, limit=limit)
|
total = request.env['woo.product.map'].search_count(domain)
|
||||||
|
maps = request.env['woo.product.map'].search(domain, limit=limit, offset=offset)
|
||||||
|
|
||||||
return [
|
return {
|
||||||
{
|
'results': [
|
||||||
'id': m.id,
|
{
|
||||||
'woo_product_id': m.woo_product_id,
|
'id': m.id,
|
||||||
'woo_product_name': m.woo_product_name or '',
|
'woo_product_id': m.woo_product_id,
|
||||||
'woo_sku': m.woo_sku or '',
|
'woo_product_name': m.woo_product_name or '',
|
||||||
'woo_product_type': m.woo_product_type or '',
|
'woo_sku': m.woo_sku or '',
|
||||||
}
|
'woo_product_type': m.woo_product_type or '',
|
||||||
for m in maps
|
}
|
||||||
]
|
for m in maps
|
||||||
|
],
|
||||||
|
'total': total,
|
||||||
|
}
|
||||||
|
|
||||||
@http.route(
|
@http.route(
|
||||||
'/woo/search/mapped',
|
'/woo/search/mapped',
|
||||||
type='jsonrpc', auth='user', methods=['POST'],
|
type='jsonrpc', auth='user', methods=['POST'],
|
||||||
)
|
)
|
||||||
def search_mapped(self, query='', instance_id=None, limit=20, **kw):
|
def search_mapped(self, query='', instance_id=None, limit=20, offset=0, **kw):
|
||||||
"""
|
"""
|
||||||
Search mapped WooCommerce ↔ Odoo product pairs.
|
Search mapped WooCommerce ↔ Odoo product pairs.
|
||||||
|
|
||||||
@@ -110,11 +122,13 @@ class WooProductSearchController(http.Controller):
|
|||||||
query (str): Matched against woo_product_name, woo_sku, and linked product name.
|
query (str): Matched against woo_product_name, woo_sku, and linked product name.
|
||||||
instance_id (int): woo.instance ID — filters results to this instance.
|
instance_id (int): woo.instance ID — filters results to this instance.
|
||||||
limit (int): Max results to return (default 20).
|
limit (int): Max results to return (default 20).
|
||||||
|
offset (int): Offset for pagination (default 0).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list of mapped product data dicts
|
dict with 'results' list and 'total' count
|
||||||
"""
|
"""
|
||||||
limit = min(int(limit or 20), 100)
|
limit = min(int(limit or 20), 100)
|
||||||
|
offset = int(offset or 0)
|
||||||
domain = [('state', '=', 'mapped')]
|
domain = [('state', '=', 'mapped')]
|
||||||
|
|
||||||
if instance_id:
|
if instance_id:
|
||||||
@@ -128,24 +142,28 @@ class WooProductSearchController(http.Controller):
|
|||||||
('product_id.name', 'ilike', query),
|
('product_id.name', 'ilike', query),
|
||||||
]
|
]
|
||||||
|
|
||||||
maps = request.env['woo.product.map'].search(domain, limit=limit)
|
total = request.env['woo.product.map'].search_count(domain)
|
||||||
|
maps = request.env['woo.product.map'].search(domain, limit=limit, offset=offset)
|
||||||
|
|
||||||
return [
|
return {
|
||||||
{
|
'results': [
|
||||||
'id': m.id,
|
{
|
||||||
'woo_product_id': m.woo_product_id,
|
'id': m.id,
|
||||||
'woo_product_name': m.woo_product_name or '',
|
'woo_product_id': m.woo_product_id,
|
||||||
'woo_sku': m.woo_sku or '',
|
'woo_product_name': m.woo_product_name or '',
|
||||||
'woo_product_type': m.woo_product_type or '',
|
'woo_sku': m.woo_sku or '',
|
||||||
'odoo_product_id': m.product_id.id if m.product_id else False,
|
'woo_product_type': m.woo_product_type or '',
|
||||||
'odoo_product_name': m.product_id.name if m.product_id else '',
|
'odoo_product_id': m.product_id.id if m.product_id else False,
|
||||||
'odoo_default_code': m.product_id.default_code or '' if m.product_id else '',
|
'odoo_product_name': m.product_id.name if m.product_id else '',
|
||||||
'odoo_price': m.product_id.list_price if m.product_id else 0.0,
|
'odoo_default_code': m.product_id.default_code or '' if m.product_id else '',
|
||||||
'woo_price': m.woo_price or 0.0,
|
'odoo_price': m.product_id.list_price if m.product_id else 0.0,
|
||||||
'sync_price': m.sync_price,
|
'woo_price': m.woo_price or 0.0,
|
||||||
'sync_inventory': m.sync_inventory,
|
'sync_price': m.sync_price,
|
||||||
'instance_id': m.instance_id.id if m.instance_id else False,
|
'sync_inventory': m.sync_inventory,
|
||||||
'instance_name': m.instance_id.name if m.instance_id else '',
|
'instance_id': m.instance_id.id if m.instance_id else False,
|
||||||
}
|
'instance_name': m.instance_id.name if m.instance_id else '',
|
||||||
for m in maps
|
}
|
||||||
]
|
for m in maps
|
||||||
|
],
|
||||||
|
'total': total,
|
||||||
|
}
|
||||||
|
|||||||
@@ -727,6 +727,30 @@ class WooInstance(models.Model):
|
|||||||
'price_unit': total,
|
'price_unit': total,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Bulk Price Sync (UI actions)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def action_bulk_price_odoo_to_wc(self):
|
||||||
|
"""Push all Odoo prices to WooCommerce for mapped products."""
|
||||||
|
self.ensure_one()
|
||||||
|
maps = self.env['woo.product.map'].search([
|
||||||
|
('instance_id', '=', self.id),
|
||||||
|
('state', '=', 'mapped'),
|
||||||
|
('product_id', '!=', False),
|
||||||
|
])
|
||||||
|
maps.action_push_price_to_wc()
|
||||||
|
|
||||||
|
def action_bulk_price_wc_to_odoo(self):
|
||||||
|
"""Pull all WC prices to Odoo for mapped products."""
|
||||||
|
self.ensure_one()
|
||||||
|
maps = self.env['woo.product.map'].search([
|
||||||
|
('instance_id', '=', self.id),
|
||||||
|
('state', '=', 'mapped'),
|
||||||
|
('product_id', '!=', False),
|
||||||
|
])
|
||||||
|
maps.action_push_price_to_odoo()
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Product / Price Sync (Task 22)
|
# Product / Price Sync (Task 22)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|||||||
@@ -43,6 +43,33 @@ class WooProductMap(models.Model):
|
|||||||
('error', 'Error'),
|
('error', 'Error'),
|
||||||
], default='unmapped')
|
], default='unmapped')
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Individual Price Sync
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def action_push_price_to_odoo(self):
|
||||||
|
"""Update Odoo product price from WC price."""
|
||||||
|
for rec in self:
|
||||||
|
if rec.product_id and rec.woo_price:
|
||||||
|
rec.product_id.list_price = rec.woo_price
|
||||||
|
rec.instance_id._log_sync(
|
||||||
|
'product', 'woo_to_odoo', rec.product_id.name, 'success',
|
||||||
|
'Price updated from WC: $%.2f' % rec.woo_price,
|
||||||
|
)
|
||||||
|
|
||||||
|
def action_push_price_to_wc(self):
|
||||||
|
"""Update WC product price from Odoo price."""
|
||||||
|
for rec in self:
|
||||||
|
if rec.product_id and rec.instance_id:
|
||||||
|
client = rec.instance_id._get_client()
|
||||||
|
new_price = str(rec.product_id.list_price)
|
||||||
|
client.update_product(rec.woo_product_id, {'regular_price': new_price})
|
||||||
|
rec.woo_price = rec.product_id.list_price
|
||||||
|
rec.instance_id._log_sync(
|
||||||
|
'product', 'odoo_to_woo', rec.product_id.name, 'success',
|
||||||
|
'Price pushed to WC: $%.2f' % rec.product_id.list_price,
|
||||||
|
)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Image Sync (Task 22)
|
# Image Sync (Task 22)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|||||||
@@ -554,3 +554,40 @@ html[style*="color-scheme: dark"] {
|
|||||||
.woo-card strong {
|
.woo-card strong {
|
||||||
color: var(--woo-text-primary);
|
color: var(--woo-text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------
|
||||||
|
Pagination
|
||||||
|
---------------------------------------------------------- */
|
||||||
|
.woo-pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
.woo-pagination-info {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--woo-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------
|
||||||
|
Icon buttons (price sync arrows)
|
||||||
|
---------------------------------------------------------- */
|
||||||
|
.woo-btn-icon {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 4px;
|
||||||
|
color: var(--woo-text-muted);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: color 0.15s, background 0.15s;
|
||||||
|
}
|
||||||
|
.woo-btn-icon:hover {
|
||||||
|
color: var(--woo-accent);
|
||||||
|
background: var(--woo-bg-hover);
|
||||||
|
}
|
||||||
|
.woo-price-sync-col {
|
||||||
|
width: 60px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,12 +40,21 @@ export class ProductMapping extends Component {
|
|||||||
// Mapped tab
|
// Mapped tab
|
||||||
mappedProducts: [],
|
mappedProducts: [],
|
||||||
selectedMapped: [],
|
selectedMapped: [],
|
||||||
|
mappedPage: 1,
|
||||||
|
mappedTotal: 0,
|
||||||
|
|
||||||
// Unmatched tab
|
// Unmatched tab
|
||||||
odooProducts: [],
|
odooProducts: [],
|
||||||
wooProducts: [],
|
wooProducts: [],
|
||||||
selectedOdooId: false,
|
selectedOdooId: false,
|
||||||
selectedWooId: false,
|
selectedWooId: false,
|
||||||
|
unmatchedOdooPage: 1,
|
||||||
|
unmatchedOdooTotal: 0,
|
||||||
|
unmatchedWooPage: 1,
|
||||||
|
unmatchedWooTotal: 0,
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
pageSize: 50,
|
||||||
|
|
||||||
// Conflicts tab
|
// Conflicts tab
|
||||||
conflicts: [],
|
conflicts: [],
|
||||||
@@ -95,12 +104,17 @@ export class ProductMapping extends Component {
|
|||||||
|
|
||||||
async _loadMapped(query = "") {
|
async _loadMapped(query = "") {
|
||||||
try {
|
try {
|
||||||
const params = { query, limit: 50 };
|
const params = {
|
||||||
|
query,
|
||||||
|
limit: this.state.pageSize,
|
||||||
|
offset: (this.state.mappedPage - 1) * this.state.pageSize,
|
||||||
|
};
|
||||||
if (this.state.instanceId) {
|
if (this.state.instanceId) {
|
||||||
params.instance_id = this.state.instanceId;
|
params.instance_id = this.state.instanceId;
|
||||||
}
|
}
|
||||||
const result = await rpc("/woo/search/mapped", params);
|
const result = await rpc("/woo/search/mapped", params);
|
||||||
this.state.mappedProducts = result || [];
|
this.state.mappedProducts = (result && result.results) || [];
|
||||||
|
this.state.mappedTotal = (result && result.total) || 0;
|
||||||
this.state.selectedMapped = [];
|
this.state.selectedMapped = [];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[ProductMapping] _loadMapped error:", err);
|
console.error("[ProductMapping] _loadMapped error:", err);
|
||||||
@@ -109,12 +123,17 @@ export class ProductMapping extends Component {
|
|||||||
|
|
||||||
async _loadOdooProducts(query = "") {
|
async _loadOdooProducts(query = "") {
|
||||||
try {
|
try {
|
||||||
const params = { query, limit: 50 };
|
const params = {
|
||||||
|
query,
|
||||||
|
limit: this.state.pageSize,
|
||||||
|
offset: (this.state.unmatchedOdooPage - 1) * this.state.pageSize,
|
||||||
|
};
|
||||||
if (this.state.instanceId) {
|
if (this.state.instanceId) {
|
||||||
params.instance_id = this.state.instanceId;
|
params.instance_id = this.state.instanceId;
|
||||||
}
|
}
|
||||||
const result = await rpc("/woo/search/odoo_products", params);
|
const result = await rpc("/woo/search/odoo_products", params);
|
||||||
this.state.odooProducts = result || [];
|
this.state.odooProducts = (result && result.results) || [];
|
||||||
|
this.state.unmatchedOdooTotal = (result && result.total) || 0;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[ProductMapping] _loadOdooProducts error:", err);
|
console.error("[ProductMapping] _loadOdooProducts error:", err);
|
||||||
}
|
}
|
||||||
@@ -122,12 +141,17 @@ export class ProductMapping extends Component {
|
|||||||
|
|
||||||
async _loadWooProducts(query = "") {
|
async _loadWooProducts(query = "") {
|
||||||
try {
|
try {
|
||||||
const params = { query, limit: 50 };
|
const params = {
|
||||||
|
query,
|
||||||
|
limit: this.state.pageSize,
|
||||||
|
offset: (this.state.unmatchedWooPage - 1) * this.state.pageSize,
|
||||||
|
};
|
||||||
if (this.state.instanceId) {
|
if (this.state.instanceId) {
|
||||||
params.instance_id = this.state.instanceId;
|
params.instance_id = this.state.instanceId;
|
||||||
}
|
}
|
||||||
const result = await rpc("/woo/search/woo_products", params);
|
const result = await rpc("/woo/search/woo_products", params);
|
||||||
this.state.wooProducts = result || [];
|
this.state.wooProducts = (result && result.results) || [];
|
||||||
|
this.state.unmatchedWooTotal = (result && result.total) || 0;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[ProductMapping] _loadWooProducts error:", err);
|
console.error("[ProductMapping] _loadWooProducts error:", err);
|
||||||
}
|
}
|
||||||
@@ -213,6 +237,9 @@ export class ProductMapping extends Component {
|
|||||||
async onInstanceChange(ev) {
|
async onInstanceChange(ev) {
|
||||||
const val = ev.target.value;
|
const val = ev.target.value;
|
||||||
this.state.instanceId = val ? parseInt(val, 10) : false;
|
this.state.instanceId = val ? parseInt(val, 10) : false;
|
||||||
|
this.state.mappedPage = 1;
|
||||||
|
this.state.unmatchedOdooPage = 1;
|
||||||
|
this.state.unmatchedWooPage = 1;
|
||||||
await this._refreshAll();
|
await this._refreshAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +248,12 @@ export class ProductMapping extends Component {
|
|||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
onMappedResults(results) {
|
onMappedResults(results) {
|
||||||
this.state.mappedProducts = results;
|
if (results && results.results) {
|
||||||
|
this.state.mappedProducts = results.results;
|
||||||
|
this.state.mappedTotal = results.total || 0;
|
||||||
|
} else {
|
||||||
|
this.state.mappedProducts = results || [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSelectMapped(id) {
|
toggleSelectMapped(id) {
|
||||||
@@ -300,15 +332,25 @@ export class ProductMapping extends Component {
|
|||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
onOdooResults(results) {
|
onOdooResults(results) {
|
||||||
this.state.odooProducts = results;
|
let items = results;
|
||||||
if (!results.find((r) => r.id === this.state.selectedOdooId)) {
|
if (results && results.results) {
|
||||||
|
items = results.results;
|
||||||
|
this.state.unmatchedOdooTotal = results.total || 0;
|
||||||
|
}
|
||||||
|
this.state.odooProducts = items || [];
|
||||||
|
if (!this.state.odooProducts.find((r) => r.id === this.state.selectedOdooId)) {
|
||||||
this.state.selectedOdooId = false;
|
this.state.selectedOdooId = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onWooResults(results) {
|
onWooResults(results) {
|
||||||
this.state.wooProducts = results;
|
let items = results;
|
||||||
if (!results.find((r) => r.id === this.state.selectedWooId)) {
|
if (results && results.results) {
|
||||||
|
items = results.results;
|
||||||
|
this.state.unmatchedWooTotal = results.total || 0;
|
||||||
|
}
|
||||||
|
this.state.wooProducts = items || [];
|
||||||
|
if (!this.state.wooProducts.find((r) => r.id === this.state.selectedWooId)) {
|
||||||
this.state.selectedWooId = false;
|
this.state.selectedWooId = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -410,6 +452,162 @@ export class ProductMapping extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Pagination — Mapped
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_calcTotalPages(total) {
|
||||||
|
return Math.max(1, Math.ceil(total / this.state.pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
get mappedTotalPages() {
|
||||||
|
return this._calcTotalPages(this.state.mappedTotal);
|
||||||
|
}
|
||||||
|
|
||||||
|
async mappedNextPage() {
|
||||||
|
if (this.state.mappedPage < this.mappedTotalPages) {
|
||||||
|
this.state.mappedPage++;
|
||||||
|
await this._loadMapped();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async mappedPrevPage() {
|
||||||
|
if (this.state.mappedPage > 1) {
|
||||||
|
this.state.mappedPage--;
|
||||||
|
await this._loadMapped();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Pagination — Unmatched Odoo
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
get unmatchedOdooTotalPages() {
|
||||||
|
return this._calcTotalPages(this.state.unmatchedOdooTotal);
|
||||||
|
}
|
||||||
|
|
||||||
|
async unmatchedOdooNextPage() {
|
||||||
|
if (this.state.unmatchedOdooPage < this.unmatchedOdooTotalPages) {
|
||||||
|
this.state.unmatchedOdooPage++;
|
||||||
|
await this._loadOdooProducts("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async unmatchedOdooPrevPage() {
|
||||||
|
if (this.state.unmatchedOdooPage > 1) {
|
||||||
|
this.state.unmatchedOdooPage--;
|
||||||
|
await this._loadOdooProducts("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Pagination — Unmatched WC
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
get unmatchedWooTotalPages() {
|
||||||
|
return this._calcTotalPages(this.state.unmatchedWooTotal);
|
||||||
|
}
|
||||||
|
|
||||||
|
async unmatchedWooNextPage() {
|
||||||
|
if (this.state.unmatchedWooPage < this.unmatchedWooTotalPages) {
|
||||||
|
this.state.unmatchedWooPage++;
|
||||||
|
await this._loadWooProducts("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async unmatchedWooPrevPage() {
|
||||||
|
if (this.state.unmatchedWooPage > 1) {
|
||||||
|
this.state.unmatchedWooPage--;
|
||||||
|
await this._loadWooProducts("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Individual price sync
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async pushPriceToOdoo(mapId) {
|
||||||
|
try {
|
||||||
|
await rpc("/web/dataset/call_kw", {
|
||||||
|
model: "woo.product.map",
|
||||||
|
method: "action_push_price_to_odoo",
|
||||||
|
args: [[mapId]],
|
||||||
|
kwargs: {},
|
||||||
|
});
|
||||||
|
this.notification.add("WC price pushed to Odoo.", { type: "success" });
|
||||||
|
await this._loadMapped();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[ProductMapping] pushPriceToOdoo error:", err);
|
||||||
|
this.notification.add("Failed to push price to Odoo.", { type: "danger" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async pushPriceToWC(mapId) {
|
||||||
|
try {
|
||||||
|
await rpc("/web/dataset/call_kw", {
|
||||||
|
model: "woo.product.map",
|
||||||
|
method: "action_push_price_to_wc",
|
||||||
|
args: [[mapId]],
|
||||||
|
kwargs: {},
|
||||||
|
});
|
||||||
|
this.notification.add("Odoo price pushed to WC.", { type: "success" });
|
||||||
|
await this._loadMapped();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[ProductMapping] pushPriceToWC error:", err);
|
||||||
|
this.notification.add("Failed to push price to WC.", { type: "danger" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Bulk price sync
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async bulkPriceOdooToWC() {
|
||||||
|
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_price_odoo_to_wc",
|
||||||
|
args: [[this.state.instanceId]],
|
||||||
|
kwargs: {},
|
||||||
|
});
|
||||||
|
this.notification.add("All Odoo prices pushed to WooCommerce.", { type: "success" });
|
||||||
|
await this._refreshAll();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[ProductMapping] bulkPriceOdooToWC error:", err);
|
||||||
|
this.notification.add("Bulk price sync failed.", { type: "danger" });
|
||||||
|
} finally {
|
||||||
|
this.state.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async bulkPriceWCToOdoo() {
|
||||||
|
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_price_wc_to_odoo",
|
||||||
|
args: [[this.state.instanceId]],
|
||||||
|
kwargs: {},
|
||||||
|
});
|
||||||
|
this.notification.add("All WC prices pulled to Odoo.", { type: "success" });
|
||||||
|
await this._refreshAll();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[ProductMapping] bulkPriceWCToOdoo error:", err);
|
||||||
|
this.notification.add("Bulk price sync failed.", { type: "danger" });
|
||||||
|
} finally {
|
||||||
|
this.state.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Top bar actions
|
// Top bar actions
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -82,12 +82,12 @@
|
|||||||
<button class="woo-tab" t-att-class="state.activeTab === 'mapped' ? 'active' : ''"
|
<button class="woo-tab" t-att-class="state.activeTab === 'mapped' ? 'active' : ''"
|
||||||
t-on-click="() => this.setTab('mapped')">
|
t-on-click="() => this.setTab('mapped')">
|
||||||
Mapped Products
|
Mapped Products
|
||||||
<span class="ms-1 woo-badge woo-badge-mapped" t-esc="state.mappedProducts.length"/>
|
<span class="ms-1 woo-badge woo-badge-mapped" t-esc="state.mappedTotal"/>
|
||||||
</button>
|
</button>
|
||||||
<button class="woo-tab" t-att-class="state.activeTab === 'unmatched' ? 'active' : ''"
|
<button class="woo-tab" t-att-class="state.activeTab === 'unmatched' ? 'active' : ''"
|
||||||
t-on-click="() => this.setTab('unmatched')">
|
t-on-click="() => this.setTab('unmatched')">
|
||||||
Unmatched Products
|
Unmatched Products
|
||||||
<span class="ms-1 woo-badge woo-badge-unmapped" t-esc="state.wooProducts.length"/>
|
<span class="ms-1 woo-badge woo-badge-unmapped" t-esc="state.unmatchedWooTotal"/>
|
||||||
</button>
|
</button>
|
||||||
<button class="woo-tab" t-att-class="state.activeTab === 'conflicts' ? 'active' : ''"
|
<button class="woo-tab" t-att-class="state.activeTab === 'conflicts' ? 'active' : ''"
|
||||||
t-on-click="() => this.setTab('conflicts')">
|
t-on-click="() => this.setTab('conflicts')">
|
||||||
@@ -115,6 +115,12 @@
|
|||||||
t-att-disabled="!state.selectedMapped.length">
|
t-att-disabled="!state.selectedMapped.length">
|
||||||
<i class="fa fa-refresh me-1"/> Sync Selected
|
<i class="fa fa-refresh me-1"/> Sync Selected
|
||||||
</button>
|
</button>
|
||||||
|
<button class="woo-btn woo-btn-secondary woo-btn-sm" t-on-click="bulkPriceOdooToWC">
|
||||||
|
<i class="fa fa-arrow-right me-1"/> All Prices Odoo → WC
|
||||||
|
</button>
|
||||||
|
<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
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<t t-if="!state.mappedProducts.length">
|
<t t-if="!state.mappedProducts.length">
|
||||||
@@ -135,6 +141,7 @@
|
|||||||
<th>SKU</th>
|
<th>SKU</th>
|
||||||
<th>Odoo Product</th>
|
<th>Odoo Product</th>
|
||||||
<th>WC Price</th>
|
<th>WC Price</th>
|
||||||
|
<th class="text-center"><i class="fa fa-exchange" title="Price Sync"/></th>
|
||||||
<th>Odoo Price</th>
|
<th>Odoo Price</th>
|
||||||
<th>Instance</th>
|
<th>Instance</th>
|
||||||
<th>Price Sync</th>
|
<th>Price Sync</th>
|
||||||
@@ -153,6 +160,16 @@
|
|||||||
<td><span class="woo-code"><t t-esc="p.woo_sku"/></span></td>
|
<td><span class="woo-code"><t t-esc="p.woo_sku"/></span></td>
|
||||||
<td><t t-esc="p.odoo_product_name"/></td>
|
<td><t t-esc="p.odoo_product_name"/></td>
|
||||||
<td class="text-end" t-esc="this.formatPrice(p.woo_price)"/>
|
<td class="text-end" t-esc="this.formatPrice(p.woo_price)"/>
|
||||||
|
<td class="text-center woo-price-sync-col">
|
||||||
|
<button class="woo-btn-icon" title="Push WC price to Odoo"
|
||||||
|
t-on-click.stop="() => this.pushPriceToOdoo(p.id)">
|
||||||
|
<i class="fa fa-arrow-right"/>
|
||||||
|
</button>
|
||||||
|
<button class="woo-btn-icon" title="Push Odoo price to WC"
|
||||||
|
t-on-click.stop="() => this.pushPriceToWC(p.id)">
|
||||||
|
<i class="fa fa-arrow-left"/>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
<td class="text-end" t-esc="this.formatPrice(p.odoo_price)"/>
|
<td class="text-end" t-esc="this.formatPrice(p.odoo_price)"/>
|
||||||
<td><t t-esc="p.instance_name"/></td>
|
<td><t t-esc="p.instance_name"/></td>
|
||||||
<td>
|
<td>
|
||||||
@@ -170,6 +187,20 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="woo-pagination">
|
||||||
|
<button class="woo-btn woo-btn-secondary woo-btn-sm" t-on-click="mappedPrevPage"
|
||||||
|
t-att-disabled="state.mappedPage <= 1">
|
||||||
|
<i class="fa fa-chevron-left"/> Prev
|
||||||
|
</button>
|
||||||
|
<span class="woo-pagination-info">
|
||||||
|
Page <t t-esc="state.mappedPage"/> of <t t-esc="mappedTotalPages"/>
|
||||||
|
(<t t-esc="state.mappedTotal"/> total)
|
||||||
|
</span>
|
||||||
|
<button class="woo-btn woo-btn-secondary woo-btn-sm" t-on-click="mappedNextPage"
|
||||||
|
t-att-disabled="state.mappedPage >= mappedTotalPages">
|
||||||
|
Next <i class="fa fa-chevron-right"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</t>
|
</t>
|
||||||
</t>
|
</t>
|
||||||
|
|
||||||
@@ -219,6 +250,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="woo-pagination">
|
||||||
|
<button class="woo-btn woo-btn-secondary woo-btn-sm" t-on-click="unmatchedOdooPrevPage"
|
||||||
|
t-att-disabled="state.unmatchedOdooPage <= 1">
|
||||||
|
<i class="fa fa-chevron-left"/> Prev
|
||||||
|
</button>
|
||||||
|
<span class="woo-pagination-info">
|
||||||
|
Page <t t-esc="state.unmatchedOdooPage"/> of <t t-esc="unmatchedOdooTotalPages"/>
|
||||||
|
(<t t-esc="state.unmatchedOdooTotal"/> total)
|
||||||
|
</span>
|
||||||
|
<button class="woo-btn woo-btn-secondary woo-btn-sm" t-on-click="unmatchedOdooNextPage"
|
||||||
|
t-att-disabled="state.unmatchedOdooPage >= unmatchedOdooTotalPages">
|
||||||
|
Next <i class="fa fa-chevron-right"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Divider -->
|
<!-- Divider -->
|
||||||
@@ -264,6 +309,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="woo-pagination">
|
||||||
|
<button class="woo-btn woo-btn-secondary woo-btn-sm" t-on-click="unmatchedWooPrevPage"
|
||||||
|
t-att-disabled="state.unmatchedWooPage <= 1">
|
||||||
|
<i class="fa fa-chevron-left"/> Prev
|
||||||
|
</button>
|
||||||
|
<span class="woo-pagination-info">
|
||||||
|
Page <t t-esc="state.unmatchedWooPage"/> of <t t-esc="unmatchedWooTotalPages"/>
|
||||||
|
(<t t-esc="state.unmatchedWooTotal"/> total)
|
||||||
|
</span>
|
||||||
|
<button class="woo-btn woo-btn-secondary woo-btn-sm" t-on-click="unmatchedWooNextPage"
|
||||||
|
t-att-disabled="state.unmatchedWooPage >= unmatchedWooTotalPages">
|
||||||
|
Next <i class="fa fa-chevron-right"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user