fix table
This commit is contained in:
@@ -31,10 +31,12 @@ 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_font_stretch,
|
||||
get_project_params_map,
|
||||
get_property_names,
|
||||
get_table_settings,
|
||||
resolve_inscriptions,
|
||||
save_cell_overflows,
|
||||
update_table_settings,
|
||||
)
|
||||
from app.services.table_service import (
|
||||
@@ -328,11 +330,13 @@ def export_document(project_id: int, body: ExportRequest, db: Session = Depends(
|
||||
)
|
||||
if body.format == "pdf":
|
||||
try:
|
||||
data = export_pdf(body.table_type, rows, resolved_inscriptions)
|
||||
stretch = get_font_stretch(db, project_id, body.table_type)
|
||||
result = export_pdf(body.table_type, rows, resolved_inscriptions, font_stretch=stretch)
|
||||
save_cell_overflows(db, project_id, body.table_type, result.overflows)
|
||||
except ValueError as e:
|
||||
raise HTTPException(400, str(e)) from e
|
||||
return Response(
|
||||
content=data,
|
||||
content=result.data,
|
||||
media_type="application/pdf",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="{project.name}_{body.table_type}.pdf"'
|
||||
|
||||
@@ -14,6 +14,7 @@ from reportlab.pdfbase.ttfonts import TTFont
|
||||
from reportlab.pdfgen import canvas
|
||||
|
||||
from app.core.config import get_settings
|
||||
from app.services.pdf_gost_frame import draw_gost_frame
|
||||
|
||||
_font_registered = False
|
||||
FONT_NAME = "GOST_A"
|
||||
@@ -234,6 +235,80 @@ def _draw_rotated_header(
|
||||
c.restoreState()
|
||||
|
||||
|
||||
@dataclass
|
||||
class PdfExportResult:
|
||||
data: bytes
|
||||
overflows: list[dict[str, Any]]
|
||||
|
||||
|
||||
def _cell_stretch(row: dict[str, Any] | None, default: int) -> int:
|
||||
if not row:
|
||||
return default
|
||||
meta = row.get("cell_stretches") or {}
|
||||
if isinstance(meta, dict) and meta:
|
||||
return default
|
||||
if row.get("stretch"):
|
||||
return min(default, 85)
|
||||
return default
|
||||
|
||||
|
||||
def _draw_cell_with_stretch(
|
||||
c: canvas.Canvas,
|
||||
x: float,
|
||||
y: float,
|
||||
w: float,
|
||||
h: float,
|
||||
text: str,
|
||||
font: str,
|
||||
size: float,
|
||||
align: Align,
|
||||
cell_stretch: int,
|
||||
underline: bool,
|
||||
overflow_sink: list[dict[str, Any]] | None,
|
||||
row: dict[str, Any] | None,
|
||||
field: str,
|
||||
global_row_index: int,
|
||||
) -> None:
|
||||
if not text:
|
||||
return
|
||||
pad = 0.8 * mm
|
||||
inner_w = max(w - 2 * pad, 1)
|
||||
scale = cell_stretch / 100.0
|
||||
c.setFont(font, size)
|
||||
text_w = c.stringWidth(text, font, size) * scale
|
||||
display = text
|
||||
is_overflow = False
|
||||
if text_w > inner_w:
|
||||
max_before_scale = inner_w / scale if scale > 0 else inner_w
|
||||
display = _elide(c, text, font, size, max_before_scale)
|
||||
is_overflow = True
|
||||
if overflow_sink is not None and row is not None:
|
||||
overflow_sink.append(
|
||||
{
|
||||
"row_id": row.get("id"),
|
||||
"row_index": row.get("row_index", global_row_index),
|
||||
"field": field,
|
||||
}
|
||||
)
|
||||
|
||||
if scale != 1.0 and not is_overflow:
|
||||
c.saveState()
|
||||
if align == "center":
|
||||
ox = x + w / 2
|
||||
elif align == "right":
|
||||
ox = x + w - pad
|
||||
else:
|
||||
ox = x + pad
|
||||
c.translate(ox, y + h / 2)
|
||||
c.scale(scale, 1)
|
||||
tw = c.stringWidth(display, font, size)
|
||||
tx = -tw / 2 if align == "center" else (-tw if align == "right" else 0)
|
||||
c.drawString(tx, -size * 0.28, display)
|
||||
c.restoreState()
|
||||
else:
|
||||
_draw_text_in_rect(c, x, y, w, h, display, font, size, align, underline=underline)
|
||||
|
||||
|
||||
def _draw_frame(
|
||||
c: canvas.Canvas,
|
||||
w: float,
|
||||
@@ -241,36 +316,10 @@ def _draw_frame(
|
||||
page: int,
|
||||
total: int,
|
||||
inscriptions: dict[int, str],
|
||||
doc_type: str,
|
||||
table_type: str,
|
||||
font: str,
|
||||
) -> None:
|
||||
font = _ensure_font()
|
||||
margin_left = 20 * mm
|
||||
margin_right = 5 * mm
|
||||
margin_top = 5 * mm
|
||||
margin_bottom = 5 * mm
|
||||
c.setLineWidth(0.8)
|
||||
c.rect(margin_left, margin_bottom, w - margin_left - margin_right, h - margin_top - margin_bottom)
|
||||
|
||||
block_h = 55 * mm if page == 1 else 15 * mm
|
||||
block_w = 185 * mm
|
||||
x0 = w - margin_right - block_w
|
||||
y0 = margin_bottom
|
||||
c.setLineWidth(0.5)
|
||||
c.rect(x0, y0, block_w, block_h)
|
||||
|
||||
c.setFont(font, 8)
|
||||
name = inscriptions.get(1, "")
|
||||
designation = inscriptions.get(2, "")
|
||||
org = inscriptions.get(9, "")
|
||||
title = DOC_TITLES.get(doc_type, "")
|
||||
c.drawString(x0 + 2 * mm, y0 + block_h - 6 * mm, title)
|
||||
c.drawString(x0 + 2 * mm, y0 + block_h - 12 * mm, (designation or "")[:80])
|
||||
c.drawString(x0 + 2 * mm, y0 + block_h - 18 * mm, (name or "")[:80])
|
||||
if page == 1:
|
||||
c.drawString(x0 + 2 * mm, y0 + 8 * mm, (org or "")[:50])
|
||||
c.drawString(x0 + 2 * mm, y0 + 3 * mm, f"Разраб. {inscriptions.get(111, '')}")
|
||||
c.drawString(x0 + 50 * mm, y0 + 3 * mm, f"Пров. {inscriptions.get(112, '')}")
|
||||
c.drawRightString(x0 + block_w - 2 * mm, y0 + 3 * mm, f"Лист {page}/{total}")
|
||||
draw_gost_frame(c, w, h, inscriptions, table_type, page, total, font)
|
||||
|
||||
|
||||
def _cell_value(row: dict[str, Any] | None, field: str, display_index: int) -> str:
|
||||
@@ -288,6 +337,9 @@ def _draw_table_page(
|
||||
layout: TableLayout,
|
||||
page_rows: list[dict[str, Any] | None],
|
||||
page_h: float,
|
||||
font_stretch: int,
|
||||
global_row_start: int,
|
||||
overflows: list[dict[str, Any]],
|
||||
) -> None:
|
||||
font = _ensure_font()
|
||||
c.setLineWidth(LINE_WIDTH)
|
||||
@@ -324,6 +376,7 @@ def _draw_table_page(
|
||||
for display_i, row in enumerate(page_rows):
|
||||
cy = table_top - header_h - (display_i + 1) * row_h
|
||||
cx = table_x
|
||||
global_idx = global_row_start + display_i
|
||||
for col in layout.columns:
|
||||
cw = col.width_mm * mm
|
||||
c.rect(cx, cy, cw, row_h)
|
||||
@@ -331,10 +384,26 @@ def _draw_table_page(
|
||||
is_header = bool(row and row.get("is_header"))
|
||||
underline = bool(row and row.get("is_underline"))
|
||||
align: Align = col.align
|
||||
if col.field in ("designation", "name"):
|
||||
if col.field in ("designation", "name", "product_code", "document_code", "supplier", "note", "where_used"):
|
||||
align = "center" if is_header else "left"
|
||||
_draw_text_in_rect(
|
||||
c, cx, cy, cw, row_h, text, font, FONT_SIZE, align, underline=underline or is_header
|
||||
stretch = _cell_stretch(row, font_stretch)
|
||||
pad_x = cx + (2 * mm if col.field in ("designation", "name") and not is_header else 0)
|
||||
_draw_cell_with_stretch(
|
||||
c,
|
||||
pad_x,
|
||||
cy,
|
||||
cw - (2 * mm if pad_x > cx else 0),
|
||||
row_h,
|
||||
text,
|
||||
font,
|
||||
FONT_SIZE,
|
||||
align,
|
||||
stretch,
|
||||
underline or is_header,
|
||||
overflows,
|
||||
row,
|
||||
col.field,
|
||||
global_idx,
|
||||
)
|
||||
cx += cw
|
||||
|
||||
@@ -343,7 +412,8 @@ def export_pdf(
|
||||
table_type: str,
|
||||
rows: list[dict[str, Any]],
|
||||
inscriptions: dict[int, str] | None = None,
|
||||
) -> bytes:
|
||||
font_stretch: int = 100,
|
||||
) -> PdfExportResult:
|
||||
if table_type == "simple_list":
|
||||
raise ValueError("PDF export is not available for simple_list")
|
||||
if table_type not in TABLE_LAYOUTS:
|
||||
@@ -353,16 +423,20 @@ def export_pdf(
|
||||
inscriptions = inscriptions or {}
|
||||
pages = _paginate_rows(rows, layout.rows_first, layout.rows_other)
|
||||
total = _total_pages(len(rows), layout.rows_first, layout.rows_other)
|
||||
overflows: list[dict[str, Any]] = []
|
||||
font = _ensure_font()
|
||||
|
||||
buf = BytesIO()
|
||||
page_w, page_h = layout.page_size
|
||||
c = canvas.Canvas(buf, pagesize=layout.page_size)
|
||||
|
||||
row_offset = 0
|
||||
for page_idx, page_rows in enumerate(pages, start=1):
|
||||
_draw_frame(c, page_w, page_h, page_idx, total, inscriptions, table_type)
|
||||
_draw_table_page(c, layout, page_rows, page_h)
|
||||
_draw_frame(c, page_w, page_h, page_idx, total, inscriptions, table_type, font)
|
||||
_draw_table_page(c, layout, page_rows, page_h, font_stretch, row_offset, overflows)
|
||||
row_offset += layout.rows_first if page_idx == 1 else layout.rows_other
|
||||
if page_idx < len(pages):
|
||||
c.showPage()
|
||||
|
||||
c.save()
|
||||
return buf.getvalue()
|
||||
return PdfExportResult(data=buf.getvalue(), overflows=overflows)
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
"""GOST title block / frame drawing (ported from controller/pdfcontroller.cpp)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.pdfgen import canvas
|
||||
|
||||
FRAME_LINE = 0.5 * mm
|
||||
M_LEFT = 20 * mm
|
||||
M_TOP = 5 * mm
|
||||
M_RIGHT = 5 * mm
|
||||
M_BOTTOM = 5 * mm
|
||||
|
||||
DOC_TYPES = {
|
||||
"perechen": 1,
|
||||
"specification_pcb": 2,
|
||||
"specification": 3,
|
||||
"vedomost": 4,
|
||||
}
|
||||
|
||||
PRIMARY_APP_FIELD = {1: 251, 2: 252, 3: 253, 4: 254}
|
||||
|
||||
|
||||
def _ins(inscriptions: dict[int, str], num: int) -> str:
|
||||
return str(inscriptions.get(num, "") or "")
|
||||
|
||||
|
||||
class Inner:
|
||||
"""Inner document area in ReportLab coordinates (origin bottom-left)."""
|
||||
|
||||
def __init__(self, page_w: float, page_h: float):
|
||||
self.page_w = page_w
|
||||
self.page_h = page_h
|
||||
self.left = M_LEFT
|
||||
self.bottom = M_BOTTOM
|
||||
self.right = page_w - M_RIGHT
|
||||
self.top = page_h - M_TOP
|
||||
|
||||
def rect(self, x: float, y: float, w: float, h: float) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def _set_frame_pen(c: canvas.Canvas) -> None:
|
||||
c.setLineWidth(FRAME_LINE)
|
||||
c.setStrokeColorRGB(0, 0, 0)
|
||||
|
||||
|
||||
def _txt(
|
||||
c: canvas.Canvas,
|
||||
x: float,
|
||||
y: float,
|
||||
w: float,
|
||||
h: float,
|
||||
text: str,
|
||||
font: str,
|
||||
size: float,
|
||||
align: str = "center",
|
||||
valign: str = "center",
|
||||
) -> None:
|
||||
if not text:
|
||||
return
|
||||
c.setFont(font, size)
|
||||
tw = c.stringWidth(text, font, size)
|
||||
if align == "center":
|
||||
tx = x + (w - tw) / 2
|
||||
elif align == "right":
|
||||
tx = x + w - tw - 0.5 * mm
|
||||
else:
|
||||
tx = x + 0.5 * mm
|
||||
if valign == "bottom":
|
||||
ty = y + 1 * mm
|
||||
elif valign == "top":
|
||||
ty = y + h - size * 0.35 - 0.5 * mm
|
||||
else:
|
||||
ty = y + (h - size * 0.35) / 2
|
||||
c.drawString(tx, ty, text)
|
||||
|
||||
|
||||
def _txt_multiline(
|
||||
c: canvas.Canvas,
|
||||
x: float,
|
||||
y: float,
|
||||
w: float,
|
||||
h: float,
|
||||
text: str,
|
||||
font: str,
|
||||
size: float,
|
||||
max_lines: int = 3,
|
||||
) -> None:
|
||||
if not text:
|
||||
return
|
||||
c.setFont(font, size)
|
||||
words = text.split()
|
||||
lines: list[str] = []
|
||||
current = ""
|
||||
for word in words:
|
||||
test = f"{current} {word}".strip()
|
||||
if c.stringWidth(test, font, size) <= w - mm:
|
||||
current = test
|
||||
else:
|
||||
if current:
|
||||
lines.append(current)
|
||||
current = word
|
||||
if current:
|
||||
lines.append(current)
|
||||
lines = lines[:max_lines]
|
||||
step = size * 1.2
|
||||
start_y = y + h - size * 0.5 - mm
|
||||
for i, line in enumerate(lines):
|
||||
tw = c.stringWidth(line, font, size)
|
||||
c.drawString(x + (w - tw) / 2, start_y - i * step, line)
|
||||
|
||||
|
||||
def _draw_inner_border(c: canvas.Canvas, inner: Inner) -> None:
|
||||
_set_frame_pen(c)
|
||||
c.rect(inner.left, inner.bottom, inner.right - inner.left, inner.top - inner.bottom)
|
||||
|
||||
|
||||
def _draw_left_binding_strip(c: canvas.Canvas, inner: Inner) -> tuple[float, float, float, float]:
|
||||
"""Left vertical strip (12×145 mm) at bottom-left of inner area."""
|
||||
w = 12 * mm
|
||||
h = 145 * mm
|
||||
x = inner.left - w
|
||||
y = inner.bottom
|
||||
c.rect(x, y, w, h)
|
||||
# horizontal dividers (from bottom): 25, 35, 25, 25, 35 mm sections
|
||||
offsets = [25, 60, 85, 110, 145]
|
||||
for off in offsets:
|
||||
yy = y + off * mm
|
||||
c.line(x, yy, inner.left, yy)
|
||||
c.line(x + w + 5 * mm, y, x + w + 5 * mm, y + h)
|
||||
return x, y, w, h
|
||||
|
||||
|
||||
def _draw_left_binding_text(c: canvas.Canvas, strip_x: float, strip_y: float, inscriptions: dict[int, str], font: str) -> None:
|
||||
size = 12
|
||||
c.saveState()
|
||||
c.translate(strip_x + 12 * mm, strip_y + 145 * mm)
|
||||
c.rotate(-90)
|
||||
labels = [
|
||||
(0, 25, "Инв. № подл"),
|
||||
(25, 35, "Подп. и дата"),
|
||||
(60, 25, "Взам. инв. №"),
|
||||
(85, 25, "Инв. № дубл"),
|
||||
(110, 35, "Подп. и дата"),
|
||||
]
|
||||
for ox, ow, label in labels:
|
||||
_txt(c, ox * mm, 0, ow * mm, 5 * mm, label, font, size)
|
||||
vals = [(0, 25, 19), (25, 35, 20), (60, 25, 21), (85, 25, 22), (110, 35, 23)]
|
||||
for ox, ow, field in vals:
|
||||
_txt(c, ox * mm, 5 * mm, ow * mm, 7 * mm, _ins(inscriptions, field), font, size)
|
||||
c.restoreState()
|
||||
|
||||
|
||||
def _draw_top_left_strip(c: canvas.Canvas, inner: Inner, inscriptions: dict[int, str], doc_type: int, font: str) -> None:
|
||||
w = 12 * mm
|
||||
h = 120 * mm
|
||||
x = inner.left - w
|
||||
y = inner.top - h
|
||||
c.rect(x, y, w, h)
|
||||
c.line(x + w + 5 * mm, y, x + w + 5 * mm, y + h)
|
||||
c.line(x, y + 60 * mm, inner.left, y + 60 * mm)
|
||||
|
||||
c.saveState()
|
||||
c.translate(x + w, y)
|
||||
c.rotate(-90)
|
||||
_txt(c, 0, 0, 60 * mm, 5 * mm, "Справ. №", font, 12)
|
||||
_txt(c, 60 * mm, 0, 60 * mm, 5 * mm, "Перв. примен.", font, 12)
|
||||
_txt(c, 0, 5 * mm, 60 * mm, 7 * mm, _ins(inscriptions, 24), font, 12)
|
||||
_txt(
|
||||
c,
|
||||
60 * mm,
|
||||
5 * mm,
|
||||
60 * mm,
|
||||
7 * mm,
|
||||
_ins(inscriptions, PRIMARY_APP_FIELD.get(doc_type, 25)),
|
||||
font,
|
||||
12,
|
||||
)
|
||||
c.restoreState()
|
||||
|
||||
|
||||
def _draw_stamp_first(c: canvas.Canvas, inner: Inner, inscriptions: dict[int, str], doc_type: int, page: int, total: int, font: str) -> None:
|
||||
mw, mh = 185 * mm, 40 * mm
|
||||
x = inner.right - mw
|
||||
y = inner.bottom
|
||||
c.rect(x, y, mw, mh)
|
||||
|
||||
# horizontal lines
|
||||
for dy in (5, 10, 15, 20, 25, 30, 35):
|
||||
if dy <= 10 or dy >= 20:
|
||||
c.line(x, y + dy * mm, x + (7 + 10 + 23 + 15 + 10) * mm, y + dy * mm)
|
||||
else:
|
||||
c.line(x, y + dy * mm, x + mw, y + dy * mm)
|
||||
c.line(x + 65 * mm, y, x + 65 * mm, y + mh)
|
||||
for dx in (7, 17, 40, 55):
|
||||
c.line(x + dx * mm, y, x + dx * mm, y + (15 * mm if dx == 7 else mh))
|
||||
c.line(x + mw - 50 * mm, y + 15 * mm, x + mw - 50 * mm, y + mh)
|
||||
c.line(x + mw - 45 * mm, y + 20 * mm, x + mw - 45 * mm, y + 25 * mm)
|
||||
c.line(x + mw - 40 * mm, y + 20 * mm, x + mw - 40 * mm, y + 25 * mm)
|
||||
c.line(x + mw - 35 * mm, y + 15 * mm, x + mw - 35 * mm, y + 25 * mm)
|
||||
c.line(x + mw - 20 * mm, y + 15 * mm, x + mw - 20 * mm, y + 25 * mm)
|
||||
c.line(x + mw - 50 * mm, y + 20 * mm, x + mw, y + 20 * mm)
|
||||
c.line(x + mw - 50 * mm, y + 25 * mm, x + mw, y + 25 * mm)
|
||||
|
||||
# idk block — 22 mm directly above main stamp top
|
||||
ix = x + mw - 120 * mm
|
||||
iy = y + mh
|
||||
iw, ih = 120 * mm, 22 * mm
|
||||
c.line(ix, iy, ix, iy + ih)
|
||||
c.line(ix + iw, iy, ix + iw, iy + ih)
|
||||
c.line(ix, iy + ih, ix + iw, iy + ih)
|
||||
c.line(ix, iy + 14 * mm, ix + iw, iy + 14 * mm)
|
||||
c.line(ix + 14 * mm, iy, ix + 14 * mm, iy + 14 * mm)
|
||||
c.line(ix + 67 * mm, iy, ix + 67 * mm, iy + 14 * mm)
|
||||
_txt(c, ix, iy, 14 * mm, 14 * mm, _ins(inscriptions, 27), font, 12)
|
||||
_txt(c, ix + 14 * mm, iy, 53 * mm, 14 * mm, _ins(inscriptions, 28), font, 12)
|
||||
_txt(c, ix + 67 * mm, iy, 53 * mm, 14 * mm, _ins(inscriptions, 29), font, 12)
|
||||
_txt(c, ix, iy + 14 * mm, iw, 8 * mm, _ins(inscriptions, 30), font, 12)
|
||||
|
||||
# change block headers
|
||||
_txt(c, x, y + mh - 5 * mm, 7 * mm, 5 * mm, _ins(inscriptions, 14), font, 12)
|
||||
_txt(c, x + 7 * mm, y + mh - 5 * mm, 10 * mm, 5 * mm, _ins(inscriptions, 15), font, 12)
|
||||
_txt(c, x + 17 * mm, y + mh - 5 * mm, 23 * mm, 5 * mm, _ins(inscriptions, 16), font, 12)
|
||||
_txt(c, x + 40 * mm, y + mh - 5 * mm, 15 * mm, 5 * mm, _ins(inscriptions, 17), font, 12)
|
||||
_txt(c, x + 55 * mm, y + mh - 5 * mm, 10 * mm, 5 * mm, _ins(inscriptions, 18), font, 12)
|
||||
_txt(c, x, y + mh - 10 * mm, 7 * mm, 5 * mm, "Изм.", font, 12)
|
||||
_txt(c, x + 7 * mm, y + mh - 10 * mm, 10 * mm, 5 * mm, "Лист", font, 12)
|
||||
_txt(c, x + 17 * mm, y + mh - 10 * mm, 23 * mm, 5 * mm, "№ докум.", font, 12)
|
||||
_txt(c, x + 40 * mm, y + mh - 10 * mm, 15 * mm, 5 * mm, "Подп.", font, 12)
|
||||
_txt(c, x + 55 * mm, y + mh - 10 * mm, 10 * mm, 5 * mm, "Дата", font, 12)
|
||||
|
||||
_txt(c, x + 1 * mm, y + mh - 15 * mm, 16 * mm, 5 * mm, "Разраб.", font, 12, "left")
|
||||
_txt(c, x + 1 * mm, y + mh - 20 * mm, 16 * mm, 5 * mm, "Пров.", font, 12, "left")
|
||||
_txt(c, x + 1 * mm, y + mh - 25 * mm, 16 * mm, 5 * mm, _ins(inscriptions, 10), font, 12, "left")
|
||||
_txt(c, x + 18 * mm, y + mh - 25 * mm, 22 * mm, 5 * mm, _ins(inscriptions, 11), font, 12, "left")
|
||||
_txt(c, x + 18 * mm, y + mh - 15 * mm, 22 * mm, 5 * mm, _ins(inscriptions, 111), font, 12, "left")
|
||||
_txt(c, x + 18 * mm, y + mh - 20 * mm, 22 * mm, 5 * mm, _ins(inscriptions, 112), font, 12, "left")
|
||||
_txt(c, x + 18 * mm, y + mh - 30 * mm, 22 * mm, 5 * mm, _ins(inscriptions, 113), font, 12, "left")
|
||||
_txt(c, x + 18 * mm, y + mh - 35 * mm, 22 * mm, 5 * mm, _ins(inscriptions, 114), font, 12, "left")
|
||||
_txt(c, x + 41 * mm, y + mh - 25 * mm, 14 * mm, 5 * mm, _ins(inscriptions, 12), font, 12, "left")
|
||||
_txt(c, x + 56 * mm, y + mh - 25 * mm, 9 * mm, 5 * mm, _ins(inscriptions, 13), font, 12, "left")
|
||||
_txt(c, x + 1 * mm, y + mh - 30 * mm, 16 * mm, 5 * mm, "Н. контр.", font, 12, "left")
|
||||
_txt(c, x + 1 * mm, y + mh - 35 * mm, 16 * mm, 5 * mm, "Утв.", font, 12, "left")
|
||||
|
||||
# product name / doc title area
|
||||
if doc_type != 4:
|
||||
name_text = _ins(inscriptions, 1002 if doc_type == 3 else 1)
|
||||
_txt_multiline(c, x + 65 * mm, y + mh - 40 * mm, 70 * mm, 25 * mm, name_text, font, 18)
|
||||
if doc_type == 1:
|
||||
_txt(c, x + 65 * mm, y + mh - 40 * mm, 70 * mm, 25 * mm, "Перечень элементов", font, 12, valign="bottom")
|
||||
else:
|
||||
_txt_multiline(c, x + 65 * mm, y + mh - 35 * mm, 70 * mm, 20 * mm, _ins(inscriptions, 1), font, 18)
|
||||
_txt(c, x + 65 * mm, y + mh - 35 * mm, 70 * mm, 5 * mm, _ins(inscriptions, 301), font, 12)
|
||||
_txt(c, x + 65 * mm, y, 70 * mm, 20 * mm, "Ведомость покупных изделий", font, 12, valign="bottom")
|
||||
|
||||
_txt(c, x + 65 * mm, y, 70 * mm, 5 * mm, "Копировал", font, 12, "left")
|
||||
|
||||
title = _ins(inscriptions, 2)
|
||||
if doc_type == 1:
|
||||
title = f"{title} ПЭ3".strip()
|
||||
elif doc_type == 4:
|
||||
title = f"{title} ВП".strip()
|
||||
elif doc_type == 3:
|
||||
title = _ins(inscriptions, 1001)
|
||||
_txt(c, x + 65 * mm, y + mh - 15 * mm, 120 * mm, 15 * mm, title, font, 18)
|
||||
|
||||
_txt(c, x + 135 * mm, y + mh - 15 * mm, 15 * mm, 5 * mm, "Лит.", font, 12)
|
||||
_txt(c, x + 150 * mm, y + mh - 15 * mm, 15 * mm, 5 * mm, "Лист", font, 12)
|
||||
_txt(c, x + 165 * mm, y + mh - 15 * mm, 20 * mm, 5 * mm, "Листов", font, 12)
|
||||
littera = _ins(inscriptions, 4)
|
||||
for i in range(min(3, len(littera))):
|
||||
_txt(c, x + (135 + 5 * i) * mm, y + mh - 20 * mm, 5 * mm, 5 * mm, littera[i], font, 12)
|
||||
if total > 1:
|
||||
_txt(c, x + 150 * mm, y + mh - 20 * mm, 15 * mm, 5 * mm, str(page), font, 12)
|
||||
_txt(c, x + 165 * mm, y + mh - 20 * mm, 20 * mm, 5 * mm, str(total), font, 12)
|
||||
_txt(c, x + 135 * mm, y + mh - 35 * mm, 50 * mm, 15 * mm, _ins(inscriptions, 9), font, 12)
|
||||
|
||||
fmt = "Формат А3" if doc_type == 4 else "Формат А4"
|
||||
_txt(c, x + 135 * mm, y, 50 * mm, 5 * mm, fmt, font, 12, "left")
|
||||
|
||||
|
||||
def _draw_stamp_other(c: canvas.Canvas, inner: Inner, inscriptions: dict[int, str], doc_type: int, page: int, font: str) -> None:
|
||||
mw, mh = 185 * mm, 15 * mm
|
||||
x = inner.right - mw
|
||||
y = inner.bottom
|
||||
c.rect(x, y, mw, mh)
|
||||
for dx in (7, 17, 40, 55, 65, 175):
|
||||
c.line(x + dx * mm, y, x + dx * mm, y + mh)
|
||||
c.line(x, y + 5 * mm, x + 65 * mm, y + 5 * mm)
|
||||
c.line(x, y + 10 * mm, x + 65 * mm, y + 10 * mm)
|
||||
c.line(x + mw - 10 * mm, y + 7 * mm, x + mw, y + 7 * mm)
|
||||
|
||||
_txt(c, x, y, 7 * mm, 5 * mm, "Изм.", font, 12)
|
||||
_txt(c, x + 7 * mm, y, 10 * mm, 5 * mm, "Лист", font, 12)
|
||||
_txt(c, x + 17 * mm, y, 23 * mm, 5 * mm, "№ докум", font, 12)
|
||||
_txt(c, x + 40 * mm, y, 15 * mm, 5 * mm, "Подп.", font, 12)
|
||||
_txt(c, x + 55 * mm, y, 10 * mm, 5 * mm, "Дата", font, 12)
|
||||
_txt(c, x, y + 5 * mm, 7 * mm, 5 * mm, _ins(inscriptions, 14), font, 12)
|
||||
_txt(c, x + 7 * mm, y + 5 * mm, 10 * mm, 5 * mm, _ins(inscriptions, 15), font, 12)
|
||||
_txt(c, x + 17 * mm, y + 5 * mm, 23 * mm, 5 * mm, _ins(inscriptions, 16), font, 12)
|
||||
_txt(c, x + 40 * mm, y + 5 * mm, 15 * mm, 5 * mm, _ins(inscriptions, 17), font, 12)
|
||||
_txt(c, x + 55 * mm, y + 5 * mm, 10 * mm, 5 * mm, _ins(inscriptions, 18), font, 12)
|
||||
|
||||
title = _ins(inscriptions, 2)
|
||||
if doc_type == 1:
|
||||
title = f"{title} ПЭ3".strip()
|
||||
elif doc_type == 4:
|
||||
title = f"{title} ВП".strip()
|
||||
elif doc_type == 3:
|
||||
title = _ins(inscriptions, 1001)
|
||||
_txt(c, x + 65 * mm, y + mh - 15 * mm, 110 * mm, 15 * mm, title, font, 24)
|
||||
_txt(c, x + 65 * mm, y, 110 * mm, 5 * mm, "Копировал", font, 12, "left")
|
||||
fmt = "Формат А3" if doc_type == 4 else "Формат А4"
|
||||
_txt(c, x + 65 * mm, y, 110 * mm, 5 * mm, fmt, font, 12, "right")
|
||||
_txt(c, x + mw - 10 * mm, y + mh - 7 * mm, 10 * mm, 7 * mm, "Лист", font, 12)
|
||||
_txt(c, x + mw - 10 * mm, y, 10 * mm, 8 * mm, str(page), font, 12)
|
||||
|
||||
|
||||
def draw_gost_frame(
|
||||
c: canvas.Canvas,
|
||||
page_w: float,
|
||||
page_h: float,
|
||||
inscriptions: dict[int, str],
|
||||
table_type: str,
|
||||
page: int,
|
||||
total: int,
|
||||
font: str,
|
||||
) -> None:
|
||||
"""Draw GOST A4/A3 frame matching desktop layout."""
|
||||
doc_type = DOC_TYPES.get(table_type, 1)
|
||||
display_total = total + 1 # desktop increments for registration sheet
|
||||
inner = Inner(page_w, page_h)
|
||||
_draw_inner_border(c, inner)
|
||||
sx, sy, sw, sh = _draw_left_binding_strip(c, inner)
|
||||
_draw_left_binding_text(c, sx, sy, inscriptions, font)
|
||||
|
||||
if page == 1:
|
||||
_draw_top_left_strip(c, inner, inscriptions, doc_type, font)
|
||||
_draw_stamp_first(c, inner, inscriptions, doc_type, page, display_total, font)
|
||||
else:
|
||||
_draw_stamp_other(c, inner, inscriptions, doc_type, page, font)
|
||||
@@ -286,6 +286,54 @@ def resolve_where_used(db: Session, project_id: int, mappings: list[str]) -> str
|
||||
return ""
|
||||
|
||||
|
||||
def get_font_stretch(db: Session, project_id: int, table_type: str) -> int:
|
||||
doc = DOC_TYPE_KEYS.get(table_type, table_type)
|
||||
raw = _get_setting(db, project_id, f"fontStretch_{doc}")
|
||||
if not raw:
|
||||
return 100
|
||||
try:
|
||||
return int(raw)
|
||||
except ValueError:
|
||||
return 100
|
||||
|
||||
|
||||
def save_cell_overflows(
|
||||
db: Session, project_id: int, table_type: str, overflows: list[dict[str, Any]]
|
||||
) -> None:
|
||||
merged: dict[str, set[str]] = {}
|
||||
for item in overflows:
|
||||
key = str(item.get("row_id") or item.get("row_index"))
|
||||
merged.setdefault(key, set()).add(item["field"])
|
||||
payload = {k: sorted(v) for k, v in merged.items()}
|
||||
_set_setting(db, project_id, f"cell_overflow_{table_type}", json.dumps(payload, ensure_ascii=False))
|
||||
db.commit()
|
||||
|
||||
|
||||
def get_cell_overflows(db: Session, project_id: int, table_type: str) -> dict[str, list[str]]:
|
||||
raw = _get_setting(db, project_id, f"cell_overflow_{table_type}")
|
||||
if not raw:
|
||||
return {}
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
if isinstance(data, dict):
|
||||
return {str(k): list(v) for k, v in data.items()}
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return {}
|
||||
|
||||
|
||||
def clear_cell_overflows(db: Session, project_id: int, table_type: str) -> None:
|
||||
row = db.scalar(
|
||||
select(ProjectSetting).where(
|
||||
ProjectSetting.project_id == project_id,
|
||||
ProjectSetting.key == f"cell_overflow_{table_type}",
|
||||
)
|
||||
)
|
||||
if row:
|
||||
db.delete(row)
|
||||
db.commit()
|
||||
|
||||
|
||||
def resolve_decimal_and_board(
|
||||
db: Session, project_id: int, project_decimal: str, project_board: str, table_type: str
|
||||
) -> tuple[str, str]:
|
||||
|
||||
@@ -20,6 +20,8 @@ from app.models import (
|
||||
VedomostRow,
|
||||
)
|
||||
from app.services.project_settings import (
|
||||
clear_cell_overflows,
|
||||
get_cell_overflows,
|
||||
get_column_mappings_list,
|
||||
get_project_params_map,
|
||||
get_simple_list_settings,
|
||||
@@ -123,7 +125,14 @@ def get_rows(db: Session, project_id: int, table_type: str) -> list[dict[str, An
|
||||
rows = db.scalars(
|
||||
select(model).where(model.project_id == project_id).order_by(model.row_index)
|
||||
).all()
|
||||
return [row_to_dict(r) for r in rows]
|
||||
overflow_map = get_cell_overflows(db, project_id, table_type)
|
||||
result = []
|
||||
for r in rows:
|
||||
data = row_to_dict(r)
|
||||
key = str(data.get("id") or data.get("row_index"))
|
||||
data["overflow_fields"] = overflow_map.get(key, [])
|
||||
result.append(data)
|
||||
return result
|
||||
|
||||
|
||||
def replace_rows(db: Session, project_id: int, table_type: str, rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
@@ -172,6 +181,7 @@ def generate_table(
|
||||
if table_type not in TABLE_TYPES:
|
||||
raise ValueError(f"Unknown table type: {table_type}")
|
||||
|
||||
clear_cell_overflows(db, project.id, table_type)
|
||||
mappings = _mappings_dict(db, project.id)
|
||||
components = load_components_for_variant(db, project)
|
||||
col_mappings = get_column_mappings_list(db, project.id, table_type)
|
||||
|
||||
@@ -234,6 +234,10 @@ export default function ProjectPage() {
|
||||
}, [tableType, project, loadTable, loadTableSettings]);
|
||||
|
||||
const columns = useMemo(() => COLUMNS[tableType] || [], [tableType]);
|
||||
const overflowCount = useMemo(
|
||||
() => rows.reduce((n, r) => n + (r.overflow_fields?.length || 0), 0),
|
||||
[rows]
|
||||
);
|
||||
|
||||
async function onUpload(e, method = "POST") {
|
||||
const file = e.target.files?.[0];
|
||||
@@ -620,11 +624,30 @@ export default function ProjectPage() {
|
||||
Excel
|
||||
</button>
|
||||
{tableType !== "simple_list" && (
|
||||
<button disabled={busy} onClick={() => api.exportDoc(projectId, tableType, "pdf")}>
|
||||
<button
|
||||
disabled={busy}
|
||||
onClick={async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
await api.exportDoc(projectId, tableType, "pdf");
|
||||
await loadTable();
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
PDF
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{overflowCount > 0 && (
|
||||
<div className="overflow-banner">
|
||||
Переполнение ячеек: {overflowCount}. Красным отмечены поля, не влезающие в PDF даже с поджимом.
|
||||
Сократите текст или уменьшите поджим в настройках.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tableSettings?.fields?.length > 0 && (
|
||||
<div className="settings-panel stack">
|
||||
@@ -714,7 +737,10 @@ export default function ProjectPage() {
|
||||
<tr key={row.id || idx} className={row.is_header ? "header-row" : ""}>
|
||||
<td className="muted">{row.row_index ?? idx}</td>
|
||||
{columns.map(([key]) => (
|
||||
<td key={key}>
|
||||
<td
|
||||
key={key}
|
||||
className={row.overflow_fields?.includes(key) ? "cell-overflow" : ""}
|
||||
>
|
||||
<input
|
||||
value={row[key] ?? ""}
|
||||
disabled={row.is_empty}
|
||||
|
||||
@@ -68,6 +68,14 @@ table.data input {
|
||||
}
|
||||
table.data input:focus { border-color: var(--accent); background: #fff; }
|
||||
table.data tr.header-row td { font-weight: 700; text-decoration: underline; }
|
||||
table.data td.cell-overflow input { background: #ffd5d5; }
|
||||
.overflow-banner {
|
||||
background: #fff0f0;
|
||||
border: 1px solid #e8a0a0;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.tabs { display: flex; gap: 0.35rem; flex-wrap: wrap; margin-bottom: 0.75rem; }
|
||||
.tab {
|
||||
background: #e7e0d2; color: var(--ink); border-radius: 999px; padding: 0.35rem 0.8rem;
|
||||
|
||||
Reference in New Issue
Block a user