Commit Graph
12 Commits
Author SHA1 Message Date
whqreza b6a1ce2ae7 feat(procurement): support multi-project line item split allocation and merge on reopen
- Add ProjectAllocationIn and allocations field to ItemDecisionIn schema
- Implement line item split allocation across projects in accept_quote
- Replicate all ItemTermsheet options and LineItemSpec records to split rows
- Ensure QuoteProjects and LineItemProjects relations are updated
- Add validation against duplicate project selection and decimal sum mismatches
- Implement _merge_split_line_items_on_reopen to consolidate rows when reverting status to offered
2026-09-23 22:02:24 +03:30
ZCode b469238153 refactor(catalog): reduce the category taxonomy to a supplier label
Categories are kept only as the label each supplier carries (suppliers.category_id
+ the material_categories list it points at). Everything that treated category as
a shared key across materials, inventory, quotes and unit conversions is removed —
including two features that were never populated (0 rows on every volume).

Removed (backend):
- routers category_attributes, category_ref_units, unit_conversions + registration
- ?category_id= filters on /materials, /inventory and /quotes (the /suppliers one
  stays — it filters suppliers by their own label)
- GET /inventory/materials/options: required a category and had no caller
- the material-payload category derivation (expand_service.category_by_material,
  the inventory serializer lookup) and the material category on /pm/on-the-way;
  the wire keys stay, now always null, so the payload shape is unchanged
- startup DDL: DROP the empty category_attributes / category_reference_units /
  unit_conversions tables (idempotent) and their CREATE statements; seed.py
  truncate list updated

Removed (frontend):
- the category-keyed conversion path: refUnitForCategory / findConversion /
  refConverted, the categoryUnits + unitConversions store slices, api clients,
  hooks and the "≈ converted quantity" line on quote detail and project pages
  (it needed rows in those tables to render, and there were none)
- material.supplier_category and its read sites (the edit modal grew one
  yesterday to restore labels; the label lives on the supplier now)
- the inventory page's category chips, the row filter and the «دسته» column,
  plus the lookup's categories fetch and catName helper

Kept: supplier label + its list and picker, /suppliers?category_id=, the global
standard_conversions feature and the units page (material_units conversions).

Verified against a restore of the production dump: startup DDL drops exactly the
three tables, suppliers keep their label (3/3), material_categories still serves
its 8 rows, the removed endpoints 404, and materials/inventory/quotes/units/
standard-conversions all return 200 with quote line items intact.
2026-09-20 19:44:23 +00:00
MMD b6cbcc30a5 Merge prod_v5 (catalog v2.7, warehouse locking, per-project accept) into integration
Conflict in init_db.py resolved keeping the audit_log table ensure
function (new-one) — prod_v5 predates it and its side was empty.
Both sides' SQL lands: audit_log table plus the category-free materials
model.
2026-09-19 18:36:29 +03:30
whqreza 65758b5e78 fix(projects): isolate quote acceptance status per project (decision-target based)
Bug: a multi-project quote accepted for «تست ۱» only also appeared under «تست ۲»'s پذیرفته شده column, because both the Projects page and /projects/{id}/stats classified quotes by their GLOBAL status over any project link.

Rule now: a quote counts as accepted/arrived for a project ONLY if at least one non-rejected line item's decision targets that project (item_decisions.project_id). A quote merely linked as a candidate — including one accepted for a different project — is categorized exclusively under پیشنهاد شده for that project. Legacy quotes without any decision targets keep the old global-status classification.

Backend: project_stats membership now spans all link kinds (legacy mirror, quote_projects, line_item_projects, decision targets) with the per-project decision-based bucketing above; bucket VALUES follow the same isolation (accepted/arrived value counts only items targeting the project; candidate items of a still-proposal quote stay in the offered bucket); the raw path string is normalized to UUID for the Python-side comparisons. /quotes?project_id=… now matches any relevant link kind instead of only the legacy mirror column.

Frontend: ProjectsPage gains acceptedForProject/hasTargets helpers; the three columns (پیشنهاد شده / پذیرفته شده / رسیده) and their value chips use per-project items (target-project items first, then candidates), so totals no longer leak other projects' items.

Verified end-to-end (2 projects × 1 quote × 2 items, accept item-1 for P1 only): P1 accepted=1 then arrived=1 after the PM receipt; P2 stays offered=1 / accepted=0 / arrived=0 at every step; /quotes?project_id=P2 still lists the quote. All fixture data removed; 7/7 checks pass. python compile + api/frontend builds clean.
2026-09-19 15:11:57 +03:30
whqreza 8adfd2267f refactor(catalog)!: decouple materials from categories (v2.7)
Materials are now global and category-free. The category lives only on the supplier (suppliers.category_id); every category-facing query goes through the supplier edge instead of the material row.

Schema (migration.sql + init_db DDL):
- drop the materials.category_id column, its FK, and the v2.5 first-link-wins automation (fn_derive_material_category + trg_supplier_materials_derive_category) — the DDL runs idempotently at API startup

Backend:
- GET /materials?category_id=… now filters via EXISTS(supplier_materials ⋈ suppliers.category_id)
- quote category filters (quotes list) use the quote's supplier category
- /pm/on-the-way derives the display category from the supplier of the quote
- supplier_materials links no longer derive/return a material category
- expand_service: material expand=category resolves to null (kept for contract compatibility)
- material create/update no longer validates or stores category_id; seed.py aligned

Frontend:
- material creation no longer sends category_id
- formatMaterialTitle reworked to a strict order with « . » separator: [Brand] . [Material] . [Spec 1] . [Spec 2], brand deduped when the stored name already starts with it; specs separator switched from « · » to « . »

Validated: python compile, api + frontend image builds (tsc + vite) clean; branch cut from the tip of prod_v4 (= origin/production, 5a9ef40 — the team's merge made them identical).
2026-09-19 13:19:26 +03:30
MMD e17d1f0883 feat(monitoring): observability stack + app instrumentation
- /health/ready (DB+MinIO+Casdoor checks, webhook post-deploy gate),
  /metrics via prometheus-fastapi-instrumentator, Sentry/GlitchTip capture,
  X-Request-ID middleware + JSON access logging (PROCUREMENT_LOG_FORMAT)
- server-side analytics (OpenPanel Track API): login, quote lifecycle,
  receipts — fire-and-forget, disabled without env config
- monitoring/ compose project: GlitchTip(+pg), Prometheus(+alerts),
  Grafana(provisioned ERP dashboard), Loki+Promtail, node/postgres
  exporters, cAdvisor — mem-limited (~2.2GB), loopback admin UIs,
  separate from app deploys; erp_shared network for scraping
- frontend: @openpanel/web + @sentry/react (build-arg gated, no-op when
  unset), funnel events (login, wizard steps, quote submitted)
- compose: api healthcheck, observability env wiring, .env.example keys
2026-09-18 22:18:02 +03:30
whqreza 5a9ef408ff feat(catalog): auto-purge orphan materials when quotes/items release them
A material that ends up referenced by nothing (no line_items anywhere, no warehouse stock) was previously left behind forever, accumulating dead catalog rows as offers were edited or deleted.

- new quote_service.cleanup_orphan_materials(db, material_ids): for each candidate it checks usage across line_items (all quotes) and warehouse inventory; only when both are zero does it remove the auxiliary rows (supplier_materials, brand_materials, material_units, material_attribute_values) and finally the materials row; returns the purged ids
- replace_quote_children (edit modal): snapshots the quote's material ids before the children are replaced, diffs against the incoming items, and purges whatever was detached
- delete_quote: collects the quote's material ids + attachment keys first, deletes the quote, purges orphans inside the transaction, and only then (post-commit) removes the MinIO objects — now both the recorded attachment keys and a recursive remove_prefix(quote_id/) sweep, so stray objects under the quote folder cannot survive; the response's cascaded block reports purged_orphan_materials
- line_items: PATCH that switches a line item's material, and DELETE of a line item, both clean up the previously referenced material
- storage.remove_prefix(prefix): best-effort recursive object deletion under a prefix (sync MinIO client wrapped in asyncio.to_thread, same pattern as remove_object)

Safety: usage checks run against the whole system before any delete, so shared materials — including those held only by warehouse stock — are never touched. Verified: python compile + api image build clean.
2026-09-17 18:04:57 +03:30
whqreza 43ff048899 fix(wizard,quotes,dates): reliable error routing, single-project accept, Persian date/time input
Wizard:
- failed saves now route to the exact step+field: a new data-field attribute on every required section (supplier_id, line_items, termsheets, project, quote_date, expiry_date, notes, proforma) plus resolveErrorTarget() that maps ApiError.field / message keywords (supplier, line item/material/unit/quantity, termsheet/schedule/milestone, project, dates, notes, file) to the offending step — the «رفع خطا» toast action scrolls/focuses the right control instead of guessing
- material records are no longer created on the fly while typing (rememberMaterial no longer fires addMaterial per blur); materials/specs/units are written once during the final save, avoiding half-created catalog rows

Accept flow:
- quote_service no longer rewrites LineItemProjects on accept — those candidate-project rows stay 100% untouched and only item_decisions.project_id is written, matching the per-item target-project model and the UI payload (item_projects: [])

Dates/time:
- JalaliDatePicker: hour/minute are now editable text fields (Persian digits, clamping, 3-digit retype handling) with a controlled useEffect that only rewrites the inputs when the numeric value actually changes — no more wiping in-progress typing; date-only detection tightened to a full YYYY-MM-DD match so datetime strings are not misread

Also: /api validation logging (RequestValidationError) now includes the path and error list for faster diagnosis.

Verified: api + frontend image builds (python, tsc, vite) clean on top of origin/production (incl. the team's mobile layout merges).
2026-09-17 16:13:27 +03:30
whqreza 5084623be5 feat(quotes): lock quotes with posted receipts; per-item target project + title dedup
Quotes that already have warehouse receipts can no longer be edited or rolled back:
- backend: quote_has_receipts() guard — replace_quote_children and every backward status transition now return 409 with «این پیشنهاد به‌دلیل ثبت رسید در انبار، غیرقابل ویرایش یا بازگشت است.»
- reopening to «offered» additionally clears item_decisions.project_id (the purchased-project link belongs to acceptance, not to a reopened offer)
- frontend EditModal: hasReceipts locks the whole form — disabled inputs and date pickers (JalaliDatePicker/NumberInput/MilestoneRow/MultiSelectCombobox gained disabled support), add/remove controls for items, termsheets and milestones hidden, status change blocked, amber warning banner; quote deletion now goes through a confirmation dialog (useDeleteQuote)
- date-only semantics: JalaliDatePicker keeps/emits YYYY-MM-DD when withTime=false (typing, «امروز», day selection) and toDateOnly no longer mangles pure date strings

Per-item target project (candidate projects stay intact):
- expand_service exposes the decision target project (decision.project + line_item.target_project)
- the accept payload derives item_decisions.project_id from that target project and sends item_projects: [] so line_item_projects keeps the original candidates untouched
- AcceptModal/EditModal select the target project per line item

Material titles:
- quoteUtils.formatMaterialTitle() composes one normalized, deduplicated «name + brand + specs» title, replacing materialLabel/materialSpecs/specsLabel usage across dashboard cards, quote detail and the modals

Verified: python compile + api/frontend image builds (tsc + vite) clean.
2026-09-16 19:14:06 +03:30
whqreza e8154ae6a6 fix(quotes): derive decision.project_id from accept item_projects
Multi-project quotes: the manager picks one project per line item in the
accept modal (item_projects), but accept_quote only auto-filled
decision.project_id from existing line_item_projects (≤1). For items
without a single existing line-project the decision stayed NULL, so
/ pm/on-the-way (filtered by ItemDecision.project_id) never showed those
items to any project manager — they accumulated only in the global admin
list. Now derivation order is: explicit decision.project_id →
payload.item_projects (single) → existing line_item_projects (single) →
NULL (office-only, never misattributed). Verified E2E: two items of one
quote accepted to two projects appear in exactly their own PM's
on-the-way.
2026-09-08 14:41:57 +03:30
ali_mohaghegh 068c51909c fix(db): fill item_receipts.confirmed_by in office paths; resilient per-statement v2.6 upgrade (root cause of 500 on receipt) 2026-08-27 15:04:25 +03:30
ali_mohaghegh 170aed66ea feat(backend): add FastAPI backend — procurement, inventory domain, Casdoor auth, PM arrival endpoints 2026-08-25 19:08:14 +03:30