feat: WC standard price + sale price with smart sync logic

- Split woo_price into woo_regular_price and woo_sale_price
- Sync to WC: if standard=0 sets regular_price, otherwise sets sale_price
- Validation: standard price cannot be less than sale price
- Both prices shown in mapping UI with sale price highlighted in green
- Refresh Prices pulls both values from WC API

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-03-31 23:19:01 -04:00
parent b1e4ed5ec8
commit 80f9ddd0d6
5 changed files with 127 additions and 26 deletions

View File

@@ -158,7 +158,8 @@ class WooProductSearchController(http.Controller):
'odoo_product_name': m.product_id.name if m.product_id else '',
'odoo_default_code': m.product_id.default_code or '' if m.product_id else '',
'odoo_price': m.product_id.list_price if m.product_id else 0.0,
'woo_price': m.woo_price or 0.0,
'woo_regular_price': m.woo_regular_price or 0.0,
'woo_sale_price': m.woo_sale_price or 0.0,
'sync_price': m.sync_price,
'sync_inventory': m.sync_inventory,
'instance_id': m.instance_id.id if m.instance_id else False,

View File

@@ -196,9 +196,14 @@ class WooInstance(models.Model):
match_state = 'mapped'
auto_matched += 1
wc_price = 0.0
wc_regular_price = 0.0
wc_sale_price = 0.0
try:
wc_price = float(wc_prod.get('regular_price') or wc_prod.get('price') or 0)
wc_regular_price = float(wc_prod.get('regular_price') or 0)
except (ValueError, TypeError):
pass
try:
wc_sale_price = float(wc_prod.get('sale_price') or 0)
except (ValueError, TypeError):
pass
@@ -210,7 +215,8 @@ class WooInstance(models.Model):
'woo_product_id': wc_id,
'woo_product_name': wc_name,
'woo_sku': wc_sku,
'woo_price': wc_price,
'woo_regular_price': wc_regular_price,
'woo_sale_price': wc_sale_price,
'woo_permalink': wc_permalink,
'woo_product_type': wc_type if wc_type in ('simple', 'variable', 'grouped', 'external') else 'simple',
'state': match_state,
@@ -249,9 +255,14 @@ class WooInstance(models.Model):
var_state = 'mapped'
auto_matched += 1
var_price = 0.0
var_regular = 0.0
var_sale = 0.0
try:
var_price = float(var.get('regular_price') or var.get('price') or 0)
var_regular = float(var.get('regular_price') or 0)
except (ValueError, TypeError):
pass
try:
var_sale = float(var.get('sale_price') or 0)
except (ValueError, TypeError):
pass
@@ -261,7 +272,8 @@ class WooInstance(models.Model):
'woo_product_id': var_id,
'woo_product_name': var_name,
'woo_sku': var_sku,
'woo_price': var_price,
'woo_regular_price': var_regular,
'woo_sale_price': var_sale,
'woo_product_type': 'simple',
'woo_parent_id': wc_id,
'is_variation': True,
@@ -284,7 +296,7 @@ class WooInstance(models.Model):
return True
def action_refresh_prices(self):
"""Refresh WC prices for all mapped products from WooCommerce API."""
"""Refresh WC standard + sale prices for all products from WooCommerce API."""
self.ensure_one()
if self.state != 'connected':
raise UserError("Instance is not connected.")
@@ -297,13 +309,24 @@ class WooInstance(models.Model):
for m in maps:
try:
wc_prod = client.get_product(m.woo_product_id)
wc_price = 0.0
regular = 0.0
sale = 0.0
try:
wc_price = float(wc_prod.get('regular_price') or wc_prod.get('price') or 0)
regular = float(wc_prod.get('regular_price') or 0)
except (ValueError, TypeError):
pass
if wc_price != m.woo_price:
m.woo_price = wc_price
try:
sale = float(wc_prod.get('sale_price') or 0)
except (ValueError, TypeError):
pass
changed = False
if abs(regular - (m.woo_regular_price or 0)) > 0.001:
m.woo_regular_price = regular
changed = True
if abs(sale - (m.woo_sale_price or 0)) > 0.001:
m.woo_sale_price = sale
changed = True
if changed:
updated += 1
except Exception as e:
_logger.warning("Failed to refresh price for WC#%s: %s", m.woo_product_id, e)

View File

@@ -6,6 +6,7 @@ import logging
import requests
from odoo import fields, models
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
@@ -25,7 +26,8 @@ class WooProductMap(models.Model):
('grouped', 'Grouped'),
('external', 'External'),
])
woo_price = fields.Float(string='WC Price', digits='Product Price')
woo_regular_price = fields.Float(string='WC Standard Price', digits='Product Price')
woo_sale_price = fields.Float(string='WC Sale Price', digits='Product Price')
woo_permalink = fields.Char(string='WC Product URL')
woo_parent_id = fields.Integer()
is_variation = fields.Boolean()
@@ -49,27 +51,89 @@ class WooProductMap(models.Model):
# ------------------------------------------------------------------
def action_push_price_to_odoo(self):
"""Update Odoo product price from WC price."""
"""Update Odoo product price from WC sale price (or regular if no sale)."""
for rec in self:
if rec.product_id and rec.woo_price:
rec.product_id.list_price = rec.woo_price
if not rec.product_id:
continue
# Use sale price if available, otherwise regular price
wc_price = rec.woo_sale_price if rec.woo_sale_price else rec.woo_regular_price
if wc_price:
rec.product_id.list_price = wc_price
rec.instance_id._log_sync(
'product', 'woo_to_odoo', rec.product_id.name, 'success',
'Price updated from WC: $%.2f' % rec.woo_price,
'Odoo price updated from WC: $%.2f' % wc_price,
)
def action_push_price_to_wc(self):
"""Update WC product price from Odoo price."""
"""Push Odoo price to WC.
Logic:
- If WC standard (regular) price is 0 or empty: set as regular_price
- If WC standard price exists: set as sale_price
- If WC standard price < Odoo price: error (standard can't be less than sale)
"""
errors = []
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
if not rec.product_id or not rec.instance_id:
continue
odoo_price = rec.product_id.list_price
wc_regular = rec.woo_regular_price or 0.0
client = rec.instance_id._get_client()
update_data = {}
if wc_regular < 0.01:
# No standard price set — push as regular_price
update_data = {
'regular_price': str(odoo_price),
'sale_price': '',
}
rec.woo_regular_price = odoo_price
rec.woo_sale_price = 0.0
else:
# Standard price exists — push as sale_price
if wc_regular < odoo_price - 0.01:
# Standard price is less than the price we want to set as sale
errors.append(
'%s: WC standard price ($%.2f) is less than Odoo price ($%.2f). '
'Update the standard price first.' % (rec.woo_product_name, wc_regular, odoo_price)
)
continue
update_data = {
'sale_price': str(odoo_price),
}
rec.woo_sale_price = odoo_price
try:
client.update_product(rec.woo_product_id, update_data)
rec.instance_id._log_sync(
'product', 'odoo_to_woo', rec.product_id.name, 'success',
'Price pushed to WC: $%.2f' % rec.product_id.list_price,
'Price pushed to WC: $%.2f' % odoo_price,
)
except Exception as e:
errors.append('%s: %s' % (rec.woo_product_name, str(e)))
if errors:
raise UserError('\n'.join(errors))
def action_set_regular_price(self, price):
"""Set the WC standard (regular) price directly."""
self.ensure_one()
if not self.instance_id:
return
client = self.instance_id._get_client()
# If there's a sale price, regular must be >= sale
if self.woo_sale_price and price < self.woo_sale_price - 0.01:
raise UserError(
'Standard price ($%.2f) cannot be less than the current sale price ($%.2f).'
% (price, self.woo_sale_price)
)
client.update_product(self.woo_product_id, {'regular_price': str(price)})
self.woo_regular_price = price
self.instance_id._log_sync(
'product', 'odoo_to_woo', self.woo_product_name, 'success',
'Standard price set to $%.2f' % price,
)
# ------------------------------------------------------------------
# Image Sync (Task 22)

View File

@@ -605,6 +605,12 @@ html[style*="color-scheme: dark"] {
opacity: 1;
}
/* Sale price highlight */
.woo-sale-price {
color: var(--woo-success);
font-weight: 600;
}
.woo-price-sync-col {
width: 60px;
white-space: nowrap;

View File

@@ -140,7 +140,8 @@
<th>WooCommerce Product</th>
<th>SKU</th>
<th>Odoo Product</th>
<th>WC Price</th>
<th>WC Standard</th>
<th>WC Sale</th>
<th class="text-center"><i class="fa fa-exchange" title="Price Sync"/></th>
<th>Odoo Price</th>
<th>Instance</th>
@@ -167,7 +168,13 @@
</td>
<td><span class="woo-code"><t t-esc="p.woo_sku"/></span></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_regular_price)"/>
<td class="text-end">
<t t-if="p.woo_sale_price">
<span class="woo-sale-price" t-esc="this.formatPrice(p.woo_sale_price)"/>
</t>
<t t-else=""><span class="woo-text-muted"></span></t>
</td>
<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)">