HAMMADDE — RAW MATERIALS
The supply chain from purchase order to QR-coded inventory. Every gram of copper, tin, plastic, catalyst, dye, palette, and reel — tracked from the moment it enters the factory gate.
Hammadde is where physical material meets digital tracking. A cable factory consumes eight categories of raw materials: copper wire rods, tin ingots, plastic granules, catalysts, dyes, anti-rodent compounds, palettes (wooden shipping platforms), and reels (cable drums). Each one arrives from a supplier, gets weighed, photographed, assigned a unique QR code, and entered into the system. From that moment, the material is traceable — through production consumption, remaining weight tracking, and all the way to the finished cable on the reel. Hammadde also manages the entire purchase order lifecycle: from creating orders to tracking delivery dates, recording date-change reasons, and rating supplier performance after delivery.
TABLE OF CONTENTS
1. WHAT HAMMADDE DOES
The Hammadde module answers four questions that the factory’s supply chain depends on:
- What raw materials do we have in stock? — Material Entry registers every incoming shipment. Copper arrives on palettes (multiple per delivery), tin as single weighed ingots, and plastics/catalysts/dyes/antirodent as lot-based entries. Each gets a QR code (A=copper, B=tin, C=plastic, D=catalyst, E=dye, F=antirodent, I=palette, H=reel), delivery and test photos, and a link to its purchase order.
- Where is every gram of material right now? — Materials List provides a full inventory view with lazy-loaded photo thumbnails, status tracking (received → approved → in_use → consumed/rejected), remaining weight after production, and real-time WebSocket updates when materials are created, modified, or deleted.
- What have we ordered and when is it arriving? — Material Orders tracks every purchase with order group codes (R1, R2, R3...), expected delivery dates, actual delivery dates, date-change history with reasons (supplier/solen/other), delivered quantity tracking, and supplier ratings (0.5–5 stars) after delivery.
- Who are our suppliers and what do they provide? — Supplier Management maintains a one-to-many relationship: one supplier company can provide multiple material types (copper, plastic, dye, etc.), each with material-specific properties like density (g/cm³), dimensions (boyut), and reel type (yazılı/yazısız).
Downstream consumer. Hammadde is the primary consumer of Teknik’s Cable Database. When the Sipariş module calculates material requirements for an order (copper weight, tin coating, plastic volumes, catalyst/dye percentages), it uses the material_calculator service which reads cable specifications from Teknik and produces procurement quantities that Hammadde must fulfill. The Material Entry page also links incoming deliveries directly to Material Orders, closing the loop between “what was ordered” and “what actually arrived.”
2. THE DATA FLOW
Data flows through Hammadde in a clear procurement-to-consumption cycle:
Procurement Flow
Registered in system
R1, R2... created
Date tracking
QR + photos
Consumption Flow
Status: received
Lab cleared
On production line
remaining_weight = 0
The Order-to-Entry Link
When a material delivery arrives, the Material Entry page can link the incoming material directly to its pending purchase order. This updates the order’s delivered_quantity, sets its status to delivered, and records the delivered_date. This tight link means the system always knows: for every kilogram ordered, how much actually arrived and when.
Real-time everywhere. Every material creation, update, and deletion broadcasts a WebSocket event. The Materials List page auto-refreshes without polling. The Supplier Management page does the same. This means multiple users (lab technicians, procurement staff) see changes instantly across all their screens.
3. THE DATABASE LAYER
6 tables serve the Hammadde module. They fall into three groups:
Material Tables (2)
raw_materials
The central table. Every physical material that enters the factory gets one row here — regardless of type.
| Column | Type | Purpose |
|---|---|---|
| id | INTEGER PK | Auto-increment primary key |
| material_type | VARCHAR(50) | raw_copper, raw_tin, raw_plastic, raw_catalyst, raw_dye, raw_antirodent, raw_palette, raw_reel |
| material_name | VARCHAR(200) | Specific material name from supplier catalog (e.g., HFX500, K388) |
| sequence_number | INTEGER | Per-type counter — separate sequence for each material_type |
| qr_code | VARCHAR(50) UNIQUE | Generated code: prefix + sequence (A1, B7, C23, I145...) |
| supplier_id | FK → suppliers | Which supplier provided this material |
| material_order_id | FK → material_orders | Links to purchase order (for delivery tracking) |
| lot_number | VARCHAR(100) | Supplier’s lot number (or auto-generated for tin: TIN-N) |
| weight | FLOAT | Measured weight when received (kg) |
| remaining_weight | FLOAT | Weight remaining after production consumption (kg) |
| form_weight | FLOAT | Official form weight (for copper palettes) |
| quantity | INTEGER | Count for lot-based materials (plastic/catalyst/dye/antirodent) |
| status | VARCHAR(50) | received → approved → in_use → consumed | rejected |
| entered_by | FK → users | Who entered this material into the system |
| notes | TEXT | Free-form notes |
| received_date | DATETIME | When the material was physically received |
| created_at | DATETIME | Record creation timestamp |
| updated_at | DATETIME | Last update timestamp (auto-set on change) |
material_photos
Photos stored separately for lazy loading performance. With thousands of materials, loading base64 photo data in every list query would be catastrophic. Instead, the list returns only photo IDs, and the frontend fetches actual photo data on demand.
| Column | Type | Purpose |
|---|---|---|
| id | INTEGER PK | Photo identifier |
| material_id | FK → raw_materials (CASCADE) | Which material this photo belongs to |
| photo_type | VARCHAR(20) | ‘delivery’ or ‘test’ |
| sequence | INTEGER | 1, 2, or 3 (up to 3 photos per type) |
| photo_data | TEXT | Base64 encoded image data |
| created_at | DATETIME | Upload timestamp |
Performance pattern: The old design stored photos directly in the raw_materials table as JSON arrays of base64 strings. This made list queries unbearably slow. The migration to a separate material_photos table with lazy loading via individual photo endpoints (GET /materials/photo/{id}) solved this completely. The old columns (delivery_form_photo, test_report_photo, delivery_form_photos, test_report_photos) are kept for backward compatibility but are no longer used.
Order Tables (2)
material_orders
Tracks purchase orders. Not official ERP purchase orders — these are internal tracking records for when materials are expected to arrive.
| Column | Type | Purpose |
|---|---|---|
| id | INTEGER PK | Order identifier |
| order_group_code | VARCHAR(20) | Grouping code (R1, R2...) — orders from same shipment share this |
| material_type | VARCHAR(50) | What is being ordered (copper, plastic, etc.) |
| supplier_id | FK → suppliers | Which supplier is fulfilling this order |
| supplier_material_id | FK → supplier_materials | Specific material being ordered |
| quantity | FLOAT | Ordered quantity |
| delivered_quantity | FLOAT | Actually delivered quantity (updated when material entry is linked) |
| unit | VARCHAR(20) | ‘kg’ for materials, ‘adet’ for palettes/reels |
| order_date | DATE | When the order was placed |
| expected_date | DATE | When delivery is expected (can change — tracked) |
| delivered_date | DATE | Actual delivery date |
| status | VARCHAR(20) | pending → shipped → delivered | cancelled |
| rating | FLOAT | Post-delivery rating (0.5–5 stars, half increments) |
| rating_note | TEXT | Rating comment |
| notes | TEXT | Order notes |
| created_by | FK → users | Who created this order |
material_order_date_changes
An audit trail for expected delivery date changes. Every time someone changes the expected date, the system records why.
| Column | Type | Purpose |
|---|---|---|
| id | INTEGER PK | Change record identifier |
| order_id | FK → material_orders (CASCADE) | Which order was changed |
| old_date | DATE | Previous expected date |
| new_date | DATE | New expected date |
| reason_type | VARCHAR(20) | ‘supplier’ | ‘solen’ | ‘other’ |
| reason_detail | TEXT | Manual explanation (required for ‘other’) |
| changed_by | FK → users | Who changed the date |
| created_at | DATETIME | When the change was recorded |
Supplier Tables (2)
suppliers
Company-level supplier records. Each supplier is a single company with contact details.
| Column | Type | Purpose |
|---|---|---|
| id | INTEGER PK | Supplier identifier |
| name | VARCHAR(200) UNIQUE | Company name (stored UPPERCASE) |
| contact_person | VARCHAR(100) | Primary contact name |
| phone | VARCHAR(50) | Auto-formatted Turkish phone number |
| VARCHAR(100) | Email (validated regex) | |
| address | TEXT | Physical address |
| tax_number | VARCHAR(50) | Tax registration number |
| is_active | BOOLEAN | Soft delete flag (toggle active/inactive) |
| notes | TEXT | Free-form notes |
supplier_materials
The one-to-many relationship. One supplier can provide multiple material types, each with type-specific properties.
| Column | Type | Purpose |
|---|---|---|
| id | INTEGER PK | Supplier-material link identifier |
| supplier_id | FK → suppliers | Which supplier |
| material_type | VARCHAR(50) | copper, tin, plastic, catalyst, dye, antirodent, palette, reel |
| material_name | VARCHAR(100) | Specific product name (HF-500, CAT-203, etc.) |
| density | FLOAT | For plastic/catalyst/dye/antirodent (g/cm³) |
| boyut | VARCHAR(100) | Dimensions for palette/reel |
| reel_type | VARCHAR(20) | For reels: Yazılı/Yazısız (labeled/unlabeled) |
| is_active | BOOLEAN | Whether this material is still offered |
Entity relationship summary: suppliers 1—N supplier_materials (what they sell), suppliers 1—N raw_materials (what they delivered), material_orders N—1 suppliers (who we ordered from), material_orders N—1 supplier_materials (what exactly), raw_materials N—1 material_orders (linked delivery), raw_materials 1—N material_photos (lazy-loaded), material_orders 1—N material_order_date_changes (audit trail).
4. THE BACKEND ARCHITECTURE
The backend is built with FastAPI and organized into 4 route files, 4 models, 2 schema files, and 1 service. Total backend code: ~3,200 lines.
4.1 Route Organization
material_routes.py (986 lines)
Router prefix: /api/materials
The main material CRUD. Handles copper entry (multi-palette), tin entry (single), lot-based entry (plastic/catalyst/dye/antirodent), photo retrieval, list, update, print preview, printer management, QR printing, status toggle, and hard delete. Includes the palette_routes sub-router.
material_order_routes.py (478 lines)
Router prefix: /api/material-orders
Order lifecycle management. List with filters, pending orders grouped by supplier/date (for Material Entry linking), create (single or batch with auto-generated R-codes), update with date-change tracking, date-change history retrieval, delivery rating, and deletion.
palette_routes.py (316 lines)
Included in material_routes (no separate prefix).
Palette and reel entry endpoints. Similar to lot-entry but specialized for inventory items: palettes get QR prefix I, reels get H. Both support order linking and photo uploads.
supplier_routes.py (435 lines)
Router prefix: /api/suppliers
Full supplier lifecycle: create with nested materials, list with eager-loaded materials, filter by material type, get unique materials list (deduplicated specs), get single supplier, update with material replacement, hard delete with dependency check, and status toggle.
4.2 The QR Code System
Every material gets a deterministic QR code based on type prefix and sequence number:
def generate_qr_code(material_type: str, sequence_number: int) -> str: prefix_map = { "raw_copper": "A", # A1, A2, A3... "raw_tin": "B", # B1, B2, B3... "raw_plastic": "C", # C1, C2, C3... "raw_catalyst": "D", # D1, D2, D3... "raw_dye": "E", # E1, E2, E3... "raw_antirodent": "F", # F1, F2, F3... "raw_palette": "I", # I1, I2, I3... "raw_reel": "H", # H1, H2, H3... } prefix = prefix_map.get(material_type, "X") return f"{prefix}{sequence_number}"
The sequence number is per-type: the system queries the maximum sequence_number for that material_type and increments. This means A1 and B1 can coexist — each type has its own independent counter.
4.3 Material Entry Logic
The entry endpoint handles three fundamentally different flows based on material type:
| Material Type | Entry Method | Key Behavior |
|---|---|---|
| Copper (raw_copper) | POST /materials/entry | Creates one row per palette. A single delivery with 5 palettes creates 5 raw_materials rows, each with its own QR code (A1, A2...), lot number, and measured weight. |
| Tin (raw_tin) | POST /materials/entry | Creates one row. Single weight entry. Lot number auto-generated as TIN-{sequence}. |
| Plastic, Catalyst, Dye, Antirodent | POST /materials/lot-entry | Creates one row per lot. Each lot has its own test photos, weight, and quantity. Material name resolved from supplier_materials table. |
| Palette | POST /materials/palette-entry | Creates one row per palette type. QR prefix I. Lot auto-generated as PALET-{sequence}. |
| Reel | POST /materials/reel-entry | Creates one row per reel type. QR prefix H. Lot auto-generated as REEL-{sequence}. |
4.4 Order Group Codes
When creating material orders, the system generates a group code (R1, R2, R3...) by finding the highest existing R-number and incrementing:
def generate_order_group_code(db: Session) -> str: # Get highest R number from existing codes result = db.query(func.max(MaterialOrder.order_group_code)).filter( MaterialOrder.order_group_code.isnot(None), MaterialOrder.order_group_code.like('R%') ).scalar() if result: match = re.search(r'R(\d+)', result) if match: return f"R{int(match.group(1)) + 1}" return "R1"
Orders from the same shipment share a group code. This allows the system to display grouped orders in the UI and link multiple material entries to the same delivery event.
4.5 Date Change Tracking
When an order’s expected date is modified, the update endpoint automatically records the change:
# If date actually changed and reason is provided, record it if old_expected_date != new_expected_date: if 'date_change_reason' in data: date_change = MaterialOrderDateChange( order_id=order.id, old_date=old_expected_date, new_date=new_expected_date, reason_type=data['date_change_reason'], # supplier | solen | other reason_detail=data.get('date_change_detail'), # free text changed_by=current_user.id ) db.add(date_change)
Three reason categories: supplier (delay caused by supplier), solen (delay caused by Solen internal), other (requires manual explanation). This creates an accountability trail for procurement performance analysis.
4.6 The Material Calculator Service
The material_calculator.py (478 lines) is a pure calculation service with no database dependencies. It provides:
Copper Calculation
calculate_copper(wire_groups, length_meters, bunching_stages)
Takes wire diameter→count map (e.g., {0.30: 73} for 6mm²), applies per-wire weight from constants, multiplies by length, and applies compound twist factor for multi-stage bunching. Returns total kg.
Tin Calculation
calculate_tin(copper_kg)
Simple percentage: copper_kg × TIN_COATING_FACTOR (~0.7%). Tin coating is ~4μm thick on 1.8mm wire.
Plastic Calculation
calculate_plastic_weight(inner_dia, thickness, density, length, is_first_layer, num_cables)
Cylindrical volume: π × (R² − r²) × length. First layer on copper gets 14% overfill for bumpy surface. Twin cables multiply by num_cables. Volume × density = weight.
Layer Calculation
calculate_layer(...)
Complete layer with all material alternatives. Calculates plastic + catalyst + dye for each alternative selection. Returns max values across all options for worst-case procurement planning.
4.7 Permission System
The module uses a hybrid permission approach:
- Role-based: Material entry requires
lab_userorsuper_admin. Material updates requiresuper_admin. Hard deletes requiresuper_admin. - Smart permission check: Supplier routes use
check_permission_smart()which works with both the old role-based system and the new ultra-granular page-button permission system simultaneously. - Frontend registration: Each page registers its buttons with the permission system via
registerPageButtons(), declaring exactly which actions exist (e.g.,add_copper,add_tin,upload_delivery_photo,print_qr_modal).
4.8 WebSocket Integration
Three entity types broadcast real-time updates:
# After material creation await broadcast_update("materials", "create", {"id": material.id, "qr_code": material.qr_code}) # After material update await broadcast_update("materials", "update", {"id": material.id, "qr_code": material.qr_code}) # After material deletion await broadcast_update("materials", "delete", {"id": material_id, "qr_code": qr_code}) # Suppliers also broadcast await broadcast_update("suppliers", "create", {"id": new_supplier.id, "name": new_supplier.name})
4.9 Print Queue Integration
Material QR code printing is non-blocking. The POST /materials/print/{material_id} endpoint forwards to the print queue system rather than blocking the HTTP request while waiting for the printer. The system generates a preview image (base64) via GET /materials/print/preview/{material_id} and allows printer IP configuration via POST /materials/print/update-printer-ip.
5. THE FRONTEND
4 React pages built with Ant Design Pro, totaling ~5,100 lines of TypeScript. All pages are dark-mode aware with conditional styling.
5.1 Page Map
| Route | Component | Lines | Purpose |
|---|---|---|---|
| /hammadde/hammadde-girisi | Lab/MaterialEntry | 1,930 | Material entry with 8 material type cards, photo uploads, order linking |
| /hammadde/hammaddeler-listesi | Hammadde/HammaddelerListesi | 1,153 | ProTable with lazy photo thumbnails, CRUD, print, status management |
| /hammadde/hammadde-siparis | Hammadde/HammaddeSiparis | 1,237 | Order tracking with date-change history, grouping, ratings |
| /hammadde/tedarikci-yonetimi | Lab/SupplierManagement | 761 | Supplier CRUD with nested material types, search, real-time updates |
5.2 Key Frontend Patterns
Card-Based Material Selection
Material Entry uses 8 colored cards (copper=orange, tin=cyan, plastic=purple, etc.) as the primary navigation. Each card type opens a different modal form with type-specific fields. Colors are theme-aware — muted in dark mode using rgba with 0.12 opacity.
Lazy Photo Loading
The LazyPhotoThumbnail component loads photos one at a time via GET /materials/photo/{id}. On click, it loads ALL photos for that material and opens a gallery viewer with navigation. This pattern keeps the list page fast even with thousands of materials.
Order Linking UX
When entering materials, the user can optionally select a pending purchase order to link to. The system fetches pending orders grouped by supplier and expected date via GET /material-orders/pending-by-type/{type}, and auto-fills supplier info from the selected order.
Real-Time WebSocket Updates
Both Materials List and Supplier Management use the useWebSocket hook subscribing to materials and suppliers rooms respectively. On any create/update/delete event, the ProTable auto-reloads via actionRef.current?.reload().
5.3 Material Types and Colors
| Type | QR Prefix | Color | Entry Method |
|---|---|---|---|
| Copper (Bakır) | A | Orange | Multi-palette with form + measured weights |
| Tin (Kalay) | B | Cyan | Single weight entry |
| Plastic (Plastik) | C | Purple | Lot-based with per-lot test photos |
| Catalyst (Katalizör) | D | Green | Lot-based with per-lot test photos |
| Dye (Boya) | E | Pink | Lot-based with per-lot test photos |
| Antirodent | F | Teal | Lot-based with per-lot test photos |
| Palette (Palet) | I | Lime | Quantity per palette type |
| Reel (Makara) | H | Gold | Quantity per reel type |
6. THE SUBMODULES
The Hammadde module consists of 4 submodules, each handling a different aspect of raw material management. The following sections will dive deep into each one.
Tedarikçi Yönetimi (Supplier Management)
Supplier company management with one-to-many material relationships. Create suppliers with nested material lists, each material type carrying its own properties (density, dimensions, reel type). Smart permission checking, real-time WebSocket updates, hard delete with FK dependency protection. The starting point — nothing can be ordered or entered without a registered supplier.
Hammadde Sipariş (Material Orders)
Purchase order tracking with order group codes (R1, R2...), date change history with categorized reasons (supplier/solen/other), delivery quantity tracking, supplier rating after delivery (0.5–5 stars with half increments), and status lifecycle management.
Hammadde Girişi (Material Entry)
The 1,930-line entry page. 8 material type cards, multi-palette copper entry, lot-based chemical entry, photo uploads (delivery form + test reports, up to 3 each), QR code generation and printing, order linking for delivery tracking. The largest and most complex page in the module.
Hammaddeler Listesi (Materials List)
Full inventory view with ProTable. Lazy-loaded photo thumbnails, material status management (received/approved/in_use/consumed/rejected), edit with QR code modification, hard delete with dependency checks, remaining weight tracking, and QR code print queue integration.
6.1 TEDARİKÇİ YÖNETİMİ (SUPPLIER MANAGEMENT)
The starting point of the entire Hammadde module. Before any material can be ordered or entered into the system, a supplier must exist. This page manages supplier companies and the specific materials each one provides.
6.1.1 What It Does
Tedarikçi Yönetimi manages a one-to-many relationship: one supplier company can provide multiple material types. A single supplier like “ORMETSAN” might provide both copper wire rod and tin ingots, while “BETEK” might provide 3 different plastic compounds, 2 catalysts, and a dye — all registered as separate material entries under the same company.
The page provides:
- ProTable with client-side search across all fields (including Turkish material type names)
- Create/Edit modal with dynamic conditional fields based on material type
- Material type grouping in the table with count badges and detail tooltips
- Soft delete (active/inactive toggle) and hard delete with FK dependency protection
- Real-time WebSocket updates across all connected clients
6.1.2 API Contract
| Method | Path | Function | Permission |
|---|---|---|---|
| POST | /api/suppliers/create | create_supplier | Smart: lab_user OR hammadde.tedarikci-yonetimi.create_supplier |
| GET | /api/suppliers/list | get_suppliers | Any authenticated user |
| GET | /api/suppliers/by-material/{type} | get_suppliers_by_material | Any authenticated user |
| GET | /api/suppliers/materials-list | get_supplier_materials_list | Any authenticated user |
| GET | /api/suppliers/{id} | get_supplier | Any authenticated user |
| PUT | /api/suppliers/{id} | update_supplier | Any authenticated user |
| PUT | /api/suppliers/{id}/toggle-status | toggle_supplier_status | Smart: super_admin OR deactivate_supplier |
| DELETE | /api/suppliers/{id}/hard-delete | hard_delete_supplier | Smart: super_admin OR hard_delete_supplier |
6.1.3 CRUD Flow
Create:
- Click “Yeni Tedarikçi” button in the toolbar
- Modal opens with company name field + dynamic materials section (initially one empty material card)
- Select material type → conditional fields appear (density for plastic, boyut for palette, boyut+reel_type for reel, nothing for copper/tin)
- Click “+” to add more materials under the same supplier
- On submit: frontend strips
undefinedvalues →POST /api/suppliers/create - On success: toast “Tedarikçi başarıyla oluşturuldu” → modal closes → WebSocket broadcasts to all clients → table refreshes
Edit:
- Click edit icon in the Actions column
- Modal opens pre-filled with current company name and all material cards
- User can modify name, add/remove/change materials
- On submit:
PUT /api/suppliers/{id}→ backend uses delete-and-recreate strategy for materials (deletes ALL existing supplier_materials, creates fresh ones) - On success: toast “Tedarikçi başarıyla güncellendi” → modal closes → WebSocket broadcast → table refreshes
Soft Delete (Deactivate):
- Click status toggle icon in the Actions column
- Popconfirm: “Bu tedarikçiyi devre dışı bırakmak istediğinize emin misiniz?”
- On confirm:
PUT /api/suppliers/{id}/toggle-status - Supplier row changes to “inactive” — still visible in table but greyed out. Can be reactivated the same way.
Hard Delete (Permanent):
- Click delete icon in the Actions column (requires
hard_delete_supplierpermission) - Popconfirm with strong warning text
- On confirm:
DELETE /api/suppliers/{id}/hard-delete - Backend checks for dependent
raw_materialsrows — if any exist, deletion is blocked with error: “Bu tedarikçiye ait {n} hammadde kaydı var. Önce hammaddeleri silin.” - If no dependencies: deletes supplier_materials first (cascade), then the supplier → WebSocket broadcast → table refreshes
6.1.4 The Conditional Form Logic
The create/edit modal contains a dynamic materials section. Each material card shows different fields based on the selected material type:
| Material Type | Fields Shown | Example |
|---|---|---|
| Copper, Tin | No extra fields — “Bu malzeme için ek bilgi gerekmez” | — |
| Plastic, Catalyst, Dye, Antirodent | material_name + density (g/cm³) | HF-500, 1.42 g/cm³ |
| Palette | material_name + boyut (dimensions) | Euro Palet, 120x80x15 cm |
| Reel | material_name + boyut + reel_type | 300mm Makara, 300x150 mm, Yazılı |
When the material type dropdown changes, all conditional fields are cleared:
const updateMaterial = (index: number, field: keyof MaterialItem, value: any) => { const newMaterials = [...materials]; newMaterials[index] = { ...newMaterials[index], [field]: value }; // Clear conditional fields when type changes if (field === 'material_type') { newMaterials[index].material_name = undefined; newMaterials[index].density = undefined; newMaterials[index].boyut = undefined; newMaterials[index].reel_type = undefined; } setMaterials(newMaterials); };
Users can add unlimited materials (via “Malzeme Ekle” dashed button) and remove any except the last one (minimum 1 material required). New materials default to copper for the first, plastic for subsequent additions.
6.1.5 Backend Validation Chain
Three Pydantic validators fire before data reaches the database:
Name Validator
@validator('name') def validate_name(cls, v): if not v or not v.strip(): raise ValueError('Tedarikçi adı boş olamaz') if any(char in v for char in ['@', '#', '$', '%', '^', '&', '*']): raise ValueError('Tedarikçi adı özel karakterler içeremez') return v.strip().upper() # Always stored UPPERCASE
Phone Validator
@validator('phone') def validate_phone(cls, v): # Strip ALL non-digits: spaces, dashes, parens, plus sign, dots clean = v.replace(' ','').replace('-','').replace('(','').replace(')','').replace('+','').replace('.','') # Remove +90 country code → 0xxx if clean.startswith('90') and len(clean) > 10: clean = '0' + clean[2:] # +90 212 → 0212 # Auto-format: "0212 555 1234" if len(clean) == 10: return f"{clean[:4]} {clean[4:7]} {clean[7:]}" elif len(clean) == 11: # Mobile: 05xx xxx xxxx return f"{clean[:4]} {clean[4:7]} {clean[7:]}"
Email Validator
Standard regex check: ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
Duplicate protection: Before creating, the backend checks Supplier.name == supplier_data.name.upper(). Since names are always stored uppercase, “ormetsan”, “ORMETSAN”, and “Ormetsan” are all treated as the same supplier. Returns 400: “Bu tedarikçi zaten kayıtlı”.
6.1.6 The Update Strategy: Delete-and-Recreate
When updating a supplier’s materials, the backend does not patch individual records. Instead, it deletes ALL existing supplier_materials rows and creates fresh ones:
# Update materials if provided if supplier_data.materials is not None: # Delete ALL old materials db.query(SupplierMaterial).filter( SupplierMaterial.supplier_id == supplier_id ).delete() # Create new materials from scratch for material_data in supplier_data.materials: supplier_material = SupplierMaterial( supplier_id=supplier.id, material_type=material_data.material_type.value, material_name=material_data.material_name, density=material_data.density, boyut=material_data.boyut, reel_type=material_data.reel_type, is_active=True ) db.add(supplier_material)
Why delete-and-recreate? This simplifies the logic enormously. The alternative — diffing existing records, updating changed ones, deleting removed ones, inserting new ones — would require tracking material IDs on the frontend and handling complex merge logic. Since supplier_materials has no inbound foreign keys from other tables (material_orders links to supplier_material_id, but orders reference a specific material entry), this approach is safe and deterministic.
6.1.7 Hard Delete with FK Protection
Before permanently deleting a supplier, the backend checks for dependent raw materials:
# Check if supplier has raw materials (would block due to FK) has_materials = db.query(RawMaterial).filter( RawMaterial.supplier_id == supplier_id ).count() if has_materials > 0: raise HTTPException( status_code=400, detail=f"Bu tedarikçiye ait {has_materials} hammadde kaydı var. Önce hammaddeleri silin." )
If the check passes, deletion order is: supplier_materials first (cascade), then the supplier itself. A WebSocket broadcast follows immediately.
6.1.8 Client-Side Search with Turkish Mappings
The search bar filters across all fields including nested materials, with Turkish translations:
const materialTurkish: Record<string, string> = { copper: 'bakır', tin: 'kalay', plastic: 'plastik', catalyst: 'katalizör', dye: 'boya', antirodent: 'antirodent', palette: 'palet', reel: 'makara', }; // Search checks: name, contact_person, phone, email, // tax_number, address, status (aktif/pasif), id, // material_type (EN + TR), material_name
Typing “bakır” finds all suppliers providing copper. Typing “HF-500” finds the supplier who provides that specific plastic. Typing “pasif” shows only deactivated suppliers.
6.1.9 Materials Column: Grouping and Tooltips
The table’s “Malzemeler” column groups materials by type and shows counts:
- If a supplier provides 1 plastic → shows
Plastiktag - If a supplier provides 3 plastics → shows
Plastik (3)tag - Hovering any tag shows a tooltip with all material details: name, density, dimensions
- Each material type gets a consistent color: copper=orange, tin=blue, plastic=purple, catalyst=green, dye=magenta, antirodent=cyan, palette=lime, reel=gold
6.1.10 The Unique Specs Endpoint
The GET /suppliers/materials-list endpoint serves a special purpose: it returns deduplicated material specifications across all suppliers. If 3 different suppliers all provide “HF-500” plastic at 1.42 g/cm³, the endpoint returns it once. Deduplication key: material_name|boyut|reel_type. This is used by other pages (Material Entry, Material Orders) to show dropdown options without duplicates.
6.1.11 Permission Architecture
The page uses a hybrid permission model with 8 registered buttons:
// Frontend registers ALL available actions registerPageButtons({ pageId: 'hammadde.tedarikci-yonetimi', buttons: [ { id: 'access_page', label: 'Sayfaya Erişim' }, { id: 'view_table', label: 'Tablo Görüntüle' }, { id: 'create_supplier', label: 'Yeni Tedarikçi' }, { id: 'edit_supplier', label: 'Düzenle' }, { id: 'add_material', label: 'Malzeme Ekle' }, { id: 'remove_material', label: 'Malzeme Kaldır' }, { id: 'deactivate_supplier', label: 'Pasifleştir' }, { id: 'hard_delete_supplier', label: 'Kalıcı Sil', critical: true } ] });
The backend uses check_permission_smart() which checks three layers in order: (1) super_admin always passes, (2) old role-based check (user_type in required list), (3) new ultra-granular page.button permission. If any layer passes, the action is allowed. This ensures backward compatibility during the transition from role-based to button-level permissions.
6.1.12 Real-Time Updates
Three supplier operations broadcast WebSocket events:
broadcast_update("suppliers", "create", {id, name})— after successful creationbroadcast_update("suppliers", "update", {id, name})— after update or status togglebroadcast_update("suppliers", "delete", {id, name})— after hard delete
The frontend subscribes to rooms ['suppliers', 'all'] and calls actionRef.current?.reload() on any event, triggering a full table refresh.
6.1.13 Eager Loading for Performance
The list endpoint uses SQLAlchemy’s joinedload to fetch suppliers with their materials in a single query:
query = db.query(Supplier).options(
joinedload(Supplier.materials_provided)
)
Without this, listing 50 suppliers would trigger 50 additional queries to fetch each supplier’s materials (the N+1 problem). With eager loading, it’s a single JOIN query.
6.1.14 Data Cleaning Before Submit
The frontend cleans the materials array before sending to the backend, stripping undefined values that Pydantic would reject:
const cleanedMaterials = materials.map(mat => { const cleaned: any = { material_type: mat.material_type }; if (mat.material_name !== undefined && mat.material_name !== '') cleaned.material_name = mat.material_name; if (mat.density !== undefined && mat.density !== null) cleaned.density = mat.density; if (mat.boyut !== undefined && mat.boyut !== '') cleaned.boyut = mat.boyut; if (mat.reel_type !== undefined && mat.reel_type !== '') cleaned.reel_type = mat.reel_type; return cleaned; });
Why this matters: Without cleaning, a copper material would be sent as {"material_type": "copper", "material_name": undefined, "density": undefined, "boyut": undefined, "reel_type": undefined}. Pydantic would accept these as None, but JSON.stringify drops undefined values entirely. The explicit check ensures only meaningful data is sent, keeping the API payload clean and predictable.
6.1.15 How Supplier Data Is Used Across the System
Tedarikçi Yönetimi is not a standalone page. The data it creates is consumed by every other submodule in Hammadde and beyond. Here is the complete map of where supplier data flows:
Material Entry (Hammadde Girişi) — Supplier Selection
When a user clicks any material card (copper, tin, plastic, etc.), the entry page calls GET /suppliers/list?active_only=true and filters locally by checking each supplier’s materials_provided array. Only suppliers who have that material type active in their supplier_materials are shown in the dropdown. This means: if you register a supplier with only “copper” and “tin”, they will never appear in the plastic entry form.
// Material Entry: filter suppliers by material type const filterSuppliers = (materialType: string) => { const filtered = suppliers.filter(supplier => { const materials = supplier.materials_provided || []; return materials.some( (mat: any) => mat.material_type === materialType && mat.is_active ); }); setFilteredSuppliers(filtered); };
Material Entry — Material Name Resolution
For lot-based entries (plastic, catalyst, dye, antirodent), when a lot is created, the backend looks up the supplier_material_id to resolve the specific material_name. This name (e.g., “HF-500”, “CAT-203”) is stored directly in the raw_materials.material_name column. For palettes and reels, it also appends the boyut dimension (e.g., “Euro Palet - 120x80x15 cm”).
# Lot entry: resolve material name from SupplierMaterial if material_type_id: supplier_material = db.query(SupplierMaterial).filter( SupplierMaterial.id == material_type_id ).first() if supplier_material: material_name = supplier_material.material_name if supplier_material.boyut: material_name = f"{material_name} - {supplier_material.boyut}"
Material Orders (Hammadde Sipariş) — Supplier + Material Selection
When creating a purchase order, the user first selects a supplier, then sees only the materials that supplier provides. The order stores both supplier_id (who) and supplier_material_id (what exactly). This link is critical: when a delivery arrives and is entered through Material Entry, the system uses supplier_material_id to match incoming materials to their pending orders.
Material Orders — Pending Order Groups for Entry Linking
The GET /material-orders/pending-by-type/{material_type} endpoint returns pending orders grouped by supplier_id + expected_date. Material Entry uses this to show a “Link to Order” option. When the user selects an order group, the system builds an order_map keyed by supplier_material_id, so each lot in the entry can be automatically linked to its corresponding order line.
Material Calculator — Density from Supplier Materials
The density field stored in supplier_materials for plastics, catalysts, dyes, and antirodent compounds is used by the Material Calculator service. When the Sipariş module calculates plastic weight for an order (volume × density = kg), the density comes from the supplier’s material specification registered here.
Unique Specs for Dropdowns
The GET /suppliers/materials-list endpoint deduplicates material specs across all suppliers and returns unique entries. This feeds dropdown menus in Material Orders and Material Entry where users select what to order or enter — without seeing the same “HF-500” three times from three different suppliers.
The dependency chain: Supplier → supplier_materials (what they sell) → material_orders (what we ordered, referencing supplier_material_id) → raw_materials (what arrived, linked to order via material_order_id, with material_name resolved from supplier_materials). Remove a supplier, and this entire chain breaks. That is why FK protection exists on hard delete, and why soft delete (active/inactive) is the preferred approach.
6.2 — Hammadde Sipariş (Material Orders)
6.2.1 — Purpose and Business Context
This is not an official purchase order system. It is an internal tracking tool that lets Solen record what raw materials they have ordered from which supplier, when they expect delivery, and what actually arrived. Its purpose is to give the purchasing and warehouse teams a clear picture of what is coming, what is late, and how well each supplier is performing.
The module sits between Supplier Management (where suppliers and their product catalogs are defined) and Material Entry (where physical deliveries are received). Orders created here become “linkable targets” for Material Entry — when a truck arrives, the operator can connect the incoming material to its corresponding order, which automatically increments the delivered quantity and provides full traceability.
6.2.2 — CRUD Flow
Create (Multi-Item):
- Click “Yeni Sipariş” button in the toolbar
- Modal opens with cascading selection: Material Type → Supplier (filtered by type) → Product panels
- Select material type → supplier dropdown shows only suppliers who carry that type
- Select supplier → product catalog appears as collapsible panels. Pick product, enter quantity (kg or adet)
- Click “+” to add more products from the same supplier in the same shipment
- Set order date (defaults to today) and expected delivery date. Add optional notes
- On submit:
POST /api/material-orders/create→ backend generates one group code (R1, R2…) and creates one DB row per item, all sharing that code. Single transaction — all or nothing - On success: toast confirmation → modal closes → table refreshes showing the new order group
Edit (Single Row):
- Click edit icon on a specific order row (not the group — you edit individual line items)
- Modal opens pre-filled with current values: supplier, material, quantity, dates, notes
- The multi-item “+” functionality is not available in edit mode
- Critical behavior: if the expected delivery date is changed, the date picker stays open and a side panel appears requiring a mandatory reason (Tedarikçi kaynaklı / Solen kaynaklı / Diğer). This creates an audit record in
material_order_date_changes - On submit:
PUT /api/material-orders/{id}→ order updated + date change logged if applicable - On success: toast → modal closes → table refreshes
Delete:
- Click delete icon on a specific order row
- Popconfirm with warning
- On confirm:
DELETE /api/material-orders/{id} - Cascade: associated
material_order_date_changesrecords are deleted with the order - Table refreshes. If this was the last item in a group, the group code disappears
Read (Date History):
- Click the clock icon (Tarih Geçmişi) on any order row
- Modal opens showing a vertical timeline of all date changes: old date (struck through) → new date, colored reason tag, detail text, who changed it and when
- Read-only — history cannot be edited or deleted independently
6.2.3 — Order Lifecycle
Every order follows a four-state lifecycle:
Status transitions can happen two ways. First, the status column in the table is an inline dropdown selector — the user clicks the current status tag and picks a new one. Second, and more importantly, status is automatically set to “Delivered” by the Material Entry module when incoming materials are linked to an order. When a delivery is saved through Material Entry, the backend automatically updates the order’s status to delivered, stamps today’s date as the delivery date, and increments the delivered quantity. This means the most common status transition (pending → delivered) happens automatically as part of the material receiving workflow, not through manual interaction on this page.
6.2.4 — Order Group Codes (Shipment Grouping)
A single shipment from a supplier often contains multiple distinct materials. For example, one truck from Supplier X might carry 5 tons of HF-500 plastic granules and 2 tons of catalyst. Instead of treating these as completely separate orders, the system groups them under a shared Order Group Code.
Group codes follow the pattern R1, R2, R3, R4… (the “R” prefix followed by a sequential number). When a user creates a new order with multiple line items, the backend generates one group code and assigns it to every order row in that batch. Even a single-item order gets a group code, ensuring consistency.
The generation logic finds the highest existing R-number in the database and increments by one. This is a simple sequential counter, not a UUID — the codes are meant to be human-readable and easy to communicate verbally (“Shipment R47 is arriving tomorrow”).
In the table, the group code appears as the first column (fixed left), displayed as a blue tag. It serves as a visual grouping mechanism — when users see multiple rows with “R12”, they immediately understand these items are from the same shipment.
6.2.5 — Creating an Order (The Multi-Step Form Flow)
The order creation follows a cascading selection flow that mirrors the supplier data hierarchy:
- Select Material Type — the user picks from 8 types (copper, tin, plastic, catalyst, dye, antirodent, palette, reel). This is the first filter.
- Select Supplier — the supplier dropdown is not a full list. It shows only suppliers who provide active materials of the selected type. If you pick “Copper”, you only see copper suppliers. This filtering happens client-side by checking each supplier’s
materials_providedarray. - Add Products and Quantities — once a supplier is selected, the user sees their specific product catalog for that material type. Each product is a collapsible panel where the user picks the exact product and enters a quantity. The unit automatically adapts: palettes and reels use “adet” (pieces), everything else uses “kg”.
- Multiple Items — the user can add more product panels with a “+” button. Each panel represents a separate line item, but all items will share the same group code when saved. The “+” button is disabled until a supplier is selected.
- Set Dates — two dates are required: the order date (defaults to today) and the expected delivery date.
- Optional Notes — free-text field for any additional context.
On save, the frontend sends all items in a single API call. The backend generates one group code and creates one database row per item, all sharing that code. This is a single transaction — either all items are created or none are.
6.2.6 — Editing an Order
Edit mode works differently from creation. When editing, the user operates on a single order row, not a group. The form loads with the existing values, and the user can change the supplier, material, quantity, dates, and notes. The multi-item “+” functionality is not available in edit mode — you edit one row at a time.
The most important behavior in edit mode is date change tracking, covered in detail below.
6.2.7 — The Date Change Tracking System
This is one of the most sophisticated features in the module. In real-world procurement, expected delivery dates shift constantly — suppliers delay, Solen requests postponement, logistics issues arise. The system does not just silently overwrite the date; it requires the user to explain why the date changed and stores a complete audit trail.
How It Works
When a user opens an existing order for editing, the system records the original expected date. If the user changes this date in the date picker, something unusual happens: the date picker does not close. Instead, a side panel slides into the right side of the calendar popup. This panel contains:
- The new selected date displayed prominently
- A reason selector (required) with three options:
- Tedarikçi kaynaklı — delay caused by the supplier
- Solen kaynaklı — delay caused by Solen (e.g., postponed due to schedule)
- Diğer — other reason (opens a free-text field)
- Confirm / Cancel buttons — the user must explicitly confirm the date change after selecting a reason
If the user selects “Diğer” (Other), a text area appears for a manual explanation, and this explanation is also required. The user cannot save the order without completing this reason flow.
If the user changes their mind and picks the original date again, the side panel disappears and the reason fields reset automatically.
Backend Storage
When the order is saved with a changed date, the backend creates a new row in the material_order_date_changes table with: the old date, the new date, the reason type, the optional detail text, who made the change, and a timestamp. This table uses CASCADE delete — if the parent order is deleted, its date change history goes with it.
Viewing the History
Every order row in the table has a clock icon button (Tarih Geçmişi / Date History). Clicking it opens a modal that shows every date change that ever happened to that order, displayed as a vertical timeline. Each entry shows:
- The old date (struck through) with an arrow pointing to the new date
- The reason as a colored tag
- Any detail text in italics
- When the change happened and who made it (shown in a right-aligned secondary column)
Timestamps in this modal are converted to Turkey timezone (Europe/Istanbul) for accurate local display.
6.2.8 — Delivery Tracking (Ordered vs. Delivered)
Each order tracks two quantities: ordered quantity (what was requested) and delivered quantity (what actually arrived). The delivered quantity is not manually updated on this page. It is incremented automatically by the Material Entry module when incoming materials are linked to an order.
In the table, both quantities are displayed side by side. The delivered column uses color coding to communicate status at a glance:
- No color — nothing delivered yet (0)
- Yellow/warning — partially delivered (some material arrived, but less than ordered)
- Green — fully delivered (delivered ≥ ordered)
6.2.9 — The Rating System
Once an order reaches “Delivered” status, a star icon appears in the actions column. This enables a supplier delivery rating system. The user can rate the delivery on a 0.5–5 star scale (half-star increments) and optionally add a text note explaining the rating.
The rating modal shows the order context (group code, supplier name, material, delivered quantity) so the user knows exactly what they are rating. The rating and note are stored directly on the order record.
This rating data contributes to supplier performance tracking — over time, Solen can see which suppliers consistently deliver on time and in good condition, and which ones have recurring issues.
Business logic: The rating button only appears for “delivered” orders. You cannot rate a pending order because there is nothing to evaluate yet. If the order already has a rating, the star icon appears filled and yellow, showing the existing score in a tooltip on hover.
6.2.10 — Client-Side Search and Localization
The table includes a search bar that filters orders across all visible columns using client-side filtering. What makes this non-trivial is that the data is stored in English (e.g., “copper”, “pending”) but the users search in Turkish. The search system maintains two translation maps:
- Material types: copper→bakır, tin→kalay, plastic→plastik, catalyst→katalizör, dye→boya, palette→palet, reel→makara
- Statuses: pending→beklemede, delivered→teslim alındı
When the user types “bakır”, the filter matches any row where material_type === 'copper'. It also searches dates in DD.MM.YYYY format (not ISO), so searching “15.03” finds orders expected on March 15. The search covers: group code, supplier name, material name, material type (both EN and TR), status (both EN and TR), quantities, dates (both raw and formatted), and notes.
6.2.11 — Table Layout and Columns
The ProTable displays 11 columns with a scrollable layout:
| Column | Description | Behavior |
|---|---|---|
| Sipariş No | Order group code (R1, R2…) | Fixed left, blue tag |
| Malzeme | Material type | Colored tag matching type |
| Ürün | Specific product name from supplier catalog | Blue tag |
| Tedarikçi | Supplier name | Blue tag |
| Sipariş Miktarı | Ordered quantity + unit | Turkish number formatting |
| Gelen Miktar | Delivered quantity + unit | Green when complete, yellow when partial |
| Sipariş Tarihi | When order was placed | DD.MM.YYYY format |
| Beklenen Tarih | Expected delivery date | Red if overdue (past today) |
| Geliş Tarihi | Actual delivery date | Green text, or “-” if not delivered |
| Durum | Order status | Inline dropdown — clickable to change |
| İşlem | Action buttons | Fixed right: history, rating, edit, delete |
The expected date column deserves special attention: it compares the date against today and renders it in red if the date is in the past. This gives instant visual feedback about overdue orders without any manual flagging.
6.2.12 — API Endpoints
| Method | Endpoint | Purpose |
|---|---|---|
GET | /material-orders/list | Fetch all orders, sorted by expected date ascending. Optional filters: status, material type. Returns enriched data with supplier name and material name resolved from related tables. |
GET | /material-orders/pending-by-type/{type} | Returns pending orders grouped by supplier + expected date. Consumed by Material Entry to let operators link deliveries to orders. Not used by the Orders page itself. |
POST | /material-orders/create | Creates one or more order rows with a shared group code. Accepts an items array; generates a new R-code; auto-determines unit (kg vs adet) from material type. |
PUT | /material-orders/update/{id} | Updates a single order. Handles status changes (auto-stamps delivered_date), date changes with audit trail recording, and quantity/notes updates. |
GET | /material-orders/date-changes/{id} | Fetches the complete date change history for one order. Returns all changes sorted newest-first, with the username of who made each change. |
PUT | /material-orders/rate/{id} | Saves a 0.5–5 star rating and optional note. Validates the range on the backend. Only meaningful for delivered orders (enforced on frontend by showing the button conditionally). |
DELETE | /material-orders/delete/{id} | Hard-deletes an order. The CASCADE relationship automatically removes associated date change history records. |
6.2.13 — Database Schema
The module uses two tables:
material_orders — the main order tracking table. Each row represents one material line in a purchase order. Key fields:
order_group_code(indexed) — groups multiple items from the same shipment (R1, R2…)supplier_id→ FK tosupplierssupplier_material_id→ FK tosupplier_materials— the specific product orderedquantity/delivered_quantity— ordered vs. actually receivedunit— auto-determined: “adet” for palette/reel, “kg” for everything elseorder_date/expected_date/delivered_date— the three date milestonesstatus— pending / shipped / delivered / cancelledrating/rating_note— post-delivery evaluationcreated_by→ FK tousers— audit trail for who created the order
material_order_date_changes — the audit trail for expected date modifications. Each row captures one change event:
order_id→ FK tomaterial_orderswithCASCADEdeleteold_date/new_date— what changedreason_type— supplier, solen, or otherreason_detail— free text (only for “other” type)changed_by→ FK tousers
6.2.14 — How Material Orders Connect to Material Entry
This is the most critical downstream connection. The pending-by-type endpoint exists specifically for Material Entry. When an operator starts entering a delivery of, say, copper wire, the Material Entry page calls this endpoint with material_type=copper. The response comes back as groups organized by supplier + expected date, showing each pending order line with its product name and quantity.
The operator selects the matching order group, and the system builds an internal order_map keyed by supplier_material_id. As each lot is entered, it is automatically linked to the correct order line. This link is stored in the raw_materials.material_order_id field.
When a material entry is saved with an order link, the backend increments the order’s delivered_quantity. This happens in the material entry routes, not in the order routes — the order module is passive in this regard. It creates the tracking records; the entry module updates them on delivery.
The default sort order tells a story: orders are sorted by expected_date ASC — the soonest deliveries appear first. Combined with the red overdue highlighting, the table naturally pushes the most urgent items to the top. A warehouse manager opening this page immediately sees what is late and what is coming next.
6.2.15 — How Material Orders Feed the Stock Projection System (Projeksiyon)
This is arguably the most important downstream consumer of Material Orders data, and it operates at a completely different abstraction level than the order page itself.
The Projeksiyon (Stock Projection) system provides a weekly forecast of raw material availability across the entire factory. It answers the question: “Week by week, will we have enough of each material, or will we run short?” To answer this, it needs two data streams:
- MINUS (İhtiyaç) — how much material will production consume each week. This comes from Work Cards (production plans) — KC cards for copper, KL cards for tin, EX cards for plastics/catalyst/dye/antirodent, Aktarma cards for reels, Paletleme cards for palettes.
- PLUS (Gelen) — how much material is arriving each week. This comes entirely from Material Orders.
The Projeksiyon service reads every non-cancelled material order and places it into the correct week based on a critical decision: if the order has been delivered, it uses the delivered_date (actual arrival). If it is still pending or shipped, it uses the expected_date (planned arrival). Similarly, for delivered orders it uses delivered_quantity (what actually came), and for pending orders it uses quantity (what was ordered).
This means that every change a user makes in the Material Orders page directly affects the projection:
- Creating a new order → adds a PLUS entry to the expected week
- Changing the expected date → moves that PLUS from one week to another (which is why date change tracking matters so much — it shifts the projection)
- Changing the quantity → increases or decreases the PLUS for that week
- Cancelling an order → removes the PLUS entirely (cancelled orders are excluded)
- Order becoming delivered → the PLUS switches from expected_date/quantity to delivered_date/delivered_quantity, reflecting reality
The projection is described in the codebase comments as a “permanent plan”, but the implementation tells a more nuanced story. It does not change when production completes (finished work cards still count as planned consumption). However, it does react to material deliveries: when Material Entry marks an order as delivered, the projeksiyon switches from using expected_date + quantity (the plan) to delivered_date + delivered_quantity (the actual). This means the PLUS entry can shift to a different week if the delivery arrived on a different date than expected, and the amount can change if the delivered quantity differs from the ordered quantity. So the projection is a living view that blends planned and actual data — pending orders show what should come, delivered orders show what actually came.
The service also resolves each order down to its specific material name via the supplier_material relationship. So the projection does not just show “+500 kg plastic in Week 12” — it shows “+500 kg HFX-500P in Week 12”. This per-material granularity allows the projection to detect shortages at the individual material level, not just the category level.
The projection calculates a cumulative balance across all weeks. If Week 10 has +500 kg copper arriving but Week 11 needs -800 kg for production, the cumulative goes negative, flagging a shortage. The frontend highlights these shortages visually. Without Material Orders providing the PLUS side, the entire projection would show only consumption with no incoming — making it useless for planning.
The dual role of Material Orders: On the surface, this module is a simple tracking table. But underneath, it is one of the two foundational data sources for the factory’s stock projection system. Every order, every date change, every quantity update ripples through the weekly projection and affects shortage detection across the entire planning horizon. This is why accurate order entry and diligent date change tracking are not administrative overhead — they are critical inputs to the factory’s production planning accuracy.
6.2.16 — Delete Behavior
Orders can be hard-deleted through a confirmation dialog. There is no soft-delete / archive mechanism. The CASCADE relationship on material_order_date_changes ensures that deleting an order also removes its date change history. However, if a raw_material entry is already linked to this order via material_order_id, that link will become a dangling reference (the foreign key on raw_materials is nullable, so it will not cause a constraint violation, but the traceability chain is broken).
6.2.17 — What This Module Does NOT Do
- It does not generate official purchase orders or invoices
- It does not manage pricing or costs — there are no price fields
- It does not send notifications or emails to suppliers
- It does not use WebSocket — the table refreshes on explicit user actions (save, delete, status change)
- It does not use the permission system — any authenticated user can access all features
- It does not handle pricing, invoicing, or financial tracking of any kind
6.3 — Hammadde Girişi (Material Entry)
6.3.1 — Purpose and Business Context
This is where physical reality meets the digital system. When a truck arrives at Solen’s factory with raw materials, the warehouse operator uses this module to register what came in. Every kilogram of copper, every barrel of plastic, every pallet — it all enters the system through this single page.
The module does not just record inventory; it is the bridge between three other systems. It consumes data from Supplier Management (who delivered), links deliveries to Material Orders (what was expected), generates the QR codes and database records that appear in Hammaddeler Listesi (the inventory), and indirectly updates the Projeksiyon (stock projection) by changing order status and delivered quantities.
The page lives under /hammadde/hammadde-girisi in the menu, but its frontend component is actually located at Lab/MaterialEntry — reflecting that material entry is a lab/warehouse operation.
6.3.2 — CRUD Flow
Create (The Primary Operation):
- Click one of the 8 material type cards on the landing page (Bakır, Kalay, Plastik, Katalizör, Boya, Antirodent, Palet, Makara)
- Step 1 — Order Selection: Modal appears showing pending orders for that material type, fetched from
GET /material-orders/pending-by-type/{type}. User selects an order group (pre-fills supplier + materials) or clicks “Manuel Giriş” (blank form) - Step 2 — Entry Form: Dynamic form opens based on material type archetype:
- Copper: collapsible palette panels (lot number + form weight + measured weight + status per palette) → “+” for more palettes
- Tin: single form (net weight + status)
- Lot-based: collapsible lot panels (material type dropdown + lot number + weights + quantity + photos per lot) → “+” for more lots
- Count-based: collapsible type panels (type dropdown + quantity + photos per type) → “+” for more types
- Upload delivery photos (irsaliye) and optional test/damage photos (per-lot for lot-based)
- On submit: type-specific endpoint called (
POST /api/materials/copper-entry,/tin-entry,/lot-entry,/palette-entry, or/reel-entry) - Backend creates one
raw_materialsrow per palette/lot/type with deterministic QR code (A1, B1, C1…). If linked to an order: automatically updatesdelivered_quantity,status=DELIVERED, anddelivered_date=today on the order - On success: success modal appears showing all generated QR codes with a “Yazdır” (Print) button per QR. WebSocket broadcasts to
materialsandallrooms
Update (Super Admin Only):
- Not available on this page directly — updates happen through the Hammaddeler Listesi (6.4) edit modal
- Backend endpoint exists:
PUT /api/materials/{id}withcheck_permission_smartrequiring super_admin oredit_materialpermission
Delete (Super Admin Only):
- Not available on this page directly — deletion happens through Hammaddeler Listesi (6.4)
- Backend endpoints:
DELETE /api/materials/{id}(soft) andDELETE /api/materials/{id}/hard(permanent) - Hard delete also removes associated
material_photosrecords
6.3.3 — The Landing Page: Eight Material Cards
When the user opens the page, they see a grid of 8 clickable cards, one for each raw material type the factory handles:
Each card has a unique background color that adapts to dark/light theme, a large icon, and the material name. The letter in parentheses is the QR code prefix for that material type. Clicking any card does not immediately open the entry form — it triggers the Order Selection flow first.
6.3.4 — The Two-Step Entry Flow
This is a critical design decision. Material entry follows a two-step process:
- Step 1: Order Selection Modal — When the user clicks a material card, the system immediately fetches pending orders for that material type from
GET /material-orders/pending-by-type/{type}. A modal appears showing:- A list of pending order groups, each showing the supplier name, the material names in that shipment, the total quantity, and the expected date
- A “Manuel Giriş” (Manual Entry) button at the bottom for deliveries that have no corresponding order
- Step 2: Entry Form — The user either picks an order group (which pre-fills supplier, material types, and quantities) or clicks Manual Entry (which opens a blank form). The entry form is a dynamic modal that changes its entire structure based on the material type.
This two-step flow serves a critical purpose: it encourages operators to link deliveries to orders, which keeps the tracking chain intact. If orders exist, they are displayed prominently. Manual entry exists as a fallback, not the default.
6.3.5 — Order Pre-filling Logic
When the user selects an order group in Step 1, the following happens automatically:
- The supplier field is auto-filled and disabled — you cannot change the supplier when entering against an order
- For lot-based materials (plastic, catalyst, dye, antirodent): the system pre-creates one lot panel per order line item, with the
material_type_id(specific product) already selected from the order’ssupplier_material_id - For palettes and reels: the system pre-creates one type panel per order line item, with the palette/reel type and quantity pre-filled from the order
- For copper and tin: the supplier is pre-filled; the rest (palette weights, lot numbers) must be entered manually since orders track total kg, not per-palette details
The order group also carries the order_ids array, which is sent to the backend on save so the system knows which orders to mark as delivered.
6.3.6 — Four Form Archetypes
The entry form is not one form. It is four completely different form structures that render conditionally based on which material type was selected. They all share two common fields (supplier and received-by person) and a common photo section, but everything in between is unique.
Archetype 1: Copper (Multi-Palette)
Copper arrives in large palettes (Filmaşin — wire rod coils on palettes). A single delivery may contain multiple palettes, each with its own weight. The form presents a dynamic list of collapsible palette panels:
- Each panel contains: Lot Number (with keyboard toggle), Form Weight (weight on the supplier’s delivery form), Measured Weight (actual weight measured at Solen), and Status (received or rejected)
- The user can add more palettes with a “+” button and remove any panel except the last one
- The form starts with one empty palette panel
- On save, the backend creates one
raw_materialsrow per palette, each with its own QR code (A1, A2, A3…) - Both form_weight and measured_weight are stored — the difference helps identify discrepancies between what the supplier claims and what actually arrived
remaining_weightis initialized tomeasured_weightand is decremented by production later
Archetype 2: Tin (Single Entry)
Tin is the simplest entry type. A single delivery is one weight measurement:
- The form shows just two fields: Net Weight (kg) and Status
- One row in
raw_materials, one QR code (B1, B2…) - The lot number is auto-generated as
TIN-{sequence} remaining_weightis initialized to the entered weight
Archetype 3: Lot-Based (Plastic, Catalyst, Dye, Antirodent)
These four material types share the same form structure because they all arrive in lots (batches from a production run at the supplier). The form presents a dynamic list of collapsible lot panels:
- Each panel contains: Material Type (dropdown of specific products from the supplier’s catalog, e.g., “HFX-500P”), Lot Number (with keyboard toggle), Form Weight, Measured Weight, Quantity (number of labels to print — how many containers in this lot), Status, and Test/Damage Photos (per-lot, up to 3)
- The Material Type dropdown is context-sensitive: if the entry is linked to an order, it only shows materials from that order’s line items. If manual, it shows all of the supplier’s materials for that type
- On save, the backend creates one row per lot with its own QR code (C1/D1/E1/F1…)
- The backend resolves the specific
material_namefromSupplierMaterialand appends theboyut(size) if present, creating names like “HFX-500P - 2mm”
Archetype 4: Count-Based (Palette, Reel)
Palettes and reels are counted items, not weighed. The form presents collapsible type panels:
- Each panel contains: Type (dropdown from supplier’s catalog, e.g., “Euro Palet - 120 cm” or “500m Makara - 40 cm - Plastik”), Quantity (count), Status, and Damage Photos (per-type, up to 3)
- The type dropdown is populated from the supplier’s
materials_providedfiltered by palette or reel type - For reels, the display includes
material_name - boyut cm - reel_typeto show the full specification - On save, the backend creates one row per type with QR code (I1/H1…). The lot number is auto-generated as
PALET-{seq}orREEL-{seq} - The
weightfield is used to store the quantity (count), andquantityalso stores the count — this is a dual-purpose field for backward compatibility
6.3.7 — The Lot Number Keyboard Toggle
This is a small but important UX feature. Lot numbers can be purely numeric (e.g., “24081901”) or alphanumeric (e.g., “LOT-A24-081”). On mobile devices, the input keyboard that appears depends on the inputMode attribute. The system defaults to numeric (number pad) but provides a toggle button inside the input field that switches between number pad and full keyboard. Each lot input tracks its own keyboard mode independently.
6.3.8 — The QR Code System
Every material entry generates a unique, deterministic QR code. The system uses a simple but effective scheme: a letter prefix identifying the material type, followed by a sequential number that increments per type.
| Prefix | Material | Examples |
|---|---|---|
| A | Copper (Bakır) | A1, A2, A3… |
| B | Tin (Kalay) | B1, B2, B3… |
| C | Plastic (Plastik) | C1, C2, C3… |
| D | Catalyst (Katalizör) | D1, D2, D3… |
| E | Dye (Boya) | E1, E2, E3… |
| F | Antirodent | F1, F2, F3… |
| I | Palette (Palet) | I1, I2, I3… |
| H | Reel (Makara) | H1, H2, H3… |
The sequence number is determined by querying the highest existing sequence_number for that material type and incrementing by one. Each material type has its own counter — copper can be at A847 while tin is at B23. The QR code is stored in the qr_code column with a unique constraint, ensuring no duplicates.
For copper entries with multiple palettes, the backend increments the sequence for each palette within the same transaction (A45, A46, A47 for a 3-palette entry).
6.3.9 — Photo Architecture
The photo system has two tiers:
- Common delivery photos (İrsaliye) — these are photos of the delivery document (waybill/invoice). They apply to the entire delivery, not individual items. Up to 3 photos, shared across all materials in the entry. Shown for all material types.
- Per-item photos (Test/Hasar) — these are test reports or damage documentation for individual items:
- For copper and tin: a common “Test Report Photo” section (up to 3) appears alongside the delivery photos
- For lot-based materials: each lot panel has its own “Test/Hasar Fotoğrafları” section (up to 3 per lot)
- For palette and reel: each type panel has its own “Hasar Fotoğrafları” section (up to 3 per type)
Photos are converted to base64 on the frontend before submission. The backend stores them in the material_photos table (not on the material row itself) with a photo_type discriminator (“delivery” or “test”) and a sequence number. This separate table enables lazy loading — when listing materials, only photo IDs are returned; the actual base64 data is fetched on demand via GET /materials/photo/{id}.
6.3.10 — Order Linking and Automatic Delivery Updates
This is where Material Entry connects back to Material Orders and, through that, to the Projeksiyon system.
When a delivery is linked to orders, the backend performs these additional steps after creating the material rows:
- For copper/tin: the first order ID from
material_order_idsis used. The total delivered weight (sum of all palette weights for copper, or the single weight for tin) is added to the order’sdelivered_quantity. The order status is set to “delivered” anddelivered_dateis stamped with today. - For lot-based materials: the system builds an
order_mapkeyed bysupplier_material_id. For each lot, it checks if the lot’smaterial_type_idmatches a key in the map, and if so, links that lot to the corresponding order. Each order’sdelivered_quantityis incremented by its specific lots’ weights. - For palette/reel: same order_map logic, but using the palette/reel type ID to match against
supplier_material_id. Delivered quantity increments by the item count.
Every linked order gets status = delivered, delivered_date = today, and delivered_quantity += amount. This is the automatic status transition discussed in Section 6.2.
6.3.11 — The Success Modal and QR Printing
After a successful save, the system does not simply close the form. It shows a success modal that displays:
- A confirmation message with the generated QR codes listed (e.g., “A45, A46, A47”)
- Two buttons: OK (close and return) and Yazdır (Print) which triggers the QR label printing flow
The print button calls POST /materials/print/{material_id} which routes to the print queue system. For lot-based entries with multiple lots, the system iterates through all created material IDs and prints each one sequentially.
The save button text is contextual: for palette and reel entries it says “Kaydet (QR Yok)” (Save, no QR) because palettes and reels use physical identification rather than printed QR labels. For all other types it says “Kaydet ve QR Kod Oluştur” (Save and Generate QR Code).
6.3.12 — Supplier Filtering
When a material card is clicked and the entry form opens, the supplier dropdown does not show all suppliers. It is filtered to show only suppliers who have active materials of the selected type in their materials_provided catalog. This filtering happens client-side by checking each supplier’s materials array. If no suppliers are found for a material type, the dropdown shows a helpful message: “Bu malzeme için tedarikçi bulunamadı. Önce tedarikçi ekleyin.”
6.3.13 — Permission System
Material Entry registers 13 page-level buttons with the ultra-granular permission system:
access_page— ability to see the page at alladd_copper,add_tin,add_plastic,add_catalyst,add_dye,add_antirodent,add_palette,add_reel— one permission per material cardsubmit_form— the save buttonupload_delivery_photo,upload_test_photo— photo upload capabilitiesprint_qr_modal— the print button in the success modal
The backend also enforces role-based access: only lab_user and super_admin user types can create material entries. Other user types receive a 403 Forbidden response.
6.3.14 — WebSocket Broadcasting
After creating material entries (copper/tin path), the backend broadcasts a WebSocket update for each created material with the event type “materials:create”, including the material ID, QR code, and type. This allows the Hammaddeler Listesi page to update in real-time without manual refresh when new materials are entered from another session.
The update and hard-delete operations also broadcast “materials:update” and “materials:delete” events respectively.
6.3.15 — Material Update (Super Admin Only)
An existing material record can be updated through PUT /materials/update/{id}, but this is restricted to super_admin only. The updatable fields include: QR code (with uniqueness validation), lot number, weight, form weight, remaining weight, quantity, status, notes, and photos. This is used for corrections — if an operator entered the wrong weight, a super admin can fix it. The QR code is changeable but must remain unique across all materials.
6.3.16 — Material Deletion
Hard deletion is available through DELETE /materials/{id}/hard-delete (super admin only). Before deleting, the system checks:
- If the material status is “in_use”, deletion is blocked — you cannot remove material that is currently in production
- Related print jobs are deleted first
- Related photos are deleted (CASCADE would handle this, but the code does it explicitly)
After deletion, a WebSocket broadcast notifies all connected clients.
6.3.17 — Material Status Lifecycle
Each material record has a status that tracks its journey through the factory:
The entry form only offers two initial statuses: “received” (default) and “rejected”. The other statuses (approved, in_use, consumed) are set by downstream systems — quality control and production modules. There is also a toggle-status endpoint that flips the material’s is_active flag for soft-disable.
6.3.18 — API Endpoints
| Method | Endpoint | Purpose |
|---|---|---|
POST | /materials/entry | Creates copper (multi-palette) or tin (single) entries. Generates QR codes, saves photos, links orders, broadcasts WebSocket. |
POST | /materials/lot-entry | Creates lot-based entries (plastic, catalyst, dye, antirodent). One row per lot with material name resolved from SupplierMaterial. Builds order_map for per-lot linking. |
POST | /materials/palette-entry | Creates palette entries. One row per palette type with I-prefix QR codes. Auto-generates PALET-{seq} lot numbers. |
POST | /materials/reel-entry | Creates reel entries. One row per reel type with H-prefix QR codes. Material name includes boyut and reel_type. Auto-generates REEL-{seq} lot numbers. |
GET | /materials/list | Returns all materials with supplier names, photo IDs (lazy), and order group codes. Used by Hammaddeler Listesi. |
GET | /materials/photo/{id} | Returns single photo base64 data on demand (lazy loading). |
PUT | /materials/update/{id} | Updates material fields including QR code (super admin only). Replaces all photos on update. |
DELETE | /materials/{id}/hard-delete | Permanent deletion (super admin). Blocks if in_use. Cleans up photos and print jobs. |
PUT | /materials/{id}/toggle-status | Toggles is_active flag for soft disable/enable. |
GET | /materials/print/preview/{id} | Generates label preview image (base64) before printing. |
POST | /materials/print/{id} | Sends QR label to print queue for physical printing. |
POST | /materials/print/update-printer-ip | Updates printer IP/port (super admin). Tests connection after update. |
GET | /materials/print/test-connection | Tests printer connectivity (lab user + super admin). |
6.3.19 — Database Schema: raw_materials
All 8 material types are stored in a single table. Key fields:
material_type— discriminator: raw_copper, raw_tin, raw_plastic, raw_catalyst, raw_dye, raw_antirodent, raw_palette, raw_reelmaterial_name— specific product name resolved from SupplierMaterial (e.g., “HFX-500P - 2mm”). NULL for copper and tin.sequence_number— per-type incrementing counter used for QR code generationqr_code— unique, deterministic identifier (A1, B5, C23, etc.)supplier_id→ FK tosuppliersmaterial_order_id→ nullable FK tomaterial_orders— the traceability link back to the purchase orderlot_number— user-entered for copper/lot-based, auto-generated for tin/palette/reelweight— measured weight for weight-based materials, count for palettes/reelsremaining_weight— initialized to weight, decremented by production consumptionform_weight— weight on the supplier’s delivery form (copper and lot-based only)quantity— number of containers/labels for lot-based materials, count for palette/reelstatus— received / approved / in_use / consumed / rejectedentered_by→ FK tousers
6.3.20 — How Material Entry Connects to Everything
This is the most interconnected module in the Hammadde system. Here is every upstream and downstream connection:
- Supplier Management (upstream) — provides the supplier list, material catalogs, and product names. The entry form’s dropdowns are populated entirely from supplier data.
- Material Orders (upstream + downstream) — provides pending orders for the Step 1 modal. On save, Material Entry writes back to orders: setting status to delivered, stamping the date, incrementing delivered quantity.
- Projeksiyon (indirect downstream) — by changing order status from pending to delivered and setting delivered_date/delivered_quantity, Material Entry shifts the Projeksiyon from planned to actual data for those orders.
- Hammaddeler Listesi (downstream) — every row created here appears immediately in the materials list. The list page consumes
/materials/listand displays all entries with their QR codes, weights, statuses, and photos. - Production System (downstream) — production modules consume materials by referencing their QR codes, decrementing
remaining_weight, and changing status to in_use/consumed. - Print Queue (downstream) — the success modal triggers label printing through the print queue system.
Why the form changes shape: Each material type has fundamentally different physical properties. Copper arrives on palettes that must be individually weighed. Tin is a single block. Plastics come in numbered lots that need individual tracking. Palettes and reels are counted, not weighed. A single generic form would either be too complex (showing irrelevant fields) or too simple (missing required data). The four-archetype approach ensures every material gets exactly the fields it needs — no more, no less.
6.4 — Hammaddeler Listesi (Materials List)
6.4.1 — Purpose and Business Context
This is the inventory view of every raw material that has ever entered the factory. While Material Entry is about recording arrivals, this module is about seeing, searching, managing, and acting on the inventory. It is a read-heavy module — the primary use case is looking up materials by QR code, checking remaining weights, viewing photos, and reprinting labels. But it also provides editing and deletion capabilities for super admins.
The page consumes the output of Material Entry: every row created through any of the four entry archetypes shows up here. It is the single source of truth for “what raw materials do we have, and what state are they in?”
6.4.2 — CRUD Flow
Read (Primary Operation):
- Page loads →
GET /api/materials/listfetches all materials (photo IDs only, not base64 data) - Table renders 14 columns. Thumbnail photos lazy-load via
GET /api/materials/{id}/photos/first - Click photo thumbnail → preview modal opens, remaining photos fetched on demand via
GET /api/materials/{id}/photos - Search bar filters client-side across QR code, material type, supplier, lot number, status (includes Turkish translations)
- WebSocket connection keeps table in sync — new materials from Hammadde Girişi appear automatically
Edit (Three Form Variants):
- Click edit icon on a material row (requires
edit_materialpermission) - Edit modal opens with fields adapted to the material type:
- Copper/Tin: QR code, lot number, form weight, measured weight, remaining weight (editable), status (received/available/in_use/consumed), photos, notes
- Lot-based: QR code, lot number, form weight, measured weight, quantity (label count), status (received/rejected), photos, notes
- Palette/Reel: QR code, quantity (adet), status (received/rejected) — simplest variant
- QR code field shows warning: “Dikkat: QR kodunu değiştirmek sistemdeki kayıtları etkileyebilir”
- Existing photos lazy-load into the modal; user can add new or remove existing photos
- On submit:
PUT /api/materials/{id}→ toast “Hammadde başarıyla güncellendi” → modal closes → WebSocket broadcast → table refreshes
Print (Label Reprint):
- Click print icon on a material row (requires
print_qrpermission; disabled for palette/reel with tooltip) - Printer selection modal opens showing QR code, label count, and dropdown of active printers with locations
- Select printer →
POST /print-queue/queue/{material_id}?printer_id={id} - Job enters Admin’s print queue as QUEUED status
Delete:
- Click delete icon (requires
delete_materialpermission) - Popconfirm with warning
- On confirm:
DELETE /api/materials/{id}(soft delete — marks as inactive) - For permanent removal:
DELETE /api/materials/{id}/hard(requireshard_delete_materialpermission, flagged as critical). Also removes associatedmaterial_photosrecords - WebSocket broadcast → table refreshes
6.4.3 — Real-Time Updates via WebSocket
Unlike Material Orders (which refreshes on user actions), this page subscribes to WebSocket rooms materials and all. When any user creates, updates, or deletes a material from any device or session, the table automatically reloads. This means if an operator enters a delivery through Material Entry on a tablet in the warehouse, a manager viewing the list on their desktop sees the new rows appear instantly without pressing refresh.
6.4.4 — The Table: 14 Columns
The ProTable displays a comprehensive view of every material record:
| Column | Description | Behavior |
|---|---|---|
| QR Kod | Unique identifier (A1, B5, C23…) | Fixed left, blue tag |
| Sipariş No | Linked order group code (R1, R2…) | Cyan tag, or “-” if no order link |
| Tip | Material type | Colored tag with Turkish name (Bakır, Kalay, Plastik…) |
| Malzeme Adı | Specific product name | Shows resolved name, defaults to “Filmaşin” for copper, “Kalay Külçe” for tin |
| Tedarikçi | Supplier name | Plain text |
| Lot No | Lot/batch number | Shows “—” for palette/reel (not applicable) |
| Form Miktar | Supplier’s declared weight | kg or adet based on type |
| Ölçülen Miktar | Actual measured weight | kg or adet based on type |
| Kalan | Remaining weight after production | Color-coded: yellow if <95% of original, red if <20%. “—” for palette/reel. |
| Giriş Tarihi | When material was entered | DD.MM.YYYY Turkish locale |
| İrsaliye | Delivery document photos | Lazy-loaded thumbnail with “+N” badge if multiple |
| Test | Test/damage photos | Lazy-loaded thumbnail with “+N” badge if multiple |
| Durum | Material status | Colored tag: green=received, red=rejected, blue=available, yellow=in_use, grey=consumed |
| İşlemler | Action buttons | Fixed right: print, edit, delete |
6.4.5 — The “Kalan” (Remaining Weight) Column
This is one of the most operationally important columns. It shows how much of the original material is still available after production has consumed portions of it. The color coding provides instant visual feedback:
- No color — the remaining weight is close to the original (within 5% tolerance)
- Yellow / bold — the material has been partially consumed (remaining is less than 95% of original)
- Red / bold — the material is nearly depleted (remaining is less than 20% of original)
For palette and reel materials, this column shows “—” because counted items do not have a remaining weight concept — they are either available or consumed as whole units.
If remaining_weight is not set (older records), the system falls back to the original weight value for backward compatibility.
6.4.6 — Lazy Photo Loading System
The photo columns are the most technically interesting part of this page. With potentially thousands of materials and up to 6 photos each (3 delivery + 3 test), loading all photo data with the table would be catastrophically slow. The system uses a two-tier lazy loading approach:
- Table load: the
/materials/listendpoint returns only photo IDs (small integers), not photo data. This keeps the API response fast. - Thumbnail load: each photo cell renders a
LazyPhotoThumbnailcomponent that independently fetches the first photo’s base64 data viaGET /materials/photo/{id}. Only the first photo is loaded for the thumbnail. - Preview load: when the user clicks a thumbnail to preview, the component fetches all remaining photos in that group. This means if a material has 3 delivery photos, only the first is loaded initially; the other two load on click.
The thumbnail shows as a 40x40 pixel image with rounded corners. If there are multiple photos, a blue badge appears at the bottom-right showing “+N” (e.g., “+2” for 3 photos). Clicking opens the full-size preview with left/right navigation and a page counter.
6.4.7 — Client-Side Search
The search bar filters across all loaded data on the client side. Like the Material Orders search, it supports bilingual matching with EN→TR translation maps for both material types and statuses. Searchable fields include: QR code, material name, material type (raw_copper→bakır, etc.), supplier name, lot number, order group code, status (received→teslim alındı, etc.), and all weight/quantity values as strings.
The search is implemented as a useMemo that recomputes on every keystroke against the full allMaterials array. The table renders the filtered result via dataSource={filteredData}.
6.4.8 — The Edit Modal: Three Form Variants
The edit modal adapts its fields based on the material type being edited, using three variants:
- Copper/Tin: QR code, lot number, form weight, measured weight, remaining weight (editable for corrections, with helper text explaining its purpose), status (4 options: received, available, in_use, consumed), photos, notes
- Lot-based (plastic, catalyst, dye, antirodent): QR code, lot number, form weight, measured weight, quantity (label count), status (2 options: received, rejected), photos, notes
- Palette/Reel: QR code, quantity (adet), status (2 options: received, rejected) — the simplest form, no weights or lot numbers
Every variant includes the QR code field with a warning: “Dikkat: QR kodunu değiştirmek sistemdeki kayıtları etkileyebilir” (Changing the QR code may affect system records).
When opening the edit modal, photos are lazy-loaded: the system fetches each photo by ID from the API and converts them into Ant Design UploadFile objects for the upload component. This means the modal shows the existing photos and allows adding new ones or removing existing ones.
6.4.9 — Print with Printer Selection
The print button in the actions column opens a printer selection modal before printing. This modal shows:
- The material’s QR code for confirmation
- The label count (for lot-based materials with
quantity > 1, multiple labels are printed) - A dropdown of active printers fetched from
GET /printers/list?active_only=true, showing each printer’s name and location
The print job is submitted to POST /print-queue/queue/{material_id}?printer_id={id}. The print button is disabled for palette and reel materials (they don’t use QR labels), with a tooltip explaining why.
6.4.10 — Permission System
The page registers 12 page-level buttons:
access_page,view_table,view_photos,search_filter— read capabilitiesprint_qr— printingedit_material— open edit modaldelete_material,hard_delete_material(marked critical) — deletionupdate_qr_code,update_weight,update_status,update_photos— granular edit field permissions
The “hard_delete_material” permission is flagged as critical: true, signaling to administrators that this is a destructive, irreversible action.
6.4.11 — Table Localization
The ProTable toolbar buttons (reload, density, column settings) are localized to Turkish through a custom ProConfigProvider that maps internal message IDs to Turkish strings. This includes: “Yenile” (Reload), “Satır Yoğunluğu” (Row Density), “Tablo Ayarları” (Table Settings), “Sütun Görünümü” (Column Display), pin directions, and more. The Ant Design locale is also set to trTR for date/number formatting.
6.4.12 — How This Module Connects to the System
- Material Entry (upstream) — the sole data source. Every row in this table was created by one of the four entry endpoints.
- Material Orders (indirect) — the “Sipariş No” column shows the order group code, providing visual traceability from inventory back to the purchase order.
- Production System (downstream) — production modules read from this inventory (via
/materials/listor direct DB queries), select materials by QR code, and updateremaining_weightandstatusas they consume materials. This page reflects those changes in real-time via WebSocket. - Print Queue (downstream) — the print action sends jobs to the print queue for physical label output.
The full traceability chain visible on one row: A single row in this table tells you: what it is (QR code + type + material name), where it came from (supplier + order group code), how much there was (form weight vs. measured weight), how much is left (remaining weight with color coding), what it looks like (delivery and test photos), and what its current state is (status). This is the complete lifecycle view of a raw material — from delivery to depletion — all in one row.
7. CONCLUSION
Hammadde is where the physical world meets the digital system. Every gram of copper, every tin ingot, every plastic lot, every palette and reel that enters the factory gate passes through this module. It assigns the identity (QR code), records the evidence (photos), tracks the quantity (weight), and maintains the state (status lifecycle). Without it, the production system has no materials to consume, the stock system has no inventory to report, and the projection system has no incoming data to forecast with.
7.1 What This Document Covered
This deep dive analyzed the Hammadde module across four submodules, from supplier definitions through purchase tracking, material entry, and inventory viewing:
| Section | Submodule | Depth | Key Insight |
|---|---|---|---|
| 6.1 | Tedarikçi Yönetimi | Full | One-to-many supplier→materials with material-specific properties (density, boyut, reel_type); delete-and-recreate update strategy; consumed by Entry, Orders, and Calculator |
| 6.2 | Hammadde Sipariş | Full | Order group codes (R1, R2…) for shipment grouping; date change audit trail with mandatory reasons; automatic status updates by Material Entry; feeds Projeksiyon as the “PLUS” side |
| 6.3 | Hammadde Girişi | Exhaustive | Four dynamic form archetypes (copper/tin/lot-based/count-based); deterministic QR codes (A–H prefixes); two-tier lazy photo loading; automatic order delivery updates |
| 6.4 | Hammaddeler Listesi | Full | 14-column inventory view with color-coded remaining weight; three edit form variants; lazy photo thumbnails with preview; real-time WebSocket updates |
7.2 Architectural Principles
Physical-First Design
Each material type has fundamentally different physical properties. Rather than forcing a single generic form, the system uses four distinct archetypes — ensuring copper palettes are individually weighed, tin gets auto-generated lot numbers, plastics track lot counts for labels, and palettes/reels are simply counted. The UI adapts to the material, not the other way around.
Lazy Everything
Photos are stored separately and loaded on demand. The materials list returns only photo IDs, not base64 data. Thumbnails load one photo each; previews fetch the rest on click. This pattern keeps a table with thousands of rows and potentially tens of thousands of photos responsive.
Automatic Cascading
When a material delivery is recorded, the linked purchase order’s delivered_quantity, status, and delivered_date update automatically. This cascades further into Projeksiyon, which switches from planned to actual data. One action in Material Entry ripples through Orders and into the projection system without manual intervention.
Deterministic Identity
QR codes follow a strict prefix+sequence pattern (A=copper, B=tin, C=plastic…). No UUIDs, no randomness. The sequence is per-type and auto-incrementing. This means QR codes are human-readable, sortable, and predictable — critical for a factory floor where operators scan labels with handheld devices.
7.3 The Numbers
7.4 How Hammadde Feeds the Factory
Suppliers define what materials are available and their properties. Orders track what has been purchased and when it’s arriving. Material Entry records the physical arrival — weighing, photographing, QR-coding, and linking back to the order. The Materials List becomes the living inventory that production consumes. As production uses materials, remaining weights drop, statuses change, and the cycle continues until the material is fully consumed.
In parallel, every order feeds the Projeksiyon system with planned and actual delivery data, enabling the factory to forecast material availability weeks ahead.
7.5 Final Word
The Hammadde module transforms a chaotic physical process — trucks arriving with copper wire, tin blocks, plastic drums, and wooden palettes — into structured, traceable, searchable digital records. Every material gets an identity, a history, and a status. From the moment a supplier is defined to the moment the last gram of copper is consumed in production, this module provides the data trail. The four-archetype form design, the lazy photo architecture, the automatic order cascading, and the real-time WebSocket updates all serve one goal: making the gap between physical reality and digital record as small as possible. This document has traced that entire chain, from supplier definition to inventory depletion, through every API endpoint, every database column, every frontend interaction, and every cross-module connection.