356 lines
12 KiB
Python
356 lines
12 KiB
Python
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()) |