Files
Odoo-Modules/fusion-woo-odoo/fusion_woocommerce/wizard/woo_variant_push.py
gsinghpal 1bf4092b39 feat: variant push wizard — review pricing, SKU, images before pushing
Replaced direct push with a wizard that shows all variants in an editable
table. User can review/edit standard price, sale price, SKU, and image
per variant before pushing. Include/exclude toggle per variant. Already
synced variants shown for reference. Geo-tags images if configured.

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

245 lines
9.9 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
return res
@api.onchange('product_map_id')
def _onchange_product_map(self):
if self.product_map_id and self.product_map_id.product_id:
tmpl = self.product_map_id.product_id.product_tmpl_id
self.product_template_id = tmpl.id
self.product_name = tmpl.name
self.woo_product_id = self.product_map_id.woo_product_id
lines = []
for variant in tmpl.product_variant_ids:
# Check if already pushed
already_mapped = self.env['woo.product.map'].search([
('instance_id', '=', self.product_map_id.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': variant.default_code or '',
'regular_price': variant.list_price,
'sale_price': 0.0,
'cost_price': variant.standard_price,
'image': variant.image_variant_1920 or variant.image_1920 or False,
'include': not bool(already_mapped),
'already_synced': bool(already_mapped),
'wc_variation_id': already_mapped.woo_product_id if already_mapped else 0,
}))
self.line_ids = lines
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
lines_to_push = self.line_ids.filtered(lambda l: l.include and not l.already_synced)
if not lines_to_push:
raise UserError("No new variants selected to push.")
# 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,
})
# Step 2: Convert product to variable with attributes
try:
client.update_product(pm.woo_product_id, {
'type': 'variable',
'attributes': wc_attributes,
})
pm.woo_product_type = 'variable'
except Exception as e:
raise UserError("Failed to convert WC product to variable: %s" % str(e))
# Step 3: Create variations
created = 0
errors = []
for line in lines_to_push:
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
# Upload variant image
if line.image:
try:
img_data = line.image
if isinstance(img_data, bytes):
img_data = img_data.decode('utf-8')
# Geo-tag if configured
if inst.geo_lat and inst.geo_lng:
img_data = ImageProcessor.geo_tag_image(
img_data,
inst.geo_company_name,
inst.geo_company_address,
inst.geo_company_phone,
inst.geo_lat,
inst.geo_lng,
)
img_bytes = base64.b64decode(img_data)
import requests as req
wp_url = inst.url.rstrip('/')
filename = (line.sku or 'variant_%d' % variant.id) + '.jpg'
resp = req.post(
f"{wp_url}/wp-json/wp/v2/media",
auth=(inst.consumer_key, inst.consumer_secret),
headers={
'Content-Disposition': f'attachment; filename="{filename}"',
'Content-Type': 'image/jpeg',
},
data=img_bytes,
timeout=60,
)
if resp.status_code in (200, 201):
media = resp.json()
var_data['image'] = {'id': media['id']}
except Exception as img_err:
_logger.warning("Variant image upload failed: %s", img_err)
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)
inst._log_sync('product', 'odoo_to_woo', tmpl.name, 'success',
'Pushed %d variants to WC product #%s' % (created, pm.woo_product_id))
msg = 'Successfully pushed %d variant(s) to WooCommerce.' % created
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', readonly=True)
variant_name = fields.Char(string='Variant', readonly=True)
attribute_values = fields.Char(string='Attributes', readonly=True)
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', readonly=True)
wc_variation_id = fields.Integer(string='WC Variation ID', readonly=True)