feat: implement order sync lifecycle (WC→Odoo SO/invoice, shipping push, completion push)

- Replace _sync_orders placeholder with full WC order import pipeline
- Add _sync_order_from_wc, _find_or_create_customer, _prepare_sale_order_vals,
  _prepare_order_line_vals, _prepare_shipping_line_vals helpers
- Add push methods on woo.order: action_push_shipping, action_push_completed,
  action_push_invoice_pdf, action_push_delivery_pdf, _push_messages_to_wc
- Override stock.picking button_validate to auto-create shipment records and
  push tracking/delivery PDFs to WooCommerce

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-03-31 21:01:48 -04:00
parent 2dd1a2be6c
commit 9c2c043de9
3 changed files with 765 additions and 10 deletions

View File

@@ -1,5 +1,9 @@
import logging
from odoo import models, fields
_logger = logging.getLogger(__name__)
class StockPicking(models.Model):
_inherit = 'stock.picking'
@@ -14,3 +18,40 @@ class StockPicking(models.Model):
picking.is_woo_delivery = bool(picking.woo_shipment_ids) or bool(
picking.sale_id and picking.sale_id.woo_bind_ids
)
def button_validate(self):
"""Override to auto-create shipment and push tracking to WC."""
res = super().button_validate()
for picking in self:
if not picking.sale_id or not picking.sale_id.woo_bind_ids:
continue
woo_order = picking.sale_id.woo_bind_ids[0]
# Create shipment record
shipment_vals = {
'order_id': woo_order.id,
'picking_id': picking.id,
'carrier_id': picking.woo_carrier_id.id if picking.woo_carrier_id else False,
'tracking_number': picking.woo_tracking_number or '',
'shipped_date': fields.Datetime.now(),
'is_backorder': bool(picking.backorder_ids),
'company_id': picking.company_id.id,
}
shipment = self.env['woo.shipment'].create(shipment_vals)
# Auto-push to WC if tracking number is set
if picking.woo_tracking_number:
try:
woo_order.action_push_shipping(
picking.woo_tracking_number,
picking.woo_carrier_id.id if picking.woo_carrier_id else False,
)
shipment.synced_to_woo = True
except Exception as e:
_logger.error("Failed to push shipping to WC: %s", e)
# Push delivery PDF
try:
woo_order.action_push_delivery_pdf(picking)
except Exception as e:
_logger.warning("Failed to push delivery PDF to WC: %s", e)
return res

View File

@@ -1,3 +1,6 @@
import base64
import hashlib
import json
import logging
import secrets
@@ -168,29 +171,594 @@ class WooInstance(models.Model):
instance._log_sync('customer', 'odoo_to_woo', instance.name, 'failed', str(e))
instance._notify_failure('customer', str(e))
@api.model
def _cron_health_check(self):
"""Ping all connected instances and flag unreachable ones."""
instances = self.search([('state', '!=', 'draft')])
for instance in instances:
try:
client = instance._get_client()
success, _info = client.test_connection()
if success:
if instance.state == 'error':
instance.state = 'connected'
instance.message_post(body="Health check passed — connection restored.")
else:
instance.state = 'error'
instance.message_post(body="Health check failed — store unreachable.")
instance._notify_failure('health', 'Store unreachable during health check.')
except Exception as e:
instance.state = 'error'
instance.message_post(body=f"Health check error: {e}")
instance._notify_failure('health', str(e))
_logger.error("Health check failed for %s: %s", instance.name, e)
# ------------------------------------------------------------------
# Sync method placeholders (filled in during later tasks)
# Order Sync (Task 20)
# ------------------------------------------------------------------
def _sync_orders(self):
"""Fetch new WC orders and create Odoo SOs."""
self.ensure_one()
client = self._get_client()
page = 1
while True:
orders = client.get_orders(page=page, status='processing,on-hold,pending')
if not orders:
break
for wc_order in orders:
try:
self._sync_order_from_wc(wc_order)
except Exception as e:
_logger.error(
"Failed to sync WC order %s for %s: %s",
wc_order.get('id'), self.name, e,
)
self._log_sync(
'order', 'woo_to_odoo',
f"WC#{wc_order.get('id')}", 'failed', str(e),
)
page += 1
if len(orders) < 100:
break
self.last_sync = fields.Datetime.now()
def _sync_order_from_wc(self, wc_order):
"""Process a single WC order into an Odoo sale order."""
self.ensure_one()
woo_order_id = wc_order['id']
# Dedup check
existing = self.env['woo.order'].search([
('instance_id', '=', self.id),
('woo_order_id', '=', woo_order_id),
], limit=1)
if existing:
return existing
# Find/create customer
partner = self._find_or_create_customer(wc_order)
# Create sale order
so_vals = self._prepare_sale_order_vals(wc_order, partner)
sale_order = self.env['sale.order'].create(so_vals)
# Add order lines
for line_item in wc_order.get('line_items', []):
line_vals = self._prepare_order_line_vals(sale_order, line_item)
if line_vals:
self.env['sale.order.line'].create(line_vals)
# Add shipping lines
for shipping in wc_order.get('shipping_lines', []):
shipping_vals = self._prepare_shipping_line_vals(sale_order, shipping)
if shipping_vals:
self.env['sale.order.line'].create(shipping_vals)
# Add fee lines
for fee in wc_order.get('fee_lines', []):
fee_vals = {
'order_id': sale_order.id,
'name': fee.get('name', 'Fee'),
'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
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,
})
# Link invoice to woo.order
if invoice:
invoice.woo_order_id = woo_order.id
# Push SO ref back to WC
try:
client = self._get_client()
client.update_order(woo_order_id, {
'meta_data': [
{'key': '_odoo_order_id', 'value': str(sale_order.id)},
{'key': '_odoo_order_ref', 'value': sale_order.name},
]
})
except Exception as e:
_logger.warning("Failed to push SO ref to WC: %s", e)
self._log_sync(
'order', 'woo_to_odoo', sale_order.name, 'success',
f'Created from WC order #{woo_order.woo_order_number}',
)
return woo_order
def _find_or_create_customer(self, wc_order):
"""Find or create an Odoo partner from a WC order's billing data."""
self.ensure_one()
billing = wc_order.get('billing', {})
email = billing.get('email', '').strip().lower()
wc_customer_id = wc_order.get('customer_id', 0)
if not email:
# Guest checkout — create a one-off partner
name = f"{billing.get('first_name', '')} {billing.get('last_name', '')}".strip()
return self.env['res.partner'].create({
'name': name or 'WooCommerce Guest',
'email': '',
'company_id': self.company_id.id,
})
# Check existing woo.customer link first
if wc_customer_id:
woo_cust = self.env['woo.customer'].search([
('instance_id', '=', self.id),
('woo_customer_id', '=', wc_customer_id),
], limit=1)
if woo_cust:
return woo_cust.partner_id
# Search by email
partner = self.env['res.partner'].search([
('email', '=ilike', email),
'|', ('company_id', '=', self.company_id.id), ('company_id', '=', False),
], limit=1)
if not partner:
partner = self.env['res.partner'].create(
self._prepare_partner_vals(billing)
)
# Ensure woo.customer link exists
if wc_customer_id and not self.env['woo.customer'].search([
('instance_id', '=', self.id),
('woo_customer_id', '=', wc_customer_id),
], limit=1):
self.env['woo.customer'].create({
'instance_id': self.id,
'partner_id': partner.id,
'woo_customer_id': wc_customer_id,
'woo_email': email,
'last_synced': fields.Datetime.now(),
'company_id': self.company_id.id,
})
return partner
def _prepare_partner_vals(self, billing):
"""Build res.partner vals from WC billing data."""
self.ensure_one()
name = f"{billing.get('first_name', '')} {billing.get('last_name', '')}".strip()
country = self.env['res.country'].search([
('code', '=', billing.get('country', '')),
], limit=1)
state = False
if billing.get('state') and country:
state = self.env['res.country.state'].search([
('country_id', '=', country.id),
('code', '=', billing.get('state')),
], limit=1)
return {
'name': name or billing.get('email', 'WooCommerce Customer'),
'email': billing.get('email', ''),
'phone': billing.get('phone', ''),
'street': billing.get('address_1', ''),
'street2': billing.get('address_2', ''),
'city': billing.get('city', ''),
'zip': billing.get('postcode', ''),
'country_id': country.id if country else False,
'state_id': state.id if state else False,
'company_id': self.company_id.id,
}
def _prepare_sale_order_vals(self, wc_order, partner):
"""Build sale.order vals dict from a WC order."""
self.ensure_one()
currency = self.env['res.currency'].search([
('name', '=', wc_order.get('currency', 'CAD')),
], limit=1)
vals = {
'partner_id': partner.id,
'company_id': self.company_id.id,
'client_order_ref': f"WC#{wc_order.get('number', wc_order['id'])}",
}
if currency:
# Set pricelist with matching currency if available
pricelist = self.env['product.pricelist'].search([
('currency_id', '=', currency.id),
'|', ('company_id', '=', self.company_id.id), ('company_id', '=', False),
], limit=1)
if pricelist:
vals['pricelist_id'] = pricelist.id
if self.default_warehouse_id:
vals['warehouse_id'] = self.default_warehouse_id.id
# Shipping address
shipping = wc_order.get('shipping', {})
if shipping.get('first_name') or shipping.get('last_name'):
ship_name = f"{shipping.get('first_name', '')} {shipping.get('last_name', '')}".strip()
ship_country = self.env['res.country'].search([
('code', '=', shipping.get('country', '')),
], limit=1)
ship_state = False
if shipping.get('state') and ship_country:
ship_state = self.env['res.country.state'].search([
('country_id', '=', ship_country.id),
('code', '=', shipping.get('state')),
], limit=1)
ship_partner = self.env['res.partner'].search([
('parent_id', '=', partner.id),
('type', '=', 'delivery'),
('street', '=', shipping.get('address_1', '')),
('city', '=', shipping.get('city', '')),
], limit=1)
if not ship_partner:
ship_partner = self.env['res.partner'].create({
'parent_id': partner.id,
'type': 'delivery',
'name': ship_name,
'street': shipping.get('address_1', ''),
'street2': shipping.get('address_2', ''),
'city': shipping.get('city', ''),
'zip': shipping.get('postcode', ''),
'country_id': ship_country.id if ship_country else False,
'state_id': ship_state.id if ship_state else False,
'company_id': self.company_id.id,
})
vals['partner_shipping_id'] = ship_partner.id
return vals
def _prepare_order_line_vals(self, sale_order, line_item):
"""Build sale.order.line vals from a WC line_item dict."""
self.ensure_one()
wc_product_id = line_item.get('product_id', 0)
wc_variation_id = line_item.get('variation_id', 0)
# Look up mapped product
lookup_id = wc_variation_id or wc_product_id
product_map = self.env['woo.product.map'].search([
('instance_id', '=', self.id),
('woo_product_id', '=', lookup_id),
('state', '=', 'mapped'),
], limit=1)
product = product_map.product_id if product_map else False
# If not mapped, try SKU match
if not product:
sku = line_item.get('sku', '')
if sku:
product = self.env['product.product'].search([
('default_code', '=', sku),
'|', ('company_id', '=', self.company_id.id), ('company_id', '=', False),
], limit=1)
vals = {
'order_id': sale_order.id,
'name': line_item.get('name', 'WooCommerce Product'),
'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', []):
# WC sends tax class on the line item
pass
wc_tax_class = line_item.get('tax_class', '')
if wc_tax_class:
odoo_tax = self.env['woo.tax.map'].get_odoo_tax(self, wc_tax_class)
if odoo_tax:
taxes.append(odoo_tax.id)
elif line_item.get('total_tax') and float(line_item.get('total_tax', 0)) > 0:
# Standard tax class
odoo_tax = self.env['woo.tax.map'].get_odoo_tax(self, 'standard')
if odoo_tax:
taxes.append(odoo_tax.id)
if taxes:
vals['tax_id'] = [(6, 0, taxes)]
return vals
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
return {
'order_id': sale_order.id,
'name': shipping_line.get('method_title', 'Shipping'),
'product_uom_qty': 1,
'price_unit': total,
}
# ------------------------------------------------------------------
# Product / Price Sync (Task 22)
# ------------------------------------------------------------------
def _sync_products(self):
"""Sync products — implemented in Task 22."""
"""Sync product prices between Odoo and WooCommerce."""
self.ensure_one()
_logger.info("Product sync for %s — not yet implemented", self.name)
client = self._get_client()
maps = self.env['woo.product.map'].search([
('instance_id', '=', self.id),
('state', '=', 'mapped'),
('sync_price', '=', True),
('product_id', '!=', False),
])
def _sync_orders(self):
"""Sync orders — implemented in Task 20."""
for pm in maps:
try:
wc_product = client.get_product(pm.woo_product_id)
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',
'map_id': pm.id,
'field_name': 'price',
'odoo_value': str(odoo_price),
'woo_value': str(wc_price),
'company_id': self.company_id.id,
})
pm.state = 'conflict'
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),
})
self._log_sync(
'product', 'odoo_to_woo', pm.product_id.display_name,
'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,
'success', f'Price updated to ${wc_price}',
)
pm.last_synced = fields.Datetime.now()
except Exception as e:
_logger.error(
"Product price sync failed for %s (WC#%s): %s",
pm.product_id.display_name, pm.woo_product_id, e,
)
pm.state = 'error'
self._log_sync(
'product', 'odoo_to_woo',
pm.product_id.display_name, 'failed', str(e),
)
def _sync_product_from_wc(self, wc_data):
"""Handle an inbound WC product webhook — update price if mapped."""
self.ensure_one()
_logger.info("Order sync for %s — not yet implemented", self.name)
wc_product_id = wc_data.get('id')
if not wc_product_id:
return
pm = self.env['woo.product.map'].search([
('instance_id', '=', self.id),
('woo_product_id', '=', wc_product_id),
('state', '=', 'mapped'),
], limit=1)
if not pm or not pm.product_id:
return
if pm.sync_price:
wc_price = float(wc_data.get('regular_price') or 0)
if wc_price and abs(wc_price - pm.product_id.list_price) > 0.01:
pm.product_id.list_price = wc_price
self._log_sync(
'product', 'woo_to_odoo', pm.product_id.display_name,
'success', f'Price updated via webhook to ${wc_price}',
)
pm.last_synced = fields.Datetime.now()
# ------------------------------------------------------------------
# Inventory Sync (Task 22)
# ------------------------------------------------------------------
def _sync_inventory(self):
"""Sync inventory — implemented in Task 22."""
"""Push Odoo stock levels to WooCommerce."""
self.ensure_one()
_logger.info("Inventory sync for %s — not yet implemented", self.name)
client = self._get_client()
maps = self.env['woo.product.map'].search([
('instance_id', '=', self.id),
('state', '=', 'mapped'),
('sync_inventory', '=', True),
('product_id', '!=', False),
])
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',
self.default_warehouse_id.lot_stock_id.id),
])
qty = sum(quant.mapped('quantity')) - sum(quant.mapped('reserved_quantity'))
else:
qty = product.qty_available
qty = max(int(qty), 0)
client.update_product(pm.woo_product_id, {
'stock_quantity': qty,
'manage_stock': True,
})
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:
_logger.error(
"Inventory sync failed for %s (WC#%s): %s",
pm.product_id.display_name, pm.woo_product_id, e,
)
self._log_sync(
'inventory', 'odoo_to_woo',
pm.product_id.display_name, 'failed', str(e),
)
# ------------------------------------------------------------------
# Customer Sync (Task 25)
# ------------------------------------------------------------------
def _sync_customers(self):
"""Sync customers — implemented in Task 25."""
"""Push updated Odoo partner addresses to WooCommerce."""
self.ensure_one()
_logger.info("Customer sync for %s — not yet implemented", self.name)
client = self._get_client()
customers = self.env['woo.customer'].search([
('instance_id', '=', self.id),
('woo_customer_id', '>', 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
billing_data = {
'first_name': (partner.name or '').split(' ', 1)[0],
'last_name': (partner.name or '').split(' ', 1)[1] if ' ' in (partner.name or '') else '',
'email': partner.email or '',
'phone': partner.phone or '',
'address_1': partner.street or '',
'address_2': partner.street2 or '',
'city': partner.city or '',
'postcode': partner.zip or '',
'country': partner.country_id.code if partner.country_id else '',
'state': partner.state_id.code if partner.state_id else '',
}
client.update_customer(cust.woo_customer_id, {
'billing': billing_data,
'shipping': billing_data,
})
cust.last_synced = fields.Datetime.now()
self._log_sync(
'customer', 'odoo_to_woo', partner.display_name,
'success', 'Address updated in WooCommerce',
)
except Exception as e:
_logger.error(
"Customer sync failed for %s (WC#%s): %s",
cust.partner_id.display_name, cust.woo_customer_id, e,
)
self._log_sync(
'customer', 'odoo_to_woo',
cust.partner_id.display_name, 'failed', str(e),
)
def _sync_customer_from_wc(self, wc_data):
"""Handle an inbound WC customer webhook — update partner if linked."""
self.ensure_one()
wc_customer_id = wc_data.get('id')
email = wc_data.get('email', '').strip().lower()
if not wc_customer_id:
return
cust = self.env['woo.customer'].search([
('instance_id', '=', self.id),
('woo_customer_id', '=', wc_customer_id),
], limit=1)
billing = wc_data.get('billing', {})
if cust:
partner = cust.partner_id
# Update partner address from WC
vals = self._prepare_partner_vals(billing)
vals.pop('company_id', None)
partner.write(vals)
cust.last_synced = fields.Datetime.now()
self._log_sync(
'customer', 'woo_to_odoo', partner.display_name,
'success', 'Updated from WC webhook',
)
elif email:
# New customer from WC
self.env['woo.customer']._find_or_create(self, email, wc_data)
# ------------------------------------------------------------------
# Notification
# ------------------------------------------------------------------
def _notify_failure(self, sync_type, error_message):
"""Send email notification on sync failure."""

View File

@@ -1,4 +1,10 @@
import base64
import logging
from odoo import fields, models
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class WooOrder(models.Model):
@@ -23,3 +29,143 @@ class WooOrder(models.Model):
('cancelled', 'Cancelled'),
], default='new')
shipment_ids = fields.One2many('woo.shipment', 'order_id')
# ------------------------------------------------------------------
# Push methods (Task 20)
# ------------------------------------------------------------------
def action_push_shipping(self, tracking_number, carrier_id=False):
"""Push shipping/tracking info to WooCommerce and update status."""
self.ensure_one()
client = self.instance_id._get_client()
update_data = {
'status': 'completed',
}
# Build tracking meta
meta = [
{'key': '_tracking_number', 'value': tracking_number},
]
if carrier_id:
carrier = self.env['woo.shipping.carrier'].browse(carrier_id)
if carrier.exists():
meta.append({'key': '_tracking_provider', 'value': carrier.name})
if carrier.tracking_url:
url = carrier.tracking_url.replace('{tracking}', tracking_number)
meta.append({'key': '_tracking_url', 'value': url})
update_data['meta_data'] = meta
client.update_order(self.woo_order_id, update_data)
self.woo_status = 'completed'
self.state = 'shipped'
self.instance_id._log_sync(
'order', 'odoo_to_woo', self.sale_order_id.name,
'success', f'Shipping pushed with tracking: {tracking_number}',
)
def action_push_completed(self):
"""Mark WC order as completed."""
self.ensure_one()
client = self.instance_id._get_client()
client.update_order(self.woo_order_id, {'status': 'completed'})
self.woo_status = 'completed'
self.state = 'completed'
self.instance_id._log_sync(
'order', 'odoo_to_woo', self.sale_order_id.name,
'success', 'Order marked as completed in WC',
)
def action_push_invoice_pdf(self):
"""Render invoice PDF and push to WC via custom plugin endpoint."""
self.ensure_one()
if not self.invoice_id:
raise UserError("No invoice linked to this WC order.")
# Generate PDF report
report = self.env.ref('account.account_invoices')
pdf_content, _content_type = report._render_qweb_pdf(
report.id, [self.invoice_id.id]
)
pdf_b64 = base64.b64encode(pdf_content).decode('utf-8')
# Push to WC via order note or meta
try:
client = self.instance_id._get_client()
client.update_order(self.woo_order_id, {
'meta_data': [
{'key': '_odoo_invoice_ref', 'value': self.invoice_id.name},
{'key': '_odoo_invoice_pdf', 'value': pdf_b64},
]
})
self.invoice_synced = True
self.instance_id._log_sync(
'invoice', 'odoo_to_woo', self.invoice_id.name,
'success', 'Invoice PDF pushed to WC',
)
except Exception as e:
_logger.error("Failed to push invoice PDF to WC: %s", e)
self.instance_id._log_sync(
'invoice', 'odoo_to_woo', self.invoice_id.name,
'failed', str(e),
)
raise
def action_push_delivery_pdf(self, picking):
"""Render delivery slip PDF and push to WC."""
self.ensure_one()
report = self.env.ref('stock.action_report_delivery')
pdf_content, _content_type = report._render_qweb_pdf(
report.id, [picking.id]
)
pdf_b64 = base64.b64encode(pdf_content).decode('utf-8')
try:
client = self.instance_id._get_client()
client.update_order(self.woo_order_id, {
'meta_data': [
{'key': '_odoo_delivery_ref', 'value': picking.name},
{'key': '_odoo_delivery_pdf', 'value': pdf_b64},
]
})
self.instance_id._log_sync(
'order', 'odoo_to_woo', picking.name,
'success', 'Delivery PDF pushed to WC',
)
except Exception as e:
_logger.error("Failed to push delivery PDF to WC: %s", e)
def _push_messages_to_wc(self):
"""Extract customer-visible messages and push as WC order notes."""
self.ensure_one()
if not self.sale_order_id:
return
client = self.instance_id._get_client()
# Get messages from the sale order that are customer-visible
messages = self.env['mail.message'].search([
('res_id', '=', self.sale_order_id.id),
('model', '=', 'sale.order'),
('message_type', 'in', ['comment', 'email']),
('subtype_id.internal', '=', False),
], order='create_date asc')
for msg in messages:
note_body = msg.body or msg.preview or ''
if not note_body:
continue
try:
# WC order notes endpoint
client.post(f'orders/{self.woo_order_id}/notes', {
'note': note_body,
'customer_note': True,
})
except Exception as e:
_logger.warning(
"Failed to push message to WC order %s: %s",
self.woo_order_id, e,
)