398 lines
14 KiB
Python
398 lines
14 KiB
Python
import csv
|
||
from proteus import config, Model
|
||
|
||
# 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\Parties.csv'
|
||
|
||
# Default values
|
||
DEFAULT_COUNTRY = 'US' # Default country code if not specified
|
||
|
||
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:
|
||
config.set_xmlrpc(f'{HTTPS}{USERNAME}:{PASSWORD}@{SERVER_URL}/{DATABASE_NAME}/')
|
||
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_country(country_code):
|
||
"""Find country by code"""
|
||
Country = Model.get('country.country')
|
||
|
||
if not country_code:
|
||
country_code = DEFAULT_COUNTRY
|
||
|
||
countries = Country.find([('code', '=', country_code.upper())])
|
||
|
||
if countries:
|
||
return countries[0]
|
||
else:
|
||
print(f" ⚠ Warning: Country '{country_code}' not found, using '{DEFAULT_COUNTRY}'")
|
||
default_countries = Country.find([('code', '=', DEFAULT_COUNTRY)])
|
||
if default_countries:
|
||
return default_countries[0]
|
||
|
||
# Get first available country as last resort
|
||
all_countries = Country.find([])
|
||
if all_countries:
|
||
print(f" ⚠ Using first available country: {all_countries[0].name}")
|
||
return all_countries[0]
|
||
|
||
raise ValueError("No countries found in database!")
|
||
|
||
def get_subdivision(country, subdivision_code):
|
||
"""Find country subdivision (state/province) by code"""
|
||
if not subdivision_code:
|
||
return None
|
||
|
||
Subdivision = Model.get('country.subdivision')
|
||
|
||
# Search for subdivision with matching code and country
|
||
subdivisions = Subdivision.find([
|
||
('code', '=', f"{country.code}-{subdivision_code}"),
|
||
('country', '=', country.id)
|
||
])
|
||
|
||
if subdivisions:
|
||
return subdivisions[0]
|
||
|
||
# Try without country prefix
|
||
subdivisions = Subdivision.find([
|
||
('code', 'ilike', f"%{subdivision_code}"),
|
||
('country', '=', country.id)
|
||
])
|
||
|
||
if subdivisions:
|
||
return subdivisions[0]
|
||
|
||
print(f" ⚠ Warning: Subdivision '{subdivision_code}' not found for country {country.code}")
|
||
return None
|
||
|
||
def check_party_exists_by_name(name):
|
||
"""Check if party with given name already exists"""
|
||
Party = Model.get('party.party')
|
||
parties = Party.find([('name', '=', name)])
|
||
return parties[0] if parties else None
|
||
|
||
|
||
|
||
def create_party_with_addresses(row):
|
||
"""Create a new party with address(es) using proteus"""
|
||
Party = Model.get('party.party')
|
||
Address = Model.get('party.address')
|
||
|
||
# Create party - let Tryton auto-generate the code
|
||
party = Party()
|
||
party.name = row['name']
|
||
|
||
if row.get('tax_identifier'):
|
||
party.tax_identifier = row['tax_identifier']
|
||
|
||
if row.get('vat_code'):
|
||
party.vat_code = row['vat_code']
|
||
|
||
# Save the party FIRST (without addresses)
|
||
party.save()
|
||
|
||
# Check if we have meaningful address data
|
||
# Require at least street OR city to be present (not empty)
|
||
has_street = bool(row.get('street'))
|
||
has_city = bool(row.get('city'))
|
||
has_postal_code = bool(row.get('postal_code'))
|
||
has_country = bool(row.get('country_code'))
|
||
|
||
# Create address only if we have at least street OR city
|
||
if has_street or has_city:
|
||
address = Address()
|
||
|
||
# Link to the party we just created
|
||
address.party = party
|
||
|
||
if row.get('address_name'):
|
||
address.name = row['address_name']
|
||
|
||
if has_street:
|
||
address.street = row['street']
|
||
|
||
if has_city:
|
||
address.city = row['city']
|
||
|
||
# Use postal_code instead of zip
|
||
if has_postal_code:
|
||
address.postal_code = row['postal_code']
|
||
|
||
# Get country
|
||
if has_country:
|
||
country_code = row['country_code']
|
||
country = get_country(country_code)
|
||
else:
|
||
country = get_country(DEFAULT_COUNTRY)
|
||
|
||
address.country = country
|
||
|
||
# Get subdivision (state/province) if provided
|
||
if row.get('subdivision_code'):
|
||
subdivision = get_subdivision(country, row['subdivision_code'])
|
||
if subdivision:
|
||
address.subdivision = subdivision
|
||
|
||
# Save the address separately
|
||
address.save()
|
||
|
||
# Clean up any empty addresses that might have been auto-created
|
||
# Reload party to get fresh data
|
||
party = Party(party.id)
|
||
|
||
# Find and delete empty addresses
|
||
addresses_to_delete = []
|
||
for addr in party.addresses:
|
||
# Consider an address empty if it has no street, city, or postal_code
|
||
is_empty = (
|
||
(not addr.street or not addr.street.strip()) and
|
||
(not addr.city or not addr.city.strip()) and
|
||
(not addr.postal_code or not addr.postal_code.strip())
|
||
)
|
||
if is_empty:
|
||
addresses_to_delete.append(addr)
|
||
|
||
# Delete empty addresses
|
||
if addresses_to_delete:
|
||
Address.delete(addresses_to_delete)
|
||
print(f" ℹ Cleaned up {len(addresses_to_delete)} empty address(es)")
|
||
|
||
# Reload party one more time to return clean data
|
||
party = Party(party.id)
|
||
|
||
return party
|
||
|
||
|
||
def import_parties(csv_file):
|
||
"""Import parties from CSV file"""
|
||
|
||
imported_count = 0
|
||
skipped_count = 0
|
||
error_count = 0
|
||
errors = []
|
||
|
||
# Track names we've already processed in this run
|
||
processed_names = set()
|
||
|
||
print(f"{'='*70}")
|
||
print(f"Importing parties 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
|
||
name = row.get('name', '').strip()
|
||
tax_identifier = row.get('tax_identifier', '').strip()
|
||
vat_code = row.get('vat_code', '').strip()
|
||
|
||
# Address fields
|
||
address_name = row.get('address_name', '').strip()
|
||
street = row.get('street', '').strip()
|
||
city = row.get('city', '').strip()
|
||
|
||
# Handle both 'zip' and 'postal_code' column names
|
||
postal_code = row.get('postal_code', '').strip() or row.get('zip', '').strip()
|
||
|
||
country_code = row.get('country_code', '').strip()
|
||
subdivision_code = row.get('subdivision_code', '').strip()
|
||
|
||
# Skip empty rows
|
||
if not name:
|
||
continue
|
||
|
||
# Skip if postal_code is 'NULL' or '0'
|
||
if postal_code and postal_code.upper() in ['NULL', '0']:
|
||
postal_code = ''
|
||
|
||
print(f"Processing Row {row_num}: {name}")
|
||
|
||
# Check if we've already processed this name in this import run
|
||
if name in processed_names:
|
||
print(f" ⚠ Duplicate name in CSV: '{name}'")
|
||
print(f" Skipping duplicate entry...\n")
|
||
skipped_count += 1
|
||
continue
|
||
|
||
# Check if party already exists in database
|
||
existing_party = check_party_exists_by_name(name)
|
||
|
||
if existing_party:
|
||
print(f" ⚠ Party '{name}' already exists with code: {existing_party.code}")
|
||
print(f" Skipping...\n")
|
||
skipped_count += 1
|
||
processed_names.add(name)
|
||
continue
|
||
|
||
# Create the party with address
|
||
row_data = {
|
||
'name': name,
|
||
'tax_identifier': tax_identifier,
|
||
'vat_code': vat_code,
|
||
'address_name': address_name,
|
||
'street': street,
|
||
'city': city,
|
||
'postal_code': postal_code,
|
||
'country_code': country_code,
|
||
'subdivision_code': subdivision_code
|
||
}
|
||
|
||
party = create_party_with_addresses(row_data)
|
||
|
||
# Mark this name as processed
|
||
processed_names.add(name)
|
||
|
||
print(f" ✓ Created party")
|
||
print(f" Party ID: {party.id}")
|
||
print(f" Auto-generated Code: {party.code}")
|
||
print(f" Name: {name}")
|
||
if tax_identifier:
|
||
print(f" Tax Identifier: {tax_identifier}")
|
||
if vat_code:
|
||
print(f" VAT Code: {vat_code}")
|
||
if party.addresses:
|
||
print(f" Addresses: {len(party.addresses)}")
|
||
for addr in party.addresses:
|
||
addr_street = (addr.street[:50] + '...') if addr.street and len(addr.street) > 50 else (addr.street or 'N/A')
|
||
addr_city = addr.city if addr.city else 'N/A'
|
||
addr_postal = addr.postal_code if addr.postal_code else 'N/A'
|
||
print(f" - {addr_street}")
|
||
print(f" {addr_city}, {addr_postal}")
|
||
else:
|
||
print(f" Addresses: 0 (no address data provided)")
|
||
print()
|
||
|
||
imported_count += 1
|
||
|
||
except Exception as e:
|
||
error_msg = f"Row {row_num} - {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} parties")
|
||
print(f"Skipped (already exist or duplicates): {skipped_count} parties")
|
||
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 parties"""
|
||
Party = Model.get('party.party')
|
||
|
||
print(f"\n{'='*70}")
|
||
print("VERIFICATION - Parties")
|
||
print(f"{'='*70}\n")
|
||
|
||
# Find all parties (or limit to recently created ones)
|
||
parties = Party.find([], order=[('id', 'DESC')])
|
||
|
||
if parties:
|
||
print(f"Found {len(parties)} parties (showing last 20):\n")
|
||
print(f"{'Code':<15} {'Name':<40} {'Addresses':<10}")
|
||
print("-" * 70)
|
||
|
||
for party in parties[:20]: # Show last 20 created
|
||
code = party.code or 'N/A'
|
||
name = party.name[:39] if party.name else 'N/A'
|
||
addr_count = len(party.addresses) if party.addresses else 0
|
||
|
||
print(f"{code:<15} {name:<40} {addr_count:<10}")
|
||
else:
|
||
print("No parties found")
|
||
|
||
print()
|
||
|
||
def list_available_countries():
|
||
"""List all available countries"""
|
||
print(f"\n{'='*70}")
|
||
print("AVAILABLE COUNTRIES (first 20)")
|
||
print(f"{'='*70}\n")
|
||
|
||
Country = Model.get('country.country')
|
||
countries = Country.find([])
|
||
|
||
if countries:
|
||
print(f"Found {len(countries)} countries:\n")
|
||
for country in countries[:20]: # Show first 20
|
||
print(f" - {country.code}: {country.name}")
|
||
if len(countries) > 20:
|
||
print(f" ... and {len(countries) - 20} more")
|
||
else:
|
||
print("No countries found")
|
||
|
||
print()
|
||
|
||
def main():
|
||
print("="*70)
|
||
print("TRYTON PARTY IMPORT SCRIPT")
|
||
print("Using Proteus with XML-RPC Connection")
|
||
print("Party codes will be auto-generated by Tryton")
|
||
print("="*70)
|
||
print()
|
||
|
||
# Connect to Tryton using XML-RPC
|
||
if not connect_to_tryton():
|
||
return 1
|
||
|
||
# Optional: List available countries
|
||
# Uncomment if you want to see what's available in your database
|
||
# list_available_countries()
|
||
|
||
# Import parties
|
||
import_parties(CSV_FILE_PATH)
|
||
|
||
# Verify import
|
||
verify_import()
|
||
|
||
return 0
|
||
|
||
if __name__ == '__main__':
|
||
exit(main()) |