#!/usr/bin/env python3 """ Add a new binding model to an existing Odoo connector module. Usage: python3 add_binding.py [--odoo-model ] Arguments: connector_module_path: Path to existing connector module entity_name: Name of the entity (e.g., 'order', 'customer', 'invoice') --odoo-model: Odoo model to bind (default: inferred from entity name) Examples: python3 add_binding.py ~/addons/shopify_connector order python3 add_binding.py ~/addons/shopify_connector customer --odoo-model res.partner python3 add_binding.py ~/addons/shopify_connector invoice --odoo-model account.move """ import argparse import os import sys from pathlib import Path # Map common entity names to Odoo models ENTITY_MODEL_MAP = { 'product': 'product.template', 'variant': 'product.product', 'order': 'sale.order', 'customer': 'res.partner', 'invoice': 'account.move', 'payment': 'account.payment', 'picking': 'stock.picking', 'inventory': 'stock.quant', 'category': 'product.category', 'pricelist': 'product.pricelist', 'tax': 'account.tax', } def sanitize_name(name): """Convert name to valid Python identifier.""" return name.lower().replace('-', '_').replace(' ', '_') def get_connector_info(module_path): """Extract connector name and module name from path.""" manifest_path = module_path / '__manifest__.py' if not manifest_path.exists(): raise ValueError(f"No __manifest__.py found at {module_path}") with open(manifest_path, 'r') as f: manifest_content = f.read() # Try to extract module name from path module_name = module_path.name if module_name.endswith('_connector'): connector_name = module_name.replace('_connector', '') else: connector_name = module_name return { 'module_name': module_name, 'connector_name': connector_name, 'module_path': module_path } def generate_binding_model(connector_info, entity_name, odoo_model): """Generate binding model Python code.""" module_name = connector_info['module_name'] connector_name = connector_info['connector_name'] entity_lower = sanitize_name(entity_name) # Generate class name class_name_parts = [word.capitalize() for word in connector_name.split('_')] class_name_parts.append(entity_name.capitalize()) class_name = ''.join(class_name_parts) + 'Binding' # Determine binding model name binding_model_name = f"{connector_name}.{odoo_model.replace('.', '.')}" # Extract Odoo model information odoo_model_parts = odoo_model.split('.') inherits_field_name = odoo_model_parts[-1] + '_id' code = f'''# -*- coding: utf-8 -*- from odoo import models, fields, api, _ from odoo.exceptions import UserError import logging _logger = logging.getLogger(__name__) class {class_name}(models.Model): """ Binding between Odoo {odoo_model} and {connector_name.title()} {entity_name}s. This model links Odoo {entity_name} records with their counterparts in the {connector_name.title()} system. """ _name = '{binding_model_name}' _inherit = 'generic.binding' _inherits = {{'{odoo_model}': 'odoo_id'}} _description = '{connector_name.title()} {entity_name.title()} Binding' odoo_id = fields.Many2one( comodel_name='{odoo_model}', string='{entity_name.title()}', required=True, ondelete='cascade', help='Odoo {entity_name} record' ) # External system fields external_status = fields.Char( string='External Status', readonly=True, help='Status in {connector_name.title()}' ) external_number = fields.Char( string='External Number', readonly=True, help='Reference number in {connector_name.title()}' ) # Synchronization metadata sync_date = fields.Datetime( string='Last Sync Date', readonly=True, help='Last successful synchronization date' ) external_created_at = fields.Datetime( string='Created in {connector_name.title()}', readonly=True ) external_updated_at = fields.Datetime( string='Updated in {connector_name.title()}', readonly=True ) # Sync control flags no_export = fields.Boolean( string='No Export', help='Prevent automatic export to {connector_name.title()}' ) _sql_constraints = [ ('backend_external_uniq', 'unique(backend_id, external_id)', 'A {entity_name} binding with the same external ID already exists for this backend.') ] @api.model def import_record(self, backend, external_id): """ Import a single {entity_name} from {connector_name.title()}. Args: backend: Backend record external_id: External ID of the {entity_name} Returns: Binding record """ _logger.info('Importing {entity_name} %s from backend %s', external_id, backend.name) with backend.work_on(self._name) as work: importer = work.component(usage='record.importer') return importer.run(external_id) @api.model def import_batch(self, backend, filters=None): """ Import {entity_name}s in batch from {connector_name.title()}. Args: backend: Backend record filters: Optional filters for the import Returns: List of imported binding records """ _logger.info('Starting batch import of {entity_name}s for backend %s', backend.name) with backend.work_on(self._name) as work: importer = work.component(usage='batch.importer') return importer.run(filters=filters) def export_record(self): """ Export {entity_name} to {connector_name.title()}. Returns: External ID of the exported record """ self.ensure_one() if self.no_export: _logger.info('{entity_name.title()} %s marked as no_export, skipping', self.odoo_id.display_name) return False _logger.info('Exporting {entity_name} %s to backend %s', self.odoo_id.display_name, self.backend_id.name) with self.backend_id.work_on(self._name) as work: exporter = work.component(usage='record.exporter') return exporter.run(self) @api.model def export_batch(self, backend, domain=None): """ Export multiple {entity_name}s to {connector_name.title()}. Args: backend: Backend record domain: Optional domain to filter records Returns: Number of exported records """ if domain is None: domain = [] domain.append(('backend_id', '=', backend.id)) domain.append(('no_export', '=', False)) bindings = self.search(domain) _logger.info('Exporting %d {entity_name}s to backend %s', len(bindings), backend.name) for binding in bindings: binding.with_delay().export_record() return len(bindings) def resync_record(self): """Re-import the record from {connector_name.title()}.""" self.ensure_one() if not self.external_id: raise UserError(_('Cannot resync: No external ID found')) return self.import_record(self.backend_id, self.external_id) ''' return code def generate_binding_view(connector_info, entity_name, odoo_model): """Generate binding view XML.""" module_name = connector_info['module_name'] connector_name = connector_info['connector_name'] entity_lower = sanitize_name(entity_name) binding_model_name = f"{connector_name}.{odoo_model.replace('.', '.')}" view_id_prefix = f"view_{connector_name}_{entity_lower}" xml = f''' {binding_model_name}.form {binding_model_name}
{binding_model_name}.tree {binding_model_name} {binding_model_name}.search {binding_model_name} {connector_name.title()} {entity_name.title()}s {binding_model_name} tree,form
''' return xml def generate_adapter_methods(connector_info, entity_name): """Generate adapter methods to add to existing adapter.""" entity_lower = sanitize_name(entity_name) entity_plural = entity_lower + 's' # Simple pluralization code = f''' # CRUD operations for {entity_plural} def get_{entity_lower}(self, external_id): """Get {entity_lower} by external ID.""" return self._make_request('GET', f'/{entity_plural}/{{external_id}}') def get_{entity_plural}(self, filters=None): """Get list of {entity_plural}.""" return self._make_request('GET', '/{entity_plural}', params=filters) def create_{entity_lower}(self, data): """Create {entity_lower}.""" return self._make_request('POST', '/{entity_plural}', data=data) def update_{entity_lower}(self, external_id, data): """Update {entity_lower}.""" return self._make_request('PUT', f'/{entity_plural}/{{external_id}}', data=data) def delete_{entity_lower}(self, external_id): """Delete {entity_lower}.""" return self._make_request('DELETE', f'/{entity_plural}/{{external_id}}') ''' return code def generate_security_entries(connector_info, entity_name, odoo_model): """Generate security entries for ir.model.access.csv.""" module_name = connector_info['module_name'] connector_name = connector_info['connector_name'] entity_lower = sanitize_name(entity_name) model_name_sanitized = odoo_model.replace('.', '_') entries = f'''access_{connector_name}_{model_name_sanitized}_user,{connector_name}.{odoo_model} user,model_{connector_name}_{model_name_sanitized},group_{connector_name}_user,1,0,0,0 access_{connector_name}_{model_name_sanitized}_manager,{connector_name}.{odoo_model} manager,model_{connector_name}_{model_name_sanitized},group_{connector_name}_manager,1,1,1,1 ''' return entries def update_module_files(connector_info, entity_name, odoo_model): """Update module files to include new binding.""" module_path = connector_info['module_path'] entity_lower = sanitize_name(entity_name) print(f"\\nšŸ“ Updating module files...") # 1. Create binding model file binding_file = module_path / 'models' / f'{entity_lower}_binding.py' binding_code = generate_binding_model(connector_info, entity_name, odoo_model) with open(binding_file, 'w') as f: f.write(binding_code) print(f"āœ… Created: models/{entity_lower}_binding.py") # 2. Update models/__init__.py models_init = module_path / 'models' / '__init__.py' with open(models_init, 'r') as f: init_content = f.read() import_line = f"from . import {entity_lower}_binding\\n" if import_line not in init_content: with open(models_init, 'a') as f: f.write(import_line) print(f"āœ… Updated: models/__init__.py") # 3. Create view file view_file = module_path / 'views' / f'{entity_lower}_views.xml' view_xml = generate_binding_view(connector_info, entity_name, odoo_model) with open(view_file, 'w') as f: f.write(view_xml) print(f"āœ… Created: views/{entity_lower}_views.xml") # 4. Update __manifest__.py data list manifest_file = module_path / '__manifest__.py' with open(manifest_file, 'r') as f: manifest_content = f.read() # Add view file to manifest if not already there view_entry = f" 'views/{entity_lower}_views.xml',\\n" if view_entry not in manifest_content: # Find the views section and add manifest_content = manifest_content.replace( " 'views/binding_views.xml',", f" 'views/binding_views.xml',\\n{view_entry}" ) with open(manifest_file, 'w') as f: f.write(manifest_content) print(f"āœ… Updated: __manifest__.py (added view reference)") # 5. Update security file security_file = module_path / 'security' / 'ir.model.access.csv' security_entries = generate_security_entries(connector_info, entity_name, odoo_model) with open(security_file, 'a') as f: f.write(security_entries) print(f"āœ… Updated: security/ir.model.access.csv") # 6. Show adapter methods to add manually print(f"\\nšŸ“‹ Add these methods to models/adapter.py:") print("=" * 60) print(generate_adapter_methods(connector_info, entity_name)) print("=" * 60) def main(): parser = argparse.ArgumentParser( description='Add a new binding model to an existing Odoo connector module' ) parser.add_argument( 'connector_module_path', help='Path to existing connector module' ) parser.add_argument( 'entity_name', help='Name of the entity (e.g., order, customer, invoice)' ) parser.add_argument( '--odoo-model', help='Odoo model to bind (default: auto-detected from entity name)' ) args = parser.parse_args() try: # Validate module path module_path = Path(args.connector_module_path) if not module_path.exists(): raise ValueError(f"Module path does not exist: {module_path}") # Get connector info connector_info = get_connector_info(module_path) # Determine Odoo model odoo_model = args.odoo_model if not odoo_model: entity_lower = sanitize_name(args.entity_name) odoo_model = ENTITY_MODEL_MAP.get(entity_lower) if not odoo_model: print(f"\\nāš ļø Could not auto-detect Odoo model for '{args.entity_name}'") print(f"Please specify using --odoo-model option") print(f"\\nCommon mappings:") for entity, model in ENTITY_MODEL_MAP.items(): print(f" {entity}: {model}") sys.exit(1) print(f"\\nšŸš€ Adding {args.entity_name.title()} Binding") print(f" Module: {connector_info['module_name']}") print(f" Odoo Model: {odoo_model}") print(f" Location: {module_path}\\n") # Update module files update_module_files(connector_info, args.entity_name, odoo_model) print(f"\\nāœ… Binding for '{args.entity_name}' added successfully!") print(f"\\nNext steps:") print(f"1. Add adapter methods to models/adapter.py (see output above)") print(f"2. Implement importer component for {args.entity_name}") print(f"3. Implement exporter component for {args.entity_name}") print(f"4. Update module and test: odoo-bin -u {connector_info['module_name']}") except Exception as e: print(f"\\nāŒ Error: {str(e)}", file=sys.stderr) sys.exit(1) if __name__ == '__main__': main()