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/cron.xml',
|
||||||
'data/mail_template.xml',
|
'data/mail_template.xml',
|
||||||
'views/woo_instance_views.xml',
|
'views/woo_instance_views.xml',
|
||||||
|
'views/woo_category_map_views.xml',
|
||||||
'views/woo_product_map_views.xml',
|
'views/woo_product_map_views.xml',
|
||||||
'views/woo_order_views.xml',
|
'views/woo_order_views.xml',
|
||||||
'views/woo_sync_log_views.xml',
|
'views/woo_sync_log_views.xml',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from . import woo_shipping_carrier
|
from . import woo_shipping_carrier
|
||||||
from . import woo_instance
|
from . import woo_instance
|
||||||
|
from . import woo_category_map
|
||||||
from . import woo_product_map
|
from . import woo_product_map
|
||||||
from . import woo_order
|
from . import woo_order
|
||||||
from . import woo_shipment
|
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')
|
customer_ids = fields.One2many('woo.customer', 'instance_id')
|
||||||
sync_log_ids = fields.One2many('woo.sync.log', '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
|
# Computed
|
||||||
mapped_count = fields.Integer(compute='_compute_counts')
|
mapped_count = fields.Integer(compute='_compute_counts')
|
||||||
unmapped_count = fields.Integer(compute='_compute_counts')
|
unmapped_count = fields.Integer(compute='_compute_counts')
|
||||||
@@ -72,6 +109,64 @@ class WooInstance(models.Model):
|
|||||||
('create_date', '>=', yesterday),
|
('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):
|
def _get_client(self):
|
||||||
"""Return a WooApiClient instance for this WooCommerce connection."""
|
"""Return a WooApiClient instance for this WooCommerce connection."""
|
||||||
self.ensure_one()
|
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_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_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_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_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_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>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</page>
|
</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>
|
</notebook>
|
||||||
</sheet>
|
</sheet>
|
||||||
<chatter/>
|
<chatter/>
|
||||||
|
|||||||
Reference in New Issue
Block a user