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:
@@ -210,20 +210,15 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<t t-esc="p.odoo_product_name"/>
|
<t t-esc="p.odoo_product_name"/>
|
||||||
<t t-if="p.needs_variant_push">
|
<t t-if="p.odoo_variant_count > 1 and !p.is_variation">
|
||||||
<button class="woo-btn woo-btn-primary woo-btn-sm ms-2"
|
<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)"
|
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"/>
|
<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>
|
</button>
|
||||||
</t>
|
</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>
|
||||||
<td class="text-end woo-editable-cell" t-on-click.stop="() => this.startEdit(p.id, 'woo_regular', p.woo_regular_price)">
|
<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')">
|
<t t-if="this.isEditing(p.id, 'woo_regular')">
|
||||||
|
|||||||
@@ -58,14 +58,15 @@ class WooVariantPushWizard(models.TransientModel):
|
|||||||
'product_id': variant.id,
|
'product_id': variant.id,
|
||||||
'variant_name': variant.display_name,
|
'variant_name': variant.display_name,
|
||||||
'attribute_values': attr_values,
|
'attribute_values': attr_values,
|
||||||
'sku': variant.default_code or '',
|
'sku': already_mapped.woo_sku if already_mapped else (variant.default_code or ''),
|
||||||
'regular_price': variant.list_price,
|
'regular_price': already_mapped.woo_regular_price if already_mapped else variant.list_price,
|
||||||
'sale_price': 0.0,
|
'sale_price': already_mapped.woo_sale_price if already_mapped else 0.0,
|
||||||
'cost_price': variant.standard_price,
|
'cost_price': variant.standard_price,
|
||||||
'image': variant.image_variant_1920 or variant.image_1920 or False,
|
'image': variant.image_variant_1920 or variant.image_1920 or False,
|
||||||
'include': not bool(already_mapped),
|
'include': True,
|
||||||
'already_synced': bool(already_mapped),
|
'already_synced': bool(already_mapped),
|
||||||
'wc_variation_id': already_mapped.woo_product_id if already_mapped else 0,
|
'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
|
self.line_ids = lines
|
||||||
|
|
||||||
@@ -77,9 +78,11 @@ class WooVariantPushWizard(models.TransientModel):
|
|||||||
client = inst._get_client()
|
client = inst._get_client()
|
||||||
tmpl = self.product_template_id
|
tmpl = self.product_template_id
|
||||||
|
|
||||||
lines_to_push = self.line_ids.filtered(lambda l: l.include and not l.already_synced)
|
lines_new = self.line_ids.filtered(lambda l: l.include and not l.already_synced)
|
||||||
if not lines_to_push:
|
lines_update = self.line_ids.filtered(lambda l: l.include and l.already_synced)
|
||||||
raise UserError("No new variants selected to push.")
|
|
||||||
|
if not lines_new and not lines_update:
|
||||||
|
raise UserError("No variants selected.")
|
||||||
|
|
||||||
# Step 1: Build WC attributes from Odoo attribute lines
|
# Step 1: Build WC attributes from Odoo attribute lines
|
||||||
wc_attributes = []
|
wc_attributes = []
|
||||||
@@ -112,10 +115,11 @@ class WooVariantPushWizard(models.TransientModel):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise UserError("Failed to convert WC product to variable: %s" % str(e))
|
raise UserError("Failed to convert WC product to variable: %s" % str(e))
|
||||||
|
|
||||||
# Step 3: Create variations
|
# Step 3: Create NEW variations
|
||||||
created = 0
|
created = 0
|
||||||
|
updated = 0
|
||||||
errors = []
|
errors = []
|
||||||
for line in lines_to_push:
|
for line in lines_new:
|
||||||
variant = line.product_id
|
variant = line.product_id
|
||||||
|
|
||||||
# Build variation attributes
|
# Build variation attributes
|
||||||
@@ -206,10 +210,89 @@ class WooVariantPushWizard(models.TransientModel):
|
|||||||
errors.append('%s: %s' % (line.variant_name, str(e)))
|
errors.append('%s: %s' % (line.variant_name, str(e)))
|
||||||
_logger.error("Failed to create variation: %s", e)
|
_logger.error("Failed to create variation: %s", e)
|
||||||
|
|
||||||
inst._log_sync('product', 'odoo_to_woo', tmpl.name, 'success',
|
# Step 4: UPDATE existing variations
|
||||||
'Pushed %d variants to WC product #%s' % (created, pm.woo_product_id))
|
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:
|
if errors:
|
||||||
msg += '\n\nErrors:\n' + '\n'.join(errors)
|
msg += '\n\nErrors:\n' + '\n'.join(errors)
|
||||||
|
|
||||||
@@ -242,3 +325,4 @@ class WooVariantPushLine(models.TransientModel):
|
|||||||
include = fields.Boolean(string='Include', default=True)
|
include = fields.Boolean(string='Include', default=True)
|
||||||
already_synced = fields.Boolean(string='Already Synced', readonly=True)
|
already_synced = fields.Boolean(string='Already Synced', readonly=True)
|
||||||
wc_variation_id = fields.Integer(string='WC Variation ID', readonly=True)
|
wc_variation_id = fields.Integer(string='WC Variation ID', readonly=True)
|
||||||
|
map_id = fields.Integer(string='Map Record ID')
|
||||||
|
|||||||
@@ -22,8 +22,9 @@
|
|||||||
|
|
||||||
<separator string="Variants to Push"/>
|
<separator string="Variants to Push"/>
|
||||||
<div class="alert alert-info" role="alert">
|
<div class="alert alert-info" role="alert">
|
||||||
Review and edit each variant's pricing, SKU, and image before pushing.
|
Review and edit each variant's pricing, SKU, and image.
|
||||||
Uncheck "Include" to skip a variant. Already synced variants are shown for reference.
|
New variants will be created, already synced variants will be updated.
|
||||||
|
Uncheck "Include" to skip a variant.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<field name="line_ids">
|
<field name="line_ids">
|
||||||
@@ -44,7 +45,7 @@
|
|||||||
</sheet>
|
</sheet>
|
||||||
<footer>
|
<footer>
|
||||||
<button name="action_push" type="object"
|
<button name="action_push" type="object"
|
||||||
string="Push Variants to WooCommerce" class="oe_highlight"
|
string="Save & Sync to WooCommerce" class="oe_highlight"
|
||||||
icon="fa-cloud-upload"/>
|
icon="fa-cloud-upload"/>
|
||||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
Reference in New Issue
Block a user