Commit Graph

274 Commits

Author SHA1 Message Date
13421b55bd feat(help-ui): add HelpPanel Drawer + TopBar button + markdown renderer (HELP-MODULE-P1)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-11 00:16:26 +03:00
c1ce29c55e feat(help): add HelpArticle model + CRUD endpoints + 5 seeded articles (HELP-MODULE-P0)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-10 23:23:52 +03:00
d5799d935a fix(contracts): add 3 safety guards to applyContractTerms (CONTRACT-MERGE-FIX)
- Guard 1: refuse contract terms if PO has lots (cashflow double-count)
- Guard 2: refuse overwrite of MANUAL schedule without force=true
- Guard 3: idempotent no-op if same contract already applied
- Add POPaymentScheduleSource enum (MANUAL/AUTO_FROM_CONTRACT/AUTO_FROM_TLT)
- upsertPaymentSchedule marks source=MANUAL, clears supplierContractId
- Endpoint returns 409 with reason mapping in Russian
- Frontend badge "Из договора #X" + Modal.confirm for force overwrite

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-10 21:31:30 +03:00
c2a949f838 feat(contracts): unify client/supplier contracts in Документооборот hub (CONTRACT-MERGE)
Phase 1: SupplierContractPaymentTerm model + SupplierPaymentTrigger enum,
terms support in supplier-contracts CRUD, validation sum=100%.
New tabs "Договора с Заказчиками" + "Договора с Поставщиками" replace
old "Контракты" + "Договоры поставщиков". Sidebar entry removed,
legacy redirects in place.

Phase 2: Auto-generate POPaymentSchedule from supplier contract terms
at PO confirm time. New endpoint POST /purchase-orders/:id/apply-contract-terms.
PurchaseOrder.supplierContractId FK links PO to contract. "Из договора"
button in PO drawer for manual application.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-10 21:04:55 +03:00
1a449a29a8 fix(weather): proxy Open-Meteo through backend to avoid CORS issues
Frontend now calls /api/v1/weather (backend proxy) instead of Open-Meteo
directly. Backend caches responses for 10min server-side.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-10 12:56:17 +03:00
80f56676ea feat(homepage): redesign dashboard + top bar + audit coverage (HOMEPAGE-V2)
- Add persistent TopBar with Russian date, ISO week number, Tver weather (Open-Meteo)
- Replace sidebar text with МД monogram SVG logo
- Redesign Dashboard: brand banner + real-time activity feed (50 events, 30s polling)
- Open GET /audit to all authenticated users (stats/export remain ADMIN-only)
- Add audit hooks in 7 services: ORDER_CREATE, SMETA_IMPORT, SMETA_REVISION_APPROVE,
  TENDER_CREATE, TENDER_AWARD, PR_SUBMIT, PO_CONFIRM, UPD_SIGN
- Configure dayjs relativeTime + isoWeek plugins

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 14:19:16 +03:00
0c222183b6 feat(estimates): add deterministic validation to smeta import (SMETA-VAL-1)
P0 structural validation (before AI) catches corrupted/missing sheets
without paying 15s of AI cost. P2.5 arithmetic validation (after parse)
adds 7 deterministic checks merged into PostVerificationReport.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 13:17:33 +03:00
4d8b446091 fix(financial): KPI forecast-expenses/kpis uses days param for exact horizon
getForecastExpenseKpis was using months=1 even for 7-day view, showing
expenses due May 28 in the "7 дн" card. Now uses days parameter for
precise horizon filtering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 21:05:36 +03:00
7b3082304e fix(financial): horizon-aware KPI cards + PENDING PRs in chart forecast
- Backend: include PENDING PaymentRequests in cashflow forecast as expense forecast
- Backend: fix APPROVED PR query (add paidAt: null filter)
- Backend: getIncomeKpis accepts optional days parameter
- Frontend: KPI cards labels and data follow selected chart horizon (7d/30d/3m)
- Frontend: merge "Расходы" and "Расходы прогноз" cards into one horizon-aware card
- Frontend: remove unused expense7d state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 20:54:06 +03:00
6c65a4c5e3 fix(financial): dashboard pendingPaymentTotal must exclude paid PRs
Added paidAt: null filter to approved PR query — previously counted
already-paid PRs in the "Расходы (7 дн)" KPI card.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 20:44:47 +03:00
8d3fca9885 feat(financial): CASHFLOW-FORECAST-V2 expenses forecast tab + cashflow chart fusion
- Backend: ForecastExpensesService (3 sources: pending PR, approved PR, orphan PO installments)
- Backend: 2 routes GET /forecast-expenses + /forecast-expenses/kpis
- Backend: expenseBreakdown {validated, forecast} added to cashflow forecast weeks
- Frontend: ExpensesForecastTab with 4 KPI cards, filters, sortable table
- Frontend: CashflowTab chart enhanced with 4 stacked bar series (income/expense x fact/forecast)
- Frontend: KPI card "Расходы прогноз (30 дн)" on Календарь ДДС
- Frontend: Chart.js legend enabled, tooltip shows expense breakdown

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 18:13:12 +03:00
90f382af6b fix(financial): ACTUAL-X-FIX-1 propagate orderId via PaymentRequest chain
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 17:53:30 +03:00
3e33222c6d feat(planning): PLANNING-DETAIL drill-down drawers for delivery and planned lots
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 16:47:04 +03:00
51bda9310d chore(scripts): add ACTUAL-X-VERIF auto-check script
Fix update hook: recompute actuals when PAID entry amount/code changes
(not just on status transitions). 15/15 tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 15:46:15 +03:00
1fb83f3384 feat(financial): ACTUAL-X-IMPL populate FinancialTracking.actualX from PAID cashflows
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 15:39:49 +03:00
ff9fddd1da feat: SUPPLIER-CONTRACT-1 + FIX-IMG-INV-UI + paste image
SUPPLIER-CONTRACT-1: SupplierContract CRUD (DRAFT/SIGNED/EXPIRED),
Document sync, isRegular flag + placeholder documents for regular
suppliers without contracts, daily cron scan, entity-documents
integration, file-tracking integration, frontend tab + supplier
detail isRegular switch.

FIX-IMG-INV-UI: 6 visual fixes — thumbnail fallbacks, deep-links
with ?tab=, payment status on invoices, action button UX.

ProductImageUploader: Ctrl+V paste from Excel (kind=file filter).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-29 15:34:40 +03:00
9106fe40cd feat(estimates): ORDER-IMG-2 product image extraction from smeta
- smeta-image-extractor.ts: anchor-based extraction from Расчет sheet
  (top-left image by row*100+col sort, pure zip XML parsing)
- smeta-image-attach.service.ts: best-effort orchestration with
  dedup hash, manual-override respect, never-throw guarantee
- Hook in importXlsx after transaction (non-blocking)
- Removed old extractProductImage/saveProductImage heuristic methods
- Cleaned up unused AdmZip/fs/path/STORAGE_PATH imports

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-29 11:52:01 +03:00
4027cf0ee9 feat(client-invoices): INVOICE-DOC-SYNC sync ClientInvoice with Document
- create() wraps in $transaction, creates Document INVOICE/CLIENT alongside
- update() propagates invoiceNumber change to Document title/reference
- issue() sets Document.sentAt
- cancel() sets Document.closedAt
- delete() removes Document before deleting ClientInvoice
- Backfill script for existing invoices (idempotent)
- Downgrade @fastify/static to v7.0.4 (Fastify 4 compat)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-29 11:33:59 +03:00
14408eedd8 feat: ORDER-IMG-3 frontend product image display in 3 locations
- GeneralTab: ProductImageUploader with upload/delete/replace
- OrdersPage: 40x40 thumbnail column prepended before Код
- PlanningOverviewPage: 32x32 thumbnail in Заказ column
- Backend: @fastify/static serves /storage/ prefix, planning service returns productImagePath/Hash
- Vite: /storage/ proxy for dev mode
- Utils: getProductImageUrl with cache-busting hash

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-29 11:06:32 +03:00
9884caae57 feat(orders): ORDER-IMG-1 product image upload backend
Add 4 fields to Order model (productImagePath/Mime/Hash/Source).
New ProductImageStorage class with magic-byte MIME detection (file-type),
SHA-256 hash, 5MB limit, PNG/JPEG/WebP only.
Routes POST/DELETE /:id/product-image in orders.routes.ts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-29 10:43:08 +03:00
e53e122e95 fix: delivery ratio badges showing 0% despite DELIVERED lots visible
computeReceivedQtyV2 only used lot-based fallback when a poLine had
zero receptions. When some receptions existed alongside DELIVERED lots
on the same poLine, those lots were ignored. Now uses hybrid approach:
count all receptions + add DELIVERED lots not covered by any reception.
Same fix applied to inline calculation in getProcurementTimeline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-28 16:37:00 +03:00
cbe0b89b07 feat: planning risk badges — purchase status + delivery ratio per position/category
Add visual risk indicators to planning page: two colored dots (purchase
status NONE/SENT/RECEIVED + delivery completion %) at position and
category levels. Clean up parent order timeline lane to show only
order-level events (created, forecast, today, deadline).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-28 13:56:25 +03:00
8cd97ebff7 feat: PO-POSTPAY-AUTO — auto PaymentRequest on SDN sign + postpay modes
- PostpayMode enum (SIMPLE/PER_DELIVERY) on POPaymentSchedule
- isPostDelivery flag on POInstallment (auto-set at creation)
- SDN.sign() → triggerPostpayOnSdnSign: creates/recalculates PaymentRequest
  - SIMPLE: 1 PR per postpay installment, dueDate = signedDate + postDeliveryDays
  - PER_DELIVERY: 1 PR per SDN, amount = deliveredAmount × postpayPercent/100
- SDN.cancel() → rejects PENDING/APPROVED PRs, preserves PAID PRs
- Frontend: postpayMode Radio in PO drawer, "После отгрузки" tag on installments
- Rename УПД tabs: исходящие → Клиенты, входящие → Поставщики
- Backfill: 2 installments marked isPostDelivery=true

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-28 13:27:57 +03:00
2a7e5ebd6d feat: auto-transition PlannedDeliveryLot status on DeliveryLot changes
Add recalcPDLStatus() in DeliveryLotsService with hooks on create,
update, cancel, and delete. Relation is 1:1 (@unique plannedLotId).

Logic: no active DL → PLANNED, DL.amount >= amountSnapshot →
FULLY_SHIPPED, else → PARTIALLY_SHIPPED. CANCELLED is terminal
(never recalculated). Option A: all non-CANCELLED DLs count
(including DRAFT).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-28 11:36:47 +03:00
3fbb92b2aa fix: FinancialTracking.budgetX × Order.quantity — 3 sites + backfill
Bug: budgetX stored per-unit smeta costs instead of totals, causing
absurd overrun percentages (7140% for qty=1700, 3831% for qty=100).

Fix 3 write sites:
- estimates.service.ts syncFinancialTracking: .mul(qty)
- financial.service.ts applyRevision: revision.delta*.mul(qty)
- revisions.service.ts applyRevisionToBudget: (smeta + deltas) * qty

financial.service.ts initFromSmeta was already correct (L36-40).
committedX/actualX from PO.totalAmount — already totals, no fix needed.

Backfill: 2 FTs corrected, alertLevel RED→NONE for both.
Script is one-shot non-idempotent (never re-run).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-28 11:23:57 +03:00
44635e4a04 feat: PO delivery date mandatory + planning UX fixes + backfill
- PO confirm: require expectedDeliveryDate for direct POs, auto-create
  lots+LotLines when confirming PO without any lots
- createLotsForPO: no longer skips items without requirementId, creates
  PurchaseLotLine records for each lot
- generateFromTender: pre-fill expectedDeliveryDate from offerDeliveryDays
- Frontend: disable confirm button without delivery date set
- Planning: diamond size d=7 for PDL forecasts (visually distinct from
  7×7 reception squares), fix empty status filter Validation error
- Backfill script: po-delivery-backfill.ts (2 POs fixed)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-27 20:52:27 +03:00
69aeab3626 refactor(planning): use S53A Option B link (PR→POLine→LotLine→Lot) instead of PR→Lot
All 3 planning query methods (getOverview, getProcurementTimeline,
getPositionsTimelineByCategory) now traverse via poLines relation
instead of the unreliable pr.lots path (which misses lots with
requirementId=NULL). Removes the orphan "PO-XXX — поставки" lane
and the 2nd-pass PO→lots workaround. Lot events are deduplicated
globally per category to prevent N× multiplication across PRs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-27 19:52:52 +03:00
4ad965bb03 fix(planning): remove duplicated PDL forecasts from category lanes + show PO lots as reception squares
Bug A: PlannedDeliveryLot (client delivery milestones) were injected into every
category lane, creating misleading orange diamonds on Лазер, Крепёж, etc.
Fix: keep PDL only on order-level timeline; remove from category and position levels.

Bug B: PurchaseLots with status=DELIVERED had no visible markers because receptions
were tracked via PurchaseLot.status, not PurchaseLineReception. Lots without
requirementId (linked via PO only) were invisible in the PR→lots query path.
Fix: emit reception_complete (green squares) for DELIVERED lots at category level
via PO→lots query; count DELIVERED lot quantities in progress/summary calculations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-27 19:13:14 +03:00
d340e8070d feat: SupplierDeliveryNote (УПД входящие) — reception-v2 workflow
- SupplierDeliveryNote model + enum DRAFT/SIGNED/CANCELLED in Prisma schema
- SDN service: create (atomic SDN + N receptions), sign (requires file, auto-verifies receptions), cancel (cascade)
- SDN routes: POST /, GET /, GET /:id, POST /:id/sign, POST /:id/cancel
- Guard on receptions.verify: refuses without linked signed УПД
- Receptions no longer auto-verify on create (ACHETEUR role) — verification goes through SDN sign
- Frontend: API client, UpdIncomingTab in Документооборот hub, SDN creation modal in PO drawer
- ReceptionsSection shows УПД column + "Оформить УПД" button alongside legacy "Приёмка без УПД"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-27 19:01:30 +03:00
115266ad6d feat(planning): add PlannedDeliveryLot forecasts + reception squares on timeline
- Backend: enrich getOverview, getProcurementTimeline, getPositionsTimelineByCategory
  with PlannedDeliveryLot events (type=forecast_delivery, CANCELLED filtered server-side,
  FULLY_SHIPPED colored grey, PLANNED/PARTIALLY_SHIPPED orange)
- Frontend OrderTimelineLane: orange diamonds for forecasts, green squares for receptions
- Frontend ExpandedOrderRow: include forecast events in collapsed category summary lanes
- Tooltips show label + status + date for forecast markers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-27 18:39:33 +03:00
5c408db03a fix: PO line finalPrice in all 4 creation paths + tender budget column + variance rounding
- createFromTender: add finalPrice from offer lines (was missing entirely)
- generateFromTender batch path: add finalPrice
- Tender drawer: add "Бюджет (смета)" column showing unit price × qty
- Budget drawer: round variance percentage to integer

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-27 18:19:53 +03:00
4507a9883e fix: PO line prices — estimatedPrice=smeta, finalPrice=offer award price
awardLines/partialAward/generateFromTender now set estimatedPrice from
PR.estimatedPrice (smeta) and finalPrice from offer unitPrice (tender).
Also adds estimatedPrice to tender requirement selects and renames
Счёт-фактура → Счёт на оплату in PO drawer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-27 17:47:11 +03:00
9d4bed05c7 fix(s53a): set originalQuantity at every PR creation point
All 9 purchaseRequirement.create/createMany calls were missing
originalQuantity, causing it to stay null. consume() then set
quantity to 0 with no way to recover the original value.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-27 17:23:14 +03:00
54ffcc91b0 feat(s53a): refactor PR lifecycle — unified consume/restore/revise, POLine FK, cascade cancel
- Prisma: PurchaseRequirement.originalQuantity, PurchaseOrderLine.purchaseRequirementId + consumedQuantity
- New PrMutationService with consume/restore/revise (centralizes all PR quantity mutations)
- tenders.service: awardLines/partialAward use prMutation.consume (no more residual PR creation)
- tenders.service: cancel cascades to PO cancel with line-by-line restore
- purchase-orders.service: cancel accepts opts.tx, restores PRs via consumedQuantity (Option B)
- purchase-orders.service: all create methods set purchaseRequirementId + consumedQuantity on POLines
- smeta-revision.service: ADDITIONAL_COST revises existing PR instead of creating "Доп:" duplicate
- planning/purchases/indirect: use originalQuantity ?? quantity for total calculations
- Eliminated: residual PR creation, notes contains pattern, fragile text matching

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-27 14:51:12 +03:00
90dd68c879 feat(planning): expandable category lanes with position-level timelines
Backend: new endpoint GET /overview/:orderId/category/:category/positions-timeline
returning per-PurchaseRequirement timeline events (reception_partial, reception_complete).
Enriched getProcurementTimeline with per-category summary (firstEvent, lastEvent, counts).

Frontend: +/− toggle on category lanes, folded=2 summary markers with tooltip,
expanded=individual position lanes via CategoryPositionsLanes component.
SessionStorage persistence. Half-circle SVG marker for partial receptions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-27 12:28:29 +03:00
bf3c2b0713 fix(s48): set contractTermId in legacy generateAdvanceInstallmentsForOrder
Aligns legacy generator with new syncOnSmetaLock to prevent future
installments from being orphaned from their contract term, which would
cause syncOnSmetaLock match-and-merge to create duplicates on next
smeta lock event. regenerateForOrder delegates to this method so all
3 call sites (Order.create, linkContract, changeContract) + admin
endpoint are covered by this one-line fix.

Manual backfill applied to existing data:
- UPDATE client_installments SET contract_term_id = 1 WHERE id = 34;
  (Order 4, regenerated via admin endpoint during S48 validation)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-27 11:15:07 +03:00
c69cf3279b feat(s48): syncOnSmetaLock hook — match-and-merge installments at smeta lock
New method OrderInstallmentsGenerator.syncOnSmetaLock() called from both
approveTechnical() and approve() in EstimateService. For each non-SHIPMENT_DATE
contract term, it creates or updates ClientInstallment with the correct amount
(selling_price × qty × percent). Handles paid > new (skip + audit log) and
ambiguous duplicates (skip + notification). Existing regenerateForOrder() and
all its call sites are untouched.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-27 11:03:50 +03:00
3f25fbc420 feat(s47-kpi-1): fix Cashflow KPIs to include ClientInstallment with anti-duplicate logic
- GET /cashflow/income-kpis returns income7d + overdue with breakdown
- income7d: CashflowEntry INCOME (excluding client/order linked) + ClientInstallment remaining
- overdue: ManualEntry1C DZ + ClientInstallment overdue remaining (amount - paid)
- Frontend: fetchCardData() uses new endpoint, overdueKpi state replaces dashboard-only source
- UTC date handling for @db.Date Prisma comparison

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 23:50:57 +03:00
0719d504a1 feat(s46-pdl-4.4): non-blocking duplicate warning on CashflowEntry INCOME with client/order
GET /cashflow/duplicate-check endpoint checks ClientPaymentReceived and
ClientInstallment within ±30d window and ±10% amount tolerance. Frontend
shows warning Alert in payment modal when type=INCOME and orderId set,
debounced at 500ms via Form.useWatch. Warning is non-blocking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 23:27:28 +03:00
7f5114e16c fix(forecast): SHIPMENT_SIGNED matching prefers forecasts linked to same PlannedDeliveryLot
handleShipmentSigned now prioritizes forecast candidates whose plannedLotId
matches the delivery lot's plannedLotId before falling back to date proximity.
Prevents orphan forecasts from being matched instead of PDL-specific ones.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 23:15:31 +03:00
ea3e4e748a feat(s46-pdl-4.3): PlannedDeliveryLot selector in UPD create form
Backend: plannedLotId added to create/update DeliveryLot schemas with
validation (same order, status not CANCELLED/FULLY_SHIPPED).
Frontend: PDL Select in УПД modal with auto-preselect when single active lot.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 23:03:47 +03:00
492d6dace5 feat(s46-pdl-2): ForecastGenerator wired on PlannedDeliveryLot for SHIPMENT_DATE multi-lot
- computeExpectedDate: uses plannedLot.plannedDate as base for SHIPMENT_DATE
- handleOrderCreated: 1 forecast per PDL per SHIPMENT_DATE term, amount proportional to lot quantity
- handleSmetaLocked: recalculates with lot quantity ratio
- PlannedDeliveryLotsService: regenerateForecasts hook on create/update/cancel/delete
- Migration script: preserves REALIZED, backfills plannedLotId on manual forecasts, regenerates 3 orders

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 22:03:04 +03:00
d396f555d7 refactor(s46-pdl-1.5): rename planned_amount → amount_snapshot with physical total semantics
- Prisma: plannedAmount → amountSnapshot (nullable Decimal)
- Schema: added quantity (Decimal, default 1) and sortOrder (Int, default 0)
- Service: computeAmountSnapshot = sellingPrice × order.quantity (full order total)
- Backfill: 2 existing PDLs recalculated from SHIPMENT_DATE portion to full total

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 22:02:54 +03:00
0091e16204 feat(s46-pdl-1): PlannedDeliveryLot model + CRUD + default lot hook + backfill
- Prisma: PlannedDeliveryLotStatus enum (PLANNED/PARTIALLY_SHIPPED/FULLY_SHIPPED/CANCELLED)
- Prisma: PlannedDeliveryLot model with relations to Order, DeliveryLot (1:1), ForecastInstallment
- Prisma: plannedLotId FK on DeliveryLot (@unique) and ForecastInstallment
- Service: CRUD + cancel + delete + createDefaultLotForOrder (auto from contract SHIPMENT_DATE terms)
- Routes: 6 endpoints under /api/v1/planned-delivery-lots (READ: ADMIN+FIN+OPS+COMMERCIAL+COMPTABLE, WRITE: ADMIN+FIN+COMMERCIAL)
- Hook: OrderService.create() creates default PDL before forecast generation
- Backfill: 2 existing orders backfilled with planned lots

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 21:28:33 +03:00
41e8f444d8 feat(s46-pdl-3): SHIPMENT_SIGNED phase closes forecast→installment loop on UPD sign
- Add SHIPMENT_SIGNED to ForecastTriggerPhase enum
- Add contractTermId FK on ClientInstallment (+ index + backfill)
- ShipmentInstallmentsGenerator stores contractTermId at creation
- ForecastGeneratorService.handleShipmentSigned: match forecast→installment
  by contractTermId (strong) or date proximity (degraded), mark REALIZED
- DeliveryLotsService.sign() calls SHIPMENT_SIGNED phase after installments

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 21:07:57 +03:00
47d8578563 feat(planning): add endpoint to list POs by order and material category
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-24 17:52:57 +03:00
dea891105f feat(calendar): add holidays API (public read + admin CRUD)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-24 17:32:40 +03:00
722954776a fix(planning): emit PO events in procurement-timeline endpoint
PurchaseOrders linked via clientOrderId now generate lot_ordered,
lot_planned and lot_delivered events in categoryLanes, grouped by
POLine.material. Only non-CANCELLED, non-DRAFT POs are included.

Previously only PurchaseLot-based events appeared — POs created via
the tender workflow had no timeline representation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-24 16:29:39 +03:00
9033427d23 fix(steel-refs): deduplicate SteelReference, add @@unique, AI self-verification
- Delete old seed (Russian profileType labels) causing weight duplicates
- Consolidate to single seed: 69 entries, 9 canonical types (SQUARE_TUBE etc.)
- Add @@unique([profileType, section]) constraint to prevent duplicates
- findWeightPerMeter prefers CANONICAL_PROFILE_TYPES over legacy labels
- bulkImport blocks duplicate sections across different profileTypes
- AI offer parser: self-verification ±1₽ per line, ±10₽ total, server-side validateRawArithmetic

Root cause: tube 20x20x1.5 had weight 0.885 (old seed) vs 0.841 (correct ГОСТ)
causing 5% quantity/price error in KP analysis.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-24 16:29:19 +03:00
91ccaa9c90 fix(planning): align OrderStatus filter with Prisma enum (11 values)
Zod schema had invalid values (CONFIRMED, DONE) causing 400 on status filter.
Replaced with all 11 actual OrderStatus enum values from schema.prisma.
Frontend now imports statusLabels/allStatuses from OrderStatusTag (DRY).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-24 00:56:49 +03:00