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>
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>
- 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>
- 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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
- 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>
- 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>
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>
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>
Add explicit any types to query result variables and lambda params in 12 files
where as-any on Order select objects broke type inference. 52 errors → 0.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cast order results to any in planning-overview, planning.service, orders.service,
orders.routes. Type Set<string> in tenders.service. Fixes VPS build.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add `as any` casts to Order select/include objects in 36 files to work around
TypeScript conflict between Prisma's Order.constructor relation and JS built-in.
Fix planningOverviewQuerySchema to use actual Prisma OrderStatus enum values
(VALIDATED/LAUNCHED/DELIVERED/ARCHIVED) instead of invalid CONFIRMED/DONE/CANCELLED.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cast selects with `as any` to work around Prisma's constructor property
collision with JS built-in constructor.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New GET /planning/overview with category matrix (5+OTHER), timeline, flags.
New GET /planning/overview/:orderId/procurement-timeline for expanded view.
New CRUD for OrderCategoryNote (PUT/DELETE/GET on /orders/:id/category-notes).
New PATCH /orders/:id/planning-fields for inline editing (5 fields).
Smoke tests all pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
getById() now converts old items[] JSON entries to proper PurchaseOrderLine rows on first access.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
normalizeUnit() now strips superscripts and handles more Russian unit variants (т¹, тн, мп).
pickBestProfile() disambiguates when multiple steel references share the same dimension.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ajout du mécanisme transactionnel de perequation automatique des écarts
entre montant attendu et réalisé sur le dernier prогноз non-lié du même
ordre+triggerPhase. Inversion auditable via ForecastRebalanceLog au unlink.
Modals frontend pour REBALANCE_NO_CANDIDATE et REBALANCE_NEGATIVE_REMAINING.
38/38 tests E2E passent (scénario "1000 chariots").
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Backend: check order.status === IN_PRODUCTION avant création УПД
- Frontend: bouton "+ УПД" grisé + tooltip si order pas IN_PRODUCTION
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Backend getVersions(): retourne {originalName, versions} au lieu d'un array brut
- Frontend uploadedBy: utilise .name (pas firstName/lastName qui n'existent pas)
- Type DocumentFile: corrigé uploadedBy shape
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- delivery-lot direction: SUPPLIER → CLIENT (УПД émis vers client)
- clientId résolu depuis entity (direct ou via Order FK)
- documentReference auto depuis contractNumber/updNumber/invoiceNumber
- sentAt = now() à la création
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9 endpoints: GET/POST/DELETE for client-contracts/:id/documents,
delivery-lots/:id/documents, client-invoices/:id/documents.
Global listing now filters out soft-deleted documents.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
list/create/delete for Document attached to ClientContract, DeliveryLot,
ClientInvoice. Auto-titles from entity fields. Soft-delete cascades to
DocumentFile.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
listDocuments() with full filter support, reuses ContractDocument type.
Backend response aligned to PaginatedResponse shape (items, total, page, limit, totalPages).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add trackingOnly query param (default false) so GET /api/v1/contract-tracking
returns all Documents when unset, and only tracked ones when trackingOnly=true.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>