1000 lines
35 KiB
Markdown
1000 lines
35 KiB
Markdown
|
|
# Suppliers Service
|
||
|
|
|
||
|
|
## Overview
|
||
|
|
|
||
|
|
The **Suppliers Service** manages the complete supplier database with performance tracking, quality ratings, and price comparison capabilities. It enables data-driven supplier selection, tracks delivery performance, manages supplier contracts, and provides scorecards for supplier evaluation. This service is essential for maintaining strong supplier relationships while optimizing costs and ensuring consistent ingredient quality.
|
||
|
|
|
||
|
|
## Key Features
|
||
|
|
|
||
|
|
### Supplier Management
|
||
|
|
- **Complete Supplier Database** - Contact details, payment terms, delivery schedules
|
||
|
|
- **Supplier Categories** - Flour mills, dairy, packaging, equipment, services
|
||
|
|
- **Multi-Contact Management** - Sales reps, delivery coordinators, accounts
|
||
|
|
- **Contract Management** - Track agreements, pricing contracts, terms
|
||
|
|
- **Supplier Status** - Active, inactive, preferred, blacklisted
|
||
|
|
- **Document Storage** - Contracts, certificates, insurance documents
|
||
|
|
- **Geographic Data** - Location, delivery zones, distance calculations
|
||
|
|
|
||
|
|
### Performance Tracking
|
||
|
|
- **Delivery Performance** - On-time delivery rate, lead time accuracy
|
||
|
|
- **Quality Metrics** - Product quality ratings, defect rates
|
||
|
|
- **Reliability Score** - Overall supplier reliability assessment
|
||
|
|
- **Order Fulfillment** - Order accuracy, complete shipment rate
|
||
|
|
- **Communication Score** - Responsiveness, issue resolution
|
||
|
|
- **Compliance Tracking** - Food safety certifications, insurance validity
|
||
|
|
|
||
|
|
### Price Management
|
||
|
|
- **Price Lists** - Current pricing per product
|
||
|
|
- **Price History** - Track price changes over time
|
||
|
|
- **Volume Discounts** - Tiered pricing based on order size
|
||
|
|
- **Contract Pricing** - Fixed prices for contract duration
|
||
|
|
- **Price Comparison** - Compare prices across suppliers
|
||
|
|
- **Price Alerts** - Notify on significant price changes
|
||
|
|
- **Cost Trend Analysis** - Identify price trends and seasonality
|
||
|
|
|
||
|
|
### Quality Assurance
|
||
|
|
- **Quality Ratings** - 1-5 star ratings per supplier
|
||
|
|
- **Quality Reviews** - Detailed quality assessments
|
||
|
|
- **Defect Tracking** - Record quality issues and resolution
|
||
|
|
- **Product Certifications** - Organic, fair trade, origin certifications
|
||
|
|
- **Lab Results** - Store test results and analysis
|
||
|
|
- **Corrective Actions** - Track quality improvement measures
|
||
|
|
- **Quality Trends** - Monitor quality over time
|
||
|
|
|
||
|
|
### Supplier Scorecards
|
||
|
|
- **Multi-Dimensional Scoring** - Price, quality, delivery, service
|
||
|
|
- **Weighted Metrics** - Customize scoring based on priorities
|
||
|
|
- **Trend Analysis** - Improve/decline over time
|
||
|
|
- **Ranking System** - Top suppliers by category
|
||
|
|
- **Performance Reports** - Monthly/quarterly scorecards
|
||
|
|
- **Benchmarking** - Compare against category averages
|
||
|
|
|
||
|
|
### Communication & Collaboration
|
||
|
|
- **Contact Log** - Track all supplier interactions
|
||
|
|
- **Email Integration** - Send POs and communications
|
||
|
|
- **Order History** - Complete purchase history per supplier
|
||
|
|
- **Issue Tracking** - Log and resolve supplier problems
|
||
|
|
- **Notes & Reminders** - Internal notes about suppliers
|
||
|
|
- **Calendar Integration** - Delivery schedules, contract renewals
|
||
|
|
|
||
|
|
## Business Value
|
||
|
|
|
||
|
|
### For Bakery Owners
|
||
|
|
- **Cost Optimization** - Data-driven supplier negotiations
|
||
|
|
- **Quality Assurance** - Track and improve supplier quality
|
||
|
|
- **Risk Management** - Identify unreliable suppliers early
|
||
|
|
- **Supplier Leverage** - Performance data strengthens negotiations
|
||
|
|
- **Compliance** - Track certifications and documentation
|
||
|
|
- **Strategic Relationships** - Focus on best-performing suppliers
|
||
|
|
|
||
|
|
### Quantifiable Impact
|
||
|
|
- **Cost Savings**: 5-10% through data-driven negotiations
|
||
|
|
- **Quality Improvement**: 15-25% fewer ingredient defects
|
||
|
|
- **Delivery Reliability**: 20-30% improvement in on-time delivery
|
||
|
|
- **Time Savings**: 3-5 hours/week on supplier management
|
||
|
|
- **Risk Reduction**: Avoid €500-5,000 in spoiled ingredients
|
||
|
|
- **Supplier Consolidation**: 20-30% fewer suppliers, better terms
|
||
|
|
|
||
|
|
### For Procurement Staff
|
||
|
|
- **Supplier Selection** - Clear data for choosing suppliers
|
||
|
|
- **Performance Visibility** - Know which suppliers excel
|
||
|
|
- **Price Comparison** - Quickly compare options
|
||
|
|
- **Issue Resolution** - Track problems to completion
|
||
|
|
- **Contract Management** - Never miss renewal dates
|
||
|
|
|
||
|
|
## Technology Stack
|
||
|
|
|
||
|
|
- **Framework**: FastAPI (Python 3.11+) - Async web framework
|
||
|
|
- **Database**: PostgreSQL 17 - Supplier data
|
||
|
|
- **Caching**: Redis 7.4 - Supplier data cache
|
||
|
|
- **Messaging**: RabbitMQ 4.1 - Event publishing
|
||
|
|
- **ORM**: SQLAlchemy 2.0 (async) - Database abstraction
|
||
|
|
- **Validation**: Pydantic 2.0 - Schema validation
|
||
|
|
- **Logging**: Structlog - Structured JSON logging
|
||
|
|
- **Metrics**: Prometheus Client - Supplier metrics
|
||
|
|
|
||
|
|
## API Endpoints (Key Routes)
|
||
|
|
|
||
|
|
### Supplier Management
|
||
|
|
- `GET /api/v1/suppliers` - List suppliers with filters
|
||
|
|
- `POST /api/v1/suppliers` - Create new supplier
|
||
|
|
- `GET /api/v1/suppliers/{supplier_id}` - Get supplier details
|
||
|
|
- `PUT /api/v1/suppliers/{supplier_id}` - Update supplier
|
||
|
|
- `DELETE /api/v1/suppliers/{supplier_id}` - Delete supplier (soft delete)
|
||
|
|
- `GET /api/v1/suppliers/{supplier_id}/contacts` - Get supplier contacts
|
||
|
|
|
||
|
|
### Performance Tracking
|
||
|
|
- `GET /api/v1/suppliers/{supplier_id}/performance` - Get performance metrics
|
||
|
|
- `POST /api/v1/suppliers/{supplier_id}/performance/review` - Add performance review
|
||
|
|
- `GET /api/v1/suppliers/{supplier_id}/performance/history` - Performance history
|
||
|
|
- `GET /api/v1/suppliers/performance/rankings` - Supplier rankings
|
||
|
|
|
||
|
|
### Price Management
|
||
|
|
- `GET /api/v1/suppliers/{supplier_id}/pricing` - Get supplier price list
|
||
|
|
- `POST /api/v1/suppliers/{supplier_id}/pricing` - Add/update pricing
|
||
|
|
- `GET /api/v1/suppliers/{supplier_id}/pricing/history` - Price history
|
||
|
|
- `POST /api/v1/suppliers/pricing/compare` - Compare prices across suppliers
|
||
|
|
|
||
|
|
### Quality Management
|
||
|
|
- `GET /api/v1/suppliers/{supplier_id}/quality` - Get quality metrics
|
||
|
|
- `POST /api/v1/suppliers/{supplier_id}/quality/review` - Add quality review
|
||
|
|
- `POST /api/v1/suppliers/{supplier_id}/quality/issue` - Report quality issue
|
||
|
|
- `GET /api/v1/suppliers/{supplier_id}/quality/defects` - Defect history
|
||
|
|
|
||
|
|
### Scorecard & Analytics
|
||
|
|
- `GET /api/v1/suppliers/{supplier_id}/scorecard` - Generate supplier scorecard
|
||
|
|
- `GET /api/v1/suppliers/analytics/dashboard` - Supplier analytics dashboard
|
||
|
|
- `GET /api/v1/suppliers/analytics/top-performers` - Top performing suppliers
|
||
|
|
- `GET /api/v1/suppliers/analytics/cost-analysis` - Cost analysis by supplier
|
||
|
|
|
||
|
|
### Communication
|
||
|
|
- `GET /api/v1/suppliers/{supplier_id}/communications` - Communication log
|
||
|
|
- `POST /api/v1/suppliers/{supplier_id}/communications` - Log communication
|
||
|
|
- `POST /api/v1/suppliers/{supplier_id}/send-email` - Send email to supplier
|
||
|
|
- `GET /api/v1/suppliers/{supplier_id}/orders` - Order history
|
||
|
|
|
||
|
|
## Database Schema
|
||
|
|
|
||
|
|
### Main Tables
|
||
|
|
|
||
|
|
**suppliers**
|
||
|
|
```sql
|
||
|
|
CREATE TABLE suppliers (
|
||
|
|
id UUID PRIMARY KEY,
|
||
|
|
tenant_id UUID NOT NULL,
|
||
|
|
supplier_name VARCHAR(255) NOT NULL,
|
||
|
|
supplier_code VARCHAR(100), -- Internal supplier code
|
||
|
|
supplier_type VARCHAR(100), -- flour_mill, dairy, packaging, equipment, service
|
||
|
|
business_legal_name VARCHAR(255),
|
||
|
|
tax_id VARCHAR(50), -- CIF/NIF for Spanish suppliers
|
||
|
|
phone VARCHAR(50),
|
||
|
|
email VARCHAR(255),
|
||
|
|
website VARCHAR(255),
|
||
|
|
address_line1 VARCHAR(255),
|
||
|
|
address_line2 VARCHAR(255),
|
||
|
|
city VARCHAR(100),
|
||
|
|
state_province VARCHAR(100),
|
||
|
|
postal_code VARCHAR(20),
|
||
|
|
country VARCHAR(100) DEFAULT 'España',
|
||
|
|
payment_terms VARCHAR(100), -- Net 30, Net 60, COD, etc.
|
||
|
|
credit_limit DECIMAL(10, 2),
|
||
|
|
currency VARCHAR(10) DEFAULT 'EUR',
|
||
|
|
lead_time_days INTEGER DEFAULT 3,
|
||
|
|
minimum_order_value DECIMAL(10, 2),
|
||
|
|
delivery_days JSONB, -- ["Monday", "Wednesday", "Friday"]
|
||
|
|
status VARCHAR(50) DEFAULT 'active', -- active, inactive, preferred, blacklisted
|
||
|
|
is_preferred BOOLEAN DEFAULT FALSE,
|
||
|
|
notes TEXT,
|
||
|
|
|
||
|
|
-- Performance metrics (cached)
|
||
|
|
quality_rating DECIMAL(3, 2), -- 1.00 to 5.00
|
||
|
|
delivery_rating DECIMAL(3, 2),
|
||
|
|
price_competitiveness DECIMAL(3, 2),
|
||
|
|
overall_score DECIMAL(3, 2),
|
||
|
|
total_orders INTEGER DEFAULT 0,
|
||
|
|
on_time_deliveries INTEGER DEFAULT 0,
|
||
|
|
on_time_delivery_percentage DECIMAL(5, 2),
|
||
|
|
|
||
|
|
-- Compliance
|
||
|
|
food_safety_cert_valid BOOLEAN DEFAULT FALSE,
|
||
|
|
food_safety_cert_expiry DATE,
|
||
|
|
insurance_valid BOOLEAN DEFAULT FALSE,
|
||
|
|
insurance_expiry DATE,
|
||
|
|
|
||
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
||
|
|
updated_at TIMESTAMP DEFAULT NOW(),
|
||
|
|
UNIQUE(tenant_id, supplier_name)
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
**supplier_contacts**
|
||
|
|
```sql
|
||
|
|
CREATE TABLE supplier_contacts (
|
||
|
|
id UUID PRIMARY KEY,
|
||
|
|
tenant_id UUID NOT NULL,
|
||
|
|
supplier_id UUID REFERENCES suppliers(id) ON DELETE CASCADE,
|
||
|
|
contact_name VARCHAR(255) NOT NULL,
|
||
|
|
job_title VARCHAR(255),
|
||
|
|
contact_type VARCHAR(50), -- sales, delivery, accounts, technical
|
||
|
|
phone VARCHAR(50),
|
||
|
|
mobile VARCHAR(50),
|
||
|
|
email VARCHAR(255),
|
||
|
|
is_primary BOOLEAN DEFAULT FALSE,
|
||
|
|
notes TEXT,
|
||
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
||
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
**supplier_products**
|
||
|
|
```sql
|
||
|
|
CREATE TABLE supplier_products (
|
||
|
|
id UUID PRIMARY KEY,
|
||
|
|
tenant_id UUID NOT NULL,
|
||
|
|
supplier_id UUID REFERENCES suppliers(id) ON DELETE CASCADE,
|
||
|
|
ingredient_id UUID NOT NULL, -- Link to inventory ingredient
|
||
|
|
supplier_product_code VARCHAR(100),
|
||
|
|
supplier_product_name VARCHAR(255),
|
||
|
|
unit_price DECIMAL(10, 2) NOT NULL,
|
||
|
|
unit VARCHAR(50) NOT NULL,
|
||
|
|
minimum_order_quantity DECIMAL(10, 2),
|
||
|
|
packaging VARCHAR(100), -- "25kg bag", "1L bottle", etc.
|
||
|
|
lead_time_days INTEGER DEFAULT 3,
|
||
|
|
is_preferred BOOLEAN DEFAULT FALSE,
|
||
|
|
quality_grade VARCHAR(50), -- A, B, C or Premium, Standard, Economy
|
||
|
|
certifications JSONB, -- ["Organic", "Non-GMO", "Fair Trade"]
|
||
|
|
valid_from DATE DEFAULT CURRENT_DATE,
|
||
|
|
valid_until DATE,
|
||
|
|
is_active BOOLEAN DEFAULT TRUE,
|
||
|
|
notes TEXT,
|
||
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
||
|
|
updated_at TIMESTAMP DEFAULT NOW(),
|
||
|
|
UNIQUE(tenant_id, supplier_id, ingredient_id)
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
**supplier_performance_reviews**
|
||
|
|
```sql
|
||
|
|
CREATE TABLE supplier_performance_reviews (
|
||
|
|
id UUID PRIMARY KEY,
|
||
|
|
tenant_id UUID NOT NULL,
|
||
|
|
supplier_id UUID REFERENCES suppliers(id) ON DELETE CASCADE,
|
||
|
|
review_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
||
|
|
review_period_start DATE NOT NULL,
|
||
|
|
review_period_end DATE NOT NULL,
|
||
|
|
|
||
|
|
-- Performance scores (1-5)
|
||
|
|
quality_score DECIMAL(3, 2) NOT NULL,
|
||
|
|
delivery_score DECIMAL(3, 2) NOT NULL,
|
||
|
|
price_score DECIMAL(3, 2) NOT NULL,
|
||
|
|
service_score DECIMAL(3, 2) NOT NULL,
|
||
|
|
overall_score DECIMAL(3, 2) NOT NULL,
|
||
|
|
|
||
|
|
-- Metrics
|
||
|
|
total_orders INTEGER DEFAULT 0,
|
||
|
|
on_time_deliveries INTEGER DEFAULT 0,
|
||
|
|
on_time_percentage DECIMAL(5, 2),
|
||
|
|
quality_issues INTEGER DEFAULT 0,
|
||
|
|
defect_rate DECIMAL(5, 2),
|
||
|
|
average_delivery_time_days DECIMAL(5, 2),
|
||
|
|
total_spend DECIMAL(10, 2) DEFAULT 0.00,
|
||
|
|
|
||
|
|
-- Qualitative feedback
|
||
|
|
strengths TEXT,
|
||
|
|
weaknesses TEXT,
|
||
|
|
recommendations TEXT,
|
||
|
|
|
||
|
|
reviewed_by UUID NOT NULL,
|
||
|
|
created_at TIMESTAMP DEFAULT NOW()
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
**supplier_quality_issues**
|
||
|
|
```sql
|
||
|
|
CREATE TABLE supplier_quality_issues (
|
||
|
|
id UUID PRIMARY KEY,
|
||
|
|
tenant_id UUID NOT NULL,
|
||
|
|
supplier_id UUID REFERENCES suppliers(id) ON DELETE CASCADE,
|
||
|
|
purchase_order_id UUID, -- Link to specific PO if applicable
|
||
|
|
ingredient_id UUID,
|
||
|
|
issue_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
||
|
|
issue_type VARCHAR(100) NOT NULL, -- defect, contamination, wrong_product, damaged, expired
|
||
|
|
severity VARCHAR(50) NOT NULL, -- critical, major, minor
|
||
|
|
description TEXT NOT NULL,
|
||
|
|
quantity_affected DECIMAL(10, 2),
|
||
|
|
unit VARCHAR(50),
|
||
|
|
financial_impact DECIMAL(10, 2),
|
||
|
|
|
||
|
|
-- Resolution
|
||
|
|
resolution_status VARCHAR(50) DEFAULT 'open', -- open, in_progress, resolved, closed
|
||
|
|
corrective_action TEXT,
|
||
|
|
supplier_response TEXT,
|
||
|
|
credit_issued DECIMAL(10, 2) DEFAULT 0.00,
|
||
|
|
resolved_date DATE,
|
||
|
|
resolved_by UUID,
|
||
|
|
|
||
|
|
reported_by UUID NOT NULL,
|
||
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
||
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
**supplier_price_history**
|
||
|
|
```sql
|
||
|
|
CREATE TABLE supplier_price_history (
|
||
|
|
id UUID PRIMARY KEY,
|
||
|
|
tenant_id UUID NOT NULL,
|
||
|
|
supplier_id UUID REFERENCES suppliers(id) ON DELETE CASCADE,
|
||
|
|
ingredient_id UUID NOT NULL,
|
||
|
|
effective_date DATE NOT NULL,
|
||
|
|
unit_price DECIMAL(10, 2) NOT NULL,
|
||
|
|
unit VARCHAR(50) NOT NULL,
|
||
|
|
previous_price DECIMAL(10, 2),
|
||
|
|
price_change_percentage DECIMAL(5, 2),
|
||
|
|
reason VARCHAR(255), -- "market_increase", "contract_renewal", etc.
|
||
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
||
|
|
INDEX idx_price_history_date (tenant_id, ingredient_id, effective_date DESC)
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
**supplier_communications**
|
||
|
|
```sql
|
||
|
|
CREATE TABLE supplier_communications (
|
||
|
|
id UUID PRIMARY KEY,
|
||
|
|
tenant_id UUID NOT NULL,
|
||
|
|
supplier_id UUID REFERENCES suppliers(id) ON DELETE CASCADE,
|
||
|
|
communication_date TIMESTAMP NOT NULL DEFAULT NOW(),
|
||
|
|
communication_type VARCHAR(50) NOT NULL, -- email, phone, meeting, visit
|
||
|
|
subject VARCHAR(255),
|
||
|
|
summary TEXT NOT NULL,
|
||
|
|
participants JSONB, -- Array of names
|
||
|
|
action_items TEXT,
|
||
|
|
follow_up_date DATE,
|
||
|
|
logged_by UUID NOT NULL,
|
||
|
|
created_at TIMESTAMP DEFAULT NOW()
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
**supplier_contracts**
|
||
|
|
```sql
|
||
|
|
CREATE TABLE supplier_contracts (
|
||
|
|
id UUID PRIMARY KEY,
|
||
|
|
tenant_id UUID NOT NULL,
|
||
|
|
supplier_id UUID REFERENCES suppliers(id) ON DELETE CASCADE,
|
||
|
|
contract_number VARCHAR(100),
|
||
|
|
contract_type VARCHAR(100), -- pricing, volume, exclusive, service
|
||
|
|
start_date DATE NOT NULL,
|
||
|
|
end_date DATE NOT NULL,
|
||
|
|
auto_renew BOOLEAN DEFAULT FALSE,
|
||
|
|
renewal_notice_days INTEGER DEFAULT 30,
|
||
|
|
contract_terms TEXT,
|
||
|
|
payment_terms VARCHAR(100),
|
||
|
|
minimum_volume DECIMAL(10, 2),
|
||
|
|
maximum_volume DECIMAL(10, 2),
|
||
|
|
fixed_pricing BOOLEAN DEFAULT FALSE,
|
||
|
|
contract_value DECIMAL(10, 2),
|
||
|
|
status VARCHAR(50) DEFAULT 'active', -- draft, active, expired, terminated
|
||
|
|
document_url VARCHAR(500),
|
||
|
|
notes TEXT,
|
||
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
||
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### Indexes for Performance
|
||
|
|
```sql
|
||
|
|
CREATE INDEX idx_suppliers_tenant_status ON suppliers(tenant_id, status);
|
||
|
|
CREATE INDEX idx_suppliers_type ON suppliers(tenant_id, supplier_type);
|
||
|
|
CREATE INDEX idx_supplier_products_supplier ON supplier_products(supplier_id);
|
||
|
|
CREATE INDEX idx_supplier_products_ingredient ON supplier_products(tenant_id, ingredient_id);
|
||
|
|
CREATE INDEX idx_performance_reviews_supplier ON supplier_performance_reviews(supplier_id, review_date DESC);
|
||
|
|
CREATE INDEX idx_quality_issues_supplier ON supplier_quality_issues(supplier_id, issue_date DESC);
|
||
|
|
CREATE INDEX idx_quality_issues_status ON supplier_quality_issues(tenant_id, resolution_status);
|
||
|
|
```
|
||
|
|
|
||
|
|
## Business Logic Examples
|
||
|
|
|
||
|
|
### Supplier Scorecard Calculation
|
||
|
|
```python
|
||
|
|
async def calculate_supplier_scorecard(
|
||
|
|
supplier_id: UUID,
|
||
|
|
start_date: date,
|
||
|
|
end_date: date
|
||
|
|
) -> SupplierScorecard:
|
||
|
|
"""
|
||
|
|
Calculate comprehensive supplier scorecard for period.
|
||
|
|
Scoring: 30% Quality, 30% Delivery, 25% Price, 15% Service
|
||
|
|
"""
|
||
|
|
# Get all purchase orders for period
|
||
|
|
purchase_orders = await db.query(PurchaseOrder).filter(
|
||
|
|
PurchaseOrder.supplier_id == supplier_id,
|
||
|
|
PurchaseOrder.order_date >= start_date,
|
||
|
|
PurchaseOrder.order_date <= end_date,
|
||
|
|
PurchaseOrder.status == 'received'
|
||
|
|
).all()
|
||
|
|
|
||
|
|
if not purchase_orders:
|
||
|
|
return None
|
||
|
|
|
||
|
|
# QUALITY SCORE (1-5 scale)
|
||
|
|
quality_issues = await db.query(SupplierQualityIssue).filter(
|
||
|
|
SupplierQualityIssue.supplier_id == supplier_id,
|
||
|
|
SupplierQualityIssue.issue_date >= start_date,
|
||
|
|
SupplierQualityIssue.issue_date <= end_date
|
||
|
|
).all()
|
||
|
|
|
||
|
|
total_orders = len(purchase_orders)
|
||
|
|
critical_issues = len([i for i in quality_issues if i.severity == 'critical'])
|
||
|
|
major_issues = len([i for i in quality_issues if i.severity == 'major'])
|
||
|
|
minor_issues = len([i for i in quality_issues if i.severity == 'minor'])
|
||
|
|
|
||
|
|
# Defect rate
|
||
|
|
defect_rate = (critical_issues * 3 + major_issues * 2 + minor_issues) / total_orders if total_orders > 0 else 0
|
||
|
|
|
||
|
|
# Quality score: 5 stars minus penalties
|
||
|
|
quality_score = 5.0 - min(defect_rate, 4.0)
|
||
|
|
|
||
|
|
# DELIVERY SCORE (1-5 scale)
|
||
|
|
on_time_deliveries = len([
|
||
|
|
po for po in purchase_orders
|
||
|
|
if po.actual_delivery_date and po.expected_delivery_date
|
||
|
|
and po.actual_delivery_date <= po.expected_delivery_date
|
||
|
|
])
|
||
|
|
|
||
|
|
on_time_percentage = (on_time_deliveries / total_orders * 100) if total_orders > 0 else 0
|
||
|
|
|
||
|
|
# Delivery score based on on-time percentage
|
||
|
|
if on_time_percentage >= 95:
|
||
|
|
delivery_score = 5.0
|
||
|
|
elif on_time_percentage >= 90:
|
||
|
|
delivery_score = 4.5
|
||
|
|
elif on_time_percentage >= 85:
|
||
|
|
delivery_score = 4.0
|
||
|
|
elif on_time_percentage >= 75:
|
||
|
|
delivery_score = 3.0
|
||
|
|
elif on_time_percentage >= 60:
|
||
|
|
delivery_score = 2.0
|
||
|
|
else:
|
||
|
|
delivery_score = 1.0
|
||
|
|
|
||
|
|
# PRICE SCORE (1-5 scale)
|
||
|
|
# Compare supplier prices against market average
|
||
|
|
supplier_products = await get_supplier_products(supplier_id)
|
||
|
|
price_comparisons = []
|
||
|
|
|
||
|
|
for sp in supplier_products:
|
||
|
|
# Get all suppliers for this ingredient
|
||
|
|
all_suppliers = await db.query(SupplierProduct).filter(
|
||
|
|
SupplierProduct.tenant_id == sp.tenant_id,
|
||
|
|
SupplierProduct.ingredient_id == sp.ingredient_id,
|
||
|
|
SupplierProduct.is_active == True
|
||
|
|
).all()
|
||
|
|
|
||
|
|
if len(all_suppliers) > 1:
|
||
|
|
prices = [s.unit_price for s in all_suppliers]
|
||
|
|
avg_price = sum(prices) / len(prices)
|
||
|
|
price_ratio = sp.unit_price / avg_price if avg_price > 0 else 1.0
|
||
|
|
price_comparisons.append(price_ratio)
|
||
|
|
|
||
|
|
if price_comparisons:
|
||
|
|
avg_price_ratio = sum(price_comparisons) / len(price_comparisons)
|
||
|
|
# Lower ratio = better price = higher score
|
||
|
|
if avg_price_ratio <= 0.90:
|
||
|
|
price_score = 5.0 # 10%+ below market
|
||
|
|
elif avg_price_ratio <= 0.95:
|
||
|
|
price_score = 4.5 # 5-10% below market
|
||
|
|
elif avg_price_ratio <= 1.00:
|
||
|
|
price_score = 4.0 # At market
|
||
|
|
elif avg_price_ratio <= 1.05:
|
||
|
|
price_score = 3.0 # 5% above market
|
||
|
|
elif avg_price_ratio <= 1.10:
|
||
|
|
price_score = 2.0 # 10% above market
|
||
|
|
else:
|
||
|
|
price_score = 1.0 # 10%+ above market
|
||
|
|
else:
|
||
|
|
price_score = 3.0 # Default if no comparison available
|
||
|
|
|
||
|
|
# SERVICE SCORE (1-5 scale)
|
||
|
|
# Based on communication responsiveness and issue resolution
|
||
|
|
communications = await db.query(SupplierCommunication).filter(
|
||
|
|
SupplierCommunication.supplier_id == supplier_id,
|
||
|
|
SupplierCommunication.communication_date >= start_date,
|
||
|
|
SupplierCommunication.communication_date <= end_date
|
||
|
|
).all()
|
||
|
|
|
||
|
|
resolved_issues = len([
|
||
|
|
i for i in quality_issues
|
||
|
|
if i.resolution_status == 'resolved'
|
||
|
|
])
|
||
|
|
total_issues = len(quality_issues)
|
||
|
|
|
||
|
|
resolution_rate = (resolved_issues / total_issues * 100) if total_issues > 0 else 100
|
||
|
|
|
||
|
|
# Service score based on issue resolution
|
||
|
|
if resolution_rate >= 90 and len(communications) >= 2:
|
||
|
|
service_score = 5.0
|
||
|
|
elif resolution_rate >= 80:
|
||
|
|
service_score = 4.0
|
||
|
|
elif resolution_rate >= 70:
|
||
|
|
service_score = 3.0
|
||
|
|
elif resolution_rate >= 50:
|
||
|
|
service_score = 2.0
|
||
|
|
else:
|
||
|
|
service_score = 1.0
|
||
|
|
|
||
|
|
# WEIGHTED OVERALL SCORE
|
||
|
|
overall_score = (
|
||
|
|
quality_score * 0.30 +
|
||
|
|
delivery_score * 0.30 +
|
||
|
|
price_score * 0.25 +
|
||
|
|
service_score * 0.15
|
||
|
|
)
|
||
|
|
|
||
|
|
# Calculate total spend
|
||
|
|
total_spend = sum(po.total_amount for po in purchase_orders)
|
||
|
|
|
||
|
|
# Average lead time
|
||
|
|
lead_times = [
|
||
|
|
(po.actual_delivery_date - po.order_date).days
|
||
|
|
for po in purchase_orders
|
||
|
|
if po.actual_delivery_date and po.order_date
|
||
|
|
]
|
||
|
|
avg_lead_time = sum(lead_times) / len(lead_times) if lead_times else 0
|
||
|
|
|
||
|
|
# Create scorecard
|
||
|
|
scorecard = SupplierScorecard(
|
||
|
|
supplier_id=supplier_id,
|
||
|
|
period_start=start_date,
|
||
|
|
period_end=end_date,
|
||
|
|
quality_score=round(quality_score, 2),
|
||
|
|
delivery_score=round(delivery_score, 2),
|
||
|
|
price_score=round(price_score, 2),
|
||
|
|
service_score=round(service_score, 2),
|
||
|
|
overall_score=round(overall_score, 2),
|
||
|
|
total_orders=total_orders,
|
||
|
|
on_time_deliveries=on_time_deliveries,
|
||
|
|
on_time_percentage=round(on_time_percentage, 2),
|
||
|
|
quality_issues_count=len(quality_issues),
|
||
|
|
defect_rate=round(defect_rate, 4),
|
||
|
|
total_spend=total_spend,
|
||
|
|
average_lead_time_days=round(avg_lead_time, 1)
|
||
|
|
)
|
||
|
|
|
||
|
|
return scorecard
|
||
|
|
```
|
||
|
|
|
||
|
|
### Supplier Recommendation Engine
|
||
|
|
```python
|
||
|
|
async def recommend_supplier_for_ingredient(
|
||
|
|
tenant_id: UUID,
|
||
|
|
ingredient_id: UUID,
|
||
|
|
quantity: float,
|
||
|
|
urgency: str = 'normal'
|
||
|
|
) -> list[dict]:
|
||
|
|
"""
|
||
|
|
Recommend best suppliers for ingredient based on multiple criteria.
|
||
|
|
Returns ranked list with scores and reasoning.
|
||
|
|
"""
|
||
|
|
# Get all suppliers for ingredient
|
||
|
|
supplier_products = await db.query(SupplierProduct).filter(
|
||
|
|
SupplierProduct.tenant_id == tenant_id,
|
||
|
|
SupplierProduct.ingredient_id == ingredient_id,
|
||
|
|
SupplierProduct.is_active == True
|
||
|
|
).all()
|
||
|
|
|
||
|
|
if not supplier_products:
|
||
|
|
return []
|
||
|
|
|
||
|
|
recommendations = []
|
||
|
|
|
||
|
|
for sp in supplier_products:
|
||
|
|
supplier = await get_supplier(sp.supplier_id)
|
||
|
|
|
||
|
|
# Get supplier performance
|
||
|
|
scorecard = await get_latest_scorecard(sp.supplier_id)
|
||
|
|
|
||
|
|
# Calculate recommendation score
|
||
|
|
scores = {}
|
||
|
|
|
||
|
|
# Price score (40% weight for normal urgency, 20% for urgent)
|
||
|
|
price_weight = 0.20 if urgency == 'urgent' else 0.40
|
||
|
|
scores['price'] = scorecard.price_score if scorecard else 3.0
|
||
|
|
|
||
|
|
# Quality score (30% weight)
|
||
|
|
scores['quality'] = scorecard.quality_score if scorecard else 3.0
|
||
|
|
|
||
|
|
# Delivery score (30% for normal, 50% for urgent)
|
||
|
|
delivery_weight = 0.50 if urgency == 'urgent' else 0.30
|
||
|
|
scores['delivery'] = scorecard.delivery_score if scorecard else 3.0
|
||
|
|
|
||
|
|
# Service score (10% weight)
|
||
|
|
scores['service'] = scorecard.service_score if scorecard else 3.0
|
||
|
|
|
||
|
|
# Calculate weighted score
|
||
|
|
if urgency == 'urgent':
|
||
|
|
weighted_score = (
|
||
|
|
scores['price'] * 0.20 +
|
||
|
|
scores['quality'] * 0.30 +
|
||
|
|
scores['delivery'] * 0.50
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
weighted_score = (
|
||
|
|
scores['price'] * 0.40 +
|
||
|
|
scores['quality'] * 0.30 +
|
||
|
|
scores['delivery'] * 0.20 +
|
||
|
|
scores['service'] * 0.10
|
||
|
|
)
|
||
|
|
|
||
|
|
# Check if minimum order quantity is met
|
||
|
|
meets_moq = quantity >= (sp.minimum_order_quantity or 0)
|
||
|
|
|
||
|
|
# Check lead time
|
||
|
|
lead_time_acceptable = sp.lead_time_days <= 3 if urgency == 'urgent' else True
|
||
|
|
|
||
|
|
# Calculate total cost
|
||
|
|
total_cost = sp.unit_price * quantity
|
||
|
|
|
||
|
|
recommendations.append({
|
||
|
|
'supplier_id': str(supplier.id),
|
||
|
|
'supplier_name': supplier.supplier_name,
|
||
|
|
'unit_price': float(sp.unit_price),
|
||
|
|
'total_cost': float(total_cost),
|
||
|
|
'lead_time_days': sp.lead_time_days,
|
||
|
|
'minimum_order_quantity': float(sp.minimum_order_quantity) if sp.minimum_order_quantity else None,
|
||
|
|
'meets_moq': meets_moq,
|
||
|
|
'lead_time_acceptable': lead_time_acceptable,
|
||
|
|
'quality_score': float(scores['quality']),
|
||
|
|
'delivery_score': float(scores['delivery']),
|
||
|
|
'price_score': float(scores['price']),
|
||
|
|
'service_score': float(scores['service']),
|
||
|
|
'weighted_score': float(weighted_score),
|
||
|
|
'recommendation_reason': generate_recommendation_reason(
|
||
|
|
scores, urgency, meets_moq, lead_time_acceptable
|
||
|
|
)
|
||
|
|
})
|
||
|
|
|
||
|
|
# Sort by weighted score descending
|
||
|
|
recommendations.sort(key=lambda x: x['weighted_score'], reverse=True)
|
||
|
|
|
||
|
|
return recommendations
|
||
|
|
|
||
|
|
def generate_recommendation_reason(
|
||
|
|
scores: dict,
|
||
|
|
urgency: str,
|
||
|
|
meets_moq: bool,
|
||
|
|
lead_time_acceptable: bool
|
||
|
|
) -> str:
|
||
|
|
"""Generate human-readable recommendation reason."""
|
||
|
|
reasons = []
|
||
|
|
|
||
|
|
if urgency == 'urgent' and lead_time_acceptable:
|
||
|
|
reasons.append("Fast delivery available")
|
||
|
|
|
||
|
|
if scores['quality'] >= 4.5:
|
||
|
|
reasons.append("Excellent quality rating")
|
||
|
|
elif scores['quality'] >= 4.0:
|
||
|
|
reasons.append("Good quality rating")
|
||
|
|
|
||
|
|
if scores['price'] >= 4.5:
|
||
|
|
reasons.append("Best price")
|
||
|
|
elif scores['price'] >= 4.0:
|
||
|
|
reasons.append("Competitive price")
|
||
|
|
|
||
|
|
if scores['delivery'] >= 4.5:
|
||
|
|
reasons.append("Excellent delivery record")
|
||
|
|
elif scores['delivery'] >= 4.0:
|
||
|
|
reasons.append("Reliable delivery")
|
||
|
|
|
||
|
|
if not meets_moq:
|
||
|
|
reasons.append("⚠️ Below minimum order quantity")
|
||
|
|
|
||
|
|
if not lead_time_acceptable:
|
||
|
|
reasons.append("⚠️ Lead time too long for urgent order")
|
||
|
|
|
||
|
|
return ", ".join(reasons) if reasons else "Standard supplier"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Price Trend Analysis
|
||
|
|
```python
|
||
|
|
async def analyze_price_trends(
|
||
|
|
tenant_id: UUID,
|
||
|
|
ingredient_id: UUID,
|
||
|
|
months_back: int = 12
|
||
|
|
) -> dict:
|
||
|
|
"""
|
||
|
|
Analyze price trends for ingredient across all suppliers.
|
||
|
|
"""
|
||
|
|
start_date = date.today() - timedelta(days=months_back * 30)
|
||
|
|
|
||
|
|
# Get price history
|
||
|
|
price_history = await db.query(SupplierPriceHistory).filter(
|
||
|
|
SupplierPriceHistory.tenant_id == tenant_id,
|
||
|
|
SupplierPriceHistory.ingredient_id == ingredient_id,
|
||
|
|
SupplierPriceHistory.effective_date >= start_date
|
||
|
|
).order_by(SupplierPriceHistory.effective_date.asc()).all()
|
||
|
|
|
||
|
|
if not price_history:
|
||
|
|
return None
|
||
|
|
|
||
|
|
# Calculate statistics
|
||
|
|
prices = [p.unit_price for p in price_history]
|
||
|
|
current_price = prices[-1]
|
||
|
|
min_price = min(prices)
|
||
|
|
max_price = max(prices)
|
||
|
|
avg_price = sum(prices) / len(prices)
|
||
|
|
|
||
|
|
# Calculate trend (simple linear regression)
|
||
|
|
import statistics
|
||
|
|
if len(prices) > 2:
|
||
|
|
# Calculate slope
|
||
|
|
x = list(range(len(prices)))
|
||
|
|
x_mean = sum(x) / len(x)
|
||
|
|
y_mean = avg_price
|
||
|
|
|
||
|
|
numerator = sum((x[i] - x_mean) * (prices[i] - y_mean) for i in range(len(prices)))
|
||
|
|
denominator = sum((x[i] - x_mean) ** 2 for i in range(len(x)))
|
||
|
|
|
||
|
|
slope = numerator / denominator if denominator != 0 else 0
|
||
|
|
trend_direction = 'increasing' if slope > 0.01 else 'decreasing' if slope < -0.01 else 'stable'
|
||
|
|
else:
|
||
|
|
trend_direction = 'insufficient_data'
|
||
|
|
|
||
|
|
# Calculate volatility (coefficient of variation)
|
||
|
|
std_dev = statistics.stdev(prices) if len(prices) > 1 else 0
|
||
|
|
volatility = (std_dev / avg_price * 100) if avg_price > 0 else 0
|
||
|
|
|
||
|
|
# Identify best and worst suppliers
|
||
|
|
supplier_avg_prices = {}
|
||
|
|
for ph in price_history:
|
||
|
|
if ph.supplier_id not in supplier_avg_prices:
|
||
|
|
supplier_avg_prices[ph.supplier_id] = []
|
||
|
|
supplier_avg_prices[ph.supplier_id].append(ph.unit_price)
|
||
|
|
|
||
|
|
supplier_averages = {
|
||
|
|
sid: sum(prices) / len(prices)
|
||
|
|
for sid, prices in supplier_avg_prices.items()
|
||
|
|
}
|
||
|
|
|
||
|
|
best_supplier_id = min(supplier_averages, key=supplier_averages.get)
|
||
|
|
worst_supplier_id = max(supplier_averages, key=supplier_averages.get)
|
||
|
|
|
||
|
|
return {
|
||
|
|
'ingredient_id': str(ingredient_id),
|
||
|
|
'period_months': months_back,
|
||
|
|
'data_points': len(price_history),
|
||
|
|
'current_price': float(current_price),
|
||
|
|
'min_price': float(min_price),
|
||
|
|
'max_price': float(max_price),
|
||
|
|
'average_price': float(avg_price),
|
||
|
|
'price_range': float(max_price - min_price),
|
||
|
|
'trend_direction': trend_direction,
|
||
|
|
'volatility_percentage': round(volatility, 2),
|
||
|
|
'price_change_percentage': round((current_price - prices[0]) / prices[0] * 100, 2),
|
||
|
|
'best_supplier_id': str(best_supplier_id),
|
||
|
|
'best_supplier_avg_price': float(supplier_averages[best_supplier_id]),
|
||
|
|
'worst_supplier_id': str(worst_supplier_id),
|
||
|
|
'worst_supplier_avg_price': float(supplier_averages[worst_supplier_id]),
|
||
|
|
'price_difference_percentage': round(
|
||
|
|
(supplier_averages[worst_supplier_id] - supplier_averages[best_supplier_id]) /
|
||
|
|
supplier_averages[best_supplier_id] * 100, 2
|
||
|
|
)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Events & Messaging
|
||
|
|
|
||
|
|
### Published Events (RabbitMQ)
|
||
|
|
|
||
|
|
**Exchange**: `suppliers`
|
||
|
|
**Routing Keys**: `suppliers.performance_alert`, `suppliers.price_change`, `suppliers.quality_issue`, `suppliers.contract_expiring`
|
||
|
|
|
||
|
|
**Supplier Performance Alert**
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"event_type": "supplier_performance_alert",
|
||
|
|
"tenant_id": "uuid",
|
||
|
|
"supplier_id": "uuid",
|
||
|
|
"supplier_name": "Harinas García",
|
||
|
|
"alert_type": "poor_delivery",
|
||
|
|
"on_time_delivery_percentage": 65.0,
|
||
|
|
"threshold": 80.0,
|
||
|
|
"period_days": 30,
|
||
|
|
"recommendation": "Consider alternative suppliers or renegotiate terms",
|
||
|
|
"timestamp": "2025-11-06T09:00:00Z"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Supplier Price Change Event**
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"event_type": "supplier_price_change",
|
||
|
|
"tenant_id": "uuid",
|
||
|
|
"supplier_id": "uuid",
|
||
|
|
"supplier_name": "Lácteos del Norte",
|
||
|
|
"ingredient_id": "uuid",
|
||
|
|
"ingredient_name": "Mantequilla",
|
||
|
|
"old_price": 4.50,
|
||
|
|
"new_price": 5.20,
|
||
|
|
"change_percentage": 15.56,
|
||
|
|
"effective_date": "2025-11-15",
|
||
|
|
"reason": "market_increase",
|
||
|
|
"timestamp": "2025-11-06T14:00:00Z"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Supplier Quality Issue Event**
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"event_type": "supplier_quality_issue",
|
||
|
|
"tenant_id": "uuid",
|
||
|
|
"supplier_id": "uuid",
|
||
|
|
"supplier_name": "Distribuidora Madrid",
|
||
|
|
"issue_id": "uuid",
|
||
|
|
"severity": "major",
|
||
|
|
"issue_type": "contamination",
|
||
|
|
"ingredient_name": "Harina Integral",
|
||
|
|
"description": "Foreign material found in bag",
|
||
|
|
"financial_impact": 125.00,
|
||
|
|
"timestamp": "2025-11-06T11:30:00Z"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Contract Expiring Alert**
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"event_type": "supplier_contract_expiring",
|
||
|
|
"tenant_id": "uuid",
|
||
|
|
"supplier_id": "uuid",
|
||
|
|
"supplier_name": "Embalajes Premium",
|
||
|
|
"contract_id": "uuid",
|
||
|
|
"contract_type": "pricing",
|
||
|
|
"expiry_date": "2025-11-30",
|
||
|
|
"days_until_expiry": 24,
|
||
|
|
"auto_renew": false,
|
||
|
|
"action_required": "Review and renew contract",
|
||
|
|
"timestamp": "2025-11-06T08:00:00Z"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Consumed Events
|
||
|
|
- **From Procurement**: Purchase orders update supplier performance
|
||
|
|
- **From Inventory**: Quality issues on received goods
|
||
|
|
- **From Accounting**: Payment history affects supplier relationships
|
||
|
|
|
||
|
|
## Custom Metrics (Prometheus)
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Supplier metrics
|
||
|
|
suppliers_total = Gauge(
|
||
|
|
'suppliers_total',
|
||
|
|
'Total suppliers',
|
||
|
|
['tenant_id', 'supplier_type', 'status']
|
||
|
|
)
|
||
|
|
|
||
|
|
supplier_performance_score = Histogram(
|
||
|
|
'supplier_performance_score',
|
||
|
|
'Supplier overall performance score',
|
||
|
|
['tenant_id', 'supplier_id'],
|
||
|
|
buckets=[1.0, 2.0, 3.0, 3.5, 4.0, 4.5, 5.0]
|
||
|
|
)
|
||
|
|
|
||
|
|
supplier_quality_issues_total = Counter(
|
||
|
|
'supplier_quality_issues_total',
|
||
|
|
'Total quality issues',
|
||
|
|
['tenant_id', 'supplier_id', 'severity']
|
||
|
|
)
|
||
|
|
|
||
|
|
supplier_on_time_delivery_percentage = Histogram(
|
||
|
|
'supplier_on_time_delivery_percentage',
|
||
|
|
'Supplier on-time delivery rate',
|
||
|
|
['tenant_id', 'supplier_id'],
|
||
|
|
buckets=[50, 60, 70, 80, 85, 90, 95, 98, 100]
|
||
|
|
)
|
||
|
|
|
||
|
|
supplier_price_changes_total = Counter(
|
||
|
|
'supplier_price_changes_total',
|
||
|
|
'Total price changes',
|
||
|
|
['tenant_id', 'supplier_id', 'direction']
|
||
|
|
)
|
||
|
|
```
|
||
|
|
|
||
|
|
## Configuration
|
||
|
|
|
||
|
|
### Environment Variables
|
||
|
|
|
||
|
|
**Service Configuration:**
|
||
|
|
- `PORT` - Service port (default: 8012)
|
||
|
|
- `DATABASE_URL` - PostgreSQL connection string
|
||
|
|
- `REDIS_URL` - Redis connection string
|
||
|
|
- `RABBITMQ_URL` - RabbitMQ connection string
|
||
|
|
|
||
|
|
**Performance Configuration:**
|
||
|
|
- `QUALITY_SCORE_WEIGHT` - Quality scoring weight (default: 0.30)
|
||
|
|
- `DELIVERY_SCORE_WEIGHT` - Delivery scoring weight (default: 0.30)
|
||
|
|
- `PRICE_SCORE_WEIGHT` - Price scoring weight (default: 0.25)
|
||
|
|
- `SERVICE_SCORE_WEIGHT` - Service scoring weight (default: 0.15)
|
||
|
|
|
||
|
|
**Alert Configuration:**
|
||
|
|
- `MIN_ON_TIME_DELIVERY_PERCENTAGE` - Alert threshold (default: 80.0)
|
||
|
|
- `MAX_DEFECT_RATE_PERCENTAGE` - Alert threshold (default: 5.0)
|
||
|
|
- `PRICE_CHANGE_ALERT_PERCENTAGE` - Alert on price change (default: 10.0)
|
||
|
|
- `CONTRACT_EXPIRY_ALERT_DAYS` - Days before expiry (default: 30)
|
||
|
|
|
||
|
|
**Quality Configuration:**
|
||
|
|
- `REQUIRE_FOOD_SAFETY_CERT` - Require certifications (default: true)
|
||
|
|
- `CERT_EXPIRY_REMINDER_DAYS` - Remind before expiry (default: 60)
|
||
|
|
|
||
|
|
## Development Setup
|
||
|
|
|
||
|
|
### Prerequisites
|
||
|
|
- Python 3.11+
|
||
|
|
- PostgreSQL 17
|
||
|
|
- Redis 7.4
|
||
|
|
- RabbitMQ 4.1
|
||
|
|
|
||
|
|
### Local Development
|
||
|
|
```bash
|
||
|
|
cd services/suppliers
|
||
|
|
python -m venv venv
|
||
|
|
source venv/bin/activate
|
||
|
|
|
||
|
|
pip install -r requirements.txt
|
||
|
|
|
||
|
|
export DATABASE_URL=postgresql://user:pass@localhost:5432/suppliers
|
||
|
|
export REDIS_URL=redis://localhost:6379/0
|
||
|
|
export RABBITMQ_URL=amqp://guest:guest@localhost:5672/
|
||
|
|
|
||
|
|
alembic upgrade head
|
||
|
|
python main.py
|
||
|
|
```
|
||
|
|
|
||
|
|
## Integration Points
|
||
|
|
|
||
|
|
### Dependencies
|
||
|
|
- **Auth Service** - User authentication
|
||
|
|
- **PostgreSQL** - Supplier data
|
||
|
|
- **Redis** - Caching
|
||
|
|
- **RabbitMQ** - Event publishing
|
||
|
|
|
||
|
|
### Dependents
|
||
|
|
- **Procurement Service** - Supplier selection for purchase orders
|
||
|
|
- **Inventory Service** - Quality tracking on receipts
|
||
|
|
- **AI Insights Service** - Supplier optimization recommendations
|
||
|
|
- **Notification Service** - Performance and contract alerts
|
||
|
|
- **Frontend Dashboard** - Supplier management UI
|
||
|
|
|
||
|
|
## Business Value for VUE Madrid
|
||
|
|
|
||
|
|
### Problem Statement
|
||
|
|
Spanish bakeries struggle with:
|
||
|
|
- No systematic supplier performance tracking
|
||
|
|
- Manual price comparison across suppliers
|
||
|
|
- No quality issue documentation
|
||
|
|
- Lost supplier history when staff changes
|
||
|
|
- No leverage in supplier negotiations
|
||
|
|
- Missed contract renewals and price increases
|
||
|
|
|
||
|
|
### Solution
|
||
|
|
Bakery-IA Suppliers Service provides:
|
||
|
|
- **Data-Driven Decisions**: Performance scorecards guide supplier selection
|
||
|
|
- **Cost Control**: Price tracking and comparison across suppliers
|
||
|
|
- **Quality Assurance**: Document and resolve quality issues systematically
|
||
|
|
- **Relationship Management**: Complete supplier history and communication log
|
||
|
|
- **Risk Management**: Track certifications, contracts, and performance
|
||
|
|
|
||
|
|
### Quantifiable Impact
|
||
|
|
|
||
|
|
**Cost Savings:**
|
||
|
|
- €100-300/month from data-driven negotiations (5-10% procurement savings)
|
||
|
|
- €150-500/month from reduced ingredient defects (15-25% quality improvement)
|
||
|
|
- €50-200/month from avoiding expired contracts with price increases
|
||
|
|
- **Total: €300-1,000/month savings**
|
||
|
|
|
||
|
|
**Time Savings:**
|
||
|
|
- 3-5 hours/week on supplier management and tracking
|
||
|
|
- 1-2 hours/week on price comparison
|
||
|
|
- 1-2 hours/week on quality issue documentation
|
||
|
|
- **Total: 5-9 hours/week saved**
|
||
|
|
|
||
|
|
**Operational Improvements:**
|
||
|
|
- 20-30% improvement in on-time delivery through supplier accountability
|
||
|
|
- 15-25% fewer ingredient defects through quality tracking
|
||
|
|
- 100% contract renewal visibility (avoid surprise price increases)
|
||
|
|
- 20-30% supplier consolidation (focus on best performers)
|
||
|
|
|
||
|
|
### Target Market Fit (Spanish Bakeries)
|
||
|
|
- **Supplier Relationships**: Spanish business culture values long-term relationships
|
||
|
|
- **Quality Focus**: Spanish consumers demand high-quality ingredients
|
||
|
|
- **Cost Pressure**: SMBs need every cost advantage in competitive market
|
||
|
|
- **Compliance**: Food safety certifications required by Spanish law
|
||
|
|
|
||
|
|
### ROI Calculation
|
||
|
|
**Investment**: €0 additional (included in platform subscription)
|
||
|
|
**Monthly Savings**: €300-1,000
|
||
|
|
**Annual ROI**: €3,600-12,000 value per bakery
|
||
|
|
**Payback**: Immediate (included in subscription)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
**Copyright © 2025 Bakery-IA. All rights reserved.**
|