gmao/app_new/core/models/cleaning.py
root 374e5e5a54
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run
Ajouter le module matériel et produits d'entretien
2026-08-21 14:13:00 +00:00

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)