#!/usr/bin/env python3
"""Генерация шаблона накладной для поставщиков (handoff §7).
Создаёт docs/supplier-invoice-template.xlsx: 9-строчная шапка (Поставщик /
Номер / Дата / пустые строки с подсказкой «← заполните» ) и таблица товаров
на 9-й строке (заголовок) с колонками Штрихкод | Наименование товара |
Количество | Ед. изм. | Цена за единицу | Сумма.
TableInvoiceParser (Infrastructure/Invoices/TableReader.cs) ищет первую
строку, где есть заголовки «Штрихкод» и «Количество» — поэтому порядок
колонок свободный, но сами слова нужны именно эти или похожие.
"""
import io
import os
import zipfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "docs" / "supplier-invoice-template.xlsx"
# ─── XML parts ───────────────────────────────────────────────────────
CONTENT_TYPES = """
"""
RELS = """
"""
WB_RELS = """
"""
WORKBOOK = """
"""
STYLES = """
"""
# Содержимое шаблона: 8 строк шапки + заголовок таблицы на 9-й строке.
ROWS = [
# row 1 — название
["Накладная поставщика", ""],
[],
["Поставщик", "← ТОО/ИП «...»"],
["Номер накладной", "← например, 1234"],
["Дата", "← ГГГГ-ММ-ДД"],
[],
["", "Заполните эту таблицу и пришлите файл: система создаст приёмку автоматически."],
[],
# row 9 — заголовок таблицы, который ищет TableInvoiceParser
["Штрихкод", "Наименование товара", "Количество", "Ед. изм.", "Цена за единицу", "Сумма"],
# примеры (можно удалить при реальном заполнении)
["4870123456789", "Молоко 3.2%, 1 л", 10, "шт", 450, 4500],
["4870987654321", "Хлеб серый, 400 г", 20, "шт", 180, 3600],
]
def _xl_col(n: int) -> str:
s = ""
while n >= 0:
s = chr(ord("A") + n % 26) + s
n = n // 26 - 1
return s
def _escape(s: str) -> str:
return (s.replace("&", "&").replace("<", "<").replace(">", ">")
.replace('"', """))
def build_shared_strings(rows):
"""Собрать уникальные строки, вернуть (xml, index-map)."""
seen = {}
order = []
for row in rows:
for c in row:
if isinstance(c, str) and c:
if c not in seen:
seen[c] = len(order); order.append(c)
body = "".join(f"{_escape(s)}" for s in order)
xml = (
'\n'
f''
f'{body}'
)
return xml, seen
def build_sheet(rows, strings_idx):
out = io.StringIO()
out.write('\n')
out.write('')
out.write('')
out.write('')
out.write('')
out.write('')
out.write('')
out.write('')
out.write('')
out.write('')
out.write('')
for r, row in enumerate(rows, start=1):
if not row:
out.write(f'
')
continue
is_header_row = (r == 1 or r == 9)
out.write(f'')
for c_idx, val in enumerate(row):
ref = f"{_xl_col(c_idx)}{r}"
style = ' s="1"' if is_header_row else ''
if isinstance(val, (int, float)):
out.write(f'{val}')
elif isinstance(val, str) and val:
out.write(f'{strings_idx[val]}')
else:
out.write(f'')
out.write('
')
out.write('')
out.write('')
return out.getvalue()
def main() -> None:
ss_xml, idx = build_shared_strings(ROWS)
sheet_xml = build_sheet(ROWS, idx)
OUT.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(OUT, "w", zipfile.ZIP_DEFLATED) as z:
z.writestr("[Content_Types].xml", CONTENT_TYPES)
z.writestr("_rels/.rels", RELS)
z.writestr("xl/_rels/workbook.xml.rels", WB_RELS)
z.writestr("xl/workbook.xml", WORKBOOK)
z.writestr("xl/styles.xml", STYLES)
z.writestr("xl/sharedStrings.xml", ss_xml)
z.writestr("xl/worksheets/sheet1.xml", sheet_xml)
print(f"wrote {OUT} ({OUT.stat().st_size} bytes)")
if __name__ == "__main__":
main()