Files
smb-online/backoffice/app/import_from_sheets.py
T
mivanchenko 94ae2d578d Add credentials store to the CRM, docs cleanup, deploy pipeline TODO
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>
2026-07-15 14:13:34 +02:00

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())