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