WooCommerce requires a file extension in image src URLs to determine file type. Added filename.png to all Odoo image URLs. Also fixed variable name ordering bugs where img_name was used before defined. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
707 lines
28 KiB
Python
707 lines
28 KiB
Python
import base64
|
|
import json
|
|
import logging
|
|
|
|
from odoo import api, fields, models
|
|
from odoo.exceptions import UserError
|
|
|
|
from ..lib.ai_service import AIService
|
|
from ..lib.image_processor import ImageProcessor
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class WooProductCreateVariantLine(models.TransientModel):
|
|
_name = 'woo.product.create.variant.line'
|
|
_description = 'Product Creation Variant Line'
|
|
|
|
wizard_id = fields.Many2one('woo.product.create.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')
|
|
sale_price = fields.Float(string='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)
|
|
|
|
|
|
class WooProductCreateWizard(models.TransientModel):
|
|
_name = 'woo.product.create.wizard'
|
|
_description = 'Create Product in WooCommerce'
|
|
|
|
# Step tracking
|
|
step = fields.Selection([
|
|
('basic', 'Basic Info'),
|
|
('images', 'Images'),
|
|
('content', 'Content & SEO'),
|
|
('review', 'Review & Create'),
|
|
], default='basic')
|
|
|
|
instance_id = fields.Many2one('woo.instance', required=True, string='WooCommerce Instance')
|
|
odoo_product_id = fields.Many2one('product.product', string='Odoo Product')
|
|
|
|
# Step 1: Basic Info
|
|
product_name = fields.Char(string='Product Name (Odoo - CAPS)')
|
|
wc_product_name = fields.Char(string='Product Name (WC - Title Case)')
|
|
odoo_category_id = fields.Many2one('product.category', string='Odoo Category')
|
|
wc_category_id = fields.Integer(string='WC Category ID')
|
|
wc_category_name = fields.Char(string='WC Category', readonly=True)
|
|
sale_price = fields.Float(string='Sale Price', digits='Product Price')
|
|
cost_price = fields.Float(string='Cost Price', digits='Product Price')
|
|
sku = fields.Char(string='Internal Reference / SKU')
|
|
sales_tax_id = fields.Many2one('account.tax', string='Sales Tax',
|
|
domain="[('type_tax_use', '=', 'sale')]")
|
|
purchase_tax_id = fields.Many2one('account.tax', string='Purchase Tax',
|
|
domain="[('type_tax_use', '=', 'purchase')]")
|
|
wc_tax_class = fields.Char(string='WC Tax Class')
|
|
|
|
# Step 2: Images
|
|
image_1 = fields.Binary(string='Image 1 (Featured)')
|
|
image_1_filename = fields.Char()
|
|
image_2 = fields.Binary(string='Image 2')
|
|
image_2_filename = fields.Char()
|
|
image_3 = fields.Binary(string='Image 3')
|
|
image_3_filename = fields.Char()
|
|
image_4 = fields.Binary(string='Image 4')
|
|
image_4_filename = fields.Char()
|
|
image_5 = fields.Binary(string='Image 5')
|
|
image_5_filename = fields.Char()
|
|
|
|
# Image AI metadata (stored as JSON text)
|
|
image_metadata = fields.Text(string='Image Metadata', default='[]')
|
|
images_geotagged = fields.Boolean(string='Images Geo-tagged')
|
|
|
|
# Step 3: Content & SEO
|
|
raw_product_info = fields.Text(string='Product Information',
|
|
help='Type everything you know about this product. The AI will use this to generate descriptions.')
|
|
ai_keywords = fields.Char(string='Keywords')
|
|
wc_title = fields.Char(string='WC Product Title')
|
|
short_description = fields.Html(string='Short Description')
|
|
long_description = fields.Html(string='Long Description')
|
|
meta_title = fields.Char(string='Meta Title')
|
|
meta_description = fields.Text(string='Meta Description')
|
|
seo_keywords = fields.Char(string='SEO Focus Keywords')
|
|
|
|
# Variant detection
|
|
has_variants = fields.Boolean(compute='_compute_has_variants')
|
|
variant_count = fields.Integer(compute='_compute_has_variants')
|
|
product_template_id = fields.Many2one('product.template', compute='_compute_has_variants')
|
|
|
|
# Variant line display
|
|
variant_line_ids = fields.One2many('woo.product.create.variant.line', 'wizard_id')
|
|
|
|
# Step 4 is review - uses fields above
|
|
|
|
# --- Variant Detection ---
|
|
|
|
@api.depends('odoo_product_id')
|
|
def _compute_has_variants(self):
|
|
for rec in self:
|
|
if rec.odoo_product_id:
|
|
tmpl = rec.odoo_product_id.product_tmpl_id
|
|
variants = tmpl.product_variant_ids
|
|
rec.product_template_id = tmpl.id
|
|
rec.has_variants = len(variants) > 1
|
|
rec.variant_count = len(variants)
|
|
else:
|
|
rec.product_template_id = False
|
|
rec.has_variants = False
|
|
rec.variant_count = 0
|
|
|
|
# --- Onchange / Defaults ---
|
|
|
|
@api.onchange('odoo_product_id')
|
|
def _onchange_odoo_product(self):
|
|
if self.odoo_product_id:
|
|
product = self.odoo_product_id
|
|
self.product_name = product.name.upper() if product.name else ''
|
|
self.wc_product_name = product.name.title() if product.name else ''
|
|
self.sale_price = product.list_price
|
|
self.cost_price = product.standard_price
|
|
self.sku = product.default_code or ''
|
|
self.odoo_category_id = product.categ_id.id if product.categ_id else False
|
|
# Auto-set sales tax
|
|
if product.taxes_id:
|
|
self.sales_tax_id = product.taxes_id[0].id
|
|
# Auto-set image
|
|
if product.image_1920:
|
|
self.image_1 = product.image_1920
|
|
|
|
# Populate variant lines
|
|
tmpl = product.product_tmpl_id
|
|
variants = tmpl.product_variant_ids
|
|
if len(variants) > 1:
|
|
lines = []
|
|
for variant in variants:
|
|
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 '',
|
|
'sale_price': variant.list_price,
|
|
'cost_price': variant.standard_price,
|
|
'image': variant.image_variant_1920 or False,
|
|
'include': True,
|
|
}))
|
|
self.variant_line_ids = lines
|
|
else:
|
|
self.variant_line_ids = [(5, 0, 0)]
|
|
|
|
@api.onchange('odoo_category_id')
|
|
def _onchange_category(self):
|
|
"""Auto-map Odoo category to WC category using woo.category.map."""
|
|
if self.odoo_category_id and self.instance_id:
|
|
mapping = self.env['woo.category.map'].search([
|
|
('instance_id', '=', self.instance_id.id),
|
|
('odoo_category_id', '=', self.odoo_category_id.id),
|
|
], limit=1)
|
|
if mapping:
|
|
self.wc_category_id = mapping.woo_category_id
|
|
self.wc_category_name = mapping.woo_category_name
|
|
|
|
@api.onchange('sales_tax_id')
|
|
def _onchange_sales_tax(self):
|
|
"""Auto-map sales tax to WC tax class and match purchase tax."""
|
|
if self.sales_tax_id and self.instance_id:
|
|
# Map to WC tax class
|
|
tax_map = self.env['woo.tax.map'].search([
|
|
('instance_id', '=', self.instance_id.id),
|
|
('tax_id', '=', self.sales_tax_id.id),
|
|
], limit=1)
|
|
if tax_map:
|
|
self.wc_tax_class = tax_map.woo_tax_class
|
|
|
|
# Match purchase tax by amount
|
|
purchase_tax = self.env['account.tax'].search([
|
|
('type_tax_use', '=', 'purchase'),
|
|
('amount', '=', self.sales_tax_id.amount),
|
|
('company_id', '=', self.instance_id.company_id.id),
|
|
], limit=1)
|
|
if purchase_tax:
|
|
self.purchase_tax_id = purchase_tax.id
|
|
|
|
# --- Navigation ---
|
|
|
|
def action_next(self):
|
|
self.ensure_one()
|
|
steps = ['basic', 'images', 'content', 'review']
|
|
idx = steps.index(self.step)
|
|
if idx < len(steps) - 1:
|
|
self.step = steps[idx + 1]
|
|
return self._reopen()
|
|
|
|
def action_back(self):
|
|
self.ensure_one()
|
|
steps = ['basic', 'images', 'content', 'review']
|
|
idx = steps.index(self.step)
|
|
if idx > 0:
|
|
self.step = steps[idx - 1]
|
|
return self._reopen()
|
|
|
|
def _reopen(self):
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': self._name,
|
|
'res_id': self.id,
|
|
'views': [(False, 'form')],
|
|
'target': 'new',
|
|
}
|
|
|
|
# --- AI Methods ---
|
|
|
|
def _get_ai_service(self):
|
|
"""Get AIService instance from the woo.instance settings."""
|
|
self.ensure_one()
|
|
inst = self.instance_id
|
|
if not inst.ai_provider or not inst.ai_api_key:
|
|
raise UserError(
|
|
"Please configure AI settings (Provider + API Key) on the WooCommerce instance first."
|
|
)
|
|
return AIService(inst.ai_provider, inst.ai_api_key, inst.ai_model or None)
|
|
|
|
def action_ai_generate_all(self):
|
|
"""Generate all content fields using AI."""
|
|
self.ensure_one()
|
|
ai = self._get_ai_service()
|
|
inst = self.instance_id
|
|
|
|
product_info = {
|
|
'name': self.product_name or '',
|
|
'category': self.odoo_category_id.complete_name if self.odoo_category_id else '',
|
|
'price': self.sale_price,
|
|
'sku': self.sku or '',
|
|
'raw_description': self.raw_product_info or '',
|
|
'keywords': self.ai_keywords or '',
|
|
}
|
|
prompts = {
|
|
'title': inst.prompt_product_title or '',
|
|
'short_desc': inst.prompt_short_description or '',
|
|
'long_desc': inst.prompt_long_description or '',
|
|
'meta_title': inst.prompt_meta_title or '',
|
|
'meta_desc': inst.prompt_meta_description or '',
|
|
'keywords': inst.prompt_keywords or '',
|
|
}
|
|
|
|
result = ai.generate_product_content(product_info, prompts)
|
|
|
|
self.wc_title = result.get('title', self.wc_product_name)
|
|
self.short_description = result.get('short_description', '')
|
|
self.long_description = result.get('long_description', '')
|
|
self.meta_title = result.get('meta_title', '')
|
|
self.meta_description = result.get('meta_description', '')
|
|
self.seo_keywords = result.get('keywords', self.ai_keywords or '')
|
|
|
|
return self._reopen()
|
|
|
|
def action_ai_generate_title(self):
|
|
self.ensure_one()
|
|
ai = self._get_ai_service()
|
|
product_info = {
|
|
'name': self.product_name,
|
|
'category': self.odoo_category_id.complete_name if self.odoo_category_id else '',
|
|
'raw_description': self.raw_product_info or '',
|
|
}
|
|
result = ai.generate_single_field(
|
|
product_info,
|
|
self.instance_id.prompt_product_title or 'Generate SEO product title in Title Case',
|
|
'title',
|
|
)
|
|
self.wc_title = result
|
|
return self._reopen()
|
|
|
|
def action_ai_generate_short_desc(self):
|
|
self.ensure_one()
|
|
ai = self._get_ai_service()
|
|
product_info = {
|
|
'name': self.product_name,
|
|
'category': self.odoo_category_id.complete_name if self.odoo_category_id else '',
|
|
'raw_description': self.raw_product_info or '',
|
|
}
|
|
result = ai.generate_single_field(
|
|
product_info,
|
|
self.instance_id.prompt_short_description or 'Write a short HTML product description',
|
|
'short_description',
|
|
)
|
|
self.short_description = result
|
|
return self._reopen()
|
|
|
|
def action_ai_generate_long_desc(self):
|
|
self.ensure_one()
|
|
ai = self._get_ai_service()
|
|
product_info = {
|
|
'name': self.product_name,
|
|
'category': self.odoo_category_id.complete_name if self.odoo_category_id else '',
|
|
'raw_description': self.raw_product_info or '',
|
|
'keywords': self.ai_keywords or '',
|
|
}
|
|
result = ai.generate_single_field(
|
|
product_info,
|
|
self.instance_id.prompt_long_description or 'Write a detailed HTML product description',
|
|
'long_description',
|
|
)
|
|
self.long_description = result
|
|
return self._reopen()
|
|
|
|
def action_ai_generate_meta(self):
|
|
self.ensure_one()
|
|
ai = self._get_ai_service()
|
|
product_info = {
|
|
'name': self.wc_title or self.product_name,
|
|
'category': self.odoo_category_id.complete_name if self.odoo_category_id else '',
|
|
'keywords': self.ai_keywords or '',
|
|
}
|
|
self.meta_title = ai.generate_single_field(
|
|
product_info,
|
|
self.instance_id.prompt_meta_title or 'Generate SEO meta title under 60 chars',
|
|
'meta_title',
|
|
)
|
|
self.meta_description = ai.generate_single_field(
|
|
product_info,
|
|
self.instance_id.prompt_meta_description or 'Generate SEO meta description under 160 chars',
|
|
'meta_description',
|
|
)
|
|
return self._reopen()
|
|
|
|
def action_ai_generate_keywords(self):
|
|
self.ensure_one()
|
|
ai = self._get_ai_service()
|
|
product_info = {
|
|
'name': self.wc_title or self.product_name,
|
|
'category': self.odoo_category_id.complete_name if self.odoo_category_id else '',
|
|
'raw_description': self.raw_product_info or '',
|
|
}
|
|
self.seo_keywords = ai.generate_single_field(
|
|
product_info,
|
|
self.instance_id.prompt_keywords or 'Generate SEO focus keywords',
|
|
'keywords',
|
|
)
|
|
return self._reopen()
|
|
|
|
# --- Image AI ---
|
|
|
|
def action_ai_tag_images(self):
|
|
"""Generate AI metadata for all uploaded images."""
|
|
self.ensure_one()
|
|
ai = self._get_ai_service()
|
|
inst = self.instance_id
|
|
category_name = self.odoo_category_id.complete_name if self.odoo_category_id else ''
|
|
|
|
metadata = []
|
|
for i in range(1, 6):
|
|
img = getattr(self, f'image_{i}', None)
|
|
if img:
|
|
meta = ai.generate_image_metadata(
|
|
self.product_name or '',
|
|
category_name,
|
|
inst.prompt_image_alt or 'Descriptive alt text under 125 chars',
|
|
inst.prompt_image_caption or 'Short product image caption',
|
|
)
|
|
meta['index'] = i
|
|
metadata.append(meta)
|
|
|
|
self.image_metadata = json.dumps(metadata)
|
|
return self._reopen()
|
|
|
|
def action_geo_tag_images(self):
|
|
"""Write EXIF geo-tag data to all uploaded images."""
|
|
self.ensure_one()
|
|
inst = self.instance_id
|
|
for i in range(1, 6):
|
|
img = getattr(self, f'image_{i}', None)
|
|
if img:
|
|
tagged = ImageProcessor.geo_tag_image(
|
|
img.decode('utf-8') if isinstance(img, bytes) else img,
|
|
inst.geo_company_name,
|
|
inst.geo_company_address,
|
|
inst.geo_company_phone,
|
|
inst.geo_lat,
|
|
inst.geo_lng,
|
|
)
|
|
setattr(self, f'image_{i}', tagged)
|
|
|
|
self.images_geotagged = True
|
|
return self._reopen()
|
|
|
|
# --- Create Product ---
|
|
|
|
def action_create_product(self):
|
|
"""Create the product in WooCommerce and update Odoo."""
|
|
self.ensure_one()
|
|
inst = self.instance_id
|
|
client = inst._get_client()
|
|
|
|
# --- Update/Create Odoo product ---
|
|
odoo_product = self.odoo_product_id
|
|
odoo_vals = {
|
|
'name': self.product_name or (self.wc_title or '').upper(),
|
|
'list_price': self.sale_price,
|
|
'standard_price': self.cost_price,
|
|
'default_code': self.sku or '',
|
|
'type': 'consu',
|
|
}
|
|
if self.odoo_category_id:
|
|
odoo_vals['categ_id'] = self.odoo_category_id.id
|
|
if self.sales_tax_id:
|
|
odoo_vals['taxes_id'] = [(6, 0, [self.sales_tax_id.id])]
|
|
if self.purchase_tax_id:
|
|
odoo_vals['supplier_taxes_id'] = [(6, 0, [self.purchase_tax_id.id])]
|
|
|
|
if odoo_product:
|
|
odoo_product.write(odoo_vals)
|
|
else:
|
|
odoo_product = self.env['product.product'].create(odoo_vals)
|
|
|
|
# Set product image
|
|
if self.image_1:
|
|
odoo_product.image_1920 = self.image_1
|
|
|
|
# --- Prepare WC product data ---
|
|
wc_data = {
|
|
'name': self.wc_title or (self.product_name or '').title(),
|
|
'type': 'simple',
|
|
'regular_price': str(self.sale_price),
|
|
'sku': self.sku or '',
|
|
'short_description': self.short_description or '',
|
|
'description': self.long_description or '',
|
|
'manage_stock': False,
|
|
'status': 'publish',
|
|
}
|
|
|
|
# Category
|
|
if self.wc_category_id:
|
|
wc_data['categories'] = [{'id': self.wc_category_id}]
|
|
|
|
# Tax class
|
|
if self.wc_tax_class:
|
|
wc_data['tax_class'] = self.wc_tax_class
|
|
|
|
# SEO meta data — compatible with Rank Math, Yoast, AIOSEO, SEOPress
|
|
seo_meta = []
|
|
if self.meta_title:
|
|
seo_meta.extend([
|
|
{'key': 'rank_math_title', 'value': self.meta_title},
|
|
{'key': '_yoast_wpseo_title', 'value': self.meta_title},
|
|
{'key': '_aioseo_title', 'value': self.meta_title},
|
|
{'key': '_seopress_titles_title', 'value': self.meta_title},
|
|
])
|
|
if self.meta_description:
|
|
seo_meta.extend([
|
|
{'key': 'rank_math_description', 'value': self.meta_description},
|
|
{'key': '_yoast_wpseo_metadesc', 'value': self.meta_description},
|
|
{'key': '_aioseo_description', 'value': self.meta_description},
|
|
{'key': '_seopress_titles_desc', 'value': self.meta_description},
|
|
])
|
|
if self.seo_keywords:
|
|
seo_meta.extend([
|
|
{'key': 'rank_math_focus_keyword', 'value': self.seo_keywords},
|
|
{'key': '_yoast_wpseo_focuskw', 'value': self.seo_keywords},
|
|
{'key': '_aioseo_keywords', 'value': self.seo_keywords},
|
|
{'key': '_seopress_analysis_target_kw', 'value': self.seo_keywords},
|
|
])
|
|
if seo_meta:
|
|
wc_data['meta_data'] = seo_meta
|
|
|
|
# --- Upload images to WC ---
|
|
image_metadata = []
|
|
try:
|
|
image_metadata = json.loads(self.image_metadata or '[]')
|
|
except (json.JSONDecodeError, TypeError):
|
|
pass
|
|
|
|
# Set product images via Odoo's public URL — WC downloads them directly
|
|
# WC consumer key/secret cannot authenticate against /wp/v2/media (401)
|
|
wc_images = []
|
|
odoo_base = inst.env['ir.config_parameter'].sudo().get_param('web.base.url', '')
|
|
if odoo_base and self.product_template_id:
|
|
tmpl_id = self.product_template_id.id
|
|
for i in range(1, 6):
|
|
img_data = getattr(self, f'image_{i}', None)
|
|
if not img_data:
|
|
continue
|
|
filename = getattr(self, f'image_{i}_filename', '') or f'product_image_{i}.jpg'
|
|
img_meta = next((m for m in image_metadata if m.get('index') == i), {})
|
|
img_url = f"{odoo_base}/web/image/product.template/{tmpl_id}/image_1920/{filename}"
|
|
wc_img = {
|
|
'src': img_url,
|
|
'name': img_meta.get('title', filename),
|
|
'alt': img_meta.get('alt_text', ''),
|
|
}
|
|
if i == 1:
|
|
wc_img['position'] = 0
|
|
wc_images.append(wc_img)
|
|
|
|
if wc_images:
|
|
wc_data['images'] = wc_images
|
|
|
|
# --- Handle variable vs simple product ---
|
|
if self.has_variants:
|
|
tmpl = self.product_template_id
|
|
|
|
# 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')
|
|
|
|
# Find or create WC attribute
|
|
wc_attr = self._find_or_create_wc_attribute(client, attr_name)
|
|
|
|
# Create terms for each value
|
|
wc_terms = []
|
|
for val_name in attr_values:
|
|
term = self._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,
|
|
})
|
|
|
|
wc_data['type'] = 'variable'
|
|
wc_data['attributes'] = wc_attributes
|
|
# Remove regular_price for variable products (set on variations)
|
|
wc_data.pop('regular_price', None)
|
|
|
|
# Create the parent product
|
|
try:
|
|
wc_product = client.create_product(wc_data)
|
|
wc_product_id = wc_product['id']
|
|
wc_permalink = wc_product.get('permalink', '')
|
|
except Exception as e:
|
|
raise UserError("Failed to create WooCommerce variable product: %s" % str(e))
|
|
|
|
# Create parent product map
|
|
self.env['woo.product.map'].create({
|
|
'instance_id': inst.id,
|
|
'product_id': odoo_product.id,
|
|
'woo_product_id': wc_product_id,
|
|
'woo_product_name': wc_data['name'],
|
|
'woo_sku': self.sku or '',
|
|
'woo_regular_price': 0,
|
|
'woo_sale_price': 0,
|
|
'woo_permalink': wc_permalink,
|
|
'woo_product_type': 'variable',
|
|
'state': 'mapped',
|
|
'company_id': inst.company_id.id,
|
|
})
|
|
|
|
# Build WC attribute ID lookup
|
|
wc_attr_id_map = {a['name'].upper(): a['id'] for a in wc_attributes}
|
|
|
|
# Create variations
|
|
variation_count = 0
|
|
for line in self.variant_line_ids:
|
|
if not line.include:
|
|
continue
|
|
|
|
variant = line.product_id
|
|
# Build variation attributes with WC IDs
|
|
var_attributes = []
|
|
for ptav in variant.product_template_attribute_value_ids:
|
|
attr_name = ptav.attribute_id.name
|
|
wc_aid = wc_attr_id_map.get(attr_name.upper(), 0)
|
|
entry = {'option': ptav.name}
|
|
if wc_aid:
|
|
entry['id'] = wc_aid
|
|
else:
|
|
entry['name'] = attr_name
|
|
var_attributes.append(entry)
|
|
|
|
var_data = {
|
|
'regular_price': str(line.sale_price),
|
|
'sku': line.sku or '',
|
|
'attributes': var_attributes,
|
|
'manage_stock': True,
|
|
'stock_quantity': int(variant.qty_available),
|
|
}
|
|
|
|
# Variant image — pass Odoo's public URL, WC downloads it directly
|
|
if line.image:
|
|
odoo_base = inst.env['ir.config_parameter'].sudo().get_param('web.base.url', '')
|
|
if odoo_base and variant.id:
|
|
var_filename = f"variant_{line.sku or variant.id}.png"
|
|
img_url = f"{odoo_base}/web/image/product.product/{variant.id}/image_1920/{var_filename}"
|
|
var_data['image'] = {
|
|
'src': img_url,
|
|
'name': var_filename,
|
|
'alt': line.variant_name or '',
|
|
}
|
|
|
|
# Tax class
|
|
if self.wc_tax_class:
|
|
var_data['tax_class'] = self.wc_tax_class
|
|
|
|
try:
|
|
wc_variation = client.create_product_variation(wc_product_id, var_data)
|
|
|
|
# Create variation product map
|
|
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.sale_price,
|
|
'woo_sale_price': 0,
|
|
'woo_permalink': wc_permalink,
|
|
'woo_product_type': 'simple',
|
|
'woo_parent_id': wc_product_id,
|
|
'is_variation': True,
|
|
'state': 'mapped',
|
|
'company_id': inst.company_id.id,
|
|
})
|
|
variation_count += 1
|
|
except Exception as e:
|
|
_logger.error("Failed to create variation for %s: %s", line.variant_name, e)
|
|
|
|
inst._log_sync(
|
|
'product', 'odoo_to_woo', tmpl.name, 'success',
|
|
'Created variable product with %d variations' % variation_count,
|
|
)
|
|
|
|
success_msg = 'Variable product "%s" created with %d variations!' % (wc_data['name'], variation_count)
|
|
else:
|
|
# --- Simple product creation ---
|
|
try:
|
|
wc_product = client.create_product(wc_data)
|
|
wc_product_id = wc_product['id']
|
|
wc_permalink = wc_product.get('permalink', '')
|
|
except Exception as e:
|
|
raise UserError("Failed to create WooCommerce product: %s" % str(e))
|
|
|
|
# Create product mapping
|
|
self.env['woo.product.map'].create({
|
|
'instance_id': inst.id,
|
|
'product_id': odoo_product.id,
|
|
'woo_product_id': wc_product_id,
|
|
'woo_product_name': wc_data['name'],
|
|
'woo_sku': self.sku or '',
|
|
'woo_regular_price': self.sale_price,
|
|
'woo_sale_price': 0.0,
|
|
'woo_permalink': wc_permalink,
|
|
'woo_product_type': 'simple',
|
|
'state': 'mapped',
|
|
'company_id': inst.company_id.id,
|
|
})
|
|
|
|
inst._log_sync(
|
|
'product', 'odoo_to_woo', odoo_product.name, 'success',
|
|
'Created WC product #%s' % wc_product_id,
|
|
)
|
|
|
|
success_msg = 'Product "%s" created in WooCommerce successfully!' % wc_data['name']
|
|
|
|
# Close wizard and show success
|
|
return {
|
|
'type': 'ir.actions.client',
|
|
'tag': 'display_notification',
|
|
'params': {
|
|
'title': 'Product Created',
|
|
'message': success_msg,
|
|
'type': 'success',
|
|
'sticky': False,
|
|
'next': {'type': 'ir.actions.act_window_close'},
|
|
},
|
|
}
|
|
|
|
# --- Variant Helper Methods ---
|
|
|
|
def _find_or_create_wc_attribute(self, client, attr_name):
|
|
"""Find or create a WC product attribute by name."""
|
|
try:
|
|
attrs = client.get_product_attributes()
|
|
for a in attrs:
|
|
if a.get('name', '').lower() == attr_name.lower():
|
|
return a
|
|
except Exception:
|
|
pass
|
|
# Create new attribute
|
|
return client.create_product_attribute({
|
|
'name': attr_name,
|
|
'slug': attr_name.lower().replace(' ', '-'),
|
|
'type': 'select',
|
|
'order_by': 'menu_order',
|
|
})
|
|
|
|
def _find_or_create_wc_attribute_term(self, client, attr_id, term_name):
|
|
"""Find or create a WC attribute term."""
|
|
try:
|
|
terms = client.get_attribute_terms(attr_id)
|
|
for t in terms:
|
|
if t.get('name', '').lower() == term_name.lower():
|
|
return t
|
|
except Exception:
|
|
pass
|
|
# Create new term
|
|
return client.create_attribute_term(attr_id, {
|
|
'name': term_name,
|
|
})
|