- 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>
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>
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>
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>
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>
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>
- 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>
- 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>
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>
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>
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>
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>
- 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>
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>