288 lines
16 KiB
Python
288 lines
16 KiB
Python
"""Modèles du module Matériel & Produits d'entretien.
|
|
|
|
Les quantités sont toujours exprimées dans l'unité de référence du produit et
|
|
stockées en DECIMAL. Les mouvements constituent l'historique immuable ; les
|
|
soldes par lot/emplacement sont des projections verrouillées pendant les
|
|
opérations transactionnelles.
|
|
"""
|
|
from datetime import datetime, timezone
|
|
from decimal import Decimal
|
|
|
|
from ...extensions import db
|
|
|
|
|
|
def utcnow():
|
|
return datetime.now(timezone.utc)
|
|
|
|
|
|
class ProductCategory(db.Model):
|
|
__tablename__ = "cleaning_product_categories"
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
name = db.Column(db.String(120), nullable=False, unique=True)
|
|
description = db.Column(db.Text)
|
|
is_active = db.Column(db.Boolean, nullable=False, default=True)
|
|
products = db.relationship("ProductGeneric", back_populates="category")
|
|
|
|
|
|
class ProductGeneric(db.Model):
|
|
__tablename__ = "cleaning_products_generic"
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
name = db.Column(db.String(200), nullable=False, unique=True)
|
|
description = db.Column(db.Text)
|
|
category_id = db.Column(db.Integer, db.ForeignKey("cleaning_product_categories.id"), index=True)
|
|
product_type = db.Column(db.String(40), nullable=False, default="consommable")
|
|
reference_unit = db.Column(db.String(30), nullable=False, default="unité")
|
|
is_active = db.Column(db.Boolean, nullable=False, default=True)
|
|
stock_minimum = db.Column(db.Numeric(14, 6), nullable=False, default=0)
|
|
stock_security = db.Column(db.Numeric(14, 6), nullable=False, default=0)
|
|
forecast_daily_quantity = db.Column(db.Numeric(14, 6), nullable=False, default=0)
|
|
display_order = db.Column(db.Integer, nullable=False, default=0)
|
|
usage_zones = db.Column(db.String(500))
|
|
request_enabled = db.Column(db.Boolean, nullable=False, default=True)
|
|
allowed_distribution_modes = db.Column(db.String(255), nullable=False, default="standard")
|
|
created_at = db.Column(db.DateTime, nullable=False, default=utcnow)
|
|
updated_at = db.Column(db.DateTime, nullable=False, default=utcnow, onupdate=utcnow)
|
|
category = db.relationship("ProductCategory", back_populates="products")
|
|
commercial_products = db.relationship("CommercialProduct", back_populates="generic_product", cascade="all, delete-orphan")
|
|
packagings = db.relationship("ProductPackaging", back_populates="generic_product", cascade="all, delete-orphan")
|
|
lots = db.relationship("StockLot", back_populates="generic_product")
|
|
|
|
@property
|
|
def stock_total(self):
|
|
return sum((b.quantity for lot in self.lots for b in lot.balances), Decimal("0"))
|
|
|
|
|
|
class CommercialProduct(db.Model):
|
|
__tablename__ = "cleaning_commercial_products"
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
generic_product_id = db.Column(db.Integer, db.ForeignKey("cleaning_products_generic.id"), nullable=False, index=True)
|
|
commercial_name = db.Column(db.String(200), nullable=False)
|
|
brand = db.Column(db.String(120))
|
|
manufacturer_id = db.Column(db.Integer, db.ForeignKey("companies.id"), index=True)
|
|
supplier_id = db.Column(db.Integer, db.ForeignKey("companies.id"), index=True)
|
|
manufacturer_reference = db.Column(db.String(120))
|
|
supplier_reference = db.Column(db.String(120))
|
|
barcode = db.Column(db.String(80), index=True)
|
|
valid_from = db.Column(db.Date)
|
|
valid_to = db.Column(db.Date)
|
|
is_active = db.Column(db.Boolean, nullable=False, default=True)
|
|
photo_path = db.Column(db.String(500))
|
|
dosage_information = db.Column(db.Text)
|
|
is_concentrated = db.Column(db.Boolean, nullable=False, default=False)
|
|
after_opening_days = db.Column(db.Integer)
|
|
safety_notes = db.Column(db.Text)
|
|
pictograms = db.Column(db.String(500))
|
|
warning_statement = db.Column(db.String(120))
|
|
hazard_statements = db.Column(db.Text)
|
|
precautionary_statements = db.Column(db.Text)
|
|
required_ppe = db.Column(db.String(500))
|
|
storage_conditions = db.Column(db.Text)
|
|
incompatibilities = db.Column(db.Text)
|
|
created_at = db.Column(db.DateTime, nullable=False, default=utcnow)
|
|
generic_product = db.relationship("ProductGeneric", back_populates="commercial_products")
|
|
manufacturer = db.relationship("Company", foreign_keys=[manufacturer_id])
|
|
supplier = db.relationship("Company", foreign_keys=[supplier_id])
|
|
lots = db.relationship("StockLot", back_populates="commercial_product")
|
|
packagings = db.relationship("ProductPackaging", back_populates="commercial_product")
|
|
documents = db.relationship("ProductDocument", back_populates="commercial_product", cascade="all, delete-orphan")
|
|
|
|
|
|
class ProductPackaging(db.Model):
|
|
__tablename__ = "cleaning_product_packagings"
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
generic_product_id = db.Column(db.Integer, db.ForeignKey("cleaning_products_generic.id"), nullable=False, index=True)
|
|
commercial_product_id = db.Column(db.Integer, db.ForeignKey("cleaning_commercial_products.id"), index=True)
|
|
name = db.Column(db.String(160), nullable=False)
|
|
purchase_unit = db.Column(db.String(40), nullable=False, default="conditionnement")
|
|
units_per_package = db.Column(db.Numeric(14, 6), nullable=False, default=1)
|
|
reference_quantity_per_package = db.Column(db.Numeric(14, 6), nullable=False)
|
|
is_active = db.Column(db.Boolean, nullable=False, default=True)
|
|
is_current = db.Column(db.Boolean, nullable=False, default=True)
|
|
generic_product = db.relationship("ProductGeneric", back_populates="packagings")
|
|
commercial_product = db.relationship("CommercialProduct", back_populates="packagings")
|
|
|
|
|
|
class StockLocation(db.Model):
|
|
__tablename__ = "cleaning_stock_locations"
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
name = db.Column(db.String(160), nullable=False, unique=True)
|
|
building_id = db.Column(db.Integer, db.ForeignKey("buildings.id"), index=True)
|
|
zone_id = db.Column(db.Integer, db.ForeignKey("zones.id"), index=True)
|
|
room_id = db.Column(db.Integer, db.ForeignKey("rooms.id"), index=True)
|
|
is_active = db.Column(db.Boolean, nullable=False, default=True)
|
|
notes = db.Column(db.Text)
|
|
balances = db.relationship("StockLotBalance", back_populates="location")
|
|
|
|
|
|
class StockLot(db.Model):
|
|
__tablename__ = "cleaning_stock_lots"
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
generic_product_id = db.Column(db.Integer, db.ForeignKey("cleaning_products_generic.id"), nullable=False, index=True)
|
|
commercial_product_id = db.Column(db.Integer, db.ForeignKey("cleaning_commercial_products.id"), nullable=False, index=True)
|
|
lot_number = db.Column(db.String(120), nullable=False, index=True)
|
|
received_at = db.Column(db.Date, nullable=False)
|
|
manufactured_at = db.Column(db.Date)
|
|
expiry_date = db.Column(db.Date, index=True)
|
|
quantity_received = db.Column(db.Numeric(14, 6), nullable=False)
|
|
opened_at = db.Column(db.Date)
|
|
after_opening_days = db.Column(db.Integer)
|
|
status = db.Column(db.String(30), nullable=False, default="disponible", index=True)
|
|
notes = db.Column(db.Text)
|
|
generic_product = db.relationship("ProductGeneric", back_populates="lots")
|
|
commercial_product = db.relationship("CommercialProduct", back_populates="lots")
|
|
balances = db.relationship("StockLotBalance", back_populates="lot", cascade="all, delete-orphan")
|
|
movements = db.relationship("StockMovement", back_populates="lot")
|
|
|
|
|
|
class StockLotBalance(db.Model):
|
|
__tablename__ = "cleaning_stock_lot_balances"
|
|
__table_args__ = (db.UniqueConstraint("lot_id", "location_id", name="uq_cleaning_lot_location"),)
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
lot_id = db.Column(db.Integer, db.ForeignKey("cleaning_stock_lots.id", ondelete="RESTRICT"), nullable=False, index=True)
|
|
location_id = db.Column(db.Integer, db.ForeignKey("cleaning_stock_locations.id", ondelete="RESTRICT"), nullable=False, index=True)
|
|
quantity = db.Column(db.Numeric(14, 6), nullable=False, default=0)
|
|
lot = db.relationship("StockLot", back_populates="balances")
|
|
location = db.relationship("StockLocation", back_populates="balances")
|
|
|
|
|
|
class StockMovement(db.Model):
|
|
__tablename__ = "cleaning_stock_movements"
|
|
id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)
|
|
generic_product_id = db.Column(db.Integer, db.ForeignKey("cleaning_products_generic.id"), nullable=False, index=True)
|
|
commercial_product_id = db.Column(db.Integer, db.ForeignKey("cleaning_commercial_products.id"), index=True)
|
|
lot_id = db.Column(db.Integer, db.ForeignKey("cleaning_stock_lots.id"), index=True)
|
|
movement_type = db.Column(db.String(30), nullable=False, index=True)
|
|
quantity_reference = db.Column(db.Numeric(14, 6), nullable=False)
|
|
source_location_id = db.Column(db.Integer, db.ForeignKey("cleaning_stock_locations.id"), index=True)
|
|
destination_location_id = db.Column(db.Integer, db.ForeignKey("cleaning_stock_locations.id"), index=True)
|
|
staff_id = db.Column(db.Integer, db.ForeignKey("staff.id"), index=True)
|
|
user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="SET NULL"), index=True)
|
|
comment = db.Column(db.Text)
|
|
dilution_ratio = db.Column(db.Numeric(8, 5))
|
|
created_at = db.Column(db.DateTime, nullable=False, default=utcnow, index=True)
|
|
lot = db.relationship("StockLot", back_populates="movements")
|
|
generic_product = db.relationship("ProductGeneric")
|
|
commercial_product = db.relationship("CommercialProduct")
|
|
source_location = db.relationship("StockLocation", foreign_keys=[source_location_id])
|
|
destination_location = db.relationship("StockLocation", foreign_keys=[destination_location_id])
|
|
staff = db.relationship("Staff")
|
|
user = db.relationship("User")
|
|
|
|
|
|
class MaterialAssignment(db.Model):
|
|
__tablename__ = "cleaning_material_assignments"
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
equipment_id = db.Column(db.Integer, db.ForeignKey("equipments.id", ondelete="RESTRICT"), nullable=False, index=True)
|
|
staff_id = db.Column(db.Integer, db.ForeignKey("staff.id"), index=True)
|
|
zone_id = db.Column(db.Integer, db.ForeignKey("zones.id"), index=True)
|
|
room_id = db.Column(db.Integer, db.ForeignKey("rooms.id"), index=True)
|
|
status = db.Column(db.String(30), nullable=False, default="affecte")
|
|
assigned_at = db.Column(db.DateTime, nullable=False, default=utcnow)
|
|
returned_at = db.Column(db.DateTime)
|
|
notes = db.Column(db.Text)
|
|
equipment = db.relationship("Equipment")
|
|
staff = db.relationship("Staff")
|
|
zone = db.relationship("Zone")
|
|
room = db.relationship("Room")
|
|
|
|
|
|
class ReusableContainer(db.Model):
|
|
__tablename__ = "cleaning_reusable_containers"
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
identifier = db.Column(db.String(80), nullable=False, unique=True)
|
|
container_type = db.Column(db.String(80), nullable=False)
|
|
volume_liters = db.Column(db.Numeric(12, 6), nullable=False)
|
|
dedicated_product_id = db.Column(db.Integer, db.ForeignKey("cleaning_products_generic.id"))
|
|
current_product_id = db.Column(db.Integer, db.ForeignKey("cleaning_products_generic.id"))
|
|
staff_id = db.Column(db.Integer, db.ForeignKey("staff.id"))
|
|
zone_id = db.Column(db.Integer, db.ForeignKey("zones.id"))
|
|
state = db.Column(db.String(30), nullable=False, default="disponible")
|
|
last_filled_at = db.Column(db.DateTime)
|
|
dedicated_product = db.relationship("ProductGeneric", foreign_keys=[dedicated_product_id])
|
|
current_product = db.relationship("ProductGeneric", foreign_keys=[current_product_id])
|
|
staff = db.relationship("Staff")
|
|
zone = db.relationship("Zone")
|
|
fills = db.relationship("ContainerFill", back_populates="container", cascade="all, delete-orphan")
|
|
|
|
|
|
class ContainerFill(db.Model):
|
|
__tablename__ = "cleaning_container_fills"
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
container_id = db.Column(db.Integer, db.ForeignKey("cleaning_reusable_containers.id", ondelete="RESTRICT"), nullable=False, index=True)
|
|
movement_id = db.Column(db.BigInteger, db.ForeignKey("cleaning_stock_movements.id", ondelete="RESTRICT"), nullable=False)
|
|
volume_liters = db.Column(db.Numeric(12, 6), nullable=False)
|
|
dilution_ratio = db.Column(db.Numeric(8, 5))
|
|
prepared_at = db.Column(db.DateTime, nullable=False, default=utcnow)
|
|
container = db.relationship("ReusableContainer", back_populates="fills")
|
|
movement = db.relationship("StockMovement")
|
|
|
|
|
|
class ProductDocument(db.Model):
|
|
__tablename__ = "cleaning_product_documents"
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
commercial_product_id = db.Column(db.Integer, db.ForeignKey("cleaning_commercial_products.id", ondelete="RESTRICT"), nullable=False, index=True)
|
|
document_type = db.Column(db.String(30), nullable=False, default="fds")
|
|
title = db.Column(db.String(255), nullable=False)
|
|
filename = db.Column(db.String(255), nullable=False)
|
|
filepath = db.Column(db.String(500), nullable=False)
|
|
version = db.Column(db.String(50))
|
|
published_at = db.Column(db.Date)
|
|
uploaded_at = db.Column(db.DateTime, nullable=False, default=utcnow)
|
|
uploaded_by_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="SET NULL"))
|
|
is_current = db.Column(db.Boolean, nullable=False, default=True)
|
|
commercial_product = db.relationship("CommercialProduct", back_populates="documents")
|
|
uploaded_by = db.relationship("User")
|
|
|
|
|
|
class RequestProfile(db.Model):
|
|
__tablename__ = "cleaning_request_profiles"
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
name = db.Column(db.String(160), nullable=False, unique=True)
|
|
description = db.Column(db.Text)
|
|
is_active = db.Column(db.Boolean, nullable=False, default=True)
|
|
items = db.relationship("RequestProfileItem", back_populates="profile", cascade="all, delete-orphan", order_by="RequestProfileItem.display_order")
|
|
|
|
|
|
class RequestProfileItem(db.Model):
|
|
__tablename__ = "cleaning_request_profile_items"
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
profile_id = db.Column(db.Integer, db.ForeignKey("cleaning_request_profiles.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
product_id = db.Column(db.Integer, db.ForeignKey("cleaning_products_generic.id", ondelete="RESTRICT"), nullable=False, index=True)
|
|
display_order = db.Column(db.Integer, nullable=False, default=0)
|
|
profile = db.relationship("RequestProfile", back_populates="items")
|
|
product = db.relationship("ProductGeneric")
|
|
|
|
|
|
class StockInventory(db.Model):
|
|
__tablename__ = "cleaning_stock_inventories"
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
location_id = db.Column(db.Integer, db.ForeignKey("cleaning_stock_locations.id"), nullable=False, index=True)
|
|
counted_at = db.Column(db.DateTime, nullable=False, default=utcnow)
|
|
status = db.Column(db.String(20), nullable=False, default="brouillon")
|
|
user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="SET NULL"))
|
|
notes = db.Column(db.Text)
|
|
location = db.relationship("StockLocation")
|
|
user = db.relationship("User")
|
|
lines = db.relationship("StockInventoryLine", back_populates="inventory", cascade="all, delete-orphan")
|
|
|
|
|
|
class StockInventoryLine(db.Model):
|
|
__tablename__ = "cleaning_stock_inventory_lines"
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
inventory_id = db.Column(db.Integer, db.ForeignKey("cleaning_stock_inventories.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
lot_id = db.Column(db.Integer, db.ForeignKey("cleaning_stock_lots.id", ondelete="RESTRICT"), nullable=False)
|
|
theoretical_quantity = db.Column(db.Numeric(14, 6), nullable=False)
|
|
counted_quantity = db.Column(db.Numeric(14, 6), nullable=False)
|
|
inventory = db.relationship("StockInventory", back_populates="lines")
|
|
lot = db.relationship("StockLot")
|
|
|
|
|
|
class CleaningForecastConfig(db.Model):
|
|
__tablename__ = "cleaning_forecast_configs"
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
name = db.Column(db.String(120), nullable=False, default="Configuration générale")
|
|
normal_coefficient = db.Column(db.Numeric(8, 5), nullable=False, default=1)
|
|
permanence_coefficient = db.Column(db.Numeric(8, 5), nullable=False, default=Decimal("0.4"))
|
|
closed_coefficient = db.Column(db.Numeric(8, 5), nullable=False, default=0)
|
|
exceptional_coefficient = db.Column(db.Numeric(8, 5), nullable=False, default=1)
|
|
is_active = db.Column(db.Boolean, nullable=False, default=True)
|