🎉 v1.0.0 — Vela Platform launch
Self-hosted P&L tracking app with component-level pricing. Offers: Atlas/Atlas+/Rif/Rif+ with granular cost breakdown. API + MCP + multi-user auth.
This commit is contained in:
96
backend/models.py
Normal file
96
backend/models.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""SQLAlchemy models: User, Service, ServiceComponent, Transaction."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import (
|
||||
Column,
|
||||
Integer,
|
||||
String,
|
||||
Float,
|
||||
Boolean,
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
)
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from database import Base
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String, unique=True, index=True, nullable=False)
|
||||
password_hash = Column(String, nullable=False)
|
||||
display_name = Column(String, nullable=False)
|
||||
role = Column(String, nullable=False, default="viewer") # admin | viewer
|
||||
|
||||
transactions = relationship("Transaction", back_populates="creator")
|
||||
|
||||
|
||||
class Service(Base):
|
||||
__tablename__ = "services"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, unique=True, nullable=False)
|
||||
description = Column(String, default="")
|
||||
sell_price = Column(Float, nullable=False) # MAD/month — auto-computed from components
|
||||
cost_price = Column(Float, nullable=False) # MAD/month — auto-computed from components
|
||||
active = Column(Boolean, default=True)
|
||||
|
||||
transactions = relationship("Transaction", back_populates="service")
|
||||
components = relationship(
|
||||
"ServiceComponent",
|
||||
back_populates="service",
|
||||
cascade="all, delete-orphan",
|
||||
order_by="ServiceComponent.id",
|
||||
)
|
||||
|
||||
def recompute_prices(self):
|
||||
"""Update sell_price and cost_price from component totals."""
|
||||
self.sell_price = sum(
|
||||
(c.unit_sell * c.quantity) for c in self.components
|
||||
) if self.components else 0.0
|
||||
self.cost_price = sum(
|
||||
(c.unit_cost * c.quantity) for c in self.components
|
||||
) if self.components else 0.0
|
||||
|
||||
|
||||
class ServiceComponent(Base):
|
||||
"""A cost/sell component of a service offer (e.g. RAM, HDD, Transport)."""
|
||||
__tablename__ = "service_components"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
service_id = Column(Integer, ForeignKey("services.id"), nullable=False)
|
||||
name = Column(String, nullable=False)
|
||||
unit_cost = Column(Float, nullable=False, default=0.0) # what we pay / unit
|
||||
unit_sell = Column(Float, nullable=False, default=0.0) # what we charge / unit
|
||||
quantity = Column(Integer, nullable=False, default=1)
|
||||
notes = Column(String, default="")
|
||||
|
||||
service = relationship("Service", back_populates="components")
|
||||
|
||||
|
||||
class Transaction(Base):
|
||||
__tablename__ = "transactions"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
service_id = Column(Integer, ForeignKey("services.id"), nullable=False)
|
||||
quantity = Column(Integer, nullable=False, default=1)
|
||||
month = Column(String, nullable=False) # YYYY-MM format
|
||||
notes = Column(String, default="")
|
||||
created_by = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
service = relationship("Service", back_populates="transactions")
|
||||
creator = relationship("User", back_populates="transactions")
|
||||
|
||||
@property
|
||||
def revenue(self) -> float:
|
||||
"""Calculated: quantity * service.sell_price"""
|
||||
return self.quantity * self.service.sell_price
|
||||
|
||||
@property
|
||||
def cost(self) -> float:
|
||||
"""Calculated: quantity * service.cost_price"""
|
||||
return self.quantity * self.service.cost_price
|
||||
Reference in New Issue
Block a user