changes
This commit is contained in:
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user