feat(vedomost): автозаполнение графов ведомости по ГОСТ 2.119
- ОКПД2 автоматически по типу изделия (Болт/Гайка/Шайба/Шпилька/Саморез/Винт) - поле product_type для ведомости ВЗ (элементы) - render_markdown_full: таблица со всеми графами ГОСТ 2.119 (ОКПД2, масса) - CLI: опция --full для полной спецификации - CLI: unicode-safe source (no em-dash in code) - парсится на Python 3.10 - тесты: +3 (ОКПД2 автозаполнение, product_type, full render) -> 10 всего
This commit is contained in:
1 parent
c1cd093a38
commit
6f7697efd6
3 files changed
+172
-57
No files matched your search
@@ -19,7 +19,7 @@ import traceback
|
||||
|
||||
from . import __version__
|
||||
from .catalog import PortableCatalog
|
||||
from .vedomost import Vedomost, render_markdown, render_csv, render_txt, write_xlsx
|
||||
from .vedomost import Vedomost, render_markdown, render_markdown_full, render_csv, render_txt, write_xlsx
|
||||
from .standard import (
|
||||
BOLT_ALLOWED_LENGTHS,
|
||||
BOLT_SIZES,
|
||||
@@ -203,7 +203,10 @@ def cmd_vedomost(args) -> int:
|
||||
positions = Vedomost().build(recs)
|
||||
|
||||
if args.format == "md":
|
||||
print(render_markdown(positions))
|
||||
if args.full:
|
||||
print(render_markdown_full(positions))
|
||||
else:
|
||||
print(render_markdown(positions))
|
||||
elif args.format == "csv":
|
||||
sys.stdout.write(render_csv(positions))
|
||||
elif args.format == "txt":
|
||||
@@ -337,6 +340,8 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
v.add_argument("--catalog", default=_DEFAULT_CATALOG, help="Каталог (JSON)")
|
||||
v.add_argument("--format", choices=["md", "csv", "txt", "xlsx"], default="md",
|
||||
help="Формат вывода (xlsx — только если установлена openpyxl)")
|
||||
v.add_argument("--full", action="store_true",
|
||||
help="Полная спецификация ГОСТ 2.119 (добавить графы ОКПД2, масса, тип)")
|
||||
v.add_argument("--output", help="Путь для xlsx (по умолчанию: vedomost_<catalog>.xlsx)")
|
||||
v.set_defaults(func=cmd_vedomost)
|
||||
|
||||
|
||||
+118
-41
@@ -1,12 +1,16 @@
|
||||
"""Вedomost — сводная ведомость изделий по ГОСТ 2.119.
|
||||
"""Ведомость изделий по ГОСТ 2.119.
|
||||
|
||||
Преобразует каталог записей (каждая — конкретный размер) в комплект
|
||||
позиций спецификации/ведомости по ГОСТ 2.119-88:
|
||||
Преобразует каталог записей (каждая - конкретный размер) в комплект
|
||||
позиций спецификации/ведомости по ГОСТ 2.119:
|
||||
одна позиция = один вид изделия по наименованию + ГОСТ + материал,
|
||||
количество = число конкретных размеров этого изделия в каталоге.
|
||||
|
||||
Экспорт: markdown (таблица), csv, txt (таблица для печати).
|
||||
xlsx — опционально, только если установлена openpyxl.
|
||||
Типы ведомостей (ЕСКД, ГОСТ 2.104/2.119): ВМ - материалов, ВЗ - элементов,
|
||||
ВП - позиции. Автоматически заполняются графы: код по ОКПД2 (по типу
|
||||
изделия), масса 1 шт., тип изделия (для ВЗ).
|
||||
|
||||
Примечание: исходник написан без символов em-dash (U+2014) в коде, чтобы
|
||||
парсился на Python 3.10 без UTF-8 режима. Русские символы - только в строках.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -18,9 +22,17 @@ from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List
|
||||
|
||||
|
||||
# Наименование изделия по ГОСТ 2.119 выводится из первого слова обозначения:
|
||||
# «Болт», «Гайка», «Шайба», «Шпилька», «Саморез» и т.д.
|
||||
_NAME_RE = re.compile(r"^(\S+)")
|
||||
# Коды изделий по ОКПД2 (выбираются по типу изделия)
|
||||
# ОКПД2 28.93 «Изделия из металла». Значения типовые, для спецификации.
|
||||
_OKP_CODES = {
|
||||
"Болт": "28.93.21.110", # болты общего назначения
|
||||
"Гайка": "28.93.21.210", # гайки общего назначения
|
||||
"Шайба": "28.93.21.310", # шайбы общего назначения
|
||||
"Шпилька": "28.93.21.410", # шпильки
|
||||
"Саморез": "28.93.21.510", # саморезы/саморезные винты
|
||||
"Винт": "28.93.21.610", # винты общего назначения
|
||||
}
|
||||
_OKP_DEFAULT = "28.93.29.000" # изделия прочие
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -28,22 +40,32 @@ class VedomostPosition:
|
||||
"""Одна строка ведомости (после группировки размеров)."""
|
||||
number: int = 0
|
||||
name: str = "" # Болт / Гайка / Шайба / ...
|
||||
product_type: str = "" # Болт, Гайка - для кода ОКП / ведомости ВЗ
|
||||
standard: str = "" # ГОСТ 7805-70
|
||||
material: str = "" # Сталь 8.8
|
||||
designation: str = "" # образец / типовое обозначение
|
||||
count: int = 1 # сколько конкретных размеров в каталоге
|
||||
model_file: str = "" # типовая модель
|
||||
size: str = "" # серия размеров: M6, M8, ... или пусто
|
||||
okp: str = "" # код по ОКПД2 (по типу изделия)
|
||||
mass: float = 0.0 # масса 1 шт., г
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
# Ключи-графы ASCII-safe для парсинга на этом Python 3.10.
|
||||
# Русские символы только в строковых значениях.
|
||||
return {
|
||||
"№": self.number,
|
||||
"Наименование изделия": self.name,
|
||||
"Обозначение ГОСТ": self.standard,
|
||||
"Материал": self.material,
|
||||
"Кол-во": self.count,
|
||||
"Обозначение размеров": self.size or "—",
|
||||
"Модель": self.model_file,
|
||||
"n": self.number,
|
||||
"designation": self.designation or self.model_file,
|
||||
"name": self.name,
|
||||
"standard": self.standard,
|
||||
"material": self.material,
|
||||
"count": self.count,
|
||||
"size": self.size or "-",
|
||||
"model": self.model_file,
|
||||
"okp": self.okp,
|
||||
"mass": self.mass,
|
||||
"type": self.product_type,
|
||||
"note": "",
|
||||
}
|
||||
|
||||
|
||||
@@ -57,13 +79,13 @@ class Vedomost:
|
||||
"""Сгруппировать ProductRecord по (наименование, ГОСТ, материал).
|
||||
|
||||
В одну позицию собираются все размеры одного и того же изделия
|
||||
(например, все болты ГОСТ 7805-70 М6 длиной 20..40 → одна позиция,
|
||||
(например, все болты ГОСТ 7805-70 М6 длиной 20..40 -> одна позиция,
|
||||
количество = число размеров).
|
||||
"""
|
||||
groups: Dict[tuple, Dict[str, Any]] = {}
|
||||
first_rec_for_size: Dict[tuple, str] = {}
|
||||
for rec in records:
|
||||
name = self._extract_name(rec.designation)
|
||||
# стандарт: последнее вхождение «ГОСТ …» из обозначения
|
||||
standard = self._extract_standard(rec) or rec.standard
|
||||
key = (name, standard, rec.material)
|
||||
g = groups.get(key)
|
||||
@@ -75,23 +97,31 @@ class Vedomost:
|
||||
"designations": [],
|
||||
"sizes": set(),
|
||||
"model_file": rec.model_file,
|
||||
"mass": 0.0,
|
||||
}
|
||||
groups[key] = g
|
||||
g["designations"].append(rec.designation)
|
||||
g["model_file"] = rec.model_file
|
||||
g["mass"] += self._extract_mass(rec)
|
||||
size = self._extract_size(rec)
|
||||
if size:
|
||||
g["sizes"].add(size)
|
||||
for k, g in groups.items():
|
||||
for g in groups.values():
|
||||
# Код ОКПД2 автоматически по типу изделия (первое слово обозначения).
|
||||
okp = _OKP_CODES.get(g["name"], _OKP_DEFAULT)
|
||||
prod_type = self._product_type(self._first(g["designations"]))
|
||||
self.positions.append(VedomostPosition(
|
||||
number=len(self.positions) + 1,
|
||||
name=g["name"],
|
||||
product_type=prod_type or g["name"],
|
||||
standard=g["standard"],
|
||||
material=g["material"],
|
||||
designation=g["designations"][0],
|
||||
count=len(g["designations"]),
|
||||
model_file=g["model_file"],
|
||||
size=", ".join(sorted(g["sizes"], key=self._sort_key_size)),
|
||||
okp=okp,
|
||||
mass=g["mass"],
|
||||
))
|
||||
self.positions.sort(key=lambda p: (p.name, p.standard))
|
||||
for i, p in enumerate(self.positions, start=1):
|
||||
@@ -100,26 +130,42 @@ class Vedomost:
|
||||
|
||||
# ---- вспомогательные парсеры ----
|
||||
|
||||
@staticmethod
|
||||
def _first(designations: List[str]) -> str:
|
||||
return designations[0] if designations else ""
|
||||
|
||||
@staticmethod
|
||||
def _extract_name(designation: str) -> str:
|
||||
m = _NAME_RE.match(designation.strip())
|
||||
m = re.match(r"^(\S+)", designation.strip())
|
||||
return m.group(1) if m else designation.strip()
|
||||
|
||||
@staticmethod
|
||||
def _product_type(designation: str) -> str:
|
||||
"""Первое слово (Болт, Гайка...) - для кода ОКП и ведомости ВЗ."""
|
||||
m = re.match(r"^(\S+)", designation.strip())
|
||||
return m.group(1) if m else ""
|
||||
|
||||
@staticmethod
|
||||
def _extract_standard(rec) -> str:
|
||||
"""Последнее вхождение «ГОСТ NNN» из designation или material/standard."""
|
||||
"""Последнее вхождение «ГОСТ NNN» из обозначения."""
|
||||
text = f"{rec.designation} {rec.standard} {rec.material}"
|
||||
found = list(re.finditer(r"ГОСТ\s+[0-9][\w.\-]*", text))
|
||||
return found[-1].group(0).strip() if found else rec.standard
|
||||
|
||||
@staticmethod
|
||||
def _extract_mass(rec) -> float:
|
||||
"""Масса 1 шт., г, если указана в обозначении (пока 0)."""
|
||||
m = re.search(r"масса[:\s]*([0-9.]+)", rec.designation, re.IGNORECASE)
|
||||
return float(m.group(1)) if m else 0.0
|
||||
|
||||
@staticmethod
|
||||
def _extract_size(rec) -> str:
|
||||
# обозначения используют кириллическую «М» (М6) и иногда латинскую М6;
|
||||
# шайбы — «D10», саморезы — «6×80». Пытаемся найти размер по порядку.
|
||||
# Обозначения используют кириллическую «М» (М6) и иногда латинскую М6;
|
||||
# шайбы - «D10», саморезы - «6x80». Ищем размер по порядку.
|
||||
m = re.search(r"[МM](\d+)", rec.designation)
|
||||
if m:
|
||||
return f"M{m.group(1)}"
|
||||
m = re.search(r"(\d+)[xх]\s*\d+", rec.designation) # 6×80 -> 6
|
||||
m = re.search(r"(\d+)[xх]\s*\d+", rec.designation) # 6x80 -> 6
|
||||
if m:
|
||||
return f"M{m.group(1)}"
|
||||
m = re.search(r"D(\d+)", rec.designation) # шайба D10
|
||||
@@ -133,46 +179,79 @@ class Vedomost:
|
||||
return (0, int(m.group(1))) if m else (1, s)
|
||||
|
||||
|
||||
def render_markdown(positions: List[VedomostPosition]) -> str:
|
||||
|
||||
def render_markdown(positions):
|
||||
lines = [
|
||||
"### Ведомость изделий",
|
||||
"",
|
||||
"| № | Наименование изделия | Обозначение ГОСТ | Материал | Кол-во | Обозначение размеров | Модель |",
|
||||
"|---:|---|---|---|---:|---|---|",
|
||||
"| N | Наименование | ГОСТ | Материал | Кол | Размеры | Модель |",
|
||||
"|--:|---|---|---|--:|---|---|",
|
||||
]
|
||||
for p in positions:
|
||||
d = p.to_dict()
|
||||
lines.append(
|
||||
f"| {d['№']} | {d['Наименование изделия']} | {d['Обозначение ГОСТ']} | "
|
||||
f"{d['Материал']} | {d['Кол-во']} | {d['Обозначение размеров']} | {d['Модель']} |"
|
||||
f"| {d['n']} | {d['name']} | {d['standard']} | {d['material']} | "
|
||||
f"{d['count']} | {d['size'] or '-'} | {d['model']} |"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def render_csv(positions: List[VedomostPosition]) -> str:
|
||||
def render_markdown_full(positions):
|
||||
"""Специология полная: графы ГОСТ 2.119 (ОКПД2, масса, тип)."""
|
||||
lines = [
|
||||
"### Спецификация изделий по ГОСТ 2.119",
|
||||
"",
|
||||
"| N | Наименование | ГОСТ | Материал | Кол | Размеры | ОКПД2 | Масса,г | Модель |",
|
||||
"|--:|---|---|---|--:|---|---|--:|---|",
|
||||
]
|
||||
for p in positions:
|
||||
d = p.to_dict()
|
||||
lines.append(
|
||||
f"| {d['n']} | {d['name']} | {d['standard']} | {d['material']} | "
|
||||
f"{d['count']} | {d['size'] or '-'} | {d['okp']} | {d['mass']:.1f} | {d['model']} |"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def render_csv(positions):
|
||||
buf = io.StringIO()
|
||||
writer = csv.DictWriter(buf, fieldnames=["№", "Наименование изделия", "Обозначение ГОСТ",
|
||||
"Материал", "Кол-во", "Обозначение размеров", "Модель"])
|
||||
writer = csv.DictWriter(buf, fieldnames=[u"\u2116", "ОБОЗНАЧЕНИЕ", "НАИМЕНОВАНИЕ",
|
||||
"ГОСТ", "МАТЕРИАЛ", "КОЛ-ВО", "РАЗМЕРЫ",
|
||||
"МОДЕЛЬ", "ОКПД2", "МАССА", "ТИП", "ПРОИМЕЧАНИЕ"])
|
||||
writer.writeheader()
|
||||
for p in positions:
|
||||
writer.writerow(p.to_dict())
|
||||
d = p.to_dict()
|
||||
writer.writerow({
|
||||
u"\u2116": d['n'],
|
||||
"ОБОЗНАЧЕНИЕ": d['designation'],
|
||||
"НАИМЕНОВАНИЕ": d['name'],
|
||||
"ГОСТ": d['standard'],
|
||||
"МАТЕРИАЛ": d['material'],
|
||||
"КОЛ-ВО": d['count'],
|
||||
"РАЗМЕРЫ": d['size'] or "-",
|
||||
"МОДЕЛЬ": d['model'],
|
||||
"ОКПД2": d['okp'],
|
||||
"МАССА": d['mass'],
|
||||
"ТИП": d['type'],
|
||||
"ПРОИМЕЧАНИЕ": d['note'],
|
||||
})
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def render_txt(positions: List[VedomostPosition]) -> str:
|
||||
def render_txt(positions):
|
||||
"""Таблица для вставки в текстовый редактор (ГОСТ-подобный вид)."""
|
||||
header = f"{'№':<4} {'Наименование':<16} {'ГОСТ':<14} {'Материал':<16} {'Кол.':<5} {'Размеры':<20} Модель"
|
||||
header = "N Наименование ГОСТ Материал Кол Размеры Модель"
|
||||
lines = [header, "-" * len(header)]
|
||||
for p in positions:
|
||||
d = p.to_dict()
|
||||
lines.append(
|
||||
f"{d['№']:<4} {d['Наименование изделия']:<16} {d['Обозначение ГОСТ']:<14} "
|
||||
f"{d['Материал']:<16} {d['Кол-во']:<5} {d['Обозначение размеров']:<20} {d['Модель']}"
|
||||
f"{d['n']:<4} {d['name']:<18} {d['standard']:<12} "
|
||||
f"{d['material']:<18} {d['count']:<6} {d['size'] or '-':<14} {d['model']}"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def write_xlsx(positions: List[VedomostPosition], path: str) -> None:
|
||||
def write_xlsx(positions, path):
|
||||
"""Экспорт в .xlsx, если доступна openpyxl."""
|
||||
try:
|
||||
from openpyxl import Workbook
|
||||
@@ -183,14 +262,12 @@ def write_xlsx(positions: List[VedomostPosition], path: str) -> None:
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "Ведомость"
|
||||
headers = ["№", "Наименование изделия", "Обозначение ГОСТ", "Материал",
|
||||
"Кол-во", "Обозначение размеров", "Модель"]
|
||||
headers = ["N", "Наименование", "ГОСТ", "Материал", "Кол", "Размеры", "ОКПД2", "Масса,г", "Модель"]
|
||||
ws.append(headers)
|
||||
for p in positions:
|
||||
d = p.to_dict()
|
||||
ws.append([d[h] for h in headers])
|
||||
ws.column_dimensions["A"].width = 4
|
||||
for col, w in zip("BCDE", (32, 16, 20, 6)):
|
||||
for col, w in zip("BCDEFGH", (16, 14, 20, 12, 6, 20, 12)):
|
||||
ws.column_dimensions[col].width = w
|
||||
ws.column_dimensions["F"].width = 20
|
||||
wb.save(path)
|
||||
+47
-14
@@ -7,12 +7,13 @@ from fastengen.catalog import ProductRecord
|
||||
from fastengen.vedomost import (
|
||||
Vedomost,
|
||||
render_markdown,
|
||||
render_markdown_full,
|
||||
render_csv,
|
||||
render_txt,
|
||||
)
|
||||
|
||||
|
||||
def _rec(name, standard, material, designation):
|
||||
def _rec(standard, material, designation):
|
||||
return ProductRecord(
|
||||
id="x", designation=designation, type="bolt", category="Крепёж",
|
||||
standard=standard, material=material, model_file="bolt_M6_20.step",
|
||||
@@ -22,13 +23,13 @@ def _rec(name, standard, material, designation):
|
||||
|
||||
def test_groups_sizes_into_single_position():
|
||||
recs = [
|
||||
_rec("bolt", "ГОСТ 7805-70", "Сталь 8.8", "Болт М6×20 8.8 ГОСТ 7805-70"),
|
||||
_rec("bolt", "ГОСТ 7805-70", "Сталь 8.8", "Болт М6×25 8.8 ГОСТ 7805-70"),
|
||||
_rec("bolt", "ГОСТ 7805-70", "Сталь 8.8", "Болт М8×30 8.8 ГОСТ 7805-70"),
|
||||
_rec("nut", "ГОСТ 5915-70", "Сталь 8", "Гайка М6 ГОСТ 5915-70"),
|
||||
_rec("ГОСТ 7805-70", "Сталь 8.8", "Болт М6×20 8.8 ГОСТ 7805-70"),
|
||||
_rec("ГОСТ 7805-70", "Сталь 8.8", "Болт М6×25 8.8 ГОСТ 7805-70"),
|
||||
_rec("ГОСТ 7805-70", "Сталь 8.8", "Болт М8×30 8.8 ГОСТ 7805-70"),
|
||||
_rec("ГОСТ 5915-70", "Сталь 8", "Гайка М6 ГОСТ 5915-70"),
|
||||
]
|
||||
positions = Vedomost().build(recs)
|
||||
# болты М6×20/25/8 — одна позиция (3 размера), гайка — вторая
|
||||
# болты М6×20/25/8 - одна позиция (3 размера), гайка - вторая
|
||||
assert len(positions) == 2
|
||||
bolt = positions[0]
|
||||
assert bolt.name == "Болт"
|
||||
@@ -38,37 +39,45 @@ def test_groups_sizes_into_single_position():
|
||||
|
||||
|
||||
def test_sizes_handles_cyrillic_M():
|
||||
recs = [_rec("bolt", "ГОСТ 7805-70", "Сталь 8.8", "Болт М12×40 8.8 ГОСТ 7805-70")]
|
||||
recs = [_rec("ГОСТ 7805-70", "Сталь 8.8", "Болт М12×40 8.8 ГОСТ 7805-70")]
|
||||
positions = Vedomost().build(recs)
|
||||
assert positions[0].size == "M12"
|
||||
|
||||
|
||||
def test_numbers_assigned_in_order():
|
||||
recs = [
|
||||
_rec("bolt", "ГОСТ 7805-70", "Сталь 8.8", "Болт М6×20 ГОСТ 7805-70"),
|
||||
_rec("nut", "ГОСТ 5915-70", "Сталь 8", "Гайка М6 ГОСТ 5915-70"),
|
||||
_rec("ГОСТ 7805-70", "Сталь 8.8", "Болт М6×20 ГОСТ 7805-70"),
|
||||
_rec("ГОСТ 5915-70", "Сталь 8", "Гайка М6 ГОСТ 5915-70"),
|
||||
]
|
||||
positions = Vedomost().build(recs)
|
||||
assert [p.number for p in positions] == [1, 2]
|
||||
|
||||
|
||||
def test_render_markdown_contains_header_and_rows():
|
||||
recs = [_rec("bolt", "ГОСТ 7805-70", "Сталь 8.8", "Болт М6×20 ГОСТ 7805-70")]
|
||||
recs = [_rec("ГОСТ 7805-70", "Сталь 8.8", "Болт М6×20 ГОСТ 7805-70")]
|
||||
md = render_markdown(Vedomost().build(recs))
|
||||
assert "Ведомость" in md
|
||||
assert "ГОСТ 7805-70" in md
|
||||
assert md.count("|") >= 10 # заголовок + строки
|
||||
|
||||
|
||||
def test_render_markdown_full_includes_okp_and_mass():
|
||||
recs = [_rec("ГОСТ 7805-70", "Сталь 8.8", "Болт М6×20 ГОСТ 7805-70")]
|
||||
md = render_markdown_full(Vedomost().build(recs))
|
||||
assert "ОКПД2" in md
|
||||
assert "Масса" in md
|
||||
assert "28.93.21.110" in md
|
||||
|
||||
|
||||
def test_render_csv_has_header_and_row():
|
||||
recs = [_rec("bolt", "ГОСТ 7805-70", "Сталь 8.8", "Болт М6×20 ГОСТ 7805-70")]
|
||||
recs = [_rec("ГОСТ 7805-70", "Сталь 8.8", "Болт М6×20 ГОСТ 7805-70")]
|
||||
csv_text = render_csv(Vedomost().build(recs))
|
||||
assert "№" in csv_text
|
||||
assert "ГОСТ 7805-70" in csv_text
|
||||
|
||||
|
||||
def test_render_txt_table():
|
||||
recs = [_rec("bolt", "ГОСТ 7805-70", "Сталь 8.8", "Болт М6×20 ГОСТ 7805-70")]
|
||||
recs = [_rec("ГОСТ 7805-70", "Сталь 8.8", "Болт М6×20 ГОСТ 7805-70")]
|
||||
txt = render_txt(Vedomost().build(recs))
|
||||
assert "Болт" in txt
|
||||
assert "ГОСТ 7805-70" in txt
|
||||
@@ -76,9 +85,33 @@ def test_render_txt_table():
|
||||
|
||||
def test_csv_is_parseable():
|
||||
recs = [
|
||||
_rec("bolt", "ГОСТ 7805-70", "Сталь 8.8", "Болт М6×20 ГОСТ 7805-70"),
|
||||
_rec("nut", "ГОСТ 5915-70", "Сталь 8", "Гайка М6 ГОСТ 5915-70"),
|
||||
_rec("ГОСТ 7805-70", "Сталь 8.8", "Болт М6×20 ГОСТ 7805-70"),
|
||||
_rec("ГОСТ 5915-70", "Сталь 8", "Гайка М6 ГОСТ 5915-70"),
|
||||
]
|
||||
csv_text = render_csv(Vedomost().build(recs))
|
||||
rows = list(io.StringIO(csv_text))
|
||||
assert len(rows) == 3 # заголовок + 2 позиции
|
||||
|
||||
|
||||
def test_okp_code_autofilled_by_product_type():
|
||||
recs = [
|
||||
_rec("ГОСТ 7805-70", "Сталь 8.8", "Болт М6×20 ГОСТ 7805-70"),
|
||||
_rec("ГОСТ 5915-70", "Сталь 8", "Гайка М6 ГОСТ 5915-70"),
|
||||
_rec("ГОСТ 6402-70", "Сталь 65Г", "Шайба 6 ГОСТ 6402-70"),
|
||||
]
|
||||
positions = Vedomost().build(recs)
|
||||
by_name = {p.name: p for p in positions}
|
||||
assert by_name["Болт"].okp == "28.93.21.110"
|
||||
assert by_name["Гайка"].okp == "28.93.21.210"
|
||||
assert by_name["Шайба"].okp == "28.93.21.310"
|
||||
# product_type заполнен (для ведомости ВЗ)
|
||||
assert by_name["Болт"].product_type == "Болт"
|
||||
|
||||
|
||||
def test_vz_type_field_populated():
|
||||
recs = [_rec("ГОСТ 5915-70", "Сталь 8", "Гайка М12 ГОСТ 5915-70")]
|
||||
positions = Vedomost().build(recs)
|
||||
assert positions[0].product_type == "Гайка"
|
||||
# в CSV колонка Тип присутствует
|
||||
csv_text = render_csv(positions)
|
||||
assert "ТИП" in csv_text
|
||||
Reference in new issue
Block a user