import csv from proteus import config, Model from decimal import Decimal # XML-RPC Configuration HTTPS = 'https://' SERVER_URL = 'itsa.open-squared.tech' DATABASE_NAME = 'tradon' USERNAME = 'admin' PASSWORD = 'dsproject' # CSV Configuration CSV_FILE_PATH = r'C:\Users\SylvainDUVERNAY\Open Squared\Production - Documents\TRADON Implementation\ITSA\Reference Data\Loaders\Services.csv' # UPDATE THIS PATH! # Product configuration PRODUCT_TYPE = 'service' # Service type products DEFAULT_CATEGORY = 'SERVICES' # Default category name if not found DEFAULT_UOM = 'Mt' # Default UOM if not found def connect_to_tryton(): """Establish connection to Tryton via XML-RPC""" print(f"Connecting to Tryton server: {SERVER_URL}") print(f"Database: {DATABASE_NAME}") print(f"Username: {USERNAME}") try: # Connect using XML-RPC with credentials in URL #connection_url = f'{SERVER_URL}/{DATABASE_NAME}/' #print(f'{USERNAME}:{PASSWORD}@{connection_url}') config.set_xmlrpc(f'{HTTPS}{USERNAME}:{PASSWORD}@{SERVER_URL}/{DATABASE_NAME}/') #config.set_xmlrpc('https://admin:dsproject@itsa.open-squared.tech/tradon/') print("✓ Connected successfully!\n") return True except Exception as e: print(f"✗ Connection failed: {e}") print("\nTroubleshooting:") print(" - Verify the server URL is correct and accessible") print(" - Check that the Tryton server is running") print(" - Verify username and password are correct") print(" - Make sure you can access the server in a browser") return False def get_or_create_category(category_name): """Find or create a product category""" Category = Model.get('product.category') # Try to find existing category categories = Category.find([('name', '=', category_name)]) if categories: print(f" Found existing category: {category_name}") return categories[0] else: # Create new category new_category = Category() new_category.name = category_name new_category.save() print(f" ✓ Created new category: {category_name}") return new_category def get_uom(uom_name): """Find Unit of Measure by name""" Uom = Model.get('product.uom') # Try exact match first uoms = Uom.find([('name', '=', uom_name)]) if uoms: return uoms[0] # Try case-insensitive search by getting all and comparing all_uoms = Uom.find([]) for uom in all_uoms: if uom.name.lower() == uom_name.lower(): return uom # If not found, try to get default 'Unit' print(f" ⚠ Warning: UOM '{uom_name}' not found, using '{DEFAULT_UOM}'") default_uoms = Uom.find([('name', '=', DEFAULT_UOM)]) if default_uoms: return default_uoms[0] # If even Unit is not found, get the first available all_uoms = Uom.find([]) if all_uoms: print(f" ⚠ Using first available UOM: {all_uoms[0].name}") return all_uoms[0] raise ValueError("No UOM found in database!") def check_product_exists(code): """Check if product with given code already exists""" Product = Model.get('product.product') products = Product.find([('code', '=', code)]) return products[0] if products else None def create_service_product(row, categories, uom): """Create a new service product using proteus""" Template = Model.get('product.template') # Create template template = Template() template.name = row['name'] template.code = row['code'] template.type = PRODUCT_TYPE template.list_price = Decimal(row['sale_price']) if row['sale_price'] else Decimal('0.00') template.cost_price_method = 'fixed' # Services use fixed cost price template.default_uom = uom # Link to categories (Many2Many relationship) # Use append() instead of direct assignment if isinstance(categories, list): template.categories.extend(categories) # Use extend for lists else: template.categories.append(categories) # Use append for single category template.salable = False # Services are not salable products by default template.purchasable = True # Services are purchasable if row.get('description'): template.description = row['description'] # Save the template first template.save() # Now update the product that was auto-created # When a template is created, Tryton automatically creates a default product if template.products: product = template.products[0] #product.code = row['code'] product.suffix_code = row['code'] # Use suffix_code to set product code # Set cost price on the product product.cost_price = Decimal(row['cost_price']) if row['cost_price'] else Decimal('0.00') product.save() return product else: raise ValueError("No product was created automatically with template") def import_services(csv_file): """Import services from CSV file""" imported_count = 0 skipped_count = 0 error_count = 0 errors = [] print(f"{'='*70}") print(f"Importing service products from: {csv_file}") print(f"{'='*70}\n") try: # Open with utf-8-sig to handle BOM with open(csv_file, 'r', encoding='utf-8-sig') as file: reader = csv.DictReader(file) # Debug: Show detected columns print(f"Detected columns: {reader.fieldnames}\n") for row_num, row in enumerate(reader, start=2): try: # Clean up values code = row.get('code', '').strip() name = row.get('name', '').strip() category_name = row.get('category', DEFAULT_CATEGORY).strip() or DEFAULT_CATEGORY uom_name = row.get('uom', DEFAULT_UOM).strip() or DEFAULT_UOM sale_price = row.get('sale_price', '0.00').strip() or '0.00' cost_price = row.get('cost_price', '0.00').strip() or '0.00' description = row.get('description', '').strip() # Skip empty rows if not code and not name: continue # Validate required fields if not code or not name: errors.append(f"Row {row_num}: Missing code or name") error_count += 1 print(f"✗ Row {row_num}: Missing required fields") continue print(f"Processing Row {row_num}: {code} - {name}") # Check if product already exists existing_product = check_product_exists(code) if existing_product: print(f" ⚠ Product code '{code}' already exists: {existing_product.template.name}") print(f" Skipping...\n") skipped_count += 1 continue # Get or create category category = get_or_create_category(category_name) # Get UOM uom = get_uom(uom_name) print(f" Using UOM: {uom.name}") # Create the product row_data = { 'code': code, 'name': name, 'sale_price': sale_price, 'cost_price': cost_price, 'description': description } product = create_service_product(row_data, category, uom) print(f" ✓ Created service product") print(f" Product ID: {product.id}, Template ID: {product.template.id}") print(f" Code: {code}") print(f" Category: {category.name}") print(f" Sale Price: {sale_price}") print(f" Cost Price: {cost_price}") if description: print(f" Description: {description[:50]}...") print() imported_count += 1 except Exception as e: error_msg = f"Row {row_num} - {code} ({name}): {str(e)}" errors.append(error_msg) error_count += 1 print(f"✗ Error on row {row_num}: {e}\n") import traceback traceback.print_exc() # Summary print(f"{'='*70}") print("IMPORT SUMMARY") print(f"{'='*70}") print(f"Successfully imported: {imported_count} service products") print(f"Skipped (already exist): {skipped_count} products") print(f"Errors: {error_count}") if errors: print(f"\nError details:") for error in errors: print(f" - {error}") print(f"\n{'='*70}") except FileNotFoundError: print(f"✗ Error: CSV file not found at {csv_file}") print(f"Please update CSV_FILE_PATH in the script with the correct path.") except Exception as e: print(f"✗ Fatal error: {e}") import traceback traceback.print_exc() def verify_import(): """Verify imported service products""" Product = Model.get('product.product') print(f"\n{'='*70}") print("VERIFICATION - Service Products") print(f"{'='*70}\n") # Find all service type products products = Product.find([('template.type', '=', 'service')]) if products: print(f"Found {len(products)} service products:\n") print(f"{'Code':<12} {'Name':<30} {'Categories':<25} {'Sale Price':<12}") print("-" * 85) for product in products: code = product.code or 'N/A' name = product.template.name[:29] if product.template.name else 'N/A' # Get categories (Many2Many relationship) if product.template.categories: categories = ', '.join([cat.name for cat in product.template.categories]) categories = categories[:24] else: categories = 'N/A' sale_price = f"{product.template.list_price:.2f}" if product.template.list_price else '0.00' print(f"{code:<12} {name:<30} {categories:<25} {sale_price:<12}") else: print("No service products found") print() def list_available_uoms(): """List all available UOMs in the database""" print(f"\n{'='*70}") print("AVAILABLE UNITS OF MEASURE") print(f"{'='*70}\n") Uom = Model.get('product.uom') uoms = Uom.find([]) if uoms: print(f"Found {len(uoms)} UOMs:\n") for uom in uoms: symbol = f"({uom.symbol})" if hasattr(uom, 'symbol') and uom.symbol else "" print(f" - {uom.name} {symbol}") else: print("No UOMs found") print() def list_available_categories(): """List all available product categories""" print(f"\n{'='*70}") print("AVAILABLE PRODUCT CATEGORIES") print(f"{'='*70}\n") Category = Model.get('product.category') categories = Category.find([]) if categories: print(f"Found {len(categories)} categories:\n") for cat in categories: print(f" - {cat.name}") else: print("No categories found") print() def main(): print("="*70) print("TRYTON SERVICE PRODUCT IMPORT SCRIPT") print("Using Proteus with XML-RPC Connection") print("="*70) print() # Connect to Tryton using XML-RPC if not connect_to_tryton(): return 1 # Optional: List available UOMs and categories # Uncomment these if you want to see what's available in your database # list_available_uoms() # list_available_categories() # Import service products import_services(CSV_FILE_PATH) # Verify import verify_import() return 0 if __name__ == '__main__': exit(main())