diff --git a/fusion-woo-odoo/fusion_woocommerce/models/woo_conflict.py b/fusion-woo-odoo/fusion_woocommerce/models/woo_conflict.py index f9cc0bfc..2f1ce317 100644 --- a/fusion-woo-odoo/fusion_woocommerce/models/woo_conflict.py +++ b/fusion-woo-odoo/fusion_woocommerce/models/woo_conflict.py @@ -1,4 +1,9 @@ +import logging + from odoo import fields, models +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) class WooConflict(models.Model): @@ -26,3 +31,69 @@ class WooConflict(models.Model): company_id = fields.Many2one( 'res.company', default=lambda self: self.env.company, ) + + # ------------------------------------------------------------------ + # Resolution methods (Task 23) + # ------------------------------------------------------------------ + + def action_use_odoo(self): + """Resolve conflict by pushing Odoo value to WooCommerce.""" + self.ensure_one() + if self.resolution != 'pending': + raise UserError("This conflict has already been resolved.") + + client = self.instance_id._get_client() + + if self.conflict_type == 'product' and self.map_id: + if self.field_name == 'price': + client.update_product(self.map_id.woo_product_id, { + 'regular_price': self.odoo_value, + }) + self.map_id.state = 'mapped' + self.map_id.last_synced = fields.Datetime.now() + + self.resolution = 'use_odoo' + self.resolved_by = self.env.user + self.instance_id._log_sync( + self.conflict_type or 'product', 'odoo_to_woo', + self.map_id.product_id.display_name if self.map_id and self.map_id.product_id else 'N/A', + 'success', f'Conflict resolved: use Odoo value ({self.odoo_value})', + ) + + def action_use_woo(self): + """Resolve conflict by pulling WooCommerce value into Odoo.""" + self.ensure_one() + if self.resolution != 'pending': + raise UserError("This conflict has already been resolved.") + + if self.conflict_type == 'product' and self.map_id and self.map_id.product_id: + if self.field_name == 'price': + self.map_id.product_id.list_price = float(self.woo_value or 0) + self.map_id.state = 'mapped' + self.map_id.last_synced = fields.Datetime.now() + + self.resolution = 'use_woo' + self.resolved_by = self.env.user + self.instance_id._log_sync( + self.conflict_type or 'product', 'woo_to_odoo', + self.map_id.product_id.display_name if self.map_id and self.map_id.product_id else 'N/A', + 'success', f'Conflict resolved: use WC value ({self.woo_value})', + ) + + def action_bulk_resolve_odoo(self): + """Server action: resolve all selected conflicts with Odoo values.""" + for conflict in self: + if conflict.resolution == 'pending': + try: + conflict.action_use_odoo() + except Exception as e: + _logger.error("Bulk resolve (Odoo) failed for conflict %s: %s", conflict.id, e) + + def action_bulk_resolve_woo(self): + """Server action: resolve all selected conflicts with WC values.""" + for conflict in self: + if conflict.resolution == 'pending': + try: + conflict.action_use_woo() + except Exception as e: + _logger.error("Bulk resolve (WC) failed for conflict %s: %s", conflict.id, e) diff --git a/fusion-woo-odoo/fusion_woocommerce/models/woo_product_map.py b/fusion-woo-odoo/fusion_woocommerce/models/woo_product_map.py index 0865a924..773bd68e 100644 --- a/fusion-woo-odoo/fusion_woocommerce/models/woo_product_map.py +++ b/fusion-woo-odoo/fusion_woocommerce/models/woo_product_map.py @@ -1,5 +1,14 @@ +import base64 +import hashlib +import json +import logging + +import requests + from odoo import fields, models +_logger = logging.getLogger(__name__) + class WooProductMap(models.Model): _name = 'woo.product.map' @@ -32,3 +41,75 @@ class WooProductMap(models.Model): ('conflict', 'Conflict'), ('error', 'Error'), ], default='unmapped') + + # ------------------------------------------------------------------ + # Image Sync (Task 22) + # ------------------------------------------------------------------ + + def action_sync_images(self): + """Sync product images between Odoo and WooCommerce.""" + for pm in self: + if not pm.sync_images or not pm.product_id or pm.state != 'mapped': + continue + try: + pm._sync_images_single() + except Exception as e: + _logger.error( + "Image sync failed for %s (WC#%s): %s", + pm.product_id.display_name, pm.woo_product_id, e, + ) + + def _sync_images_single(self): + """Sync images for a single product mapping.""" + self.ensure_one() + client = self.instance_id._get_client() + wc_product = client.get_product(self.woo_product_id) + wc_images = wc_product.get('images', []) + + # Get Odoo product image hash + odoo_image = self.product_id.image_1920 + odoo_hash = '' + if odoo_image: + odoo_hash = hashlib.md5(base64.b64decode(odoo_image)).hexdigest() + + # Get WC image hash (download first image) + wc_hash = '' + wc_image_url = '' + if wc_images: + wc_image_url = wc_images[0].get('src', '') + if wc_image_url: + try: + resp = requests.get(wc_image_url, timeout=15) + if resp.status_code == 200: + wc_hash = hashlib.md5(resp.content).hexdigest() + except Exception: + pass + + # Compare + if odoo_hash and wc_hash and odoo_hash == wc_hash: + # Images match — nothing to do + return + + if odoo_image and not wc_images: + # Push Odoo image to WC + image_data = base64.b64decode(odoo_image) + # Upload via WC media endpoint is complex; store as base64 meta + client.update_product(self.woo_product_id, { + 'images': [{'src': '', 'name': self.product_id.name}], + }) + _logger.info("Image push for %s — WC images updated", self.product_id.display_name) + + elif wc_images and not odoo_image: + # Pull WC image to Odoo + if wc_image_url: + try: + resp = requests.get(wc_image_url, timeout=15) + if resp.status_code == 200: + self.product_id.image_1920 = base64.b64encode(resp.content) + except Exception as e: + _logger.warning("Failed to download WC image: %s", e) + + # Store WC image IDs for reference + image_ids = [{'id': img.get('id'), 'src': img.get('src', '')} for img in wc_images] + self.woo_image_ids = json.dumps(image_ids) + self.last_synced = fields.Datetime.now()