import base64 import logging from odoo import api, fields, models from odoo.exceptions import UserError _logger = logging.getLogger(__name__) class WooOrder(models.Model): _name = 'woo.order' _description = 'WooCommerce Order' _inherit = ['mail.thread', 'mail.activity.mixin'] _rec_name = 'display_name' _order = 'id desc' display_name = fields.Char(compute='_compute_display_name', store=True) instance_id = fields.Many2one('woo.instance', required=True, ondelete='cascade') sale_order_id = fields.Many2one('sale.order') woo_order_id = fields.Integer(index=True) woo_order_number = fields.Char(string='WC Order #') woo_status = fields.Selection([ ('pending', 'Pending Payment'), ('processing', 'Processing'), ('on-hold', 'On Hold'), ('completed', 'Completed'), ('cancelled', 'Cancelled'), ('refunded', 'Refunded'), ('failed', 'Failed'), ('trash', 'Trashed'), ], string='WC Status', tracking=True) invoice_id = fields.Many2one('account.move') invoice_synced = fields.Boolean() company_id = fields.Many2one( 'res.company', required=True, default=lambda self: self.env.company, ) state = fields.Selection([ ('new', 'New'), ('confirmed', 'Confirmed'), ('shipped', 'Shipped'), ('completed', 'Completed'), ('cancelled', 'Cancelled'), ], default='new', tracking=True) WC_STATUS_TO_STATE = { 'pending': 'new', 'on-hold': 'new', 'processing': 'confirmed', 'completed': 'completed', 'cancelled': 'cancelled', 'refunded': 'cancelled', 'failed': 'cancelled', 'trash': 'cancelled', } @api.onchange('woo_status') def _onchange_woo_status(self): if self.woo_status: self.state = self.WC_STATUS_TO_STATE.get(self.woo_status, self.state) def _set_woo_status(self, wc_status): """Set woo_status and auto-map to Odoo state.""" vals = {} if wc_status: vals['woo_status'] = wc_status mapped_state = self.WC_STATUS_TO_STATE.get(wc_status) if mapped_state: vals['state'] = mapped_state if vals: self.write(vals) shipment_ids = fields.One2many('woo.shipment', 'order_id') delivery_count = fields.Integer(compute='_compute_delivery_count') invoice_count = fields.Integer(compute='_compute_invoice_count') @api.depends('woo_order_number', 'sale_order_id', 'sale_order_id.name') def _compute_display_name(self): for rec in self: parts = [] if rec.woo_order_number: parts.append('WC#%s' % rec.woo_order_number) if rec.sale_order_id: parts.append(rec.sale_order_id.name) rec.display_name = ' — '.join(parts) if parts else 'WC Order #%s' % rec.id @api.depends('sale_order_id') def _compute_delivery_count(self): for rec in self: if rec.sale_order_id: rec.delivery_count = self.env['stock.picking'].search_count([ ('origin', '=', rec.sale_order_id.name), ]) else: rec.delivery_count = 0 @api.depends('sale_order_id') def _compute_invoice_count(self): for rec in self: if rec.sale_order_id: rec.invoice_count = self.env['account.move'].search_count([ ('invoice_origin', '=', rec.sale_order_id.name), ('move_type', 'in', ['out_invoice', 'out_refund']), ]) else: rec.invoice_count = 0 def action_view_sale_order(self): self.ensure_one() if not self.sale_order_id: return return { 'type': 'ir.actions.act_window', 'res_model': 'sale.order', 'res_id': self.sale_order_id.id, 'views': [(False, 'form')], } def action_view_deliveries(self): self.ensure_one() pickings = self.env['stock.picking'].search([ ('origin', '=', self.sale_order_id.name), ]) if len(pickings) == 1: return { 'type': 'ir.actions.act_window', 'res_model': 'stock.picking', 'res_id': pickings.id, 'views': [(False, 'form')], } return { 'type': 'ir.actions.act_window', 'name': 'Deliveries', 'res_model': 'stock.picking', 'view_mode': 'list,form', 'domain': [('origin', '=', self.sale_order_id.name)], } def action_view_invoices(self): self.ensure_one() invoices = self.env['account.move'].search([ ('invoice_origin', '=', self.sale_order_id.name), ('move_type', 'in', ['out_invoice', 'out_refund']), ]) if len(invoices) == 1: return { 'type': 'ir.actions.act_window', 'res_model': 'account.move', 'res_id': invoices.id, 'views': [(False, 'form')], } return { 'type': 'ir.actions.act_window', 'name': 'Invoices', 'res_model': 'account.move', 'view_mode': 'list,form', 'domain': [('invoice_origin', '=', self.sale_order_id.name), ('move_type', 'in', ['out_invoice', 'out_refund'])], } # ------------------------------------------------------------------ # 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._set_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._set_woo_status('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, )