/
Paff
/
declarant
Обзор
Документация
Войти
/
Paff
/
declarant
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
florunner_parser.py
278 строк
11 KB
1pash1985-gif
Декларант - веб-версия для Timeweb Cloud
15 июл 2026, 09:36
15 июл 2026, 09:36
a6b08b8
Код
Авторство
О чём код?
# -*- coding: utf-8 -*- """ Парсер Florunner B.V. счётов-фактур (`Invoice - 6NNNNN.xlsx`). Извлекает: • debnr — код заказчика (пример: `ATROR`, `LEKOS`); в мастере используется как маркировка (столбец D). • boxes — точное число коробок из строки `Colli` футер-блока (Carrier / Incoterm / AWB / Colli / VolKG). • carrier — перевозчик (обычно `BESTFLORA`). • awb_nr — MAWB, если проставлен. • volume_kg — VolKG. • invoice_nr — номер счёта (`656237`). Формат листа (Florunner B.V.): ... R006: Florunner B.V. Debnr : ATROR 195381 R009: 1118 BG Schiphol Invoice : 656237 ... R027: 4 | x | 855Z | Emballage | 5.85 | 23.4 ← коробки по типам R028: 15| x | 856 | Emballage | 6.05 | 90.75 ... R055: Carrier | Incoterm | AWB nr | ... | Colli | VolKG R056: BESTFLORA| Free Carrier, Honselersdijk | ... | 19 | 341 """ from __future__ import annotations import re from dataclasses import dataclass from typing import Optional from openpyxl import load_workbook @dataclass class FlorunnerData: debnr: str = "" # → marking boxes: int = 0 # → total_boxes carrier: str = "" awb_nr: str = "" volume_kg: float = 0.0 invoice_nr: str = "" def parse_florunner_invoice(path: str) -> Optional[FlorunnerData]: """Прочитать Florunner-инвойс и вернуть FlorunnerData; None — если формат не тот.""" try: wb = load_workbook(path, data_only=True, read_only=True) except Exception: return None ws = wb[wb.sheetnames[0]] data = FlorunnerData() # Собираем все непустые ячейки в структуру R×C для гибкого поиска grid: list[list] = [] for row in ws.iter_rows(values_only=True): grid.append(list(row)) # === 1. Debnr — ищем ячейку строго равную "Debnr" (либо "Debnr.") === for r_idx, row in enumerate(grid): for c_idx, v in enumerate(row): if v is None: continue s = str(v).strip() if s.rstrip('.').lower() != 'debnr': continue # Debnr code — первая нестандартная строка вправо, длиной 3-10 симв, # обычно после ":" или сразу. Пропускаем сами ":" и числовые id (`195381`). candidates = [] for cc in range(c_idx + 1, min(c_idx + 15, len(row))): val = row[cc] if val is None: continue sval = str(val).strip() if sval in (':', '-'): continue candidates.append(sval) for cand in candidates: # текстовый код (не только цифры), 3-10 символов if re.fullmatch(r'[A-Z][A-Z0-9]{2,9}', cand.upper()): data.debnr = cand.upper() break if data.debnr: break if data.debnr: break # === 2. Invoice nr === for r_idx, row in enumerate(grid): for c_idx, v in enumerate(row): if v is None: continue if str(v).strip().lower() == 'invoice': for cc in range(c_idx + 1, min(c_idx + 15, len(row))): val = row[cc] if val is None or str(val).strip() == ':': continue m = re.fullmatch(r'\d{5,8}', str(val).strip()) if m: data.invoice_nr = m.group() break if data.invoice_nr: break if data.invoice_nr: break # === 3. Colli / VolKG / Carrier / AWB nr — футер === # Ищем строку-header, содержащую одновременно "Colli" и "VolKG" header_row = None col_carrier = col_incoterm = col_awb = col_colli = col_volkg = -1 for r_idx, row in enumerate(grid): header_map = {} for c_idx, v in enumerate(row): if v is None: continue s = str(v).strip().lower() if s == 'carrier': header_map['carrier'] = c_idx elif 'incoterm' in s: header_map['incoterm'] = c_idx elif s in ('awb nr', 'awb'): header_map['awb'] = c_idx elif s == 'colli': header_map['colli'] = c_idx elif s == 'volkg': header_map['volkg'] = c_idx if 'colli' in header_map and 'volkg' in header_map: header_row = r_idx col_carrier = header_map.get('carrier', -1) col_awb = header_map.get('awb', -1) col_colli = header_map.get('colli', -1) col_volkg = header_map.get('volkg', -1) break if header_row is not None and header_row + 1 < len(grid): vrow = grid[header_row + 1] def _get(col: int): return vrow[col] if 0 <= col < len(vrow) else None if col_carrier >= 0: c = _get(col_carrier) if c is not None: data.carrier = str(c).strip() if col_awb >= 0: c = _get(col_awb) if c is not None and str(c).strip(): data.awb_nr = str(c).strip() if col_colli >= 0: c = _get(col_colli) try: data.boxes = int(float(c)) except (TypeError, ValueError): pass if col_volkg >= 0: c = _get(col_volkg) try: data.volume_kg = float(c) except (TypeError, ValueError): pass # === Fallback: если Colli не нашли — сумма Emballage-строк === if data.boxes == 0: for row in grid: has_emb = any(v is not None and 'emballage' in str(v).lower() for v in row) if not has_emb: continue # первое числовое в строке — Quantity for v in row: if v is None: continue try: q = int(float(v)) if 0 < q < 1000: data.boxes += q break except (TypeError, ValueError): continue return data if (data.debnr or data.boxes) else None # ============================================================================ # BloomX (Factuur_*.xlsx) — второй формат от OzExport-поставщиков # ============================================================================ @dataclass class BloomXData: order_ref: str = "" # → marking (пример: `MUSTANG`) debnr: str = "" # цифровой код (`001640`) consignee_city: str = "" # для проверки, что рейс наш boxes: int = 0 invoice_nr: str = "" def parse_bloomx_invoice(path: str) -> Optional[BloomXData]: """Прочитать BloomX (Factuur_*.xlsx). Возвращает BloomXData или None.""" try: wb = load_workbook(path, data_only=True, read_only=True) except Exception: return None ws = wb[wb.sheetnames[0]] grid = [list(r) for r in ws.iter_rows(values_only=True)] data = BloomXData() def _label_val(label_regex: str, max_r: int = 20) -> str: """Найти ячейку с текстом label_regex и вернуть значение из той же строки.""" pat = re.compile(label_regex, re.IGNORECASE) for r in grid[:max_r]: for c_idx, v in enumerate(r): if v is None: continue if not pat.fullmatch(str(v).strip().rstrip('.:').strip()): continue # значение — следующая непустая ячейка, минуя ":" for cc in range(c_idx + 1, min(c_idx + 10, len(r))): val = r[cc] if val is None: continue sval = str(val).strip().lstrip(':').strip() if sval: return sval return "" order_raw = _label_val(r'Order\s*ref') if order_raw: # "MUSTANG" или "MUSTRAN 3-7-2026 MUSTANG" m = re.search(r'([A-Z][A-Z0-9]{2,})\s*$', order_raw.upper()) data.order_ref = m.group(1) if m else order_raw.upper() data.debnr = _label_val(r'Debnr') data.invoice_nr = _label_val(r'Factuurnummer') # Consignee city — ищем в шапке (обычно cols 5-8) блок с "<индекс> <город>\n<страна>" # проще: собрать все текстовые ячейки первых 12 строк, найти паттерн `NNNNN CITY` for r in grid[:15]: for v in r: if v is None: continue s = str(v) m = re.search(r'\b(\d{4,6})\s+([A-Z][A-Z\s]{2,})\b', s.upper()) if m and not data.consignee_city: data.consignee_city = m.group(2).strip() break if data.consignee_city: break # Boxes — попробуем найти "Aant" колонку в строке-заголовке и суммировать # столбец. Или сумму "Doosnummers" (числа в колонке A body). # Простая эвристика: последний уникальный doos-nr = число боксов. doos_max = 0 for r in grid: v0 = r[0] if len(r) > 0 else None if v0 is None: continue try: n = int(float(v0)) if 0 < n < 500: doos_max = max(doos_max, n) except (TypeError, ValueError): continue data.boxes = doos_max return data if (data.order_ref or data.debnr or data.boxes) else None if __name__ == '__main__': import sys, os try: sys.stdout.reconfigure(encoding='utf-8') except Exception: pass folder = sys.argv[1] if len(sys.argv) > 1 else \ r'c:\Users\0\Desktop\Задача по логистике\03.07.2026\MTL 700\Инвойсы\03-MTL700\03-MTL700' for f in sorted(os.listdir(folder)): p = os.path.join(folder, f) if 'Invoice - 6' in f: d = parse_florunner_invoice(p) print(f'FLR {f:35s} → {d}') elif f.startswith('Factuur'): d = parse_bloomx_invoice(p) print(f'BLX {f:35s} → {d}')