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:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)">
|
||||
|
||||
Reference in New Issue
Block a user