feat: add category mapping model and AI settings on woo.instance
Category mapping between Odoo product categories and WC categories with auto-match by name and manual mapping UI. AI settings for Claude/OpenAI with customizable prompts for product content generation. GPS coordinates for image geo-tagging pulled from company settings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
'data/cron.xml',
|
||||
'data/mail_template.xml',
|
||||
'views/woo_instance_views.xml',
|
||||
'views/woo_category_map_views.xml',
|
||||
'views/woo_product_map_views.xml',
|
||||
'views/woo_order_views.xml',
|
||||
'views/woo_sync_log_views.xml',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from . import woo_shipping_carrier
|
||||
from . import woo_instance
|
||||
from . import woo_category_map
|
||||
from . import woo_product_map
|
||||
from . import woo_order
|
||||
from . import woo_shipment
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class WooCategoryMap(models.Model):
|
||||
_name = 'woo.category.map'
|
||||
_description = 'WooCommerce Category Mapping'
|
||||
_order = 'odoo_category_id'
|
||||
|
||||
instance_id = fields.Many2one('woo.instance', required=True, ondelete='cascade')
|
||||
odoo_category_id = fields.Many2one('product.category', string='Odoo Category')
|
||||
woo_category_id = fields.Integer(string='WC Category ID', required=True)
|
||||
woo_category_name = fields.Char(string='WC Category Name')
|
||||
woo_category_slug = fields.Char(string='WC Category Slug')
|
||||
company_id = fields.Many2one('res.company', required=True, default=lambda self: self.env.company)
|
||||
@@ -51,6 +51,43 @@ class WooInstance(models.Model):
|
||||
customer_ids = fields.One2many('woo.customer', 'instance_id')
|
||||
sync_log_ids = fields.One2many('woo.sync.log', 'instance_id')
|
||||
|
||||
# Category mapping
|
||||
category_map_ids = fields.One2many('woo.category.map', 'instance_id', string='Category Mappings')
|
||||
|
||||
# AI Configuration
|
||||
ai_provider = fields.Selection([
|
||||
('claude', 'Claude (Anthropic)'),
|
||||
('openai', 'OpenAI'),
|
||||
], string='AI Provider')
|
||||
ai_api_key = fields.Char(string='AI API Key', groups='base.group_system')
|
||||
ai_model = fields.Char(string='AI Model',
|
||||
help='e.g., claude-sonnet-4-5-20250514 for Claude or gpt-4o for OpenAI')
|
||||
|
||||
# AI Prompts
|
||||
prompt_product_title = fields.Text(string='Product Title Prompt',
|
||||
default='Generate an SEO-optimized product title in Title Case. Keep it concise, include the brand name and key product features. Do not use ALL CAPS.')
|
||||
prompt_short_description = fields.Text(string='Short Description Prompt',
|
||||
default='Write a compelling 2-3 sentence product summary in HTML format. Highlight key benefits and features. Use <p> tags.')
|
||||
prompt_long_description = fields.Text(string='Long Description Prompt',
|
||||
default='Write a detailed SEO-optimized product description in HTML format. Include sections with <h3> headings for Features, Specifications, and Benefits. Use <ul> lists for features. Make it informative and persuasive.')
|
||||
prompt_meta_title = fields.Text(string='Meta Title Prompt',
|
||||
default='Generate an SEO meta title under 60 characters. Include the primary keyword and brand name.')
|
||||
prompt_meta_description = fields.Text(string='Meta Description Prompt',
|
||||
default='Generate an SEO meta description under 160 characters. Include a call to action and primary keyword.')
|
||||
prompt_image_alt = fields.Text(string='Image Alt Text Prompt',
|
||||
default='Generate descriptive alt text for this product image. Be specific about the product shown. Keep under 125 characters.')
|
||||
prompt_image_caption = fields.Text(string='Image Caption Prompt',
|
||||
default='Generate a short image caption for this product photo. Include the product name and key visible feature.')
|
||||
prompt_keywords = fields.Text(string='Keywords Prompt',
|
||||
default='Generate 5-8 SEO focus keywords for this product, comma-separated. Include long-tail keywords.')
|
||||
|
||||
# Company Info for Image Geo-tagging
|
||||
geo_company_name = fields.Char(string='Company Name (Geo-tag)', compute='_compute_geo_info', store=False)
|
||||
geo_company_address = fields.Char(string='Company Address (Geo-tag)', compute='_compute_geo_info', store=False)
|
||||
geo_company_phone = fields.Char(string='Company Phone (Geo-tag)', compute='_compute_geo_info', store=False)
|
||||
geo_lat = fields.Float(string='GPS Latitude', digits=(10, 7))
|
||||
geo_lng = fields.Float(string='GPS Longitude', digits=(10, 7))
|
||||
|
||||
# Computed
|
||||
mapped_count = fields.Integer(compute='_compute_counts')
|
||||
unmapped_count = fields.Integer(compute='_compute_counts')
|
||||
@@ -72,6 +109,64 @@ class WooInstance(models.Model):
|
||||
('create_date', '>=', yesterday),
|
||||
])
|
||||
|
||||
@api.depends('company_id')
|
||||
def _compute_geo_info(self):
|
||||
for rec in self:
|
||||
company = rec.company_id
|
||||
rec.geo_company_name = company.name or ''
|
||||
rec.geo_company_address = ', '.join(filter(None, [
|
||||
company.street,
|
||||
company.city,
|
||||
company.state_id.name if company.state_id else '',
|
||||
company.zip,
|
||||
company.country_id.name if company.country_id else '',
|
||||
]))
|
||||
rec.geo_company_phone = company.phone or ''
|
||||
|
||||
def action_fetch_wc_categories(self):
|
||||
"""Fetch all WooCommerce categories and display for mapping."""
|
||||
self.ensure_one()
|
||||
client = self._get_client()
|
||||
CategoryMap = self.env['woo.category.map']
|
||||
page = 1
|
||||
fetched = 0
|
||||
while True:
|
||||
try:
|
||||
cats = client.get('products/categories', params={'page': page, 'per_page': 100})
|
||||
except Exception as e:
|
||||
raise UserError('Failed to fetch WC categories: %s' % str(e))
|
||||
if not cats:
|
||||
break
|
||||
for wc_cat in cats:
|
||||
wc_id = wc_cat['id']
|
||||
existing = CategoryMap.search([
|
||||
('instance_id', '=', self.id),
|
||||
('woo_category_id', '=', wc_id),
|
||||
], limit=1)
|
||||
if existing:
|
||||
existing.write({
|
||||
'woo_category_name': wc_cat.get('name', ''),
|
||||
'woo_category_slug': wc_cat.get('slug', ''),
|
||||
})
|
||||
else:
|
||||
# Try auto-match by name
|
||||
odoo_cat = self.env['product.category'].search([
|
||||
('name', '=ilike', wc_cat.get('name', '')),
|
||||
], limit=1)
|
||||
CategoryMap.create({
|
||||
'instance_id': self.id,
|
||||
'odoo_category_id': odoo_cat.id if odoo_cat else False,
|
||||
'woo_category_id': wc_id,
|
||||
'woo_category_name': wc_cat.get('name', ''),
|
||||
'woo_category_slug': wc_cat.get('slug', ''),
|
||||
'company_id': self.company_id.id,
|
||||
})
|
||||
fetched += 1
|
||||
page += 1
|
||||
if len(cats) < 100:
|
||||
break
|
||||
return True
|
||||
|
||||
def _get_client(self):
|
||||
"""Return a WooApiClient instance for this WooCommerce connection."""
|
||||
self.ensure_one()
|
||||
|
||||
@@ -23,5 +23,7 @@ access_woo_return_user,woo.return.user,model_woo_return,fusion_woocommerce.group
|
||||
access_woo_return_manager,woo.return.manager,model_woo_return,fusion_woocommerce.group_woo_manager,1,1,1,1
|
||||
access_woo_return_line_user,woo.return.line.user,model_woo_return_line,fusion_woocommerce.group_woo_user,1,0,0,0
|
||||
access_woo_return_line_manager,woo.return.line.manager,model_woo_return_line,fusion_woocommerce.group_woo_manager,1,1,1,1
|
||||
access_woo_category_map_user,woo.category.map.user,model_woo_category_map,fusion_woocommerce.group_woo_user,1,0,0,0
|
||||
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
|
||||
|
||||
|
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- ===== List View ===== -->
|
||||
<record id="woo_category_map_view_list" model="ir.ui.view">
|
||||
<field name="name">woo.category.map.list</field>
|
||||
<field name="model">woo.category.map</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="instance_id"/>
|
||||
<field name="odoo_category_id"/>
|
||||
<field name="woo_category_name"/>
|
||||
<field name="woo_category_slug"/>
|
||||
<field name="woo_category_id"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== Form View ===== -->
|
||||
<record id="woo_category_map_view_form" model="ir.ui.view">
|
||||
<field name="name">woo.category.map.form</field>
|
||||
<field name="model">woo.category.map</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<group string="WooCommerce Category">
|
||||
<field name="woo_category_id"/>
|
||||
<field name="woo_category_name"/>
|
||||
<field name="woo_category_slug"/>
|
||||
</group>
|
||||
<group string="Odoo Mapping">
|
||||
<field name="instance_id"/>
|
||||
<field name="odoo_category_id"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -133,6 +133,60 @@
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Category Mapping" name="category_mapping">
|
||||
<div class="mb-2">
|
||||
<button name="action_fetch_wc_categories" type="object"
|
||||
string="Fetch WC Categories" class="btn btn-primary"/>
|
||||
</div>
|
||||
<field name="category_map_ids">
|
||||
<list editable="bottom">
|
||||
<field name="odoo_category_id" string="Odoo Category"/>
|
||||
<field name="woo_category_name" string="WC Category Name" readonly="1"/>
|
||||
<field name="woo_category_slug" string="WC Category Slug" readonly="1"/>
|
||||
<field name="woo_category_id" string="WC ID" readonly="1"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="AI Settings" name="ai_settings">
|
||||
<group string="AI Provider">
|
||||
<group>
|
||||
<field name="ai_provider"/>
|
||||
<field name="ai_api_key" password="True"/>
|
||||
<field name="ai_model"/>
|
||||
</group>
|
||||
<group string="GPS Coordinates">
|
||||
<field name="geo_lat"/>
|
||||
<field name="geo_lng"/>
|
||||
<field name="geo_company_name"/>
|
||||
<field name="geo_company_address"/>
|
||||
<field name="geo_company_phone"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Product Title Prompt">
|
||||
<field name="prompt_product_title" nolabel="1" widget="text"/>
|
||||
</group>
|
||||
<group string="Short Description Prompt">
|
||||
<field name="prompt_short_description" nolabel="1" widget="text"/>
|
||||
</group>
|
||||
<group string="Long Description Prompt">
|
||||
<field name="prompt_long_description" nolabel="1" widget="text"/>
|
||||
</group>
|
||||
<group string="Meta Title Prompt">
|
||||
<field name="prompt_meta_title" nolabel="1" widget="text"/>
|
||||
</group>
|
||||
<group string="Meta Description Prompt">
|
||||
<field name="prompt_meta_description" nolabel="1" widget="text"/>
|
||||
</group>
|
||||
<group string="Image Alt Text Prompt">
|
||||
<field name="prompt_image_alt" nolabel="1" widget="text"/>
|
||||
</group>
|
||||
<group string="Image Caption Prompt">
|
||||
<field name="prompt_image_caption" nolabel="1" widget="text"/>
|
||||
</group>
|
||||
<group string="Keywords Prompt">
|
||||
<field name="prompt_keywords" nolabel="1" widget="text"/>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
|
||||
Reference in New Issue
Block a user