This commit is contained in:
gsinghpal
2026-04-02 03:30:02 -04:00
parent 5b76037988
commit 2a363c6b40
15 changed files with 580 additions and 506 deletions

View File

@@ -365,6 +365,10 @@ class WooInstance(models.Model):
wc_permalink = wc_prod.get('permalink', '')
wc_categories = wc_prod.get('categories', [])
wc_cat_id = wc_categories[0].get('id', 0) if wc_categories else 0
wc_cat_name = wc_categories[0].get('name', '') if wc_categories else ''
ProductMap.create({
'instance_id': self.id,
'product_id': odoo_product.id if odoo_product else False,
@@ -374,6 +378,8 @@ class WooInstance(models.Model):
'woo_regular_price': wc_regular_price,
'woo_sale_price': wc_sale_price,
'woo_permalink': wc_permalink,
'woo_category_id': wc_cat_id,
'woo_category_name': wc_cat_name,
'woo_product_type': wc_type if wc_type in ('simple', 'variable', 'grouped', 'external') else 'simple',
'state': match_state,
'company_id': self.company_id.id,
@@ -451,6 +457,9 @@ class WooInstance(models.Model):
'Fetched %d products, auto-matched %d by SKU' % (total_fetched, auto_matched))
return True
# Max consecutive API errors before aborting a sync loop
_MAX_CONSECUTIVE_ERRORS = 5
def action_refresh_prices(self):
"""Refresh WC standard + sale prices for all products from WooCommerce API."""
self.ensure_one()
@@ -462,9 +471,11 @@ class WooInstance(models.Model):
('woo_product_id', '>', 0),
])
updated = 0
consecutive_errors = 0
for m in maps:
try:
wc_prod = client.get_product(m.woo_product_id)
consecutive_errors = 0
regular = 0.0
sale = 0.0
try:
@@ -485,7 +496,14 @@ class WooInstance(models.Model):
if changed:
updated += 1
except Exception as e:
consecutive_errors += 1
_logger.warning("Failed to refresh price for WC#%s: %s", m.woo_product_id, e)
if consecutive_errors >= self._MAX_CONSECUTIVE_ERRORS:
_logger.error(
"Aborting price refresh after %d consecutive errors", consecutive_errors)
self._log_sync('product', 'woo_to_odoo', self.name, 'failed',
'Aborted after %d consecutive API errors' % consecutive_errors)
return True
self._log_sync('product', 'woo_to_odoo', self.name, 'success',
'Refreshed prices for %d products' % updated)
return True
@@ -589,8 +607,20 @@ class WooInstance(models.Model):
self.ensure_one()
client = self._get_client()
page = 1
consecutive_fetch_errors = 0
while True:
orders = client.get_orders(page=page, status='processing,on-hold,pending')
try:
orders = client.get_orders(page=page, status='processing,on-hold,pending')
consecutive_fetch_errors = 0
except Exception as e:
consecutive_fetch_errors += 1
_logger.error("Failed to fetch orders page %d: %s", page, e)
if consecutive_fetch_errors >= 3:
self._log_sync('order', 'woo_to_odoo', self.name, 'failed',
'Aborted order fetch after %d consecutive page errors' % consecutive_fetch_errors)
return
page += 1
continue
if not orders:
break
for wc_order in orders:
@@ -644,37 +674,51 @@ class WooInstance(models.Model):
# Add fee lines
for fee in wc_order.get('fee_lines', []):
fee_product = self._get_service_product('WC Fee', 'WC-FEE')
fee_vals = {
'order_id': sale_order.id,
'name': fee.get('name', 'Fee'),
'product_id': fee_product.id,
'product_uom_qty': 1,
'price_unit': float(fee.get('total', 0)),
}
self.env['sale.order.line'].create(fee_vals)
# Confirm SO
sale_order.action_confirm()
# Create draft invoice
invoice = False
if self.sync_invoices:
try:
invoice = sale_order._create_invoices()
except Exception as e:
_logger.warning("Could not auto-create invoice for %s: %s", sale_order.name, e)
# Create woo.order tracking record
# Create woo.order tracking record FIRST to prevent infinite retries
# if action_confirm or invoicing fails later.
woo_order = self.env['woo.order'].create({
'instance_id': self.id,
'sale_order_id': sale_order.id,
'woo_order_id': woo_order_id,
'woo_order_number': wc_order.get('number', str(woo_order_id)),
'woo_status': wc_order.get('status', ''),
'invoice_id': invoice.id if invoice else False,
'state': 'confirmed',
'company_id': self.company_id.id,
})
# Confirm SO (non-fatal — keeps SO in draft if it fails)
try:
sale_order.action_confirm()
except Exception as e:
_logger.warning(
"Could not auto-confirm SO %s (WC#%s): %s — order left in draft",
sale_order.name, woo_order_id, e,
)
self._log_sync(
'order', 'woo_to_odoo', sale_order.name, 'failed',
f'Order imported but could not auto-confirm: {e}',
)
return woo_order
# Create draft invoice
invoice = False
if self.sync_invoices:
try:
invoice = sale_order._create_invoices()
woo_order.invoice_id = invoice.id
except Exception as e:
_logger.warning("Could not auto-create invoice for %s: %s", sale_order.name, e)
# Link invoice to woo.order
if invoice:
invoice.woo_order_id = woo_order.id
@@ -860,16 +904,41 @@ class WooInstance(models.Model):
'|', ('company_id', '=', self.company_id.id), ('company_id', '=', False),
], limit=1)
if not product:
wc_name = line_item.get('name', 'WooCommerce Product')
wc_sku = line_item.get('sku', '')
wc_price = float(line_item.get('price', 0))
product = self.env['product.product'].create({
'name': wc_name.upper() if wc_name else 'WC PRODUCT',
'default_code': wc_sku,
'list_price': wc_price,
'type': 'consu',
})
if lookup_id:
self.env['woo.product.map'].create({
'instance_id': self.id,
'product_id': product.id,
'woo_product_id': lookup_id,
'woo_product_name': wc_name,
'woo_sku': wc_sku,
'woo_regular_price': wc_price,
'woo_product_type': 'simple',
'state': 'mapped',
'company_id': self.company_id.id,
})
_logger.info(
"Auto-created Odoo product '%s' (SKU: %s) for WC order line",
product.name, wc_sku,
)
vals = {
'order_id': sale_order.id,
'name': line_item.get('name', 'WooCommerce Product'),
'product_id': product.id,
'product_uom_qty': line_item.get('quantity', 1),
'price_unit': float(line_item.get('price', 0)),
}
if product:
vals['product_id'] = product.id
# Tax mapping
taxes = []
for tax_entry in line_item.get('taxes', []):
@@ -891,15 +960,34 @@ class WooInstance(models.Model):
return vals
def _get_service_product(self, name, internal_ref):
"""Find or create a generic service product (for shipping, fees, etc.)."""
product = self.env['product.product'].search([
('default_code', '=', internal_ref),
'|', ('company_id', '=', self.company_id.id), ('company_id', '=', False),
], limit=1)
if not product:
product = self.env['product.product'].create({
'name': name,
'default_code': internal_ref,
'type': 'service',
'list_price': 0,
'sale_ok': True,
'purchase_ok': False,
})
return product
def _prepare_shipping_line_vals(self, sale_order, shipping_line):
"""Build sale.order.line vals from a WC shipping_lines entry."""
self.ensure_one()
total = float(shipping_line.get('total', 0))
if not total:
return False
shipping_product = self._get_service_product('WC Shipping', 'WC-SHIPPING')
return {
'order_id': sale_order.id,
'name': shipping_line.get('method_title', 'Shipping'),
'product_id': shipping_product.id,
'product_uom_qty': 1,
'price_unit': total,
}
@@ -967,29 +1055,26 @@ class WooInstance(models.Model):
('product_id', '!=', False),
])
consecutive_errors = 0
for pm in maps:
try:
wc_product = client.get_product(pm.woo_product_id)
consecutive_errors = 0
wc_price = float(wc_product.get('regular_price') or 0)
odoo_price = pm.product_id.list_price
if abs(wc_price - odoo_price) < 0.01:
# Prices match — nothing to do
pm.last_synced = fields.Datetime.now()
continue
# Check if Odoo price was changed since last sync
odoo_changed = True
woo_changed = True
if pm.last_synced:
# If product write_date is newer than last sync, Odoo changed
odoo_changed = pm.product_id.write_date > pm.last_synced
# WC always returns current price, assume changed if different
woo_changed = True
if odoo_changed and woo_changed:
# Both changed — conflict
self.env['woo.conflict'].create({
'instance_id': self.id,
'conflict_type': 'product',
@@ -999,13 +1084,11 @@ class WooInstance(models.Model):
'woo_value': str(wc_price),
'company_id': self.company_id.id,
})
# Keep state as mapped — conflict is tracked separately
self._log_sync(
'product', 'woo_to_odoo', pm.product_id.display_name,
'conflict', f'Price conflict: Odoo=${odoo_price}, WC=${wc_price}',
)
elif odoo_changed:
# Odoo is authoritative — push to WC
client.update_product(pm.woo_product_id, {
'regular_price': str(odoo_price),
})
@@ -1014,7 +1097,6 @@ class WooInstance(models.Model):
'success', f'Price updated to ${odoo_price}',
)
else:
# WC changed — pull into Odoo
pm.product_id.list_price = wc_price
self._log_sync(
'product', 'woo_to_odoo', pm.product_id.display_name,
@@ -1023,15 +1105,22 @@ class WooInstance(models.Model):
pm.last_synced = fields.Datetime.now()
except Exception as e:
consecutive_errors += 1
_logger.error(
"Product price sync failed for %s (WC#%s): %s",
pm.product_id.display_name, pm.woo_product_id, e,
)
# Don't change map state on sync errors — keep it mapped
self._log_sync(
'product', 'odoo_to_woo',
pm.product_id.display_name, 'failed', str(e),
)
if consecutive_errors >= self._MAX_CONSECUTIVE_ERRORS:
_logger.error(
"Aborting product sync after %d consecutive errors",
consecutive_errors)
self._log_sync('product', 'woo_to_odoo', self.name, 'failed',
'Aborted after %d consecutive API errors' % consecutive_errors)
return
def _sync_product_from_wc(self, wc_data):
"""Handle an inbound WC product webhook — update price if mapped."""
@@ -1074,11 +1163,11 @@ class WooInstance(models.Model):
('product_id', '!=', False),
])
consecutive_errors = 0
for pm in maps:
try:
product = pm.product_id
if self.default_warehouse_id:
# Get qty for specific warehouse
quant = self.env['stock.quant'].search([
('product_id', '=', product.id),
('location_id', 'child_of',
@@ -1094,12 +1183,14 @@ class WooInstance(models.Model):
'stock_quantity': qty,
'manage_stock': True,
})
consecutive_errors = 0
pm.last_synced = fields.Datetime.now()
self._log_sync(
'inventory', 'odoo_to_woo', product.display_name,
'success', f'Stock set to {qty}',
)
except Exception as e:
consecutive_errors += 1
_logger.error(
"Inventory sync failed for %s (WC#%s): %s",
pm.product_id.display_name, pm.woo_product_id, e,
@@ -1108,6 +1199,13 @@ class WooInstance(models.Model):
'inventory', 'odoo_to_woo',
pm.product_id.display_name, 'failed', str(e),
)
if consecutive_errors >= self._MAX_CONSECUTIVE_ERRORS:
_logger.error(
"Aborting inventory sync after %d consecutive errors",
consecutive_errors)
self._log_sync('inventory', 'odoo_to_woo', self.name, 'failed',
'Aborted after %d consecutive API errors' % consecutive_errors)
return
# ------------------------------------------------------------------
# Customer Sync (Task 25)
@@ -1122,10 +1220,10 @@ class WooInstance(models.Model):
('woo_customer_id', '>', 0),
])
consecutive_errors = 0
for cust in customers:
try:
partner = cust.partner_id
# Only sync if partner was modified since last sync
if cust.last_synced and partner.write_date <= cust.last_synced:
continue
@@ -1146,6 +1244,7 @@ class WooInstance(models.Model):
'billing': billing_data,
'shipping': billing_data,
})
consecutive_errors = 0
cust.last_synced = fields.Datetime.now()
self._log_sync(
@@ -1153,6 +1252,7 @@ class WooInstance(models.Model):
'success', 'Address updated in WooCommerce',
)
except Exception as e:
consecutive_errors += 1
_logger.error(
"Customer sync failed for %s (WC#%s): %s",
cust.partner_id.display_name, cust.woo_customer_id, e,
@@ -1161,6 +1261,13 @@ class WooInstance(models.Model):
'customer', 'odoo_to_woo',
cust.partner_id.display_name, 'failed', str(e),
)
if consecutive_errors >= self._MAX_CONSECUTIVE_ERRORS:
_logger.error(
"Aborting customer sync after %d consecutive errors",
consecutive_errors)
self._log_sync('customer', 'odoo_to_woo', self.name, 'failed',
'Aborted after %d consecutive API errors' % consecutive_errors)
return
def _sync_customer_from_wc(self, wc_data):
"""Handle an inbound WC customer webhook — update partner if linked."""