Initial commit
This commit is contained in:
356
Reference Data/python_project/scripts/import_services v2.py
Normal file
356
Reference Data/python_project/scripts/import_services v2.py
Normal file
@@ -0,0 +1,356 @@
|
||||
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())
|
||||
Reference in New Issue
Block a user