197 lines
6.1 KiB
Python
197 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
ADP Mobility Manual CSV to JSON Converter
|
||
|
||
This script reads the ADP Mobility Manual CSV file, cleans the data,
|
||
and outputs a JSON file that can be imported into Odoo's fusion.adp.device.code model.
|
||
|
||
Usage:
|
||
python import_adp_mobility_manual.py input.csv output.json
|
||
|
||
Or run without arguments to use default paths.
|
||
|
||
Copyright 2024-2025 Nexa Systems Inc.
|
||
License OPL-1 (Odoo Proprietary License v1.0)
|
||
"""
|
||
|
||
import csv
|
||
import json
|
||
import re
|
||
import sys
|
||
import os
|
||
|
||
|
||
def clean_text(text):
|
||
"""Clean text from weird characters, normalize encoding."""
|
||
if not text:
|
||
return ''
|
||
# Convert to string if not already
|
||
text = str(text)
|
||
# Replace curly quotes with straight quotes
|
||
text = text.replace('"', '"').replace('"', '"')
|
||
text = text.replace(''', "'").replace(''', "'")
|
||
# Replace various dashes with standard hyphen
|
||
text = text.replace('–', '-').replace('—', '-')
|
||
# Remove non-printable characters except newlines
|
||
text = ''.join(char if char.isprintable() or char in '\n\r\t' else ' ' for char in text)
|
||
# Normalize multiple spaces
|
||
text = re.sub(r'\s+', ' ', text)
|
||
# Strip leading/trailing whitespace
|
||
return text.strip()
|
||
|
||
|
||
def parse_price(price_str):
|
||
"""Parse price string like '$64.00' or '$2,578.00' to float."""
|
||
if not price_str:
|
||
return 0.0
|
||
# Remove currency symbols, commas, spaces, quotes
|
||
price_str = str(price_str).strip()
|
||
price_str = re.sub(r'[\$,"\'\s]', '', price_str)
|
||
try:
|
||
return float(price_str)
|
||
except ValueError:
|
||
return 0.0
|
||
|
||
|
||
def convert_csv_to_json(input_path, output_path):
|
||
"""Convert ADP Mobility Manual CSV to JSON format."""
|
||
data = []
|
||
errors = []
|
||
skipped = 0
|
||
|
||
# Try different encodings
|
||
encodings = ['utf-8-sig', 'utf-8', 'latin-1', 'cp1252']
|
||
content = None
|
||
|
||
for encoding in encodings:
|
||
try:
|
||
with open(input_path, 'r', encoding=encoding) as f:
|
||
content = f.read()
|
||
break
|
||
except UnicodeDecodeError:
|
||
continue
|
||
|
||
if content is None:
|
||
print(f"Error: Could not read file with any known encoding")
|
||
return None
|
||
|
||
# Parse CSV
|
||
reader = csv.DictReader(content.splitlines())
|
||
|
||
for idx, row in enumerate(reader, start=2): # Start at 2 (header is line 1)
|
||
try:
|
||
# Get device code - skip if empty
|
||
device_code = clean_text(row.get('Device Code', ''))
|
||
if not device_code:
|
||
skipped += 1
|
||
continue
|
||
|
||
# Get device type
|
||
device_type = clean_text(row.get('Device Type', ''))
|
||
if not device_type:
|
||
skipped += 1
|
||
continue
|
||
|
||
# Get manufacturer
|
||
manufacturer = clean_text(row.get('Manufacturer', ''))
|
||
|
||
# Get device description - clean it
|
||
device_description = clean_text(row.get('Device Description', ''))
|
||
|
||
# Parse quantity
|
||
qty_str = row.get('Qty', '1') or '1'
|
||
try:
|
||
quantity = int(qty_str)
|
||
except ValueError:
|
||
quantity = 1
|
||
|
||
# Parse price (handle both ' Approved Price ' with spaces and 'Approved Price')
|
||
price = 0.0
|
||
for key in row.keys():
|
||
if 'price' in key.lower() and 'approved' in key.lower():
|
||
price = parse_price(row.get(key, ''))
|
||
break
|
||
|
||
# Parse serial requirement
|
||
serial_str = clean_text(row.get('Serial', 'No')).upper()
|
||
sn_required = serial_str in ('YES', 'Y', 'TRUE', '1')
|
||
|
||
data.append({
|
||
'Device Type': device_type,
|
||
'Manufacturer': manufacturer,
|
||
'Device Description': device_description,
|
||
'Device Code': device_code,
|
||
'Quantity': quantity,
|
||
'ADP Price': price,
|
||
'SN Required': 'Yes' if sn_required else 'No',
|
||
})
|
||
|
||
except Exception as e:
|
||
errors.append(f"Row {idx}: {str(e)}")
|
||
|
||
# Write JSON output
|
||
with open(output_path, 'w', encoding='utf-8') as f:
|
||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||
|
||
# Print summary
|
||
print(f"\n{'='*60}")
|
||
print(f"ADP Mobility Manual Import Summary")
|
||
print(f"{'='*60}")
|
||
print(f"Input file: {input_path}")
|
||
print(f"Output file: {output_path}")
|
||
print(f"Records processed: {len(data)}")
|
||
print(f"Records skipped: {skipped}")
|
||
print(f"Errors: {len(errors)}")
|
||
|
||
if errors:
|
||
print(f"\nFirst 10 errors:")
|
||
for err in errors[:10]:
|
||
print(f" - {err}")
|
||
|
||
# Print device type summary
|
||
device_types = {}
|
||
for item in data:
|
||
dt = item['Device Type']
|
||
device_types[dt] = device_types.get(dt, 0) + 1
|
||
|
||
print(f"\nDevice Types ({len(device_types)} unique):")
|
||
for dt in sorted(device_types.keys())[:20]:
|
||
print(f" - {dt}: {device_types[dt]} devices")
|
||
if len(device_types) > 20:
|
||
print(f" ... and {len(device_types) - 20} more")
|
||
|
||
print(f"\n{'='*60}")
|
||
print(f"JSON file ready for import into Odoo!")
|
||
print(f"Use: Sales > Configuration > ADP Device Codes > Import")
|
||
print(f"{'='*60}\n")
|
||
|
||
return data
|
||
|
||
|
||
def main():
|
||
# Default paths
|
||
default_input = r"C:\Users\gur_p\Downloads\ADP-Mobility-Manual.csv"
|
||
default_output = r"C:\Users\gur_p\Downloads\ADP-Mobility-Manual-cleaned.json"
|
||
|
||
if len(sys.argv) >= 3:
|
||
input_path = sys.argv[1]
|
||
output_path = sys.argv[2]
|
||
elif len(sys.argv) == 2:
|
||
input_path = sys.argv[1]
|
||
output_path = os.path.splitext(input_path)[0] + '-cleaned.json'
|
||
else:
|
||
input_path = default_input
|
||
output_path = default_output
|
||
|
||
if not os.path.exists(input_path):
|
||
print(f"Error: Input file not found: {input_path}")
|
||
print(f"\nUsage: python {sys.argv[0]} input.csv [output.json]")
|
||
sys.exit(1)
|
||
|
||
convert_csv_to_json(input_path, output_path)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|