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 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') # Step 4 is review - uses fields above # --- 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 @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 wc_images = [] 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' # Find AI metadata for this image img_meta = next((m for m in image_metadata if m.get('index') == i), {}) # Upload image via WordPress REST API try: img_bytes = base64.b64decode( img_data if isinstance(img_data, str) else img_data.decode('utf-8') ) import requests wp_url = inst.url.rstrip('/') upload_url = f"{wp_url}/wp-json/wp/v2/media" headers = { 'Content-Disposition': f'attachment; filename="{filename}"', 'Content-Type': 'image/jpeg', } resp = requests.post( upload_url, auth=(inst.consumer_key, inst.consumer_secret), headers=headers, data=img_bytes, timeout=60, ) if resp.status_code in (200, 201): media = resp.json() wc_img = { 'id': media['id'], 'alt': img_meta.get('alt_text', ''), 'name': img_meta.get('title', filename), 'caption': img_meta.get('caption', ''), 'description': img_meta.get('description', ''), } # First image is featured if i == 1: wc_img['position'] = 0 wc_images.append(wc_img) else: _logger.warning( "Image upload failed (HTTP %s): %s", resp.status_code, resp.text[:200], ) except Exception as e: _logger.error("Image upload error: %s", str(e)) if wc_images: wc_data['images'] = wc_images # --- Create WC 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 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, ) # Close wizard and show success return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'Product Created', 'message': 'Product "%s" created in WooCommerce successfully!' % wc_data['name'], 'type': 'success', 'sticky': False, 'next': {'type': 'ir.actions.act_window_close'}, }, }