Files
Odoo-Modules/fusion-woo-odoo/fusion_woocommerce/wizard/woo_variant_push.py
gsinghpal 21c1e37211 fix: save wizard images to Odoo product before passing URL to WC
The wizard's binary image field wasn't being saved to the product.product
record, so the public URL returned the default placeholder. Now saves
line.image → variant.image_1920 first, then generates the URL with a
cache-busting timestamp. Only includes image in WC data if the wizard
line has an actual image uploaded.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 21:35:03 -04:00

332 lines
14 KiB
Python

import base64
import json
import logging
from odoo import api, fields, models
from odoo.exceptions import UserError
from ..lib.image_processor import ImageProcessor
_logger = logging.getLogger(__name__)
class WooVariantPushWizard(models.TransientModel):
_name = 'woo.variant.push.wizard'
_description = 'Push Variants to WooCommerce'
instance_id = fields.Many2one('woo.instance', required=True)
product_map_id = fields.Many2one('woo.product.map', required=True, string='Product Mapping')
product_template_id = fields.Many2one('product.template', string='Product Template', readonly=True)
product_name = fields.Char(readonly=True)
woo_product_id = fields.Integer(readonly=True)
line_ids = fields.One2many('woo.variant.push.line', 'wizard_id', string='Variants')
@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
map_id = self.env.context.get('default_product_map_id')
if map_id:
pm = self.env['woo.product.map'].browse(map_id)
if pm.exists() and pm.product_id:
tmpl = pm.product_id.product_tmpl_id
res['instance_id'] = pm.instance_id.id
res['product_template_id'] = tmpl.id
res['product_name'] = tmpl.name
res['woo_product_id'] = pm.woo_product_id
# Populate variant lines — only active variants with attribute values
lines = []
active_variants = tmpl.product_variant_ids.filtered(
lambda v: v.active and v.product_template_attribute_value_ids
)
for variant in active_variants:
already_mapped = self.env['woo.product.map'].search([
('instance_id', '=', pm.instance_id.id),
('product_id', '=', variant.id),
('is_variation', '=', True),
], limit=1)
attr_values = ', '.join(
variant.product_template_attribute_value_ids.mapped('name')
)
lines.append((0, 0, {
'product_id': variant.id,
'variant_name': variant.display_name,
'attribute_values': attr_values,
'sku': already_mapped.woo_sku if already_mapped else (variant.default_code or ''),
'regular_price': already_mapped.woo_regular_price if already_mapped else variant.list_price,
'sale_price': already_mapped.woo_sale_price if already_mapped else 0.0,
'cost_price': variant.standard_price,
'image': variant.image_variant_1920 or variant.image_1920 or False,
'include': True,
'already_synced': bool(already_mapped),
'wc_variation_id': already_mapped.woo_product_id if already_mapped else 0,
'map_id': already_mapped.id if already_mapped else 0,
}))
res['line_ids'] = lines
return res
def action_push(self):
"""Push selected variants to WooCommerce."""
self.ensure_one()
pm = self.product_map_id
inst = pm.instance_id
client = inst._get_client()
tmpl = self.product_template_id
_logger.info(
"Variant push wizard: %d lines total, fields: %s",
len(self.line_ids),
[(l.variant_name, l.already_synced, l.wc_variation_id, l.map_id) for l in self.line_ids],
)
lines_new = self.line_ids.filtered(lambda l: l.include and not l.already_synced)
lines_update = self.line_ids.filtered(lambda l: l.include and l.already_synced)
_logger.info("Variant push: %d new, %d update", len(lines_new), len(lines_update))
if not lines_new and not lines_update:
raise UserError("No variants selected.")
# Step 1: Build WC attributes from Odoo attribute lines
wc_attributes = []
for attr_line in tmpl.attribute_line_ids:
attr_name = attr_line.attribute_id.name
attr_values = attr_line.value_ids.mapped('name')
wc_attr = pm._find_or_create_wc_attribute(client, attr_name)
wc_terms = []
for val_name in attr_values:
term = pm._find_or_create_wc_attribute_term(client, wc_attr['id'], val_name)
wc_terms.append(term['name'])
wc_attributes.append({
'id': wc_attr['id'],
'name': attr_name,
'position': 0,
'visible': True,
'variation': True,
'options': wc_terms,
})
# Build a lookup: Odoo attribute name → WC attribute ID
wc_attr_id_map = {}
for wc_attr in wc_attributes:
wc_attr_id_map[wc_attr['name'].upper()] = wc_attr['id']
# Step 2: Update product as variable with attributes + set default
# Default attribute = first included variant's attribute values
default_attrs = []
first_included = self.line_ids.filtered('include')[:1]
if first_included and first_included.product_id:
for ptav in first_included.product_id.product_template_attribute_value_ids:
default_attrs.append({
'name': ptav.attribute_id.name,
'option': ptav.name,
})
parent_update = {
'type': 'variable',
'attributes': wc_attributes,
}
if default_attrs:
parent_update['default_attributes'] = default_attrs
try:
client.update_product(pm.woo_product_id, parent_update)
pm.woo_product_type = 'variable'
except Exception as e:
raise UserError("Failed to update WC product: %s" % str(e))
# Step 3: Create NEW variations
created = 0
updated = 0
errors = []
for line in lines_new:
variant = line.product_id
# Build variation attributes
var_attributes = []
for ptav in variant.product_template_attribute_value_ids:
var_attributes.append({
'name': ptav.attribute_id.name,
'option': ptav.name,
})
var_data = {
'regular_price': str(line.regular_price),
'sku': line.sku or '',
'attributes': var_attributes,
'manage_stock': True,
'stock_quantity': int(variant.qty_available),
}
# Sale price
if line.sale_price > 0:
var_data['sale_price'] = str(line.sale_price)
# Tax class
wc_tax_class = self.env['woo.tax.map'].get_wc_tax_class(
inst, variant.taxes_id[:1].id if variant.taxes_id else False
)
if wc_tax_class:
var_data['tax_class'] = wc_tax_class
# Save wizard image to Odoo product, then pass URL to WC
if line.image:
variant.image_1920 = line.image
odoo_base = inst.env['ir.config_parameter'].sudo().get_param('web.base.url', '')
if odoo_base:
img_name = (line.sku or variant.default_code or 'variant') + '.png'
import time
cache_bust = int(time.time())
img_url = f"{odoo_base}/web/image/product.product/{variant.id}/image_1920/{img_name}?t={cache_bust}"
var_data['image'] = {
'src': img_url,
'name': img_name,
'alt': line.variant_name or '',
}
try:
wc_variation = client.create_product_variation(pm.woo_product_id, var_data)
self.env['woo.product.map'].create({
'instance_id': inst.id,
'product_id': variant.id,
'woo_product_id': wc_variation['id'],
'woo_product_name': line.variant_name,
'woo_sku': line.sku or '',
'woo_regular_price': line.regular_price,
'woo_sale_price': line.sale_price,
'woo_permalink': pm.woo_permalink or '',
'woo_product_type': 'simple',
'woo_parent_id': pm.woo_product_id,
'is_variation': True,
'state': 'mapped',
'company_id': inst.company_id.id,
})
created += 1
except Exception as e:
errors.append('%s: %s' % (line.variant_name, str(e)))
_logger.error("Failed to create variation: %s", e)
# Step 4: UPDATE existing variations
for line in lines_update:
variant = line.product_id
wc_var_id = line.wc_variation_id
if not wc_var_id:
continue
# Build variation attributes with WC attribute IDs
var_attributes = []
for ptav in variant.product_template_attribute_value_ids:
attr_name = ptav.attribute_id.name
wc_attr_id = wc_attr_id_map.get(attr_name.upper(), 0)
attr_entry = {'option': ptav.name}
if wc_attr_id:
attr_entry['id'] = wc_attr_id
else:
attr_entry['name'] = attr_name
var_attributes.append(attr_entry)
var_data = {
'regular_price': str(line.regular_price),
'sku': line.sku or '',
'attributes': var_attributes,
'manage_stock': True,
'stock_quantity': int(variant.qty_available),
}
if line.sale_price > 0:
var_data['sale_price'] = str(line.sale_price)
else:
var_data['sale_price'] = ''
wc_tax_class = self.env['woo.tax.map'].get_wc_tax_class(
inst, variant.taxes_id[:1].id if variant.taxes_id else False
)
if wc_tax_class:
var_data['tax_class'] = wc_tax_class
# Save wizard image to Odoo product, then pass URL to WC
if line.image:
# Save the image from the wizard to the actual Odoo product
variant.image_1920 = line.image
_logger.info("Saved image to Odoo product %d", variant.id)
# Now build the public URL for WC to download
odoo_base = inst.env['ir.config_parameter'].sudo().get_param('web.base.url', '')
if odoo_base:
img_name = (line.sku or variant.default_code or 'variant') + '.png'
# Add timestamp to bust WC cache
import time
cache_bust = int(time.time())
img_url = f"{odoo_base}/web/image/product.product/{variant.id}/image_1920/{img_name}?t={cache_bust}"
var_data['image'] = {
'src': img_url,
'name': img_name,
'alt': line.variant_name or '',
}
_logger.info("Variant image URL: %s", img_url)
try:
client.update_product_variation(pm.woo_product_id, wc_var_id, var_data)
# Update local map record
if line.map_id:
map_rec = self.env['woo.product.map'].browse(line.map_id)
if map_rec.exists():
map_rec.write({
'woo_sku': line.sku or '',
'woo_regular_price': line.regular_price,
'woo_sale_price': line.sale_price,
})
updated += 1
except Exception as e:
errors.append('Update %s: %s' % (line.variant_name, str(e)))
_logger.error("Failed to update variation: %s", e)
parts = []
if created:
parts.append('%d created' % created)
if updated:
parts.append('%d updated' % updated)
summary = ', '.join(parts) if parts else 'No changes'
inst._log_sync('product', 'odoo_to_woo', tmpl.name, 'success',
'Variants: %s for WC product #%s' % (summary, pm.woo_product_id))
msg = 'Variants: %s.' % summary
if errors:
msg += '\n\nErrors:\n' + '\n'.join(errors)
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Variants Pushed',
'message': msg,
'type': 'success' if not errors else 'warning',
'sticky': bool(errors),
'next': {'type': 'ir.actions.act_window_close'},
},
}
class WooVariantPushLine(models.TransientModel):
_name = 'woo.variant.push.line'
_description = 'Variant Push Line'
wizard_id = fields.Many2one('woo.variant.push.wizard', ondelete='cascade')
product_id = fields.Many2one('product.product', string='Variant')
variant_name = fields.Char(string='Variant')
attribute_values = fields.Char(string='Attributes')
sku = fields.Char(string='SKU')
regular_price = fields.Float(string='Standard Price', digits='Product Price')
sale_price = fields.Float(string='Sale Price', digits='Product Price')
cost_price = fields.Float(string='Cost', digits='Product Price')
image = fields.Binary(string='Image')
include = fields.Boolean(string='Include', default=True)
already_synced = fields.Boolean(string='Already Synced')
wc_variation_id = fields.Integer(string='WC Variation ID')
map_id = fields.Integer(string='Map Record ID')