feat: AI-powered product creation wizard with SEO and image geo-tagging
4-step wizard: Basic Info → Images → Content & SEO → Review & Create. AI generates product titles, descriptions, meta data using Claude or OpenAI. Image geo-tagging with company EXIF data. SEO meta pushed to Rank Math, Yoast, AIOSEO, and SEOPress simultaneously. Products created with CAPS name in Odoo, Title Case in WooCommerce. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,7 @@
|
||||
'views/woo_menus.xml',
|
||||
'wizard/woo_setup_wizard_views.xml',
|
||||
'wizard/woo_product_fetch_views.xml',
|
||||
'wizard/woo_product_create_views.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
|
||||
@@ -27,3 +27,4 @@ access_woo_category_map_user,woo.category.map.user,model_woo_category_map,fusion
|
||||
access_woo_category_map_manager,woo.category.map.manager,model_woo_category_map,fusion_woocommerce.group_woo_manager,1,1,1,1
|
||||
access_woo_setup_wizard_manager,woo.setup.wizard.manager,model_woo_setup_wizard,fusion_woocommerce.group_woo_manager,1,1,1,1
|
||||
access_woo_product_fetch_manager,woo.product.fetch.manager,model_woo_product_fetch,fusion_woocommerce.group_woo_manager,1,1,1,1
|
||||
access_woo_product_create_wizard_manager,woo.product.create.wizard.manager,model_woo_product_create_wizard,fusion_woocommerce.group_woo_manager,1,1,1,1
|
||||
|
||||
|
@@ -410,7 +410,20 @@ export class ProductMapping extends Component {
|
||||
}
|
||||
|
||||
async createInWC(odooProductId) {
|
||||
this.notification.add("Create in WooCommerce queued.", { type: "info" });
|
||||
if (!this.state.instanceId) {
|
||||
this.notification.add("Please select an instance first.", { type: "warning" });
|
||||
return;
|
||||
}
|
||||
this.actionService.doAction({
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: 'woo.product.create.wizard',
|
||||
views: [[false, 'form']],
|
||||
target: 'new',
|
||||
context: {
|
||||
default_instance_id: this.state.instanceId,
|
||||
default_odoo_product_id: odooProductId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async createInOdoo(wooMapId) {
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
from . import woo_setup_wizard
|
||||
from . import woo_product_fetch
|
||||
from . import woo_product_create
|
||||
|
||||
509
fusion-woo-odoo/fusion_woocommerce/wizard/woo_product_create.py
Normal file
509
fusion-woo-odoo/fusion_woocommerce/wizard/woo_product_create.py
Normal file
@@ -0,0 +1,509 @@
|
||||
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'},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="woo_product_create_wizard_form" model="ir.ui.view">
|
||||
<field name="name">woo.product.create.wizard.form</field>
|
||||
<field name="model">woo.product.create.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Create Product in WooCommerce">
|
||||
<sheet>
|
||||
<!-- Step indicator -->
|
||||
<div class="o_statusbar_status mb-3">
|
||||
<span class="badge bg-primary me-1" invisible="step != 'basic'">
|
||||
1. Basic Info
|
||||
</span>
|
||||
<span class="badge bg-secondary me-1" invisible="step == 'basic'">
|
||||
1. Basic Info ✓
|
||||
</span>
|
||||
|
||||
<span class="badge bg-primary me-1" invisible="step != 'images'">
|
||||
2. Images
|
||||
</span>
|
||||
<span class="badge bg-secondary me-1" invisible="step in ('basic', 'images')">
|
||||
2. Images ✓
|
||||
</span>
|
||||
<span class="badge bg-light text-muted me-1" invisible="step not in ('basic',)">
|
||||
2. Images
|
||||
</span>
|
||||
|
||||
<span class="badge bg-primary me-1" invisible="step != 'content'">
|
||||
3. Content & SEO
|
||||
</span>
|
||||
<span class="badge bg-secondary me-1" invisible="step == 'review'">
|
||||
</span>
|
||||
<span class="badge bg-light text-muted me-1" invisible="step not in ('basic', 'images')">
|
||||
3. Content & SEO
|
||||
</span>
|
||||
|
||||
<span class="badge bg-primary me-1" invisible="step != 'review'">
|
||||
4. Review & Create
|
||||
</span>
|
||||
<span class="badge bg-light text-muted me-1" invisible="step == 'review'">
|
||||
4. Review & Create
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<field name="instance_id" invisible="1"/>
|
||||
|
||||
<!-- ========== STEP 1: Basic Info ========== -->
|
||||
<group invisible="step != 'basic'">
|
||||
<group string="Product Selection">
|
||||
<field name="odoo_product_id"/>
|
||||
</group>
|
||||
<group string="Product Names">
|
||||
<field name="product_name" placeholder="PRODUCT NAME IN CAPS"/>
|
||||
<field name="wc_product_name" placeholder="Product Name In Title Case"/>
|
||||
</group>
|
||||
<group string="Category">
|
||||
<field name="odoo_category_id"/>
|
||||
<field name="wc_category_name"/>
|
||||
</group>
|
||||
<group string="Pricing">
|
||||
<field name="sale_price"/>
|
||||
<field name="cost_price"/>
|
||||
</group>
|
||||
<group string="Identification">
|
||||
<field name="sku"/>
|
||||
</group>
|
||||
<group string="Taxes">
|
||||
<field name="sales_tax_id"/>
|
||||
<field name="purchase_tax_id"/>
|
||||
<field name="wc_tax_class" readonly="1"/>
|
||||
</group>
|
||||
<field name="wc_category_id" invisible="1"/>
|
||||
</group>
|
||||
|
||||
<!-- ========== STEP 2: Images ========== -->
|
||||
<group invisible="step != 'images'">
|
||||
<group string="Product Images">
|
||||
<field name="image_1" filename="image_1_filename" widget="image"/>
|
||||
<field name="image_1_filename" invisible="1"/>
|
||||
<field name="image_2" filename="image_2_filename" widget="image"/>
|
||||
<field name="image_2_filename" invisible="1"/>
|
||||
<field name="image_3" filename="image_3_filename" widget="image"/>
|
||||
<field name="image_3_filename" invisible="1"/>
|
||||
<field name="image_4" filename="image_4_filename" widget="image"/>
|
||||
<field name="image_4_filename" invisible="1"/>
|
||||
<field name="image_5" filename="image_5_filename" widget="image"/>
|
||||
<field name="image_5_filename" invisible="1"/>
|
||||
</group>
|
||||
<group string="Image Processing">
|
||||
<div>
|
||||
<button name="action_ai_tag_images" type="object"
|
||||
string="AI Tag Images" class="btn btn-primary me-2"
|
||||
icon="fa-magic"/>
|
||||
<button name="action_geo_tag_images" type="object"
|
||||
string="Geo-tag Images" class="btn btn-secondary"
|
||||
icon="fa-map-marker"/>
|
||||
</div>
|
||||
<field name="images_geotagged" readonly="1"/>
|
||||
<field name="image_metadata" readonly="1" widget="text"
|
||||
placeholder="AI metadata will appear here after tagging..."/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<!-- ========== STEP 3: Content & SEO ========== -->
|
||||
<group invisible="step != 'content'">
|
||||
<group string="Product Information for AI" colspan="2">
|
||||
<field name="raw_product_info" colspan="2"
|
||||
placeholder="Type everything you know about this product — features, materials, use cases, target audience, dimensions, etc. The AI will use this to generate all descriptions."/>
|
||||
<field name="ai_keywords"
|
||||
placeholder="mobility, wheelchair, medical equipment, ..."/>
|
||||
</group>
|
||||
<div colspan="2" class="mb-3">
|
||||
<button name="action_ai_generate_all" type="object"
|
||||
string="Generate All with AI" class="btn btn-primary"
|
||||
icon="fa-magic"/>
|
||||
</div>
|
||||
<group string="Product Title" colspan="2">
|
||||
<div class="d-flex align-items-start">
|
||||
<field name="wc_title" class="flex-grow-1"/>
|
||||
<button name="action_ai_generate_title" type="object"
|
||||
string="AI" class="btn btn-outline-primary btn-sm ms-2"
|
||||
icon="fa-magic"/>
|
||||
</div>
|
||||
</group>
|
||||
<group string="Short Description" colspan="2">
|
||||
<div>
|
||||
<button name="action_ai_generate_short_desc" type="object"
|
||||
string="AI Generate" class="btn btn-outline-primary btn-sm mb-1"
|
||||
icon="fa-magic"/>
|
||||
</div>
|
||||
<field name="short_description" widget="html" nolabel="1" colspan="2"/>
|
||||
</group>
|
||||
<group string="Long Description" colspan="2">
|
||||
<div>
|
||||
<button name="action_ai_generate_long_desc" type="object"
|
||||
string="AI Generate" class="btn btn-outline-primary btn-sm mb-1"
|
||||
icon="fa-magic"/>
|
||||
</div>
|
||||
<field name="long_description" widget="html" nolabel="1" colspan="2"/>
|
||||
</group>
|
||||
<group string="SEO Meta Data" colspan="2">
|
||||
<div class="mb-2">
|
||||
<button name="action_ai_generate_meta" type="object"
|
||||
string="AI Generate Meta" class="btn btn-outline-primary btn-sm"
|
||||
icon="fa-magic"/>
|
||||
<button name="action_ai_generate_keywords" type="object"
|
||||
string="AI Generate Keywords" class="btn btn-outline-primary btn-sm ms-2"
|
||||
icon="fa-magic"/>
|
||||
</div>
|
||||
<field name="meta_title" placeholder="SEO title (under 60 characters)"/>
|
||||
<field name="meta_description" placeholder="SEO description (under 160 characters)"/>
|
||||
<field name="seo_keywords" placeholder="keyword1, keyword2, keyword3"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<!-- ========== STEP 4: Review & Create ========== -->
|
||||
<group invisible="step != 'review'">
|
||||
<group string="Product Names">
|
||||
<field name="product_name" readonly="1"/>
|
||||
<field name="wc_product_name" readonly="1"/>
|
||||
</group>
|
||||
<group string="Category & Tax">
|
||||
<field name="odoo_category_id" readonly="1"/>
|
||||
<field name="wc_category_name" readonly="1"/>
|
||||
<field name="wc_tax_class" readonly="1"/>
|
||||
</group>
|
||||
<group string="Pricing">
|
||||
<field name="sale_price" readonly="1"/>
|
||||
<field name="cost_price" readonly="1"/>
|
||||
<field name="sku" readonly="1"/>
|
||||
</group>
|
||||
<group string="Taxes">
|
||||
<field name="sales_tax_id" readonly="1"/>
|
||||
<field name="purchase_tax_id" readonly="1"/>
|
||||
</group>
|
||||
<group string="WooCommerce Content" colspan="2">
|
||||
<field name="wc_title" readonly="1"/>
|
||||
<field name="meta_title" readonly="1"/>
|
||||
<field name="meta_description" readonly="1"/>
|
||||
<field name="seo_keywords" readonly="1"/>
|
||||
</group>
|
||||
<group string="Images" colspan="2">
|
||||
<field name="images_geotagged" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
</sheet>
|
||||
<footer>
|
||||
<button string="← Back" type="object" name="action_back"
|
||||
class="btn btn-secondary"
|
||||
invisible="step == 'basic'"/>
|
||||
<button string="Next →" type="object" name="action_next"
|
||||
class="btn btn-primary"
|
||||
invisible="step == 'review'"/>
|
||||
<button string="Create in WooCommerce" type="object" name="action_create_product"
|
||||
class="btn btn-success"
|
||||
invisible="step != 'review'"
|
||||
icon="fa-cloud-upload"/>
|
||||
<button string="Cancel" class="btn btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user