Back to Modules

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.

February 2026 • Solen Kablo • Living Document

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.

4
SUBMODULES
~30
API ENDPOINTS
6
DATABASE TABLES
5100+
LINES OF FRONTEND

TABLE OF CONTENTS

1. What Hammadde Does 2. The Data Flow 3. The Database Layer 4. The Backend Architecture 5. The Frontend 6. The Submodules 6.1 Tedarikçi Yönetimi 6.2 Hammadde Sipariş 6.3 Hammadde Girişi 6.4 Hammaddeler Listesi 7. Conclusion

1. WHAT HAMMADDE DOES

The Hammadde module answers four questions that the factory’s supply chain depends on:

  1. 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.
  2. 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.
  3. 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.
  4. 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

SUPPLIER
Registered in system
ORDER
R1, R2... created
DELIVERY
Date tracking
ENTRY
QR + photos

Consumption Flow

MATERIAL
Status: received
APPROVED
Lab cleared
IN USE
On production line
CONSUMED
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.

ColumnTypePurpose
idINTEGER PKAuto-increment primary key
material_typeVARCHAR(50)raw_copper, raw_tin, raw_plastic, raw_catalyst, raw_dye, raw_antirodent, raw_palette, raw_reel
material_nameVARCHAR(200)Specific material name from supplier catalog (e.g., HFX500, K388)
sequence_numberINTEGERPer-type counter — separate sequence for each material_type
qr_codeVARCHAR(50) UNIQUEGenerated code: prefix + sequence (A1, B7, C23, I145...)
supplier_idFK → suppliersWhich supplier provided this material
material_order_idFK → material_ordersLinks to purchase order (for delivery tracking)
lot_numberVARCHAR(100)Supplier’s lot number (or auto-generated for tin: TIN-N)
weightFLOATMeasured weight when received (kg)
remaining_weightFLOATWeight remaining after production consumption (kg)
form_weightFLOATOfficial form weight (for copper palettes)
quantityINTEGERCount for lot-based materials (plastic/catalyst/dye/antirodent)
statusVARCHAR(50)received → approved → in_use → consumed | rejected
entered_byFK → usersWho entered this material into the system
notesTEXTFree-form notes
received_dateDATETIMEWhen the material was physically received
created_atDATETIMERecord creation timestamp
updated_atDATETIMELast 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.

ColumnTypePurpose
idINTEGER PKPhoto identifier
material_idFK → raw_materials (CASCADE)Which material this photo belongs to
photo_typeVARCHAR(20)‘delivery’ or ‘test’
sequenceINTEGER1, 2, or 3 (up to 3 photos per type)
photo_dataTEXTBase64 encoded image data
created_atDATETIMEUpload 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.

ColumnTypePurpose
idINTEGER PKOrder identifier
order_group_codeVARCHAR(20)Grouping code (R1, R2...) — orders from same shipment share this
material_typeVARCHAR(50)What is being ordered (copper, plastic, etc.)
supplier_idFK → suppliersWhich supplier is fulfilling this order
supplier_material_idFK → supplier_materialsSpecific material being ordered
quantityFLOATOrdered quantity
delivered_quantityFLOATActually delivered quantity (updated when material entry is linked)
unitVARCHAR(20)‘kg’ for materials, ‘adet’ for palettes/reels
order_dateDATEWhen the order was placed
expected_dateDATEWhen delivery is expected (can change — tracked)
delivered_dateDATEActual delivery date
statusVARCHAR(20)pending → shipped → delivered | cancelled
ratingFLOATPost-delivery rating (0.5–5 stars, half increments)
rating_noteTEXTRating comment
notesTEXTOrder notes
created_byFK → usersWho 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.

ColumnTypePurpose
idINTEGER PKChange record identifier
order_idFK → material_orders (CASCADE)Which order was changed
old_dateDATEPrevious expected date
new_dateDATENew expected date
reason_typeVARCHAR(20)‘supplier’ | ‘solen’ | ‘other’
reason_detailTEXTManual explanation (required for ‘other’)
changed_byFK → usersWho changed the date
created_atDATETIMEWhen the change was recorded

Supplier Tables (2)

suppliers

Company-level supplier records. Each supplier is a single company with contact details.

ColumnTypePurpose
idINTEGER PKSupplier identifier
nameVARCHAR(200) UNIQUECompany name (stored UPPERCASE)
contact_personVARCHAR(100)Primary contact name
phoneVARCHAR(50)Auto-formatted Turkish phone number
emailVARCHAR(100)Email (validated regex)
addressTEXTPhysical address
tax_numberVARCHAR(50)Tax registration number
is_activeBOOLEANSoft delete flag (toggle active/inactive)
notesTEXTFree-form notes

supplier_materials

The one-to-many relationship. One supplier can provide multiple material types, each with type-specific properties.

ColumnTypePurpose
idINTEGER PKSupplier-material link identifier
supplier_idFK → suppliersWhich supplier
material_typeVARCHAR(50)copper, tin, plastic, catalyst, dye, antirodent, palette, reel
material_nameVARCHAR(100)Specific product name (HF-500, CAT-203, etc.)
densityFLOATFor plastic/catalyst/dye/antirodent (g/cm³)
boyutVARCHAR(100)Dimensions for palette/reel
reel_typeVARCHAR(20)For reels: Yazılı/Yazısız (labeled/unlabeled)
is_activeBOOLEANWhether 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 TypeEntry MethodKey Behavior
Copper (raw_copper)POST /materials/entryCreates 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/entryCreates one row. Single weight entry. Lot number auto-generated as TIN-{sequence}.
Plastic, Catalyst, Dye, AntirodentPOST /materials/lot-entryCreates one row per lot. Each lot has its own test photos, weight, and quantity. Material name resolved from supplier_materials table.
PalettePOST /materials/palette-entryCreates one row per palette type. QR prefix I. Lot auto-generated as PALET-{sequence}.
ReelPOST /materials/reel-entryCreates 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:

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

RouteComponentLinesPurpose
/hammadde/hammadde-girisiLab/MaterialEntry1,930Material entry with 8 material type cards, photo uploads, order linking
/hammadde/hammaddeler-listesiHammadde/HammaddelerListesi1,153ProTable with lazy photo thumbnails, CRUD, print, status management
/hammadde/hammadde-siparisHammadde/HammaddeSiparis1,237Order tracking with date-change history, grouping, ratings
/hammadde/tedarikci-yonetimiLab/SupplierManagement761Supplier 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

TypeQR PrefixColorEntry Method
Copper (Bakır)AOrangeMulti-palette with form + measured weights
Tin (Kalay)BCyanSingle weight entry
Plastic (Plastik)CPurpleLot-based with per-lot test photos
Catalyst (Katalizör)DGreenLot-based with per-lot test photos
Dye (Boya)EPinkLot-based with per-lot test photos
AntirodentFTealLot-based with per-lot test photos
Palette (Palet)ILimeQuantity per palette type
Reel (Makara)HGoldQuantity 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.

6.1

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.

761 LINES 1:N MATERIALS SMART PERMISSIONS REAL-TIME FK PROTECTION
6.2

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.

1,237 LINES ORDER GROUPS DATE TRACKING SUPPLIER RATING 7 ENDPOINTS
6.3

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.

1,930 LINES 14 ENDPOINTS 8 MATERIAL TYPES QR CODES PHOTO UPLOAD
6.4

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.

1,153 LINES PROTABLE LAZY PHOTOS REAL-TIME PRINT QUEUE

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:

6.1.2 API Contract

MethodPathFunctionPermission
POST/api/suppliers/createcreate_supplierSmart: lab_user OR hammadde.tedarikci-yonetimi.create_supplier
GET/api/suppliers/listget_suppliersAny authenticated user
GET/api/suppliers/by-material/{type}get_suppliers_by_materialAny authenticated user
GET/api/suppliers/materials-listget_supplier_materials_listAny authenticated user
GET/api/suppliers/{id}get_supplierAny authenticated user
PUT/api/suppliers/{id}update_supplierAny authenticated user
PUT/api/suppliers/{id}/toggle-statustoggle_supplier_statusSmart: super_admin OR deactivate_supplier
DELETE/api/suppliers/{id}/hard-deletehard_delete_supplierSmart: super_admin OR hard_delete_supplier

6.1.3 CRUD Flow

Create:

  1. Click “Yeni Tedarikçi” button in the toolbar
  2. Modal opens with company name field + dynamic materials section (initially one empty material card)
  3. Select material type → conditional fields appear (density for plastic, boyut for palette, boyut+reel_type for reel, nothing for copper/tin)
  4. Click “+” to add more materials under the same supplier
  5. On submit: frontend strips undefined values → POST /api/suppliers/create
  6. On success: toast “Tedarikçi başarıyla oluşturuldu” → modal closes → WebSocket broadcasts to all clients → table refreshes

Edit:

  1. Click edit icon in the Actions column
  2. Modal opens pre-filled with current company name and all material cards
  3. User can modify name, add/remove/change materials
  4. On submit: PUT /api/suppliers/{id} → backend uses delete-and-recreate strategy for materials (deletes ALL existing supplier_materials, creates fresh ones)
  5. On success: toast “Tedarikçi başarıyla güncellendi” → modal closes → WebSocket broadcast → table refreshes

Soft Delete (Deactivate):

  1. Click status toggle icon in the Actions column
  2. Popconfirm: “Bu tedarikçiyi devre dışı bırakmak istediğinize emin misiniz?”
  3. On confirm: PUT /api/suppliers/{id}/toggle-status
  4. Supplier row changes to “inactive” — still visible in table but greyed out. Can be reactivated the same way.

Hard Delete (Permanent):

  1. Click delete icon in the Actions column (requires hard_delete_supplier permission)
  2. Popconfirm with strong warning text
  3. On confirm: DELETE /api/suppliers/{id}/hard-delete
  4. Backend checks for dependent raw_materials rows — if any exist, deletion is blocked with error: “Bu tedarikçiye ait {n} hammadde kaydı var. Önce hammaddeleri silin.”
  5. 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 TypeFields ShownExample
Copper, TinNo extra fields — “Bu malzeme için ek bilgi gerekmez”
Plastic, Catalyst, Dye, Antirodentmaterial_name + density (g/cm³)HF-500, 1.42 g/cm³
Palettematerial_name + boyut (dimensions)Euro Palet, 120x80x15 cm
Reelmaterial_name + boyut + reel_type300mm 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:

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:

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

  1. Click “Yeni Sipariş” button in the toolbar
  2. Modal opens with cascading selection: Material Type → Supplier (filtered by type) → Product panels
  3. Select material type → supplier dropdown shows only suppliers who carry that type
  4. Select supplier → product catalog appears as collapsible panels. Pick product, enter quantity (kg or adet)
  5. Click “+” to add more products from the same supplier in the same shipment
  6. Set order date (defaults to today) and expected delivery date. Add optional notes
  7. 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
  8. On success: toast confirmation → modal closes → table refreshes showing the new order group

Edit (Single Row):

  1. Click edit icon on a specific order row (not the group — you edit individual line items)
  2. Modal opens pre-filled with current values: supplier, material, quantity, dates, notes
  3. The multi-item “+” functionality is not available in edit mode
  4. 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
  5. On submit: PUT /api/material-orders/{id} → order updated + date change logged if applicable
  6. On success: toast → modal closes → table refreshes

Delete:

  1. Click delete icon on a specific order row
  2. Popconfirm with warning
  3. On confirm: DELETE /api/material-orders/{id}
  4. Cascade: associated material_order_date_changes records are deleted with the order
  5. Table refreshes. If this was the last item in a group, the group code disappears

Read (Date History):

  1. Click the clock icon (Tarih Geçmişi) on any order row
  2. 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
  3. Read-only — history cannot be edited or deleted independently

6.2.3 — Order Lifecycle

Every order follows a four-state lifecycle:

Beklemede
Pending — order placed, waiting for shipment
Yolda
Shipped — supplier confirmed dispatch
Teslim Alındı
Delivered — material arrived at factory
İptal
Cancelled — order abandoned

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:

  1. Select Material Type — the user picks from 8 types (copper, tin, plastic, catalyst, dye, antirodent, palette, reel). This is the first filter.
  2. 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_provided array.
  3. 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”.
  4. 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.
  5. Set Dates — two dates are required: the order date (defaults to today) and the expected delivery date.
  6. 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:

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:

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:

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:

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ş NoOrder group code (R1, R2…)Fixed left, blue tag
MalzemeMaterial typeColored tag matching type
ÜrünSpecific product name from supplier catalogBlue tag
TedarikçiSupplier nameBlue tag
Sipariş MiktarıOrdered quantity + unitTurkish number formatting
Gelen MiktarDelivered quantity + unitGreen when complete, yellow when partial
Sipariş TarihiWhen order was placedDD.MM.YYYY format
Beklenen TarihExpected delivery dateRed if overdue (past today)
Geliş TarihiActual delivery dateGreen text, or “-” if not delivered
DurumOrder statusInline dropdown — clickable to change
İşlemAction buttonsFixed 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/listFetch 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/createCreates 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:

material_order_date_changes — the audit trail for expected date modifications. Each row captures one change event:

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:

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:

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

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

  1. Click one of the 8 material type cards on the landing page (Bakır, Kalay, Plastik, Katalizör, Boya, Antirodent, Palet, Makara)
  2. 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)
  3. 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
  4. Upload delivery photos (irsaliye) and optional test/damage photos (per-lot for lot-based)
  5. On submit: type-specific endpoint called (POST /api/materials/copper-entry, /tin-entry, /lot-entry, /palette-entry, or /reel-entry)
  6. Backend creates one raw_materials row per palette/lot/type with deterministic QR code (A1, B1, C1…). If linked to an order: automatically updates delivered_quantity, status=DELIVERED, and delivered_date=today on the order
  7. On success: success modal appears showing all generated QR codes with a “Yazdır” (Print) button per QR. WebSocket broadcasts to materials and all rooms

Update (Super Admin Only):

  1. Not available on this page directly — updates happen through the Hammaddeler Listesi (6.4) edit modal
  2. Backend endpoint exists: PUT /api/materials/{id} with check_permission_smart requiring super_admin or edit_material permission

Delete (Super Admin Only):

  1. Not available on this page directly — deletion happens through Hammaddeler Listesi (6.4)
  2. Backend endpoints: DELETE /api/materials/{id} (soft) and DELETE /api/materials/{id}/hard (permanent)
  3. Hard delete also removes associated material_photos records

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:

Bakır (A)
Copper wire rod — Filmaşin
Kalay (B)
Tin — single weight entry
Plastik (C)
Plastic granules — lot-based
Katalizör (D)
Catalyst — lot-based
Boya (E)
Dye — lot-based
Antirodent (F)
Rodent repellent — lot-based
Palet (I)
Wooden palettes — count-based
Makara (H)
Cable reels — count-based

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:

  1. 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
  2. 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 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:

Archetype 2: Tin (Single Entry)

Tin is the simplest entry type. A single delivery is one weight measurement:

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:

Archetype 4: Count-Based (Palette, Reel)

Palettes and reels are counted items, not weighed. The form presents collapsible type panels:

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
ACopper (Bakır)A1, A2, A3…
BTin (Kalay)B1, B2, B3…
CPlastic (Plastik)C1, C2, C3…
DCatalyst (Katalizör)D1, D2, D3…
EDye (Boya)E1, E2, E3…
FAntirodentF1, F2, F3…
IPalette (Palet)I1, I2, I3…
HReel (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:

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:

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:

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:

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:

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:

received
Just arrived, entered in system
approved
Passed quality check
in_use
Currently in production
consumed
Fully used up
rejected
Failed quality, returned

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/entryCreates copper (multi-palette) or tin (single) entries. Generates QR codes, saves photos, links orders, broadcasts WebSocket.
POST/materials/lot-entryCreates 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-entryCreates palette entries. One row per palette type with I-prefix QR codes. Auto-generates PALET-{seq} lot numbers.
POST/materials/reel-entryCreates 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/listReturns 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-deletePermanent deletion (super admin). Blocks if in_use. Cleans up photos and print jobs.
PUT/materials/{id}/toggle-statusToggles 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-ipUpdates printer IP/port (super admin). Tests connection after update.
GET/materials/print/test-connectionTests 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:

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:

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

  1. Page loads → GET /api/materials/list fetches all materials (photo IDs only, not base64 data)
  2. Table renders 14 columns. Thumbnail photos lazy-load via GET /api/materials/{id}/photos/first
  3. Click photo thumbnail → preview modal opens, remaining photos fetched on demand via GET /api/materials/{id}/photos
  4. Search bar filters client-side across QR code, material type, supplier, lot number, status (includes Turkish translations)
  5. WebSocket connection keeps table in sync — new materials from Hammadde Girişi appear automatically

Edit (Three Form Variants):

  1. Click edit icon on a material row (requires edit_material permission)
  2. 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
  3. QR code field shows warning: “Dikkat: QR kodunu değiştirmek sistemdeki kayıtları etkileyebilir”
  4. Existing photos lazy-load into the modal; user can add new or remove existing photos
  5. On submit: PUT /api/materials/{id} → toast “Hammadde başarıyla güncellendi” → modal closes → WebSocket broadcast → table refreshes

Print (Label Reprint):

  1. Click print icon on a material row (requires print_qr permission; disabled for palette/reel with tooltip)
  2. Printer selection modal opens showing QR code, label count, and dropdown of active printers with locations
  3. Select printer → POST /print-queue/queue/{material_id}?printer_id={id}
  4. Job enters Admin’s print queue as QUEUED status

Delete:

  1. Click delete icon (requires delete_material permission)
  2. Popconfirm with warning
  3. On confirm: DELETE /api/materials/{id} (soft delete — marks as inactive)
  4. For permanent removal: DELETE /api/materials/{id}/hard (requires hard_delete_material permission, flagged as critical). Also removes associated material_photos records
  5. 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 KodUnique identifier (A1, B5, C23…)Fixed left, blue tag
Sipariş NoLinked order group code (R1, R2…)Cyan tag, or “-” if no order link
TipMaterial typeColored tag with Turkish name (Bakır, Kalay, Plastik…)
Malzeme AdıSpecific product nameShows resolved name, defaults to “Filmaşin” for copper, “Kalay Külçe” for tin
TedarikçiSupplier namePlain text
Lot NoLot/batch numberShows “—” for palette/reel (not applicable)
Form MiktarSupplier’s declared weightkg or adet based on type
Ölçülen MiktarActual measured weightkg or adet based on type
KalanRemaining weight after productionColor-coded: yellow if <95% of original, red if <20%. “—” for palette/reel.
Giriş TarihiWhen material was enteredDD.MM.YYYY Turkish locale
İrsaliyeDelivery document photosLazy-loaded thumbnail with “+N” badge if multiple
TestTest/damage photosLazy-loaded thumbnail with “+N” badge if multiple
DurumMaterial statusColored tag: green=received, red=rejected, blue=available, yellow=in_use, grey=consumed
İşlemlerAction buttonsFixed 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:

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:

  1. Table load: the /materials/list endpoint returns only photo IDs (small integers), not photo data. This keeps the API response fast.
  2. Thumbnail load: each photo cell renders a LazyPhotoThumbnail component that independently fetches the first photo’s base64 data via GET /materials/photo/{id}. Only the first photo is loaded for the thumbnail.
  3. 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:

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

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

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:

SectionSubmoduleDepthKey Insight
6.1Tedarikçi YönetimiFullOne-to-many supplier→materials with material-specific properties (density, boyut, reel_type); delete-and-recreate update strategy; consumed by Entry, Orders, and Calculator
6.2Hammadde SiparişFullOrder 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.3Hammadde GirişiExhaustiveFour 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.4Hammaddeler ListesiFull14-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

4
SUBMODULES
~30
API ENDPOINTS
6
DATABASE TABLES
~2,200
LINES BACKEND
~5,100
LINES FRONTEND
8
MATERIAL TYPES

7.4 How Hammadde Feeds the Factory

Tedarikçi Sipariş Hammadde Girişi Hammaddeler Listesi Üretim (Production)

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.