168 lines
9 KiB
Python
168 lines
9 KiB
Python
#!/usr/bin/env python3
|
||
# ═══════════════════════════════════════════════════════════
|
||
# SPA filler — Studio_Participation_Agreement_2026-09 → per-founder
|
||
# Задачи t-261008143050 (Siarhei Zhukovich, когорта с 16-го) и
|
||
# t-261008144131 (7 участников 42 Labs за 2026-09).
|
||
# Паттерн подстановки — как _contract_docx_bytes в /root/coop/app.py.
|
||
# ═══════════════════════════════════════════════════════════
|
||
import os, sys, copy
|
||
import docx
|
||
|
||
TEMPLATE = "/root/coop/legal/Studio_Participation_Agreement_2026-09.docx"
|
||
OUT = "/root/coop/legal"
|
||
|
||
DRAFT_LINE = "DRAFT TEMPLATE — for review by U.S. counsel before first use. Not legal advice."
|
||
|
||
# Перепривязка платежного графика на 16-е число (когорты с 16-го, t-261008143050)
|
||
SIXTEENTH = [
|
||
("no later than the 1st day of the first month of participation",
|
||
"no later than the 16th day of the first month of participation"),
|
||
("each subsequent Monthly Fee no later than the 1st day of the corresponding month of participation",
|
||
"each subsequent Monthly Fee no later than the 16th day of the corresponding month of participation"),
|
||
("If the Agreement is signed after the 1st, the Program Start is the 1st day of the next calendar month",
|
||
"If the Agreement is signed after the 16th, the Program Start is the 16th day of the next calendar month"),
|
||
("for Monthly Fee tranches, the 1st day of the participation month",
|
||
"for Monthly Fee tranches, the 16th day of the participation month"),
|
||
("takes effect on the 1st day of the next participation month",
|
||
"takes effect on the 16th day of the next participation month"),
|
||
("payable in advance by the 1st of each participation month",
|
||
"payable in advance by the 16th of each participation month"),
|
||
("the 1st day of the month of signature if signed on the 1st, otherwise the 1st day of the next calendar month",
|
||
"the 16th day of the month of signature if signed on the 16th, otherwise the 16th day of the next calendar month"),
|
||
]
|
||
|
||
|
||
def founder_replacements(f):
|
||
"""f: dict(name, passport, pid, address, email, project, one_line, rev_share,
|
||
effective, cohort, program_start, sixteenth: bool)"""
|
||
r = {
|
||
"[DATE]": f["effective"],
|
||
"[FULL LEGAL NAME]": f["name"],
|
||
"[NAME]": f["name"],
|
||
"[EMAIL]": f.get("email") or "[EMAIL]",
|
||
"cohort [MONTH YEAR]": f"cohort {f['cohort']}",
|
||
"1 [MONTH] [YEAR]": f["program_start"],
|
||
"[MONTH YEAR]": f["cohort"],
|
||
}
|
||
if f.get("rev_share"):
|
||
r["[X]"] = str(f["rev_share"])
|
||
if f.get("project"):
|
||
r["[PROJECT NAME]"] = f["project"]
|
||
if f.get("one_line"):
|
||
r["[ONE LINE]"] = f["one_line"]
|
||
# Строка Founder: имя + статус + паспорт + адрес одной подстановкой
|
||
line_old = "[FULL LEGAL NAME], an individual / [ENTITY TYPE], residing at [ADDRESS], [COUNTRY]"
|
||
line_new = (f"{f['name']}, an individual, passport of the Republic of Belarus "
|
||
f"{f.get('passport') or '[PASSPORT NO.]'} (personal identification No. "
|
||
f"{f.get('pid') or '[PERSONAL ID NO.]'}), residing at "
|
||
f"{f.get('address') or '[ADDRESS]'}, Republic of Belarus")
|
||
r[line_old] = line_new
|
||
# фолбэк, если строка разбита рант differently
|
||
r["an individual / [ENTITY TYPE]"] = "an individual"
|
||
# паспорт/ид.номер вставляются после схлопывания individual-строки
|
||
# (порядок ключей dict важен: этот ключ применяется после предыдущего)
|
||
pass_line = (f"passport of the Republic of Belarus {f.get('passport') or '[PASSPORT NO.]'}"
|
||
f" (personal identification No. {f.get('pid') or '[PERSONAL ID NO.]'}), residing at")
|
||
r["an individual, residing at"] = f"an individual, {pass_line}"
|
||
r["[ADDRESS], [COUNTRY]"] = f"{f.get('address') or '[ADDRESS]'}, Republic of Belarus"
|
||
r["[ADDRESS]"] = f.get("address") or "[ADDRESS]"
|
||
r["[COUNTRY]"] = "Republic of Belarus"
|
||
return r
|
||
|
||
|
||
def fill(f, out_docx):
|
||
doc = docx.Document(TEMPLATE)
|
||
reps = founder_replacements(f)
|
||
if f.get("sixteenth"):
|
||
reps = dict(SIXTEENTH) | reps
|
||
reps[DRAFT_LINE] = ""
|
||
for p in doc.paragraphs:
|
||
if not p.runs:
|
||
continue
|
||
full = "".join(r.text for r in p.runs)
|
||
new = full
|
||
for k, v in reps.items():
|
||
if k in new:
|
||
new = new.replace(k, v)
|
||
if new != full:
|
||
p.runs[0].text = new
|
||
for r in p.runs[1:]:
|
||
r.text = ""
|
||
doc.save(out_docx)
|
||
return out_docx
|
||
|
||
|
||
def to_pdf(docx_path):
|
||
import subprocess, tempfile
|
||
with tempfile.TemporaryDirectory() as td:
|
||
r = subprocess.run(["soffice", "--headless", "--convert-to", "pdf", "--outdir", td, docx_path],
|
||
capture_output=True, timeout=90)
|
||
pdf = os.path.join(td, os.path.basename(docx_path).replace(".docx", ".pdf"))
|
||
if os.path.exists(pdf):
|
||
dst = docx_path.replace(".docx", ".pdf")
|
||
open(dst, "wb").write(open(pdf, "rb").read())
|
||
return dst
|
||
return None
|
||
|
||
|
||
def leftover_placeholders(docx_path):
|
||
import re
|
||
d = docx.Document(docx_path)
|
||
found = set()
|
||
for p in d.paragraphs:
|
||
found.update(re.findall(r"\[[A-Z][A-Z0-9 /.,%()\[\]-]*\]", p.text))
|
||
return sorted(found)
|
||
|
||
|
||
FOUNDERS = [
|
||
# ── t-261008143050: Siarhei Zhukovich, когорта с 16-го ──
|
||
dict(file="SPA_Siarhei_Zhukovich", name="Siarhei Zhukovich",
|
||
passport="AB3393306", pid=None, # ид. номер — нет в базе, плейсхолдер
|
||
address="28 July str., 33B-42, Brest", email=None,
|
||
project="podrabotki.by (rebranding to StaffGo)",
|
||
one_line="a platform for hiring line staff: employment/contract placement and turnkey contractor paperwork (documents and payments)",
|
||
rev_share=50, effective="October 16, 2026", cohort="October 2026",
|
||
program_start="16 October 2026", sixteenth=True),
|
||
# ── t-261008144131: участники 42 Labs за 2026-09 (lab_debts) ──
|
||
dict(file="SPA_Alex_Shkor", name="Alex Shkor", passport=None, pid=None,
|
||
address=None, email="alex@shkor.com", project=None, one_line=None,
|
||
rev_share=None, effective="September 1, 2026", cohort="September 2026",
|
||
program_start="1 September 2026", sixteenth=False),
|
||
dict(file="SPA_Vlad", name="Vlad", passport=None, pid=None,
|
||
address=None, email="vla2music.com@gmail.com", project=None, one_line=None,
|
||
rev_share=None, effective="September 1, 2026", cohort="September 2026",
|
||
program_start="1 September 2026", sixteenth=False),
|
||
dict(file="SPA_Yura", name="Yura", passport=None, pid=None,
|
||
address=None, email="yura28440@gmail.com", project=None, one_line=None,
|
||
rev_share=None, effective="September 1, 2026", cohort="September 2026",
|
||
program_start="1 September 2026", sixteenth=False),
|
||
dict(file="SPA_Stas_Dashchinskiy", name="Stas Dashchinskiy", passport=None, pid=None,
|
||
address=None, email="dashchinskiystas@gmail.com", project=None, one_line=None,
|
||
rev_share=None, effective="September 1, 2026", cohort="September 2026",
|
||
program_start="1 September 2026", sixteenth=False),
|
||
dict(file="SPA_Alexander", name="Александр", passport=None, pid=None,
|
||
address=None, email="Justgbox@gmail.com", project=None, one_line=None,
|
||
rev_share=None, effective="September 1, 2026", cohort="September 2026",
|
||
program_start="1 September 2026", sixteenth=False),
|
||
dict(file="SPA_Melnikau", name="Andrei Melnikau", passport="MP2848482",
|
||
pid="3120386A064PB1", address=None, email="ex3.mac@gmail.com",
|
||
project="tYESter",
|
||
one_line="a family of software tools for testing computer hardware components without the need to purchase additional equipment",
|
||
rev_share=None, effective="September 1, 2026", cohort="September 2026",
|
||
program_start="1 September 2026", sixteenth=False),
|
||
dict(file="SPA_Dmitry_Siasitski", name="Dmitry Siasitski", passport=None, pid=None,
|
||
address=None, email="sesitski.dmitry@gmail.com", project=None, one_line=None,
|
||
rev_share=None, effective="September 1, 2026", cohort="September 2026",
|
||
program_start="1 September 2026", sixteenth=False),
|
||
]
|
||
|
||
if __name__ == "__main__":
|
||
only = sys.argv[1:] or None
|
||
for f in FOUNDERS:
|
||
if only and f["file"] not in only:
|
||
continue
|
||
docx_path = os.path.join(OUT, f["file"] + ".docx")
|
||
fill(f, docx_path)
|
||
pdf = to_pdf(docx_path)
|
||
print(f"{f['file']}: docx={'OK' if os.path.exists(docx_path) else 'FAIL'} "
|
||
f"pdf={'OK' if pdf else 'FAIL'} leftover={leftover_placeholders(docx_path)}")
|