gigafibre-fsm/scripts/migration/geocode_locations.py
louispaulb 101faa21f1 feat: inline editing, search, notifications + full repo cleanup
- InlineField component + useInlineEdit composable for Odoo-style dblclick editing
- Client search by name, account ID, and legacy_customer_id (or_filters)
- SMS/Email notification panel on ContactCard via n8n webhooks
- Ticket reply thread via Communication docs
- All migration scripts (51 files) now tracked
- Client portal and field tech app added to monorepo
- README rewritten with full feature list, migration summary, architecture
- CHANGELOG updated with all recent work
- ROADMAP updated with current completion status
- Removed hardcoded tokens from docs (use $ERP_SERVICE_TOKEN)
- .gitignore updated (docker/, .claude/, exports/, .quasar/)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 07:34:41 -04:00

152 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""
Geocode Service Locations using the rqa_addresses (Adresses Québec) table.
Matches by extracting numero from address_line, then fuzzy-matching
against rqa_addresses using city + street similarity.
Run inside erpnext-backend-1:
nohup python3 /tmp/geocode_locations.py > /tmp/geocode_locations.log 2>&1 &
"""
import psycopg2
import re
from datetime import datetime, timezone
PG = {"host": "db", "port": 5432, "user": "postgres", "password": "123",
"dbname": "_eb65bdc0c4b1b2d6"}
# Common city name normalizations for matching
CITY_NORMALIZE = {
"st-": "saint-",
"ste-": "sainte-",
"st ": "saint-",
"ste ": "sainte-",
"st.": "saint-",
"ste.": "sainte-",
}
def log(msg):
print("[{}] {}".format(datetime.now(timezone.utc).strftime("%H:%M:%S"), msg), flush=True)
def normalize_city(city):
"""Normalize city name for matching."""
c = city.lower().strip()
for old, new in CITY_NORMALIZE.items():
if c.startswith(old):
c = new + c[len(old):]
# Remove accents would be ideal but keep it simple
# Remove " de " variants
c = re.sub(r'\s+de\s+', '-', c)
c = re.sub(r'\s+', '-', c)
return c
def extract_numero(address):
"""Extract civic number from address string."""
addr = address.strip()
# Try to find number at start: "1185 Route 133" → "1185"
m = re.match(r'^(\d+[A-Za-z]?)\s*[,\s]', addr)
if m:
return m.group(1)
# Just digits at start
m = re.match(r'^(\d+)', addr)
if m:
return m.group(1)
return None
def main():
log("=== Geocode Service Locations via AQ ===")
pg = psycopg2.connect(**PG)
pg.autocommit = False
pgc = pg.cursor()
# Get locations needing GPS
pgc.execute("""
SELECT name, address_line, city, postal_code
FROM "tabService Location"
WHERE (latitude = 0 OR latitude IS NULL)
AND address_line NOT IN ('N/A', '', 'xxx')
AND city NOT IN ('N/A', '')
""")
locations = pgc.fetchall()
log(" {} locations to geocode".format(len(locations)))
matched = missed = 0
for i, (loc_name, addr, city, postal) in enumerate(locations):
lat = lon = None
# Strategy 1: Match by postal code + numero (most precise)
numero = extract_numero(addr)
if postal and len(postal) >= 6:
postal_clean = postal.strip().upper().replace(" ", "")
if numero:
pgc.execute("""
SELECT latitude, longitude FROM rqa_addresses
WHERE REPLACE(UPPER(code_postal), ' ', '') = %s AND numero = %s
LIMIT 1
""", (postal_clean, numero))
row = pgc.fetchone()
if row:
lat, lon = row
# Strategy 2: Match by numero + city + fuzzy street
if not lat and numero and city:
city_norm = normalize_city(city)
# Build search string for trigram matching
search = "{} {}".format(numero, addr.lower())
pgc.execute("""
SELECT latitude, longitude,
similarity(search_text, %s) as sim
FROM rqa_addresses
WHERE numero = %s
AND LOWER(ville) %% %s
ORDER BY similarity(search_text, %s) DESC
LIMIT 1
""", (search, numero, city_norm, search))
row = pgc.fetchone()
if row and row[2] > 0.15:
lat, lon = row[0], row[1]
# Strategy 3: Full address fuzzy match against address_full
if not lat and city:
full_addr = "{}, {}".format(addr, city).lower()
pgc.execute("""
SELECT latitude, longitude,
similarity(address_full, %s) as sim
FROM rqa_addresses
WHERE address_full %% %s
ORDER BY similarity(address_full, %s) DESC
LIMIT 1
""", (full_addr, full_addr, full_addr))
row = pgc.fetchone()
if row and row[2] > 0.25:
lat, lon = row[0], row[1]
if lat and lon:
pgc.execute("""
UPDATE "tabService Location"
SET latitude = %s, longitude = %s, modified = NOW()
WHERE name = %s
""", (lat, lon, loc_name))
matched += 1
else:
missed += 1
if (matched + missed) % 500 == 0:
pg.commit()
log(" [{}/{}] matched={} missed={}".format(i+1, len(locations), matched, missed))
pg.commit()
pg.close()
log("")
log("=" * 60)
log("GEOCODE COMPLETE")
log(" Matched: {} ({:.1f}%)".format(matched, 100*matched/len(locations) if locations else 0))
log(" Missed: {}".format(missed))
log("=" * 60)
if __name__ == "__main__":
main()