The quotations module in src/cluster_erp/modules/quotations/ holds sales quotations. A quote (Quote) answers a customer RFQ of the crm module and moves through feasibility, design, BOM, costing, and a management approval before it is sent. Its lines carry cost and price, and the header totals and margin are rolled up from them. Sales orders reference quote lines, which can mark a quote as won.
At a glance¶
| Item | Value | Source |
|---|---|---|
| Package | src/cluster_erp/modules/quotations/ |
folder |
| Module class | Module |
src/cluster_erp/modules/quotations/module.py:18 |
name / version |
quotations / 0.1.0 |
src/cluster_erp/modules/quotations/module.py:20-25 |
| API prefix | /api/v1/quotations (ModuleBase.api_prefix) |
src/cluster_erp/core/module_base.py:37-38 |
| Sub-modules | 2: quotes, line_items |
folder listing |
| Tables | 5: quot_quotes, quot_mgmt_approvals, quot_quote_line_items, quot_rejection_reasons, quot_document_flow |
__tablename__ in the two models.py files |
| Entities in metadata | 5 keys in entities |
src/cluster_erp/modules/quotations/module.py:224-230 |
| Routes | 34 route decorators in the package | grep for @...router.get/post/put/patch/delete |
| Actions | 15 ActionMeta entries |
grep "ActionMeta(" in the package |
| Migrations | 20 files | files in migrations/versions/ whose text contains one of the five table names |
| Web app presence | Catalogue app quotations |
src/cluster_erp/modules/app_store/catalog.json (entry quotations) |
| Desktop presence | generic widgets only | src/cluster_erp/desktop/modules/ has no quotations folder |
What the module does¶
- Creates a quote for a customer RFQ, one active quote per RFQ, and takes commercial defaults from the customer (
src/cluster_erp/modules/quotations/quotes/services.py:175-239). - Moves the quote through feasibility, optional design, BOM, and costing (
src/cluster_erp/modules/quotations/quotes/services.py:59-71). - Records a management approval meeting and its decision (
src/cluster_erp/modules/quotations/quotes/services.py:467-549). - Sends the quote after an approval and a completeness check, then marks it won, lost, or expired (
src/cluster_erp/modules/quotations/quotes/services.py:550-686). - Rolls up total price, total cost, and margin from the lines (
src/cluster_erp/modules/quotations/rollup.py:1-17). - Records which sales order lines reference which quote lines (
src/cluster_erp/modules/quotations/quotes/services.py:925-1042).
Sub-modules¶
| Sub-module | Purpose | Main entities | Router | Source |
|---|---|---|---|---|
quotes |
Quotes, approvals, lifecycle, versions, history, document flow | Quote, MgmtApproval |
/quotes |
src/cluster_erp/modules/quotations/quotes/routes.py:37-52 |
line_items |
Lines, rejection reasons, document flow rows | QuoteLineItem, QuoteRejectionReason, QuoteDocumentFlow |
/quotes/.../line-items |
src/cluster_erp/modules/quotations/line_items/routes.py:21-24 |
Data model¶
| Entity | Table | Key fields | Notes | Source |
|---|---|---|---|---|
Quote |
quot_quotes |
quote_no, rfq_id, version_no, status, feasibility_done, requires_new_design, bom_id, nesting_id, costing_id, mgmt_approval_id, superseded_by_id, total_price, total_cost, overall_margin_pct, currency, validity_days, payment_terms, incoterm, sent_at, won_at, lost_at, lost_reason |
totals Numeric(19, 4), margin Numeric(5, 2) |
src/cluster_erp/modules/quotations/quotes/models.py:37-169 |
MgmtApproval |
quot_mgmt_approvals |
quote_id, scheduled_at, held_at, attendees, decision, adjustments, notes, recorded_by |
decision is approved, conditional, or rejected |
src/cluster_erp/modules/quotations/quotes/models.py:171-199, src/cluster_erp/modules/quotations/quotes/schemas.py:24 |
QuoteLineItem |
quot_quote_line_items |
quote_id, line_no, ordered_qty, part_id, part_model_id, part_version_id, customer_part_id, qty, uom, unit_cost, margin_pct, unit_price, line_total, currency, bom_id, costing_id, reason_for_rejection |
money Numeric(18, 4) |
src/cluster_erp/modules/quotations/line_items/models.py:49-137 |
QuoteRejectionReason |
quot_rejection_reasons |
code, label_en, label_tr, is_active |
reference data | src/cluster_erp/modules/quotations/line_items/models.py:23-46 |
QuoteDocumentFlow |
quot_document_flow |
quote_id, quote_line_item_id, order_id, order_line_id, referenced_qty, referenced_value, currency |
one row per sales order reference | src/cluster_erp/modules/quotations/line_items/models.py:139-171 |
Line breakdowns are checked with ensure_product_breakdown (src/cluster_erp/modules/quotations/line_items/services.py:102-106, src/cluster_erp/modules/quotations/line_items/services.py:131-136).
Screens and metadata¶
get_metadata declares a sidebar group Sales Quotations (Turkish Satış Teklifleri) with one list section of the same name (src/cluster_erp/modules/quotations/module.py:203-222). The entities are quote, quote_line_item, mgmt_approval, quote_document_flow, and quote_history (src/cluster_erp/modules/quotations/module.py:224-230). The quote card reads its completeness, document flow, history, versions, and similar projects from dedicated routes (src/cluster_erp/modules/quotations/quotes/routes.py:384-439). See generic detail.
Status and actions¶
The service allows these transitions (src/cluster_erp/modules/quotations/quotes/services.py:59-74).
stateDiagram-v2 [*] --> draft draft --> feasibility: start_feasibility feasibility --> design_required: mark_design_required feasibility --> bom_pending: complete_feasibility design_required --> bom_pending: complete_design bom_pending --> costing: attach_bom costing --> bom_pending: return_to_bom costing --> pending_mgmt_approval: attach_costing pending_mgmt_approval --> mgmt_rejected: decision rejected pending_mgmt_approval --> sent: send sent --> won: mark_won sent --> lost: mark_lost sent --> expired: mark_expired
In prose: a quote starts as draft and goes through feasibility. If a new design is needed it passes design_required. A BOM moves it to costing, and a costing to pending_mgmt_approval. A rejected decision ends it as mgmt_rejected. Sending needs an approved or conditional decision (src/cluster_erp/modules/quotations/quotes/services.py:550-616). won, lost, expired, and mgmt_rejected are terminal; revise creates a new version from pending_mgmt_approval, mgmt_rejected, expired, or lost (src/cluster_erp/modules/quotations/quotes/services.py:73, src/cluster_erp/modules/quotations/quotes/services.py:687-743). feasibility_failed soft-deletes the quote and emits RfqDeclined (src/cluster_erp/modules/quotations/quotes/services.py:744-774).
| Action | Label | Condition | Endpoint | Permission | Source |
|---|---|---|---|---|---|
start_feasibility |
Start Feasibility | status is draft |
POST /quotes/{id}/start-feasibility |
quote.start_feasibility |
src/cluster_erp/modules/quotations/quotes/metadata.py:103-113 |
mark_design_required |
Design Required | status is feasibility |
POST /quotes/{id}/mark-design-required |
quote.mark_design_required |
src/cluster_erp/modules/quotations/quotes/metadata.py:114-127 |
complete_feasibility |
Complete Feasibility | status is feasibility |
POST /quotes/{id}/complete-feasibility |
quote.complete_feasibility |
src/cluster_erp/modules/quotations/quotes/metadata.py:128-152 |
complete_design |
Design Complete | status is design_required |
POST /quotes/{id}/complete-design |
quote.complete_design |
src/cluster_erp/modules/quotations/quotes/metadata.py:153-166 |
attach_bom |
Attach BOM | status is bom_pending |
POST /quotes/{id}/attach-bom |
quote.attach_bom |
src/cluster_erp/modules/quotations/quotes/metadata.py:167-202 |
attach_costing |
Attach Costing | status is costing |
POST /quotes/{id}/attach-costing |
quote.attach_costing |
src/cluster_erp/modules/quotations/quotes/metadata.py:203-249 |
schedule_mgmt_approval |
Schedule Approval | status is pending_mgmt_approval |
POST /quotes/{id}/schedule-mgmt-approval |
quote.schedule_mgmt_approval |
src/cluster_erp/modules/quotations/quotes/metadata.py:250-285 |
record_decision |
Record Decision | on an approval row | POST /quotes/{parent_id}/record-mgmt-decision |
mgmt_approval.record_decision |
src/cluster_erp/modules/quotations/quotes/metadata.py:790-819 |
send |
Send to Customer | status is pending_mgmt_approval |
POST /quotes/{id}/send |
quote.send |
src/cluster_erp/modules/quotations/quotes/metadata.py:286-307 |
mark_won, mark_lost, mark_expired |
Mark as Won, Mark as Lost, Mark as Expired | status is sent |
POST /quotes/{id}/mark-won, /mark-lost, /mark-expired |
quote.mark_won, quote.mark_lost, quote.mark_expired |
src/cluster_erp/modules/quotations/quotes/metadata.py:308-338, src/cluster_erp/modules/quotations/quotes/metadata.py:394-407 |
revise |
Revise | status is pending_mgmt_approval, mgmt_rejected, expired, or lost |
POST /quotes/{id}/revise |
quote.revise |
src/cluster_erp/modules/quotations/quotes/metadata.py:339-354 |
feasibility_failed |
Feasibility Failed | status is feasibility |
POST /quotes/{id}/feasibility-failed |
quote.feasibility_failed |
src/cluster_erp/modules/quotations/quotes/metadata.py:357-379 |
return_to_bom |
Return to BOM | status is costing |
POST /quotes/{id}/return-to-bom |
quote.return_to_bom |
src/cluster_erp/modules/quotations/quotes/metadata.py:380-393 |
Routes and permissions are in src/cluster_erp/modules/quotations/quotes/routes.py:156-366.
Business rules¶
- A quote needs an existing RFQ, read with
GetRfqById; only one active quote per RFQ is allowed (src/cluster_erp/modules/quotations/quotes/services.py:175-190,src/cluster_erp/modules/quotations/quotes/services.py:1297-1317). - Currency and payment terms default from the customer through
GetCustomerByIdwhen not given (src/cluster_erp/modules/quotations/quotes/services.py:190-205). quote_nocomes from the yearly sequencequotation; versions count per RFQ (src/cluster_erp/modules/quotations/quotes/services.py:209,src/cluster_erp/modules/quotations/quotes/services.py:1318-1326).- Attaching a costing reads
GetCostingById; the header cost falls back to the costing's total cost when no line carries a unit cost (src/cluster_erp/modules/quotations/quotes/services.py:399-466). - The roll-up sets total price to the sum of line totals, total cost to the sum of quantity times unit cost, and margin to price minus cost over cost times 100. Amounts are rounded with
ROUND_HALF_EVENto the decimal places of the currency in theCurrencyreference table (src/cluster_erp/modules/quotations/rollup.py:35-117). - A management decision can be recorded once per approval; a price adjustment overrides the net value (
src/cluster_erp/modules/quotations/quotes/services.py:492-549). - Sending is blocked while the completeness log has unmet items: a non-rejected line, a total price, a currency, and payment terms (
src/cluster_erp/modules/quotations/quotes/services.py:574-588,src/cluster_erp/modules/quotations/quotes/services.py:828-876). - Lines cannot change after the quote is sent; a rejection reason must exist and be active (
src/cluster_erp/modules/quotations/line_items/services.py:150-175,src/cluster_erp/modules/quotations/line_items/services.py:255-270). - A sent quote cannot be deleted; delete sets
deleted_at(src/cluster_erp/modules/quotations/quotes/services.py:273-287). - When sales order references cover every referenceable line, a
sentquote becomeswonandQuoteWonis emitted (src/cluster_erp/modules/quotations/quotes/services.py:925-997).
Events and queries¶
The module consumes 6 contracts and produces 18, counted as on the parties module page. The full list is on the contracts reference.
| Direction | Contract class | Handler or emitter | Source |
|---|---|---|---|
| emits | QuoteCreated, QuoteFeasibilityCompleted, QuoteBomAttached, QuoteCostingCompleted, QuoteMgmtApprovalRecorded, QuoteSent, QuoteWon, QuoteLost, QuoteExpired, RfqDeclined |
QuoteService lifecycle methods |
src/cluster_erp/modules/quotations/quotes/services.py:230, src/cluster_erp/modules/quotations/quotes/services.py:340, src/cluster_erp/modules/quotations/quotes/services.py:631, src/cluster_erp/modules/quotations/quotes/services.py:766, src/cluster_erp/modules/quotations/quotes/services.py:989 |
| queries | GetRfqById, GetCustomerById, GetCostingById, GetRfqIdsByCustomer |
services | src/cluster_erp/modules/quotations/quotes/services.py:178, src/cluster_erp/modules/quotations/quotes/services.py:414 |
| answers query | GetQuoteById, GetQuoteLineItems, ReferenceQuoteLines, ReleaseQuoteLineReferences |
handlers in module.py |
src/cluster_erp/modules/quotations/module.py:63-68 |
| subscribes | DescribeAnalyticsFacts, DescribeFieldConsumers |
analytics and tenant fields | src/cluster_erp/modules/quotations/module.py:69-84 |
Permissions¶
| Permission key | Used by | Source |
|---|---|---|
quote.read, .create, .update, .delete, .bulk_delete |
quote records and read routes | src/cluster_erp/modules/quotations/quotes/routes.py:52-141, src/cluster_erp/modules/quotations/quotes/routes.py:384-439 |
one key per lifecycle route, for example quote.send and quote.revise |
lifecycle routes | src/cluster_erp/modules/quotations/quotes/routes.py:156-366 |
mgmt_approval.read, mgmt_approval.record_decision |
approval routes | src/cluster_erp/modules/quotations/quotes/routes.py:236, src/cluster_erp/modules/quotations/quotes/routes.py:267 |
quote_line_item.read, .create, .update, .delete, .bulk_delete |
line routes | src/cluster_erp/modules/quotations/line_items/routes.py:24-142 |
Privacy, retention, and history¶
The module has no privacy_handlers.py. Quotes and lines are soft-deleted (src/cluster_erp/modules/quotations/quotes/services.py:273-287). The route GET /quotes/{quote_id}/history reads the quote's change history (src/cluster_erp/modules/quotations/quotes/services.py:1094-1183); see change history.
Analytics facts¶
analytics_facts.py contributes the fact set sales_quote over quot_quotes, with the measures quote_count, total_price, total_cost, and median_price in the field currency, read with quote.read (src/cluster_erp/modules/quotations/analytics_facts.py:25-108). See analytics facts.
Settings and reference data¶
Rejection reasons are reference data in quot_rejection_reasons, listed by GET /quotes/rejection-reasons (src/cluster_erp/modules/quotations/line_items/routes.py:24). The currency's decimal places come from the core currency table (src/cluster_erp/modules/quotations/rollup.py:35-45); see money and decimal.
Tests¶
Found by a grep for modules.quotations in tests/ (no hit) and for the path /quotations/ and file names in web-client/.
| Kind | File | Covers |
|---|---|---|
| e2e | web-client/_e2e_quotations_app.mjs |
the quotations app |
| e2e | web-client/_e2e_quote_line_bom.mjs |
quote line to order line to BOM chain |
| e2e | web-client/_e2e_order_shipment_chain.mjs |
quote to order to shipment |
| e2e | web-client/_e2e_project_otd.mjs, web-client/_e2e_mrp_pir_faz1.mjs |
quotes as demand context |
Discrepancies¶
Terms introduced¶
| Term | Code identifier | Definition | Turkish term | Source |
|---|---|---|---|---|
| sales quote | Quote |
A priced offer to a customer that answers a customer RFQ. | Satış teklifi | src/cluster_erp/modules/quotations/quotes/models.py:37 |
| management approval | MgmtApproval |
The meeting record and decision that releases a quote for sending. | Yönetim onayı | src/cluster_erp/modules/quotations/quotes/models.py:171 |
| document flow | QuoteDocumentFlow |
The record of which sales order lines reference which quote lines. | Belge akışı | src/cluster_erp/modules/quotations/line_items/models.py:139 |
| completeness log | QuoteRequirement |
The list of checks that must pass before a quote is sent. | Eksiksizlik günlüğü | src/cluster_erp/modules/quotations/quotes/services.py:77-88 |
Related pages¶
- Modules index
- CRM module
- Sales orders module
- Costing module
- BOM module
- API reference for this module
Verified against commit 92f8051 on 2026-11-13.