94ae2d578d
Adds a `credentials` entity to the back office (never mirrored to Sheets, gated by the CRM token even to read) so client logins like the auto-generated Easy!Appointments provider password can be viewed and copied from the dashboard instead of getting lost — the actual cause of the happynails password going missing. Onboarding now saves that generated password instead of discarding it. Also adds Documentation.md, brings README/TODO in line with the current Postgres-first architecture, and tidies the backlog. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
58 lines
2.0 KiB
Python
58 lines
2.0 KiB
Python
"""One-time (idempotent) seed of Postgres from the existing Google Sheets.
|
|
|
|
This is the ONLY place the Sheet is treated as authoritative — to bootstrap the
|
|
DB. After this, Postgres is the source of truth and Sheets is a downstream
|
|
mirror. Safe to re-run: keyed tables upsert by primary key; activity_log is
|
|
replaced wholesale.
|
|
|
|
Run inside the container: docker compose exec smb-crm python import_from_sheets.py
|
|
"""
|
|
import os
|
|
import sys
|
|
|
|
import db
|
|
from sheets import Sheets
|
|
|
|
|
|
def upsert(cur, entity, records):
|
|
spec = db.TABLES[entity]
|
|
cols = spec["cols"]
|
|
pk = spec["pk"]
|
|
n = 0
|
|
for rec in records:
|
|
row = db.coerce_row(entity, rec)
|
|
if entity != "activity_log" and not row.get(pk):
|
|
continue # skip rows without a primary key
|
|
placeholders = ", ".join(["%s"] * len(cols))
|
|
collist = ", ".join(cols)
|
|
if entity == "activity_log":
|
|
cur.execute(f"INSERT INTO {entity} ({collist}) VALUES ({placeholders})",
|
|
[row[c] for c in cols])
|
|
else:
|
|
updates = ", ".join(f"{c} = EXCLUDED.{c}" for c in cols if c != pk)
|
|
cur.execute(
|
|
f"INSERT INTO {entity} ({collist}) VALUES ({placeholders}) "
|
|
f"ON CONFLICT ({pk}) DO UPDATE SET {updates}",
|
|
[row[c] for c in cols])
|
|
n += 1
|
|
return n
|
|
|
|
|
|
def main():
|
|
sh = Sheets(os.environ["GOOGLE_SA_JSON"], os.environ["SHEET_ID"])
|
|
with db.connect() as conn:
|
|
with conn.cursor() as cur:
|
|
cur.execute("TRUNCATE activity_log RESTART IDENTITY")
|
|
for entity, spec in db.TABLES.items():
|
|
if spec.get("mirror") is False:
|
|
continue # e.g. credentials — never lived in the Sheet, nothing to import
|
|
records = sh.read_records(spec["tab"])
|
|
n = upsert(cur, entity, records)
|
|
print(f" {entity:<13} <- {spec['tab']:<13} {n} row(s)")
|
|
conn.commit()
|
|
print("import complete.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|