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:
gsinghpal
2026-04-01 14:30:02 -04:00
parent 3179cc1f7b
commit 3493c43916
7 changed files with 210 additions and 0 deletions

View File

@@ -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',

View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
23 access_woo_return_manager woo.return.manager model_woo_return fusion_woocommerce.group_woo_manager 1 1 1 1
24 access_woo_return_line_user woo.return.line.user model_woo_return_line fusion_woocommerce.group_woo_user 1 0 0 0
25 access_woo_return_line_manager woo.return.line.manager model_woo_return_line fusion_woocommerce.group_woo_manager 1 1 1 1
26 access_woo_category_map_user woo.category.map.user model_woo_category_map fusion_woocommerce.group_woo_user 1 0 0 0
27 access_woo_category_map_manager woo.category.map.manager model_woo_category_map fusion_woocommerce.group_woo_manager 1 1 1 1
28 access_woo_setup_wizard_manager woo.setup.wizard.manager model_woo_setup_wizard fusion_woocommerce.group_woo_manager 1 1 1 1
29 access_woo_product_fetch_manager woo.product.fetch.manager model_woo_product_fetch fusion_woocommerce.group_woo_manager 1 1 1 1

View File

@@ -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>

View File

@@ -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/>