fixed complex strings

This commit is contained in:
2026-09-02 14:35:30 +03:00
parent b7a1609a39
commit f132e83288
8 changed files with 636 additions and 22 deletions
+45 -1
View File
@@ -23,11 +23,20 @@ from app.schemas import (
ProjectUpdate,
TableRowsPatch,
TableRowsResponse,
TableSettingsUpdate,
)
from app.services.excel_export import export_xlsx
from app.services.llm import chat_edit_table
from app.services.pdf_export import export_pdf
from app.services.project_service import get_project_full, ingest_zip, project_dir
from app.services.project_settings import (
TABLE_SETTINGS_META,
get_property_names,
get_project_params_map,
get_table_settings,
resolve_inscriptions,
update_table_settings,
)
from app.services.table_service import (
TABLE_TYPES,
apply_llm_edits,
@@ -204,6 +213,39 @@ def get_pcb(project_id: int, db: Session = Depends(get_db)):
}
@secured.get("/projects/{project_id}/property-names")
def read_property_names(project_id: int, db: Session = Depends(get_db)):
_get_project(db, project_id)
return {"property_names": get_property_names(db, project_id)}
@secured.get("/projects/{project_id}/table-settings/{table_type}")
def read_table_settings(project_id: int, table_type: str, db: Session = Depends(get_db)):
_get_project(db, project_id)
if table_type not in TABLE_TYPES:
raise HTTPException(400, f"Unknown table type. Use one of: {TABLE_TYPES}")
return get_table_settings(db, project_id, table_type)
@secured.patch("/projects/{project_id}/table-settings/{table_type}")
def patch_table_settings(
project_id: int,
table_type: str,
body: TableSettingsUpdate,
db: Session = Depends(get_db),
):
_get_project(db, project_id)
if table_type not in TABLE_TYPES:
raise HTTPException(400, f"Unknown table type")
return update_table_settings(db, project_id, table_type, body.model_dump(exclude_unset=True))
@secured.get("/projects/{project_id}/table-settings-meta")
def read_table_settings_meta(project_id: int, db: Session = Depends(get_db)):
_get_project(db, project_id)
return TABLE_SETTINGS_META
@secured.get("/projects/{project_id}/inscriptions")
def read_inscriptions(project_id: int, db: Session = Depends(get_db)):
_get_project(db, project_id)
@@ -272,6 +314,8 @@ def export_document(project_id: int, body: ExportRequest, db: Session = Depends(
if not rows:
raise HTTPException(400, "Table is empty. Generate it first.")
inscriptions = get_inscriptions(db, project_id)
params = get_project_params_map(db, project_id)
resolved_inscriptions = resolve_inscriptions(inscriptions, params)
if body.format == "xlsx":
data = export_xlsx(body.table_type, rows, title=project.name)
@@ -284,7 +328,7 @@ def export_document(project_id: int, body: ExportRequest, db: Session = Depends(
)
if body.format == "pdf":
try:
data = export_pdf(body.table_type, rows, inscriptions)
data = export_pdf(body.table_type, rows, resolved_inscriptions)
except ValueError as e:
raise HTTPException(400, str(e)) from e
return Response(
+6
View File
@@ -191,3 +191,9 @@ class DesignatorMappingOut(BaseModel):
prefix: str
singular_name: str
plural_name: str
class TableSettingsUpdate(BaseModel):
mappings: Optional[list[str]] = None
column_mappings: Optional[dict[str, str]] = None
simple_list: Optional[dict[str, Any]] = None
@@ -0,0 +1,310 @@
"""Per-project document settings (column mappings, param references)."""
from __future__ import annotations
import json
from typing import Any
from sqlalchemy import distinct, select
from sqlalchemy.orm import Session
from app.models import Component, ComponentProperty, ProjectParam, ProjectSetting
DOC_TYPE_KEYS = {
"perechen": "Perechen",
"specification_pcb": "SpecificationPCB",
"specification": "Specification",
"vedomost": "Vedomost",
"simple_list": "SimpleList",
}
TABLE_SETTINGS_META: dict[str, dict[str, Any]] = {
"perechen": {
"columns": [{"label": "Наименование", "key": "name", "mapping_index": 0}],
"defaults": ["Name"],
},
"specification_pcb": {
"columns": [
{"label": "Обозначение", "key": "designation", "mapping_index": 0},
{"label": "Наименование", "key": "name", "mapping_index": 1},
],
"defaults": ["Designator", "ManufacturerPartNumber", "", "", "", "", "", ""],
"project_params": [
{"key": "board_name", "label": "Название платы", "mapping_index": 6},
{"key": "decimal_number", "label": "Децимальный №", "mapping_index": 7},
],
},
"specification": {
"columns": [],
"defaults": [],
"project_params": [
{"key": "decimal_number", "label": "Децимальный №", "mapping_index": 7},
{"key": "document_name", "label": "Название документа", "mapping_index": 8},
],
},
"vedomost": {
"columns": [
{"label": "Наименование", "key": "name", "mapping_index": 0},
{"label": "Код продукции", "key": "product_code", "mapping_index": 1},
{"label": "Обозначение документа", "key": "document_code", "mapping_index": 2},
{"label": "Поставщик", "key": "supplier", "mapping_index": 3},
{"label": "Примечание", "key": "note", "mapping_index": 5},
],
"defaults": ["Name", "ProductCode", "DocumentCode", "Supplier", "", "Note"],
"project_params": [
{"key": "where_used", "label": "Куда входит", "mapping_index": 4},
],
},
"simple_list": {
"columns": [],
"defaults": [],
"component_fields": [
{"key": "name_field", "label": "Поле наименования", "setting_key": "SimpleList_nameField"},
],
"extra": [
{"key": "tech_reserve_percent", "label": "Тех. запас, %", "setting_key": "SimpleList_techReservePercent", "default": "10"},
{"key": "boards_count", "label": "Кол-во плат", "setting_key": "SimpleList_boardsCount", "default": "1"},
],
},
}
def _setting_key(table_type: str) -> str:
doc = DOC_TYPE_KEYS.get(table_type, table_type)
return f"columnMappings_{doc}"
def _get_setting(db: Session, project_id: int, key: str) -> str | None:
row = db.scalar(
select(ProjectSetting).where(
ProjectSetting.project_id == project_id,
ProjectSetting.key == key,
)
)
return row.value if row else None
def _set_setting(db: Session, project_id: int, key: str, value: str) -> None:
row = db.scalar(
select(ProjectSetting).where(
ProjectSetting.project_id == project_id,
ProjectSetting.key == key,
)
)
if row:
row.value = value
else:
db.add(ProjectSetting(project_id=project_id, key=key, value=value))
def get_project_params_map(db: Session, project_id: int) -> dict[str, str]:
rows = db.scalars(select(ProjectParam).where(ProjectParam.project_id == project_id)).all()
return {r.name: r.value for r in rows}
def resolve_param_value(stored: str, params: dict[str, str]) -> str:
if not stored or stored == "-- Не выбрано --":
return ""
if stored in params and params[stored]:
return params[stored]
return stored
def resolve_inscriptions(
inscriptions: dict[int, str], params: dict[str, str]
) -> dict[int, str]:
resolved: dict[int, str] = {}
for num, val in inscriptions.items():
resolved[int(num)] = resolve_param_value(val or "", params)
return resolved
def get_property_names(db: Session, project_id: int) -> list[str]:
keys = db.scalars(
select(distinct(ComponentProperty.key))
.join(Component, Component.id == ComponentProperty.component_id)
.where(Component.project_id == project_id)
.order_by(ComponentProperty.key)
).all()
names = sorted({k for k in keys if k})
if "Designator" not in names:
names.insert(0, "Designator")
return names
def get_column_mappings_list(db: Session, project_id: int, table_type: str) -> list[str]:
meta = TABLE_SETTINGS_META.get(table_type, {})
defaults = list(meta.get("defaults", []))
raw = _get_setting(db, project_id, _setting_key(table_type))
if not raw:
return defaults
try:
loaded = json.loads(raw)
if isinstance(loaded, list):
return [str(x) for x in loaded]
except json.JSONDecodeError:
pass
return defaults
def set_column_mappings_list(
db: Session, project_id: int, table_type: str, mappings: list[str]
) -> list[str]:
_set_setting(db, project_id, _setting_key(table_type), json.dumps(mappings, ensure_ascii=False))
db.commit()
return get_column_mappings_list(db, project_id, table_type)
def get_simple_list_settings(db: Session, project_id: int) -> dict[str, Any]:
name_field = _get_setting(db, project_id, "SimpleList_nameField") or "Name"
tech = _get_setting(db, project_id, "SimpleList_techReservePercent") or "10"
boards = _get_setting(db, project_id, "SimpleList_boardsCount") or "1"
return {
"name_field": name_field,
"tech_reserve_percent": float(tech),
"boards_count": int(boards),
}
def set_simple_list_settings(db: Session, project_id: int, data: dict[str, Any]) -> dict[str, Any]:
if "name_field" in data and data["name_field"] is not None:
_set_setting(db, project_id, "SimpleList_nameField", str(data["name_field"]))
if "tech_reserve_percent" in data and data["tech_reserve_percent"] is not None:
_set_setting(db, project_id, "SimpleList_techReservePercent", str(data["tech_reserve_percent"]))
if "boards_count" in data and data["boards_count"] is not None:
_set_setting(db, project_id, "SimpleList_boardsCount", str(data["boards_count"]))
db.commit()
return get_simple_list_settings(db, project_id)
def get_table_settings(db: Session, project_id: int, table_type: str) -> dict[str, Any]:
meta = TABLE_SETTINGS_META.get(table_type, {})
mappings = get_column_mappings_list(db, project_id, table_type)
columns = meta.get("columns", [])
column_mappings = {}
for col in columns:
idx = col.get("mapping_index", columns.index(col) if col in columns else 0)
column_mappings[col["key"]] = mappings[idx] if idx < len(mappings) else ""
result: dict[str, Any] = {
"table_type": table_type,
"columns": columns,
"column_mappings": column_mappings,
"mappings": mappings,
"property_names": get_property_names(db, project_id),
"project_params": [
{"name": n, "value": v} for n, v in get_project_params_map(db, project_id).items()
],
}
for pp in meta.get("project_params", []):
idx = pp["mapping_index"]
result["column_mappings"][pp["key"]] = mappings[idx] if idx < len(mappings) else ""
fields: list[dict[str, str]] = []
for col in columns:
fields.append({"key": col["key"], "label": col["label"], "source": "property"})
for pp in meta.get("project_params", []):
fields.append({"key": pp["key"], "label": pp["label"], "source": "project_param"})
for cf in meta.get("component_fields", []):
fields.append({"key": cf["key"], "label": cf["label"], "source": "component_field"})
result["fields"] = fields
if table_type == "simple_list":
result["simple_list"] = get_simple_list_settings(db, project_id)
sl = result["simple_list"]
result["column_mappings"]["name_field"] = sl["name_field"]
return result
def update_table_settings(
db: Session, project_id: int, table_type: str, body: dict[str, Any]
) -> dict[str, Any]:
meta = TABLE_SETTINGS_META.get(table_type, {})
mappings = get_column_mappings_list(db, project_id, table_type)
if "mappings" in body and body["mappings"] is not None:
mappings = [str(x) for x in body["mappings"]]
elif "column_mappings" in body and body["column_mappings"]:
cm = body["column_mappings"]
columns = meta.get("columns", [])
for col in columns:
key = col["key"]
if key not in cm:
continue
idx = col.get("mapping_index", 0)
while len(mappings) <= idx:
mappings.append("")
mappings[idx] = cm[key] or ""
for pp in meta.get("project_params", []):
key = pp["key"]
idx = pp["mapping_index"]
if key in cm:
while len(mappings) <= idx:
mappings.append("")
mappings[idx] = cm[key] or ""
set_column_mappings_list(db, project_id, table_type, mappings)
sl_patch = body.get("simple_list") or {}
if table_type == "simple_list":
cm = body.get("column_mappings") or {}
if "name_field" in cm:
sl_patch = {**sl_patch, "name_field": cm["name_field"]}
if sl_patch:
set_simple_list_settings(db, project_id, sl_patch)
return get_table_settings(db, project_id, table_type)
def vedomost_column_map(mappings: list[str]) -> dict[str, str]:
field_indices = {
"Name": 0,
"ProductCode": 1,
"DocumentCode": 2,
"Supplier": 3,
"Note": 5,
}
defaults = {
"Name": "Name",
"ProductCode": "ProductCode",
"DocumentCode": "DocumentCode",
"Supplier": "Supplier",
"Note": "Note",
}
return {
k: mappings[idx] if idx < len(mappings) and mappings[idx] else defaults[k]
for k, idx in field_indices.items()
}
def resolve_where_used(db: Session, project_id: int, mappings: list[str]) -> str:
params = get_project_params_map(db, project_id)
if len(mappings) > 4 and mappings[4]:
return resolve_param_value(mappings[4], params)
return ""
def resolve_decimal_and_board(
db: Session, project_id: int, project_decimal: str, project_board: str, table_type: str
) -> tuple[str, str]:
params = get_project_params_map(db, project_id)
mappings = get_column_mappings_list(db, project_id, table_type)
dec = project_decimal
board = project_board
if table_type == "specification_pcb":
if len(mappings) > 7 and mappings[7]:
dec = resolve_param_value(mappings[7], params)
elif dec:
dec = resolve_param_value(dec, params)
if len(mappings) > 6 and mappings[6]:
board = resolve_param_value(mappings[6], params)
elif board:
board = resolve_param_value(board, params)
elif dec:
dec = resolve_param_value(dec, params)
return dec, board
+2 -1
View File
@@ -147,6 +147,7 @@ def generate_specification_pcb(
board_name: str = "",
pcb_doc_name: str = "",
mappings: dict[str, tuple[str, str]] | None = None,
name_field: str = "ManufacturerPartNumber",
) -> list[dict[str, Any]]:
rows: list[dict[str, Any]] = []
dec = decimal_number or ""
@@ -226,7 +227,7 @@ def generate_specification_pcb(
for letter, comps in groups.items():
subgroups: OrderedDict[str, list[ComponentView]] = OrderedDict()
for c in comps:
key = c.get("ManufacturerPartNumber", "") or c.get("Name", "")
key = c.get(name_field, "") or c.get("ManufacturerPartNumber", "") or c.get("Name", "")
subgroups.setdefault(key, []).append(c)
for part_number, group in subgroups.items():
+44 -15
View File
@@ -19,6 +19,15 @@ from app.models import (
VariantProperty,
VedomostRow,
)
from app.services.project_settings import (
get_column_mappings_list,
get_project_params_map,
get_simple_list_settings,
resolve_decimal_and_board,
resolve_param_value,
resolve_where_used,
vedomost_column_map,
)
from app.services.table_generators import (
TABLE_TYPES,
ComponentView,
@@ -165,16 +174,23 @@ def generate_table(
mappings = _mappings_dict(db, project.id)
components = load_components_for_variant(db, project)
col_mappings = get_column_mappings_list(db, project.id, table_type)
if table_type == "perechen":
rows = generate_perechen(components, name_field=name_field or "Name", mappings=mappings)
nf = name_field or (col_mappings[0] if col_mappings else "Name") or "Name"
rows = generate_perechen(components, name_field=nf, mappings=mappings)
elif table_type == "specification_pcb":
dec, board = resolve_decimal_and_board(
db, project.id, project.decimal_number, project.board_name, table_type
)
name_prop = col_mappings[1] if len(col_mappings) > 1 and col_mappings[1] else "ManufacturerPartNumber"
rows = generate_specification_pcb(
components,
decimal_number=project.decimal_number,
board_name=project.board_name,
decimal_number=dec,
board_name=board,
pcb_doc_name=project.pcb_doc_name or "",
mappings=mappings,
name_field=name_prop,
)
elif table_type == "specification":
materials: list[MaterialView] = []
@@ -191,22 +207,35 @@ def generate_table(
layer_number=m.layer_number,
)
)
rows = generate_specification(materials, layer_count=layer_count, decimal_number=project.decimal_number)
dec, _ = resolve_decimal_and_board(
db, project.id, project.decimal_number, project.board_name, table_type
)
rows = generate_specification(materials, layer_count=layer_count, decimal_number=dec)
elif table_type == "vedomost":
inscriptions = {
i.field_number: i.field_value
for i in db.scalars(
select(TitleInscription).where(TitleInscription.project_id == project.id)
).all()
}
where_used = inscriptions.get(101) or inscriptions.get(1001) or project.decimal_number
rows = generate_vedomost(components, where_used=where_used, mappings=mappings)
params = get_project_params_map(db, project.id)
where_used = resolve_where_used(db, project.id, col_mappings)
if not where_used:
inscriptions = {
i.field_number: i.field_value
for i in db.scalars(
select(TitleInscription).where(TitleInscription.project_id == project.id)
).all()
}
raw = inscriptions.get(101) or inscriptions.get(1001) or project.decimal_number or ""
where_used = resolve_param_value(raw, params)
rows = generate_vedomost(
components,
where_used=where_used,
mappings=mappings,
column_mappings=vedomost_column_map(col_mappings),
)
else:
sl = get_simple_list_settings(db, project.id)
rows = generate_simple_list(
components,
name_field=name_field or "Name",
tech_reserve_percent=tech_reserve_percent if tech_reserve_percent is not None else 10.0,
boards_count=boards_count if boards_count is not None else 1,
name_field=name_field or sl["name_field"],
tech_reserve_percent=tech_reserve_percent if tech_reserve_percent is not None else sl["tech_reserve_percent"],
boards_count=boards_count if boards_count is not None else sl["boards_count"],
)
return replace_rows(db, project.id, table_type, rows)