#!/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()