From 91cf1fa04fe47ba742c23371bfc44fa8beafdaab Mon Sep 17 00:00:00 2001
From: Amritanshu <git@tanshu.com>
Date: Fri, 4 Aug 2023 16:02:30 +0530
Subject: [PATCH] Fix: Account delete did not work due to not using unique
 Chore: Sqlalchemy using the right way to write expressions.

---
 .../versions/a1372ed99c45_recipe_upgrade.py   |  2 +-
 brewman/brewman/models/inventory.py           |  4 +++-
 brewman/brewman/models/journal.py             |  2 +-
 brewman/brewman/routers/account.py            |  1 +
 .../brewman/routers/reports/product_ledger.py |  6 +++---
 brewman/brewman/schemas/closing_stock.py      | 21 +++++++++++++++++++
 brewman/brewman/schemas/voucher.py            |  8 +++----
 7 files changed, 34 insertions(+), 10 deletions(-)

diff --git a/brewman/alembic/versions/a1372ed99c45_recipe_upgrade.py b/brewman/alembic/versions/a1372ed99c45_recipe_upgrade.py
index a82fad68..ebc52ec0 100644
--- a/brewman/alembic/versions/a1372ed99c45_recipe_upgrade.py
+++ b/brewman/alembic/versions/a1372ed99c45_recipe_upgrade.py
@@ -61,10 +61,10 @@ def upgrade():
     op.add_column("recipes", sa.Column("instructions", sa.Text(), nullable=False, server_default=""))
     op.add_column("recipes", sa.Column("garnishing", sa.Text(), nullable=False, server_default=""))
     op.add_column("recipes", sa.Column("plating", sa.Text(), nullable=False, server_default=""))
+    op.drop_constraint(op.f("uq_recipes_sku_id"), "recipes", type_="unique")
     op.alter_column("recipes", "notes", existing_type=sa.VARCHAR(length=255),type=sa.Text(), nullable=False)
     op.create_unique_constraint(op.f('uq_recipes_sku_id'), 'recipes', ['sku_id', 'date'])
     op.create_index(op.f("ix_recipes_date"), "recipes", ["date"], unique=False)
-    op.drop_constraint(op.f("uq_recipes_sku_id"), "recipes", type_="unique")
     op.drop_constraint("fk_recipes_period_id_periods", "recipes", type_="foreignkey")
     op.drop_column("recipes", "period_id")
     op.drop_column('recipes', 'sale_price')
diff --git a/brewman/brewman/models/inventory.py b/brewman/brewman/models/inventory.py
index 44156280..858b7d9d 100644
--- a/brewman/brewman/models/inventory.py
+++ b/brewman/brewman/models/inventory.py
@@ -70,4 +70,6 @@ class Inventory:
     @amount.inplace.expression
     @classmethod
     def _amount_expression(cls) -> ColumnElement[Decimal]:
-        return type_coerce(cls.quantity * cls.rate * (1 + cls.tax) * (1 - cls.discount), Decimal)
+        return type_coerce(
+            cls.quantity * cls.rate * (1 + cls.tax) * (1 - cls.discount), Numeric(precision=15, scale=2)
+        ).label("amount")
diff --git a/brewman/brewman/models/journal.py b/brewman/brewman/models/journal.py
index e3687304..e3d078c2 100644
--- a/brewman/brewman/models/journal.py
+++ b/brewman/brewman/models/journal.py
@@ -51,7 +51,7 @@ class Journal:
     @signed_amount.inplace.expression
     @classmethod
     def _signed_amount_expression(cls) -> ColumnElement[Decimal]:
-        return type_coerce(cls.debit * cls.amount, Decimal)
+        return type_coerce(cls.debit * cls.amount, Numeric(precision=15, scale=2)).label("signed_amount")
 
     def __init__(
         self,
diff --git a/brewman/brewman/routers/account.py b/brewman/brewman/routers/account.py
index 1685d940..0ed1c2bb 100644
--- a/brewman/brewman/routers/account.py
+++ b/brewman/brewman/routers/account.py
@@ -226,6 +226,7 @@ def delete_with_data(account: Account, db: Session) -> None:
             .options(joinedload(Voucher.journals, innerjoin=True).joinedload(Journal.account, innerjoin=True))
             .where(Voucher.journals.any(Journal.account_id == account.id))
         )
+        .unique()
         .scalars()
         .all()
     )
diff --git a/brewman/brewman/routers/reports/product_ledger.py b/brewman/brewman/routers/reports/product_ledger.py
index f9faaa4a..f8766a11 100644
--- a/brewman/brewman/routers/reports/product_ledger.py
+++ b/brewman/brewman/routers/reports/product_ledger.py
@@ -72,7 +72,7 @@ def build_report(
     running_total_q, running_total_a, opening = opening_balance(product_id, start_date, db)
     body = opening
 
-    query = db.execute(
+    query: list[tuple[Voucher, Inventory, Journal, StockKeepingUnit]] = db.execute(
         select(Voucher, Inventory, Journal, StockKeepingUnit)
         .join(Voucher.journals)
         .join(Voucher.inventories)
@@ -112,14 +112,14 @@ def build_report(
         body.append(
             schemas.ProductLedgerItem(
                 id_=voucher.id,
-                date_=voucher.date,
+                date_=voucher.date_,
                 name=name,
                 url=[
                     "/",
                     voucher.voucher_type.name.replace("_", "-").lower(),
                     str(voucher.id)
                     if voucher.voucher_type != VoucherType.CLOSING_STOCK
-                    else voucher.date.strftime("%d-%b-%Y"),
+                    else voucher.date_.strftime("%d-%b-%Y"),
                 ],
                 type_=voucher.voucher_type.name.replace("_", " ").title(),
                 narration=voucher.narration,
diff --git a/brewman/brewman/schemas/closing_stock.py b/brewman/brewman/schemas/closing_stock.py
index bb5c30bd..25f3b01c 100644
--- a/brewman/brewman/schemas/closing_stock.py
+++ b/brewman/brewman/schemas/closing_stock.py
@@ -27,6 +27,27 @@ class ClosingStockItem(BaseModel):
     cost_centre: CostCentreLink | None = None
     model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
 
+    @field_validator("quantity", mode="before")
+    @classmethod
+    def parse_quantity(cls, value: Decimal | float) -> Decimal:
+        if isinstance(value, float):
+            return Decimal(round(value, 2))
+        return round(value, 2)
+
+    @field_validator("amount", mode="before")
+    @classmethod
+    def parse_amount(cls, value: Decimal | float) -> Decimal:
+        if isinstance(value, float):
+            return Decimal(round(value, 2))
+        return round(value, 2)
+
+    @field_validator("physical", mode="before")
+    @classmethod
+    def parse_physical(cls, value: Decimal | float) -> Decimal:
+        if isinstance(value, float):
+            return Decimal(round(value, 2))
+        return round(value, 2)
+
 
 class ClosingStock(BaseModel):
     date_: date
diff --git a/brewman/brewman/schemas/voucher.py b/brewman/brewman/schemas/voucher.py
index 14d41754..02617a0e 100644
--- a/brewman/brewman/schemas/voucher.py
+++ b/brewman/brewman/schemas/voucher.py
@@ -94,8 +94,8 @@ class Voucher(VoucherIn):
         return datetime.strptime(value, "%d-%b-%Y %H:%M")
 
     @field_serializer("creation_date")
-    def serialize_creation_date(self, value: datetime, info: FieldSerializationInfo) -> str | None:
-        return value.strftime("%d-%b-%Y %H:%M")
+    def serialize_creation_date(self, value: datetime | None, info: FieldSerializationInfo) -> str | None:
+        return None if value is None else value.strftime("%d-%b-%Y %H:%M")
 
     @field_validator("last_edit_date", mode="before")
     @classmethod
@@ -107,5 +107,5 @@ class Voucher(VoucherIn):
         return datetime.strptime(value, "%d-%b-%Y %H:%M")
 
     @field_serializer("last_edit_date")
-    def serialize_last_edit_date(self, value: datetime, info: FieldSerializationInfo) -> str:
-        return value.strftime("%d-%b-%Y %H:%M")
+    def serialize_last_edit_date(self, value: datetime, info: FieldSerializationInfo) -> str | None:
+        return None if value is None else value.strftime("%d-%b-%Y %H:%M")