feat: variant wizard now creates AND updates existing WC variations

Already synced variants are editable — change price, SKU, image and click
Save & Sync to update them on WooCommerce. New variants are created,
existing ones updated in a single action. Button shows on all products
with variants (purple for new, grey for already synced).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-01 18:10:07 -04:00
parent 1bf4092b39
commit 8983c8bd50
3 changed files with 105 additions and 25 deletions

View File

@@ -210,20 +210,15 @@
</td>
<td>
<t t-esc="p.odoo_product_name"/>
<t t-if="p.needs_variant_push">
<button class="woo-btn woo-btn-primary woo-btn-sm ms-2"
<t t-if="p.odoo_variant_count > 1 and !p.is_variation">
<button class="woo-btn woo-btn-sm ms-2"
t-att-class="p.needs_variant_push ? 'woo-btn woo-btn-primary woo-btn-sm ms-2' : 'woo-btn woo-btn-secondary woo-btn-sm ms-2'"
t-on-click.stop="() => this.pushVariantsToWC(p.id)"
title="Push Odoo variants to WooCommerce">
title="Manage variants — create new or update existing">
<i class="fa fa-sitemap me-1"/>
Push <t t-esc="p.odoo_variant_count"/> variants
<t t-esc="p.odoo_variant_count"/> variants
</button>
</t>
<t t-elif="p.odoo_variant_count > 1 and !p.is_variation">
<span class="woo-badge woo-badge-mapped ms-2">
<i class="fa fa-sitemap me-1"/>
<t t-esc="p.odoo_variant_count"/> synced
</span>
</t>
</td>
<td class="text-end woo-editable-cell" t-on-click.stop="() => this.startEdit(p.id, 'woo_regular', p.woo_regular_price)">
<t t-if="this.isEditing(p.id, 'woo_regular')">

View File

@@ -58,14 +58,15 @@ class WooVariantPushWizard(models.TransientModel):
'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,
'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': not bool(already_mapped),
'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 False,
}))
self.line_ids = lines
@@ -77,9 +78,11 @@ class WooVariantPushWizard(models.TransientModel):
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.")
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)
if not lines_new and not lines_update:
raise UserError("No variants selected.")
# Step 1: Build WC attributes from Odoo attribute lines
wc_attributes = []
@@ -112,10 +115,11 @@ class WooVariantPushWizard(models.TransientModel):
except Exception as e:
raise UserError("Failed to convert WC product to variable: %s" % str(e))
# Step 3: Create variations
# Step 3: Create NEW variations
created = 0
updated = 0
errors = []
for line in lines_to_push:
for line in lines_new:
variant = line.product_id
# Build variation attributes
@@ -206,10 +210,89 @@ class WooVariantPushWizard(models.TransientModel):
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))
# 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
msg = 'Successfully pushed %d variant(s) to WooCommerce.' % created
var_data = {
'regular_price': str(line.regular_price),
'sku': line.sku or '',
'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
# Upload new image if changed
if line.image:
try:
img_data = line.image
if isinstance(img_data, bytes):
img_data = img_data.decode('utf-8')
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):
var_data['image'] = {'id': resp.json()['id']}
except Exception as img_err:
_logger.warning("Variant image upload failed: %s", img_err)
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)
@@ -242,3 +325,4 @@ class WooVariantPushLine(models.TransientModel):
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)
map_id = fields.Integer(string='Map Record ID')

View File

@@ -22,8 +22,9 @@
<separator string="Variants to Push"/>
<div class="alert alert-info" role="alert">
Review and edit each variant's pricing, SKU, and image before pushing.
Uncheck "Include" to skip a variant. Already synced variants are shown for reference.
Review and edit each variant's pricing, SKU, and image.
New variants will be created, already synced variants will be updated.
Uncheck "Include" to skip a variant.
</div>
<field name="line_ids">
@@ -44,7 +45,7 @@
</sheet>
<footer>
<button name="action_push" type="object"
string="Push Variants to WooCommerce" class="oe_highlight"
string="Save &amp; Sync to WooCommerce" class="oe_highlight"
icon="fa-cloud-upload"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>