From 8983c8bd503b54143be4ff0a5cff8306fbf241c8 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Wed, 1 Apr 2026 18:10:07 -0400 Subject: [PATCH] feat: variant wizard now creates AND updates existing WC variations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../static/src/xml/product_mapping.xml | 15 +-- .../wizard/woo_variant_push.py | 108 ++++++++++++++++-- .../wizard/woo_variant_push_views.xml | 7 +- 3 files changed, 105 insertions(+), 25 deletions(-) diff --git a/fusion-woo-odoo/fusion_woocommerce/static/src/xml/product_mapping.xml b/fusion-woo-odoo/fusion_woocommerce/static/src/xml/product_mapping.xml index 13e54133..8b43c3b1 100644 --- a/fusion-woo-odoo/fusion_woocommerce/static/src/xml/product_mapping.xml +++ b/fusion-woo-odoo/fusion_woocommerce/static/src/xml/product_mapping.xml @@ -210,20 +210,15 @@ - - - - - - synced - - diff --git a/fusion-woo-odoo/fusion_woocommerce/wizard/woo_variant_push.py b/fusion-woo-odoo/fusion_woocommerce/wizard/woo_variant_push.py index 910b66f7..c048f266 100644 --- a/fusion-woo-odoo/fusion_woocommerce/wizard/woo_variant_push.py +++ b/fusion-woo-odoo/fusion_woocommerce/wizard/woo_variant_push.py @@ -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') diff --git a/fusion-woo-odoo/fusion_woocommerce/wizard/woo_variant_push_views.xml b/fusion-woo-odoo/fusion_woocommerce/wizard/woo_variant_push_views.xml index 113e758c..b912ddc0 100644 --- a/fusion-woo-odoo/fusion_woocommerce/wizard/woo_variant_push_views.xml +++ b/fusion-woo-odoo/fusion_woocommerce/wizard/woo_variant_push_views.xml @@ -22,8 +22,9 @@ @@ -44,7 +45,7 @@