diff --git a/fusion-woo-odoo/fusion_woocommerce/models/stock_picking.py b/fusion-woo-odoo/fusion_woocommerce/models/stock_picking.py index ea8fc901..12c3a29c 100644 --- a/fusion-woo-odoo/fusion_woocommerce/models/stock_picking.py +++ b/fusion-woo-odoo/fusion_woocommerce/models/stock_picking.py @@ -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 diff --git a/fusion-woo-odoo/fusion_woocommerce/models/woo_instance.py b/fusion-woo-odoo/fusion_woocommerce/models/woo_instance.py index e94dfd70..63b15b65 100644 --- a/fusion-woo-odoo/fusion_woocommerce/models/woo_instance.py +++ b/fusion-woo-odoo/fusion_woocommerce/models/woo_instance.py @@ -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.""" diff --git a/fusion-woo-odoo/fusion_woocommerce/models/woo_order.py b/fusion-woo-odoo/fusion_woocommerce/models/woo_order.py index 84a8d89a..153433fb 100644 --- a/fusion-woo-odoo/fusion_woocommerce/models/woo_order.py +++ b/fusion-woo-odoo/fusion_woocommerce/models/woo_order.py @@ -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, + )