feat: add setup wizard and product fetch wizard with SKU auto-match
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
171
fusion-woo-odoo/fusion_woocommerce/wizard/woo_product_fetch.py
Normal file
171
fusion-woo-odoo/fusion_woocommerce/wizard/woo_product_fetch.py
Normal file
@@ -0,0 +1,171 @@
|
||||
import difflib
|
||||
import logging
|
||||
|
||||
from odoo import fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WooProductFetch(models.TransientModel):
|
||||
_name = 'woo.product.fetch'
|
||||
_description = 'Fetch WooCommerce Products'
|
||||
|
||||
instance_id = fields.Many2one('woo.instance', required=True, string='WooCommerce Instance')
|
||||
state = fields.Selection([
|
||||
('draft', 'Ready'),
|
||||
('fetching', 'Fetching...'),
|
||||
('matching', 'Matching...'),
|
||||
('done', 'Complete'),
|
||||
], default='draft')
|
||||
total_fetched = fields.Integer(readonly=True)
|
||||
auto_matched = fields.Integer(string='Auto-matched (SKU)', readonly=True)
|
||||
suggested = fields.Integer(string='Name Suggestions', readonly=True)
|
||||
unmatched = fields.Integer(readonly=True)
|
||||
|
||||
def action_fetch(self):
|
||||
self.ensure_one()
|
||||
instance = self.instance_id
|
||||
if not instance:
|
||||
raise UserError("Please select a WooCommerce instance.")
|
||||
|
||||
client = instance._get_client()
|
||||
|
||||
# --- Fetch all products (paginated) ---
|
||||
self.state = 'fetching'
|
||||
all_products = []
|
||||
page = 1
|
||||
while True:
|
||||
batch = client.get_products(page=page, per_page=100)
|
||||
if not batch:
|
||||
break
|
||||
all_products.extend(batch)
|
||||
if len(batch) < 100:
|
||||
break
|
||||
page += 1
|
||||
|
||||
# Expand variable products with their variations
|
||||
expanded = []
|
||||
for product in all_products:
|
||||
expanded.append(product)
|
||||
if product.get('type') == 'variable':
|
||||
var_page = 1
|
||||
while True:
|
||||
variations = client.get_product_variations(
|
||||
product['id'], page=var_page, per_page=100
|
||||
)
|
||||
if not variations:
|
||||
break
|
||||
for var in variations:
|
||||
var['_parent_id'] = product['id']
|
||||
var['_is_variation'] = True
|
||||
expanded.extend(variations)
|
||||
if len(variations) < 100:
|
||||
break
|
||||
var_page += 1
|
||||
|
||||
total_fetched = len(expanded)
|
||||
|
||||
# --- Match products ---
|
||||
self.state = 'matching'
|
||||
|
||||
ProductMap = self.env['woo.product.map']
|
||||
ProductProduct = self.env['product.product']
|
||||
|
||||
# Pre-load existing maps for this instance to avoid repeated queries
|
||||
existing_woo_ids = set(
|
||||
ProductMap.search([('instance_id', '=', instance.id)]).mapped('woo_product_id')
|
||||
)
|
||||
|
||||
# Pre-load all odoo products with a SKU for fast lookup
|
||||
odoo_products_with_sku = ProductProduct.search([('default_code', '!=', False)])
|
||||
sku_index = {p.default_code: p for p in odoo_products_with_sku}
|
||||
|
||||
# For name matching, load all product names
|
||||
all_odoo_products = ProductProduct.search([])
|
||||
odoo_name_list = [(p.name or '', p) for p in all_odoo_products]
|
||||
|
||||
auto_matched = 0
|
||||
suggested_count = 0
|
||||
unmatched = 0
|
||||
|
||||
for wc_product in expanded:
|
||||
woo_id = wc_product.get('id')
|
||||
if not woo_id:
|
||||
continue
|
||||
|
||||
# Skip already-mapped
|
||||
if woo_id in existing_woo_ids:
|
||||
continue
|
||||
|
||||
woo_sku = wc_product.get('sku') or ''
|
||||
woo_name = wc_product.get('name') or ''
|
||||
woo_type = wc_product.get('type', 'simple')
|
||||
is_variation = wc_product.get('_is_variation', False)
|
||||
parent_id = wc_product.get('_parent_id') or wc_product.get('parent_id') or 0
|
||||
|
||||
map_vals = {
|
||||
'instance_id': instance.id,
|
||||
'woo_product_id': woo_id,
|
||||
'woo_product_name': woo_name,
|
||||
'woo_sku': woo_sku,
|
||||
'woo_product_type': woo_type if not is_variation else 'variable',
|
||||
'is_variation': is_variation,
|
||||
'woo_parent_id': parent_id,
|
||||
'company_id': instance.company_id.id,
|
||||
}
|
||||
|
||||
# a) SKU match
|
||||
matched_product = None
|
||||
if woo_sku and woo_sku in sku_index:
|
||||
matched_product = sku_index[woo_sku]
|
||||
map_vals['product_id'] = matched_product.id
|
||||
map_vals['state'] = 'mapped'
|
||||
auto_matched += 1
|
||||
else:
|
||||
# b) Name similarity
|
||||
if woo_name and odoo_name_list:
|
||||
ratios = [
|
||||
(difflib.SequenceMatcher(None, woo_name.lower(), name.lower()).ratio(), p)
|
||||
for name, p in odoo_name_list
|
||||
]
|
||||
best_ratio, best_product = max(ratios, key=lambda x: x[0])
|
||||
if best_ratio > 0.8:
|
||||
map_vals['product_id'] = best_product.id
|
||||
map_vals['state'] = 'unmapped'
|
||||
suggested_count += 1
|
||||
else:
|
||||
map_vals['state'] = 'unmapped'
|
||||
unmatched += 1
|
||||
else:
|
||||
map_vals['state'] = 'unmapped'
|
||||
unmatched += 1
|
||||
|
||||
ProductMap.create(map_vals)
|
||||
|
||||
self.write({
|
||||
'total_fetched': total_fetched,
|
||||
'auto_matched': auto_matched,
|
||||
'suggested': suggested_count,
|
||||
'unmatched': unmatched,
|
||||
'state': 'done',
|
||||
})
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': self._name,
|
||||
'res_id': self.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
def action_open_mapping(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Product Mapping',
|
||||
'res_model': 'woo.product.map',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('instance_id', '=', self.instance_id.id)],
|
||||
'target': 'current',
|
||||
}
|
||||
Reference in New Issue
Block a user