- Pagination with page nav for mapped, unmatched Odoo, and unmatched WC tabs - Per-product arrow buttons to push price in either direction - Bulk price sync buttons: All Prices Odoo→WC and All Prices WC→Odoo - Server-side offset/limit with total count in search endpoints Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
144 lines
5.4 KiB
Python
144 lines
5.4 KiB
Python
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'
|
|
_description = 'WooCommerce Product Mapping'
|
|
|
|
instance_id = fields.Many2one('woo.instance', required=True, ondelete='cascade')
|
|
product_id = fields.Many2one('product.product')
|
|
woo_product_id = fields.Integer()
|
|
woo_product_name = fields.Char()
|
|
woo_sku = fields.Char()
|
|
woo_product_type = fields.Selection([
|
|
('simple', 'Simple'),
|
|
('variable', 'Variable'),
|
|
('grouped', 'Grouped'),
|
|
('external', 'External'),
|
|
])
|
|
woo_price = fields.Float(string='WC Price', digits='Product Price')
|
|
woo_parent_id = fields.Integer()
|
|
is_variation = fields.Boolean()
|
|
sync_price = fields.Boolean(default=True)
|
|
sync_inventory = fields.Boolean(default=True)
|
|
sync_images = fields.Boolean(default=True)
|
|
woo_image_ids = fields.Char() # JSON
|
|
last_synced = fields.Datetime()
|
|
company_id = fields.Many2one(
|
|
'res.company', required=True, default=lambda self: self.env.company,
|
|
)
|
|
state = fields.Selection([
|
|
('unmapped', 'Unmapped'),
|
|
('mapped', 'Mapped'),
|
|
('conflict', 'Conflict'),
|
|
('error', 'Error'),
|
|
], default='unmapped')
|
|
|
|
# ------------------------------------------------------------------
|
|
# Individual Price Sync
|
|
# ------------------------------------------------------------------
|
|
|
|
def action_push_price_to_odoo(self):
|
|
"""Update Odoo product price from WC price."""
|
|
for rec in self:
|
|
if rec.product_id and rec.woo_price:
|
|
rec.product_id.list_price = rec.woo_price
|
|
rec.instance_id._log_sync(
|
|
'product', 'woo_to_odoo', rec.product_id.name, 'success',
|
|
'Price updated from WC: $%.2f' % rec.woo_price,
|
|
)
|
|
|
|
def action_push_price_to_wc(self):
|
|
"""Update WC product price from Odoo price."""
|
|
for rec in self:
|
|
if rec.product_id and rec.instance_id:
|
|
client = rec.instance_id._get_client()
|
|
new_price = str(rec.product_id.list_price)
|
|
client.update_product(rec.woo_product_id, {'regular_price': new_price})
|
|
rec.woo_price = rec.product_id.list_price
|
|
rec.instance_id._log_sync(
|
|
'product', 'odoo_to_woo', rec.product_id.name, 'success',
|
|
'Price pushed to WC: $%.2f' % rec.product_id.list_price,
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# 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()
|