From 0574f9df14f67e0775771ec992eb9dda301337a6 Mon Sep 17 00:00:00 2001 From: tanshu Date: Sun, 31 Oct 2021 18:41:06 +0530 Subject: [PATCH] Chore: Changed the account_type and voucher_type enum. The account type enum is not stored in the database as an enum. The voucher_type enum is now a table in the database. Feature: Closing stock can now be saved and in each department. --- .../alembic/versions/7ba0aff64237_enums.py | 224 ++++++++++++ .../versions/d6f96b7b16c6_closing_stock.py | 45 +++ brewman/brewman/core/security.py | 6 +- brewman/brewman/db/base.py | 3 + brewman/brewman/db/init_db.py | 25 -- brewman/brewman/models/account_base.py | 23 +- brewman/brewman/models/account_type.py | 57 +-- brewman/brewman/models/closing_stock.py | 43 +++ brewman/brewman/models/employee.py | 4 +- brewman/brewman/models/inventory.py | 2 +- brewman/brewman/models/voucher.py | 26 +- brewman/brewman/models/voucher_type.py | 50 +-- brewman/brewman/routers/__init__.py | 9 +- brewman/brewman/routers/account.py | 37 +- brewman/brewman/routers/account_types.py | 7 +- brewman/brewman/routers/attendance_report.py | 4 +- brewman/brewman/routers/batch_integrity.py | 30 +- brewman/brewman/routers/credit_salary.py | 2 +- brewman/brewman/routers/employee.py | 10 +- brewman/brewman/routers/employee_benefit.py | 12 +- brewman/brewman/routers/incentive.py | 16 +- brewman/brewman/routers/issue.py | 12 +- brewman/brewman/routers/issue_grid.py | 2 +- brewman/brewman/routers/journal.py | 16 +- brewman/brewman/routers/lock_information.py | 4 +- brewman/brewman/routers/purchase.py | 26 +- brewman/brewman/routers/purchase_return.py | 12 +- brewman/brewman/routers/rebase.py | 12 +- brewman/brewman/routers/recipe.py | 35 +- .../brewman/routers/reports/balance_sheet.py | 34 +- brewman/brewman/routers/reports/cash_flow.py | 82 +++-- .../brewman/routers/reports/closing_stock.py | 336 +++++++++++++++++- brewman/brewman/routers/reports/daybook.py | 10 +- brewman/brewman/routers/reports/entries.py | 12 +- brewman/brewman/routers/reports/ledger.py | 9 +- .../routers/reports/net_transactions.py | 11 +- .../brewman/routers/reports/product_ledger.py | 18 +- .../brewman/routers/reports/profit_loss.py | 35 +- .../routers/reports/purchase_entries.py | 4 +- brewman/brewman/routers/reports/purchases.py | 3 +- .../routers/reports/raw_material_cost.py | 9 +- brewman/brewman/routers/reports/reconcile.py | 11 +- .../brewman/routers/reports/stock_movement.py | 5 +- .../brewman/routers/reports/trial_balance.py | 22 +- brewman/brewman/routers/role.py | 3 +- brewman/brewman/routers/user.py | 3 +- brewman/brewman/routers/voucher.py | 57 +-- brewman/brewman/routers/voucher_types.py | 2 +- brewman/brewman/schemas/account.py | 5 +- brewman/brewman/schemas/closing_stock.py | 20 +- brewman/brewman/schemas/product.py | 2 +- brewman/brewman/schemas/user.py | 2 +- brewman/pyproject.toml | 1 + .../closing-stock/closing-stock-datasource.ts | 4 +- .../app/closing-stock/closing-stock-item.ts | 14 +- .../closing-stock-resolver.service.ts | 3 +- .../closing-stock-routing.module.ts | 5 + .../closing-stock/closing-stock.component.css | 13 + .../closing-stock.component.html | 166 ++++++--- .../closing-stock/closing-stock.component.ts | 150 +++++++- .../app/closing-stock/closing-stock.module.ts | 2 + .../closing-stock/closing-stock.service.ts | 35 +- .../src/app/closing-stock/closing-stock.ts | 19 +- overlord/src/app/core/product.ts | 3 +- overlord/src/app/core/voucher.service.ts | 2 +- .../product-ledger.component.ts | 2 +- .../app/product/product-resolver.service.ts | 2 +- overlord/src/app/product/product.service.ts | 4 + .../app/purchase/purchase-dialog.component.ts | 2 +- .../src/app/purchase/purchase.component.ts | 1 + .../rate-contract-detail.component.ts | 2 +- 71 files changed, 1382 insertions(+), 497 deletions(-) create mode 100644 brewman/alembic/versions/7ba0aff64237_enums.py create mode 100644 brewman/alembic/versions/d6f96b7b16c6_closing_stock.py delete mode 100644 brewman/brewman/db/init_db.py create mode 100644 brewman/brewman/models/closing_stock.py diff --git a/brewman/alembic/versions/7ba0aff64237_enums.py b/brewman/alembic/versions/7ba0aff64237_enums.py new file mode 100644 index 00000000..0d5a2c51 --- /dev/null +++ b/brewman/alembic/versions/7ba0aff64237_enums.py @@ -0,0 +1,224 @@ +"""enums + +Revision ID: 7ba0aff64237 +Revises: d6f96b7b16c6 +Create Date: 2021-10-29 18:26:58.200044 + +""" +import sqlalchemy as sa + +from alembic import op +from sqlalchemy import column, table + + +# revision identifiers, used by Alembic. +revision = "7ba0aff64237" +down_revision = "d6f96b7b16c6" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + voucher_type = sa.Enum( + "JOURNAL", + "PURCHASE", + "ISSUE", + "PAYMENT", + "RECEIPT", + "PURCHASE_RETURN", + "OPENING_ACCOUNTS", + "OPENING_BATCHES", + "CLOSING_STOCK", + "OPENING_BALANCE", + "CLOSING_BALANCE", + "EMPLOYEE_BENEFIT", + "INCENTIVE", + name="voucher_type", + ) + voucher_type.create(op.get_bind()) + op.add_column( + "vouchers", + sa.Column( + "vt", + voucher_type, + nullable=True, + ), + ) + v = table("vouchers", column("voucher_type", sa.INTEGER()), column("vt", voucher_type)) + op.execute(v.update().where(v.c.voucher_type == 1).values(vt="JOURNAL")) + op.execute(v.update().where(v.c.voucher_type == 2).values(vt="PURCHASE")) + op.execute(v.update().where(v.c.voucher_type == 3).values(vt="ISSUE")) + op.execute(v.update().where(v.c.voucher_type == 4).values(vt="PAYMENT")) + op.execute(v.update().where(v.c.voucher_type == 5).values(vt="RECEIPT")) + op.execute(v.update().where(v.c.voucher_type == 6).values(vt="PURCHASE_RETURN")) + op.execute(v.update().where(v.c.voucher_type == 7).values(vt="OPENING_ACCOUNTS")) + op.execute(v.update().where(v.c.voucher_type == 8).values(vt="OPENING_BATCHES")) + op.execute(v.update().where(v.c.voucher_type == 9).values(vt="CLOSING_STOCK")) + op.execute(v.update().where(v.c.voucher_type == 13).values(vt="INCENTIVE")) + op.drop_column("vouchers", "voucher_type") + + op.alter_column("vouchers", "vt", new_column_name="voucher_type", existing_nullable=True, nullable=False) + + op.create_table( + "account_types", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.Unicode(length=255), nullable=False), + sa.Column("balance_sheet", sa.Boolean(), nullable=False), + sa.Column("debit", sa.Boolean(), nullable=False), + sa.Column("cash_flow_classification", sa.Unicode(length=255), nullable=False), + sa.Column("order", sa.Integer(), nullable=False), + sa.Column("show_in_list", sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint("id", name=op.f("pk_account_types")), + sa.UniqueConstraint("name", name=op.f("uq_account_types_name")), + ) + at = table( + "account_types", + column("id", sa.Integer()), + column("name", sa.Unicode(length=255)), + column("balance_sheet", sa.Boolean()), + column("debit", sa.Boolean()), + column("cash_flow_classification", sa.Unicode(length=255)), + column("order", sa.Integer()), + column("show_in_list", sa.Boolean()), + ) + op.execute( + at.insert().values( + id=1, + name="Cash", + balance_sheet=True, + debit=True, + cash_flow_classification="Cash", + order=10000, + show_in_list=True, + ) + ) + op.execute( + at.insert().values( + id=2, + name="Purchase", + balance_sheet=False, + debit=True, + cash_flow_classification="Operating", + order=20000, + show_in_list=True, + ) + ) + op.execute( + at.insert().values( + id=3, + name="Sale", + balance_sheet=False, + debit=False, + cash_flow_classification="Operating", + order=10000, + show_in_list=True, + ) + ) + op.execute( + at.insert().values( + id=4, + name="Assets", + balance_sheet=True, + debit=True, + cash_flow_classification="Investing", + order=20000, + show_in_list=True, + ) + ) + op.execute( + at.insert().values( + id=5, + name="Capital", + balance_sheet=True, + debit=False, + cash_flow_classification="Financing", + order=70000, + show_in_list=True, + ) + ) + op.execute( + at.insert().values( + id=6, + name="Debtors", + balance_sheet=True, + debit=True, + cash_flow_classification="Operating", + order=30000, + show_in_list=True, + ) + ) + op.execute( + at.insert().values( + id=7, + name="Expenses", + balance_sheet=False, + debit=True, + cash_flow_classification="Operating", + order=40000, + show_in_list=True, + ) + ) + op.execute( + at.insert().values( + id=9, + name="Creditors", + balance_sheet=True, + debit=False, + cash_flow_classification="Operating", + order=60000, + show_in_list=True, + ) + ) + op.execute( + at.insert().values( + id=10, + name="Salary", + balance_sheet=True, + debit=True, + cash_flow_classification="Operating", + order=40000, + show_in_list=False, + ) + ) + op.execute( + at.insert().values( + id=11, + name="Liabilities", + balance_sheet=True, + debit=False, + cash_flow_classification="Operating", + order=50000, + show_in_list=True, + ) + ) + op.execute( + at.insert().values( + id=12, + name="Revenue", + balance_sheet=False, + debit=False, + cash_flow_classification="Operating", + order=30000, + show_in_list=True, + ) + ) + op.execute( + at.insert().values( + id=13, + name="Tax", + balance_sheet=True, + debit=False, + cash_flow_classification="Operating", + order=80000, + show_in_list=True, + ) + ) + op.create_foreign_key(op.f("fk_accounts_type_account_types"), "accounts", "account_types", ["type"], ["id"]) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/brewman/alembic/versions/d6f96b7b16c6_closing_stock.py b/brewman/alembic/versions/d6f96b7b16c6_closing_stock.py new file mode 100644 index 00000000..71164523 --- /dev/null +++ b/brewman/alembic/versions/d6f96b7b16c6_closing_stock.py @@ -0,0 +1,45 @@ +"""closing stock + +Revision ID: d6f96b7b16c6 +Revises: c09ff00ec84a +Create Date: 2021-10-12 21:59:28.473737 + +""" +import sqlalchemy as sa + +from alembic import op +from sqlalchemy.dialects import postgresql + + +# revision identifiers, used by Alembic. +revision = "d6f96b7b16c6" +down_revision = "0670868fe171" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "closing_stocks", + sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("date", sa.Date(), nullable=False), + sa.Column("cost_centre_id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("sku_id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("quantity", sa.Numeric(precision=15, scale=2), nullable=False), + sa.ForeignKeyConstraint( + ["cost_centre_id"], ["cost_centres.id"], name=op.f("fk_closing_stocks_cost_centre_id_cost_centres") + ), + sa.ForeignKeyConstraint( + ["sku_id"], ["stock_keeping_units.id"], name=op.f("fk_closing_stocks_sku_id_stock_keeping_units") + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_closing_stocks")), + sa.UniqueConstraint("date", "cost_centre_id", "sku_id", name=op.f("uq_closing_stocks_date")), + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("closing_stocks") + # ### end Alembic commands ### diff --git a/brewman/brewman/core/security.py b/brewman/brewman/core/security.py index 0fb8579d..648b0bd0 100644 --- a/brewman/brewman/core/security.py +++ b/brewman/brewman/core/security.py @@ -1,7 +1,7 @@ import uuid from datetime import datetime, timedelta -from typing import List, Optional +from typing import List, Optional, Tuple from fastapi import Depends, HTTPException, Security, status from fastapi.security import OAuth2PasswordBearer, SecurityScopes @@ -29,7 +29,7 @@ class Token(BaseModel): class TokenData(BaseModel): - username: str = None + username: str scopes: List[str] = [] @@ -65,7 +65,7 @@ def authenticate_user(username: str, password: str, db: Session) -> Optional[Use return user -def client_allowed(user: UserModel, client_id: int, otp: Optional[int] = None, db: Session = None) -> (bool, Client): +def client_allowed(user: UserModel, client_id: int, otp: Optional[int], db: Session) -> Tuple[bool, Client]: client: Client = ( db.execute(select(Client).where(Client.code == client_id)).scalar_one_or_none() if client_id else None ) diff --git a/brewman/brewman/db/base.py b/brewman/brewman/db/base.py index 39f68ca2..5763db65 100644 --- a/brewman/brewman/db/base.py +++ b/brewman/brewman/db/base.py @@ -2,9 +2,11 @@ # imported by Alembic from ..models.account import Account # noqa: F401 from ..models.account_base import AccountBase # noqa: F401 +from ..models.account_type import AccountType # noqa: F401 from ..models.attendance import Attendance # noqa: F401 from ..models.batch import Batch # noqa: F401 from ..models.client import Client # noqa: F401 +from ..models.closing_stock import ClosingStock # noqa: F401 from ..models.cost_centre import CostCentre # noqa: F401 from ..models.db_image import DbImage # noqa: F401 from ..models.db_setting import DbSetting # noqa: F401 @@ -28,4 +30,5 @@ from ..models.stock_keeping_unit import StockKeepingUnit # noqa: F401 from ..models.user import User # noqa: F401 from ..models.user_role import user_role # noqa: F401 from ..models.voucher import Voucher # noqa: F401 +from ..models.voucher_type import VoucherType # noqa: F401 from .base_class import Base # noqa: F401 diff --git a/brewman/brewman/db/init_db.py b/brewman/brewman/db/init_db.py deleted file mode 100644 index ff61dcd0..00000000 --- a/brewman/brewman/db/init_db.py +++ /dev/null @@ -1,25 +0,0 @@ -from app import crud, schemas -from brewman.core.config import settings -from brewman.db import base # noqa: F401 -from sqlalchemy.orm import Session - - -# make sure all SQL Alchemy models are imported (app.db.base) before initializing DB -# otherwise, SQL Alchemy might fail to initialize relationships properly -# for more details: https://github.com/tiangolo/full-stack-fastapi-postgresql/issues/28 - - -def init_db(db: Session) -> None: - # Tables should be created with Alembic migrations - # But if you don't want to use migrations, create - # the tables un-commenting the next line - # Base.metadata.create_all(bind=engine) - - user = crud.user.get_by_email(db, email=settings.FIRST_SUPERUSER) - if not user: - user_in = schemas.UserCreate( - email=settings.FIRST_SUPERUSER, - password=settings.FIRST_SUPERUSER_PASSWORD, - is_superuser=True, - ) - user = crud.user.create(db, obj_in=user_in) # noqa: F841 diff --git a/brewman/brewman/models/account_base.py b/brewman/brewman/models/account_base.py index 2da8a8d6..d3cdf68a 100644 --- a/brewman/brewman/models/account_base.py +++ b/brewman/brewman/models/account_base.py @@ -1,10 +1,11 @@ import uuid +from typing import Optional + from sqlalchemy import Boolean, Column, ForeignKey, Integer, Unicode, func, select from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Session, relationship -from .account_type import AccountType from .meta import Base @@ -14,7 +15,7 @@ class AccountBase(Base): id = Column("id", UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) code = Column("code", Integer, nullable=False) name = Column("name", Unicode(255), unique=True, nullable=False) - type = Column("type", Integer, nullable=False) + type_id = Column("type", Integer, ForeignKey("account_types.id"), nullable=False) account_type = Column("account_type", Unicode(50), nullable=False) is_starred = Column("is_starred", Boolean, nullable=False) is_active = Column("is_active", Boolean, nullable=False) @@ -29,6 +30,8 @@ class AccountBase(Base): __mapper_args__ = {"polymorphic_on": account_type} + type_ = relationship("AccountType") + journals = relationship("Journal", back_populates="account") cost_centre = relationship("CostCentre", back_populates="accounts") products = relationship("Product", back_populates="account") @@ -39,15 +42,11 @@ class AccountBase(Base): def __name__(self): return self.name - @property - def type_object(self): - return AccountType.by_id(self.type) - def __init__( self, code=None, name=None, - type_=None, + type_id=None, is_starred=None, is_active=None, is_reconcilable=False, @@ -57,7 +56,7 @@ class AccountBase(Base): ): self.code = code self.name = name - self.type = type_ + self.type_id = type_id self.is_starred = is_starred self.is_active = is_active self.is_reconcilable = is_reconcilable @@ -66,12 +65,12 @@ class AccountBase(Base): self.is_fixture = is_fixture @classmethod - def query(cls, q: str, type_: int, reconcilable: bool = None, active: bool = None, db: Session = None): + def query(cls, q: str, type_: int, reconcilable: Optional[bool], active: Optional[bool], db: Session): query_ = select(cls) if type_ is not None: if not isinstance(type_, int): type_ = int(type_) - query_ = query_.where(cls.type == type_) + query_ = query_.where(cls.type_id == type_) if reconcilable is not None: query_ = query_.where(cls.is_reconcilable == reconcilable) if active is not None: @@ -83,7 +82,7 @@ class AccountBase(Base): def create(self, db: Session): self.code = db.execute( - select(func.coalesce(func.max(AccountBase.code), 0) + 1).where(AccountBase.type == self.type) + select(func.coalesce(func.max(AccountBase.code), 0) + 1).where(AccountBase.type_id == self.type_id) ).scalar_one() db.add(self) return self @@ -100,7 +99,7 @@ class AccountBase(Base): @classmethod def get_code(cls, type_: int, db: Session): return db.execute( - select(func.coalesce(func.max(AccountBase.code), 0) + 1).where(AccountBase.type == type_) + select(func.coalesce(func.max(AccountBase.code), 0) + 1).where(AccountBase.type_id == type_) ).scalar_one() @classmethod diff --git a/brewman/brewman/models/account_type.py b/brewman/brewman/models/account_type.py index d0690432..6bbcf6b1 100644 --- a/brewman/brewman/models/account_type.py +++ b/brewman/brewman/models/account_type.py @@ -1,48 +1,15 @@ -class AccountType: - def __init__( - self, - id_, - name, - balance_sheet=None, - debit=None, - cash_flow_classification=None, - order=None, - show_in_list=None, - ): - self.id = id_ - self.name = name - self.balance_sheet = balance_sheet - self.debit = debit - self.cash_flow_classification = cash_flow_classification - # Cash flow Classifications are: - # Cash - # Operating - # Investing - # Financing - self.order = order - self.show_in_list = show_in_list +from sqlalchemy import Boolean, Column, Integer, Unicode - @classmethod - def list(cls): - return [ - AccountType(1, "Cash", True, True, "Cash", 10000, True), - AccountType(2, "Purchase", False, True, "Operating", 20000, True), - AccountType(3, "Sale", False, False, "Operating", 10000, True), - AccountType(4, "Assets", True, True, "Investing", 20000, True), - AccountType(5, "Capital", True, False, "Financing", 70000, True), - AccountType(6, "Debtors", True, True, "Operating", 30000, True), - AccountType(7, "Expenses", False, True, "Operating", 40000, True), - AccountType(9, "Creditors", True, False, "Operating", 60000, True), - AccountType(10, "Salary", True, True, "Operating", 40000, False), - AccountType(11, "Liabilities", True, False, "Operating", 50000, True), - AccountType(12, "Revenue", False, False, "Operating", 30000, True), - AccountType(13, "Tax", True, False, "Operating", 80000, True), - ] +from .meta import Base - @classmethod - def by_name(cls, name): - return next(i for i in cls.list() if i.name == name) - @classmethod - def by_id(cls, id_): - return next(i for i in cls.list() if i.id == id_) +class AccountType(Base): + __tablename__ = "account_types" + + id = Column("id", Integer, primary_key=True) + name = Column("name", Unicode(255), unique=True, nullable=False) + balance_sheet = Column("balance_sheet", Boolean, nullable=False) + debit = Column("debit", Boolean, nullable=False) + cash_flow_classification = Column("cash_flow_classification", Unicode(255), nullable=False) + order = Column("order", Integer, nullable=False) + show_in_list = Column("show_in_list", Boolean, nullable=False) diff --git a/brewman/brewman/models/closing_stock.py b/brewman/brewman/models/closing_stock.py new file mode 100644 index 00000000..64522da1 --- /dev/null +++ b/brewman/brewman/models/closing_stock.py @@ -0,0 +1,43 @@ +import uuid + +from sqlalchemy import Column, Date, ForeignKey, Numeric, UniqueConstraint +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import relationship + +from .meta import Base + + +class ClosingStock(Base): + __tablename__ = "closing_stocks" + __table_args__ = (UniqueConstraint("date", "cost_centre_id", "sku_id"),) + id = Column("id", UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + date = Column("date", Date, nullable=False) + cost_centre_id = Column( + "cost_centre_id", + UUID(as_uuid=True), + ForeignKey("cost_centres.id"), + nullable=False, + ) + sku_id = Column("sku_id", UUID(as_uuid=True), ForeignKey("stock_keeping_units.id"), nullable=False) + quantity = Column("quantity", Numeric(precision=15, scale=2), nullable=False) + + cost_centre = relationship("CostCentre") + sku = relationship("StockKeepingUnit") + + def __init__( + self, + date_=None, + cost_centre_id=None, + sku_id=None, + quantity=None, + sku=None, + id_=None, + ): + self.date = date_ + self.cost_centre_id = cost_centre_id + self.sku_id = sku_id + self.quantity = quantity + if sku is not None: + self.sku = sku + self.sku_id = sku.id + self.id = id_ diff --git a/brewman/brewman/models/employee.py b/brewman/brewman/models/employee.py index 4ba7ce3b..29a44428 100644 --- a/brewman/brewman/models/employee.py +++ b/brewman/brewman/models/employee.py @@ -40,7 +40,7 @@ class Employee(AccountBase): super().__init__( code=code, name=name, - type_=10, + type_id=10, is_starred=is_starred, is_active=is_active, is_reconcilable=False, @@ -49,7 +49,7 @@ class Employee(AccountBase): def create(self, db: Session): self.code = db.execute( - select(func.coalesce(func.max(AccountBase.code), 0) + 1).filter(AccountBase.type == self.type) + select(func.coalesce(func.max(AccountBase.code), 0) + 1).filter(AccountBase.type_id == self.type_id) ).scalar_one() self.name += f" ({str(self.code)})" db.add(self) diff --git a/brewman/brewman/models/inventory.py b/brewman/brewman/models/inventory.py index a10000a0..97f96b53 100644 --- a/brewman/brewman/models/inventory.py +++ b/brewman/brewman/models/inventory.py @@ -54,6 +54,6 @@ class Inventory(Base): def amount(self): return self.quantity * self.rate * (1 + self.tax) * (1 - self.discount) - @amount.expression + @amount.expression # type: ignore[no-redef] def amount(cls): return cls.quantity * cls.rate * (1 + cls.tax) * (1 - cls.discount) diff --git a/brewman/brewman/models/voucher.py b/brewman/brewman/models/voucher.py index 685db9bd..9b892ccb 100644 --- a/brewman/brewman/models/voucher.py +++ b/brewman/brewman/models/voucher.py @@ -2,11 +2,12 @@ import uuid from datetime import datetime -from sqlalchemy import Boolean, Column, Date, DateTime, ForeignKey, Integer, Unicode +from sqlalchemy import Boolean, Column, Date, DateTime, Enum, ForeignKey, Unicode from sqlalchemy.dialects.postgresql import UUID -from sqlalchemy.orm import relationship, synonym +from sqlalchemy.orm import relationship from .meta import Base +from .voucher_type import VoucherType class Voucher(Base): @@ -20,7 +21,7 @@ class Voucher(Base): is_starred = Column("is_starred", Boolean, nullable=False) creation_date = Column("creation_date", DateTime(), nullable=False) last_edit_date = Column("last_edit_date", DateTime(), nullable=False) - _type = Column("voucher_type", Integer, nullable=False) + voucher_type = Column("voucher_type", Enum(VoucherType), nullable=False) user_id = Column("user_id", UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) posted = Column("is_posted", Boolean, nullable=False) poster_id = Column("poster_id", UUID(as_uuid=True), ForeignKey("users.id")) @@ -54,21 +55,6 @@ class Voucher(Base): cascade_backrefs=False, ) - def _get_type(self): - return self._type - # for item in VoucherType.list(): - # if self._type == item.id: - # return item - - def _set_type(self, value): - if type(value) == int: - self._type = value - else: - self._type = value.id - - type = property(_get_type, _set_type) - type = synonym("_type", descriptor=type) - def __init__( self, date=None, @@ -79,7 +65,7 @@ class Voucher(Base): posted=False, creation_date=None, last_edit_date=None, - type_=None, + voucher_type=None, user_id=None, poster_id=None, ): @@ -91,6 +77,6 @@ class Voucher(Base): self.posted = posted self.creation_date = creation_date or datetime.utcnow() self.last_edit_date = last_edit_date or datetime.utcnow() - self.type = type_ + self.voucher_type = voucher_type self.user_id = user_id self.poster_id = poster_id diff --git a/brewman/brewman/models/voucher_type.py b/brewman/brewman/models/voucher_type.py index a24141e4..9d7fa46f 100644 --- a/brewman/brewman/models/voucher_type.py +++ b/brewman/brewman/models/voucher_type.py @@ -1,37 +1,17 @@ -class VoucherType: - def __init__(self, id_, name): - self.id = id_ - self.name = name +import enum - @classmethod - def list(cls): - list_ = [ - VoucherType(1, "Journal"), - VoucherType(2, "Purchase"), - VoucherType(3, "Issue"), - VoucherType(4, "Payment"), - VoucherType(5, "Receipt"), - VoucherType(6, "Purchase Return"), - VoucherType(7, "Opening Accounts"), - VoucherType(8, "Opening Batches"), - VoucherType(9, "Verification"), - VoucherType(10, "Opening Balance"), - VoucherType(11, "Closing Balance"), - VoucherType(12, "Employee Benefit"), - VoucherType(13, "Incentive"), - ] - return list_ - @classmethod - def by_name(cls, name): - list_ = cls.list() - for item in list_: - if item.name == name: - return item - - @classmethod - def by_id(cls, id_): - list_ = cls.list() - for item in list_: - if item.id == id_: - return item +class VoucherType(enum.IntEnum): + JOURNAL = 1 + PURCHASE = 2 + ISSUE = 3 + PAYMENT = 4 + RECEIPT = 5 + PURCHASE_RETURN = 6 + OPENING_ACCOUNTS = 7 + OPENING_BATCHES = 8 + CLOSING_STOCK = 9 + OPENING_BALANCE = 10 + CLOSING_BALANCE = 11 + EMPLOYEE_BENEFIT = 12 + INCENTIVE = 13 diff --git a/brewman/brewman/routers/__init__.py b/brewman/brewman/routers/__init__.py index 86bb4152..50eeece0 100644 --- a/brewman/brewman/routers/__init__.py +++ b/brewman/brewman/routers/__init__.py @@ -3,7 +3,7 @@ import uuid from datetime import date, timedelta from decimal import Decimal -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Tuple from sqlalchemy import or_, select from sqlalchemy.orm import Session @@ -21,8 +21,8 @@ from ..schemas.settings import ( def get_lock_info( - voucher_dates: List[date], voucher_type: VoucherType, account_types: List[AccountType], db: Session -) -> (bool, str): + voucher_dates: List[date], voucher_type: VoucherType, account_types: List[int], db: Session +) -> Tuple[bool, str]: date_ = date.today() start: Optional[date] finish: Optional[date] @@ -72,11 +72,12 @@ def get_lock_info( if sum(1 for v in voucher_dates if v > finish): return False, f"Vouchers after {finish.strftime('%d-%b-%Y')} have been locked." if li.finish.date_ is not None: - finish = li.start.date_ + finish = li.finish.date_ if sum(1 for v in voucher_dates if v > finish): return False, f"Vouchers after {finish.strftime('%d-%b-%Y')} have been locked." return True, "Voucher allowed" + return False, "Default Fallthrough" def to_uuid(value): diff --git a/brewman/brewman/routers/account.py b/brewman/brewman/routers/account.py index 652bfce1..1ac97335 100644 --- a/brewman/brewman/routers/account.py +++ b/brewman/brewman/routers/account.py @@ -35,7 +35,7 @@ def save( with SessionFuture() as db: item = Account( name=data.name, - type_=data.type, + type_id=data.type_, is_starred=data.is_starred, is_active=data.is_active, is_reconcilable=data.is_reconcilable, @@ -64,9 +64,9 @@ def update_route( status_code=status.HTTP_423_LOCKED, detail=f"{item.name} is a fixture and cannot be edited or deleted.", ) - if not item.type == data.type: - item.code = Account.get_code(data.type, db) - item.type = data.type + if not item.type_id == data.type_: + item.code = Account.get_code(data.type_, db) + item.type_id = data.type_ item.name = data.name item.is_active = data.is_active item.is_reconcilable = data.is_reconcilable @@ -92,7 +92,7 @@ def delete_route( if can_delete: delete_with_data(account, db) db.commit() - return account_blank() + return account_blank(db) else: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -104,7 +104,8 @@ def delete_route( def show_blank( user: UserToken = Security(get_user, scopes=["accounts"]), ) -> schemas.AccountBlank: - return account_blank() + with SessionFuture() as db: + return account_blank(db) @router.get("/list", response_model=List[schemas.Account]) @@ -112,7 +113,9 @@ async def show_list(user: UserToken = Depends(get_user)) -> List[schemas.Account with SessionFuture() as db: return [ account_info(item) - for item in db.execute(select(Account).order_by(Account.type).order_by(Account.name).order_by(Account.code)) + for item in db.execute( + select(Account).order_by(Account.type_id).order_by(Account.name).order_by(Account.code) + ) .scalars() .all() ] @@ -160,14 +163,14 @@ def show_id( def balance(id_: uuid.UUID, date, db: Session) -> Decimal: account = db.execute(select(AccountBase).where(AccountBase.id == id_)).scalar_one() - if not account.type_object.balance_sheet: - return 0 + if not account.type_.balance_sheet: + return Decimal(0) bal = select(func.sum(Journal.amount * Journal.debit)).join(Journal.voucher) if date is not None: bal = bal.where(Voucher.date <= date) - bal = bal.where(Voucher.type != VoucherType.by_name("Issue").id).where(Journal.account_id == id_) + bal = bal.where(Voucher.voucher_type != VoucherType.ISSUE).where(Journal.account_id == id_) result = db.execute(bal).scalar() return 0 if result is None else result @@ -177,7 +180,7 @@ def account_info(item: Account) -> schemas.Account: id=item.id, code=item.code, name=item.name, - type=item.type, + type=item.type_id, isActive=item.is_active, isReconcilable=item.is_reconcilable, isStarred=item.is_starred, @@ -189,10 +192,10 @@ def account_info(item: Account) -> schemas.Account: ) -def account_blank() -> schemas.AccountBlank: +def account_blank(db: Session) -> schemas.AccountBlank: return schemas.AccountBlank( name="", - type=AccountType.by_name("Creditors").id, + type=db.execute(select(AccountType.id).where(AccountType.name == "Creditors")).scalar_one(), isActive=True, isReconcilable=False, isStarred=False, @@ -237,9 +240,9 @@ def delete_with_data(account: Account, db: Session) -> None: sus_jnl.amount = abs(amount) sus_jnl.debit = -1 if amount < 0 else 1 voucher.narration += f"\nDeleted \u20B9{acc_jnl.amount * acc_jnl.debit:,.2f} of {account.name}" - if voucher.type in ( - VoucherType.by_name("Payment").id, - VoucherType.by_name("Receipt").id, + if voucher.voucher_type in ( + VoucherType.PAYMENT, + VoucherType.RECEIPT, ): - voucher.type = VoucherType.by_name("Journal") + voucher.voucher_type = VoucherType.JOURNAL db.delete(account) diff --git a/brewman/brewman/routers/account_types.py b/brewman/brewman/routers/account_types.py index 37bb5245..63969552 100644 --- a/brewman/brewman/routers/account_types.py +++ b/brewman/brewman/routers/account_types.py @@ -3,8 +3,10 @@ from typing import List import brewman.schemas.account_type as schemas from fastapi import APIRouter, Depends +from sqlalchemy import select from ..core.security import get_current_active_user as get_user +from ..db.session import SessionFuture from ..models.account_type import AccountType from ..schemas.user import UserToken @@ -14,4 +16,7 @@ router = APIRouter() @router.get("", response_model=List[schemas.AccountType]) def account_type_list(user: UserToken = Depends(get_user)) -> List[schemas.AccountType]: - return [schemas.AccountType(id=item.id, name=item.name) for item in AccountType.list()] + with SessionFuture() as db: + return [ + schemas.AccountType(id=item.id, name=item.name) for item in db.execute(select(AccountType)).scalars().all() + ] diff --git a/brewman/brewman/routers/attendance_report.py b/brewman/brewman/routers/attendance_report.py index 8f570469..7bf735dc 100644 --- a/brewman/brewman/routers/attendance_report.py +++ b/brewman/brewman/routers/attendance_report.py @@ -20,8 +20,8 @@ router = APIRouter() @router.get("") def get_report( - s: str = None, - f: str = None, + s: str, + f: str, # user: UserToken = Security(get_user, scopes=["attendance"]) ## removed as jwt headers are a pain in the ass ): try: diff --git a/brewman/brewman/routers/batch_integrity.py b/brewman/brewman/routers/batch_integrity.py index bfad5791..122e854e 100644 --- a/brewman/brewman/routers/batch_integrity.py +++ b/brewman/brewman/routers/batch_integrity.py @@ -47,12 +47,12 @@ def negative_batches(db: Session) -> List[schemas.BatchIntegrity]: .join(Inventory.voucher) .join(Voucher.journals) .where( - Voucher.type.in_( + Voucher.voucher_type.in_( [ - VoucherType.by_name("Purchase").id, - VoucherType.by_name("Purchase Return").id, - VoucherType.by_name("Issue").id, - VoucherType.by_name("Opening Batches").id, + VoucherType.PURCHASE, + VoucherType.PURCHASE_RETURN, + VoucherType.ISSUE, + VoucherType.OPENING_BATCHES, ] ), Journal.cost_centre_id == CostCentre.cost_centre_purchase(), @@ -82,17 +82,17 @@ def negative_batches(db: Session) -> List[schemas.BatchIntegrity]: def batch_details(batch_id: uuid.UUID, db: Session) -> List[schemas.BatchIntegrityItem]: list_ = db.execute( - select(Voucher.id, Voucher.date, Voucher.type, Inventory.quantity, Inventory.rate) + select(Voucher.id, Voucher.date, Voucher.voucher_type, Inventory.quantity, Inventory.rate) .join(Inventory.voucher) .join(Voucher.journals) .where( Inventory.batch_id == batch_id, - Voucher.type.in_( + Voucher.voucher_type.in_( [ - VoucherType.by_name("Purchase").id, - VoucherType.by_name("Purchase Return").id, - VoucherType.by_name("Issue").id, - VoucherType.by_name("Opening Batches").id, + VoucherType.PURCHASE, + VoucherType.PURCHASE_RETURN, + VoucherType.ISSUE, + VoucherType.OPENING_BATCHES, ] ), Journal.cost_centre_id == CostCentre.cost_centre_purchase(), @@ -101,8 +101,8 @@ def batch_details(batch_id: uuid.UUID, db: Session) -> List[schemas.BatchIntegri return [ schemas.BatchIntegrityItem( date=date_, - type=VoucherType.by_id(type_).name, - url=["/", VoucherType.by_id(type_).name.replace(" ", "-").lower(), str(id_)], + type=type_.name.replace("_", " ").title(), + url=["/", type_.name.replace("_", "-").lower(), str(id_)], quantity=quantity, price=rate, ) @@ -142,12 +142,12 @@ def batch_dates(db: Session) -> List[schemas.BatchIntegrity]: details=[ schemas.BatchIntegrityItem( date=inv.voucher.date, - type=VoucherType.by_id(inv.voucher.type).name, + type=inv.voucher.voucher_type.name.replace("_", " ").title(), quantity=inv.quantity, price=inv.rate, url=[ "/", - VoucherType.by_id(inv.voucher.type).name.replace(" ", "-").lower(), + inv.voucher.voucher_type.name.replace("_", "-").lower(), str(inv.voucher.id), ], ) diff --git a/brewman/brewman/routers/credit_salary.py b/brewman/brewman/routers/credit_salary.py index 5fe4efa6..f6d8867c 100644 --- a/brewman/brewman/routers/credit_salary.py +++ b/brewman/brewman/routers/credit_salary.py @@ -34,7 +34,7 @@ def credit_salary( date=finish_date, narration="Auto Generated Salary Entry", user_id=user.id_, - type_=VoucherType.by_name("Journal"), + voucher_type=VoucherType.JOURNAL, posted=True, poster_id=user.id_, ) diff --git a/brewman/brewman/routers/employee.py b/brewman/brewman/routers/employee.py index 50aaf960..2a99b636 100644 --- a/brewman/brewman/routers/employee.py +++ b/brewman/brewman/routers/employee.py @@ -135,7 +135,7 @@ async def show_term( ): list_ = [] with SessionFuture() as db: - for index, item in enumerate(Employee.query(q=q, type_=10, db=db)): + for index, item in enumerate(Employee.query(q=q, type_=10, reconcilable=None, active=None, db=db)): list_.append( { "id": item.id, @@ -235,11 +235,11 @@ def delete_with_data(employee: Employee, db: Session): sus_jnl.amount = abs(amount) sus_jnl.debit = -1 if amount < 0 else 1 voucher.narration += f"\nDeleted \u20B9 {acc_jnl.amount * acc_jnl.debit:,.2f} of {employee.name}" - if voucher.type in ( - VoucherType.by_name("Payment").id, - VoucherType.by_name("Receipt").id, + if voucher.voucher_type in ( + VoucherType.PAYMENT, + VoucherType.RECEIPT, ): - voucher.type = VoucherType.by_name("Journal") + voucher.voucher_type = VoucherType.JOURNAL for fingerprint in employee.fingerprints: db.delete(fingerprint) for attendance in employee.attendances: diff --git a/brewman/brewman/routers/employee_benefit.py b/brewman/brewman/routers/employee_benefit.py index f3345694..fe0d8baa 100644 --- a/brewman/brewman/routers/employee_benefit.py +++ b/brewman/brewman/routers/employee_benefit.py @@ -63,14 +63,14 @@ def save_route( def save(data: schema_in.EmployeeBenefitIn, date_: date, user: UserToken, db: Session) -> Voucher: account_types = ( db.execute( - select(distinct(AccountBase.type)).where( + select(distinct(AccountBase.type_id)).where( AccountBase.id.in_([dj.employee.id_ for dj in data.employee_benefits]) ) ) .scalars() .all() ) - allowed, message = get_lock_info([data.date_], VoucherType.by_name(data.type_).id, account_types, db) + allowed, message = get_lock_info([data.date_], VoucherType[data.type_.replace(" ", "_").upper()], account_types, db) if not allowed: raise HTTPException( status_code=status.HTTP_423_LOCKED, @@ -81,7 +81,7 @@ def save(data: schema_in.EmployeeBenefitIn, date_: date, user: UserToken, db: Se narration=data.narration, is_starred=data.is_starred, user_id=user.id_, - type_=VoucherType.by_name(data.type_), + voucher_type=VoucherType[data.type_.replace(" ", "_").upper()], ) db.add(voucher) return voucher @@ -182,7 +182,7 @@ def update_voucher(id_: uuid.UUID, data: schema_in.EmployeeBenefitIn, user: User voucher: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one() account_types = ( db.execute( - select(distinct(AccountBase.type)).where( + select(distinct(AccountBase.type_id)).where( AccountBase.id.in_( [dj.employee.id_ for dj in data.employee_benefits] + [vj.account_id for vj in voucher.journals] ) @@ -191,7 +191,7 @@ def update_voucher(id_: uuid.UUID, data: schema_in.EmployeeBenefitIn, user: User .scalars() .all() ) - allowed, message = get_lock_info([voucher.date, data.date_], voucher.type, account_types, db) + allowed, message = get_lock_info([voucher.date, data.date_], voucher.voucher_type, account_types, db) if not allowed: raise HTTPException( status_code=status.HTTP_423_LOCKED, @@ -255,7 +255,7 @@ def show_blank( request: Request, user: UserToken = Security(get_user, scopes=["employee-benefit"]), ): - additional_info = {"date": get_date(request.session), "type": "Employee Benefit"} + additional_info = {"date": get_date(request.session), "type": VoucherType.EMPLOYEE_BENEFIT} with SessionFuture() as db: return blank_voucher(additional_info, db) diff --git a/brewman/brewman/routers/incentive.py b/brewman/brewman/routers/incentive.py index 531ccf7a..d5d142d6 100644 --- a/brewman/brewman/routers/incentive.py +++ b/brewman/brewman/routers/incentive.py @@ -63,12 +63,12 @@ def save_route( def save(data: schema_in.IncentiveIn, user: UserToken, db: Session) -> Voucher: account_types = ( db.execute( - select(distinct(AccountBase.type)).where(AccountBase.id.in_([dj.employee_id for dj in data.incentives])) + select(distinct(AccountBase.type_id)).where(AccountBase.id.in_([dj.employee_id for dj in data.incentives])) ) .scalars() .all() ) - allowed, message = get_lock_info([data.date_], VoucherType.by_name(data.type_).id, account_types, db) + allowed, message = get_lock_info([data.date_], VoucherType[data.type_.replace(" ", "_").upper()], account_types, db) if not allowed: raise HTTPException( status_code=status.HTTP_423_LOCKED, @@ -79,7 +79,7 @@ def save(data: schema_in.IncentiveIn, user: UserToken, db: Session) -> Voucher: narration=data.narration, is_starred=data.is_starred, user_id=user.id_, - type_=VoucherType.by_name(data.type_), + voucher_type=VoucherType[data.type_.replace(" ", "_").upper()], ) db.add(voucher) return voucher @@ -143,7 +143,7 @@ def update_voucher(id_: uuid.UUID, data: schema_in.IncentiveIn, user: UserToken, voucher: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one() account_types = ( db.execute( - select(distinct(AccountBase.type)).where( + select(distinct(AccountBase.type_id)).where( AccountBase.id.in_( [dj.employee_id for dj in data.incentives] + [vj.account_id for vj in voucher.journals] ) @@ -152,7 +152,7 @@ def update_voucher(id_: uuid.UUID, data: schema_in.IncentiveIn, user: UserToken, .scalars() .all() ) - allowed, message = get_lock_info([voucher.date, data.date_], voucher.type, account_types, db) + allowed, message = get_lock_info([voucher.date, data.date_], voucher.voucher_type, account_types, db) if not allowed: raise HTTPException( status_code=status.HTTP_423_LOCKED, @@ -208,7 +208,7 @@ def show_blank( d: str = None, user: UserToken = Security(get_user, scopes=["incentive"]), ): - additional_info = {"date": d or get_date(request.session), "type": "Incentive"} + additional_info = {"date": d or get_date(request.session), "type": VoucherType.INCENTIVE} with SessionFuture() as db: return blank_voucher(additional_info, db) @@ -265,12 +265,12 @@ def balance(date_: date, voucher_id: Optional[uuid.UUID], db: Session): .join(Journal.voucher) .where( Journal.account_id == Account.incentive_id(), - Voucher.type != VoucherType.by_name("Issue").id, + Voucher.voucher_type != VoucherType.ISSUE, or_( Voucher.date <= date_, and_( Voucher.date == date_, - Voucher.type != VoucherType.by_name("Incentive").id, + Voucher.voucher_type != VoucherType.INCENTIVE, ), ), ) diff --git a/brewman/brewman/routers/issue.py b/brewman/brewman/routers/issue.py index 98f18def..fc18efa1 100644 --- a/brewman/brewman/routers/issue.py +++ b/brewman/brewman/routers/issue.py @@ -74,9 +74,9 @@ def save(data: schema_in.IssueIn, user: UserToken, db: Session) -> (Voucher, Opt .where(StockKeepingUnit.id.in_([i.batch.sku.id_ for i in data.inventories])) ) account_types = ( - db.execute(select(distinct(AccountBase.type)).where(AccountBase.id.in_(product_accounts))).scalars().all() + db.execute(select(distinct(AccountBase.type_id)).where(AccountBase.id.in_(product_accounts))).scalars().all() ) - allowed, message = get_lock_info([data.date_], VoucherType.by_name(data.type_).id, account_types, db) + allowed, message = get_lock_info([data.date_], VoucherType[data.type_.replace(" ", "_").upper()], account_types, db) if not allowed: raise HTTPException( status_code=status.HTTP_423_LOCKED, @@ -87,7 +87,7 @@ def save(data: schema_in.IssueIn, user: UserToken, db: Session) -> (Voucher, Opt narration=data.narration, is_starred=data.is_starred, user_id=user.id_, - type_=VoucherType.by_name(data.type_), + voucher_type=VoucherType[data.type_.replace(" ", "_").upper()], ) db.add(voucher) if data.source.id_ == data.destination.id_: @@ -207,7 +207,7 @@ def update_voucher(id_: uuid.UUID, data: schema_in.IssueIn, user: UserToken, db: ) account_types = ( db.execute( - select(distinct(AccountBase.type)).where( + select(distinct(AccountBase.type_id)).where( or_( AccountBase.id.in_([vj.account_id for vj in voucher.journals]), AccountBase.id.in_(product_accounts), @@ -217,7 +217,7 @@ def update_voucher(id_: uuid.UUID, data: schema_in.IssueIn, user: UserToken, db: .scalars() .all() ) - allowed, message = get_lock_info([voucher.date, data.date_], voucher.type, account_types, db) + allowed, message = get_lock_info([voucher.date, data.date_], voucher.voucher_type, account_types, db) if not allowed: raise HTTPException( status_code=status.HTTP_423_LOCKED, @@ -384,7 +384,7 @@ def show_blank( user: UserToken = Security(get_user, scopes=["issue"]), ): date_ = date or get_date(request.session) - additional_info = {"date": date_, "type": "Issue"} + additional_info = {"date": date_, "type": VoucherType.ISSUE} if source: additional_info["source"] = source if destination: diff --git a/brewman/brewman/routers/issue_grid.py b/brewman/brewman/routers/issue_grid.py index fa4b1d23..2b7e23c1 100644 --- a/brewman/brewman/routers/issue_grid.py +++ b/brewman/brewman/routers/issue_grid.py @@ -30,7 +30,7 @@ def get_grid(date, db: Session): db.execute( select(Voucher) .join(Voucher.journals) - .where(Voucher.date == date, Voucher.type == VoucherType.by_name("Issue").id) + .where(Voucher.date == date, Voucher.voucher_type == VoucherType.ISSUE) .order_by(Voucher.creation_date) ) .unique() diff --git a/brewman/brewman/routers/journal.py b/brewman/brewman/routers/journal.py index 63914421..19d8aebc 100644 --- a/brewman/brewman/routers/journal.py +++ b/brewman/brewman/routers/journal.py @@ -56,12 +56,12 @@ def save_route( def save(data: schema_in.JournalIn, user: UserToken, db: Session) -> Voucher: account_types = ( db.execute( - select(distinct(AccountBase.type)).where(AccountBase.id.in_([dj.account.id_ for dj in data.journals])) + select(distinct(AccountBase.type_id)).where(AccountBase.id.in_([dj.account.id_ for dj in data.journals])) ) .scalars() .all() ) - allowed, message = get_lock_info([data.date_], VoucherType.by_name(data.type_).id, account_types, db) + allowed, message = get_lock_info([data.date_], VoucherType[data.type_.replace(" ", "_").upper()], account_types, db) if not allowed: raise HTTPException( status_code=status.HTTP_423_LOCKED, @@ -72,7 +72,7 @@ def save(data: schema_in.JournalIn, user: UserToken, db: Session) -> Voucher: narration=data.narration, is_starred=data.is_starred, user_id=user.id_, - type_=VoucherType.by_name(data.type_), + voucher_type=VoucherType[data.type_.replace(" ", "_").upper()], ) db.add(voucher) for item in data.journals: @@ -120,7 +120,7 @@ def update_voucher(id_: uuid.UUID, data: schema_in.JournalIn, user: UserToken, d voucher: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one() account_types = ( db.execute( - select(distinct(AccountBase.type)).where( + select(distinct(AccountBase.type_id)).where( AccountBase.id.in_( [dj.account.id_ for dj in data.journals] + [vj.account_id for vj in voucher.journals] ) @@ -129,7 +129,7 @@ def update_voucher(id_: uuid.UUID, data: schema_in.JournalIn, user: UserToken, d .scalars() .all() ) - allowed, message = get_lock_info([voucher.date, data.date_], voucher.type, account_types, db) + allowed, message = get_lock_info([voucher.date, data.date_], voucher.voucher_type, account_types, db) if not allowed: raise HTTPException( status_code=status.HTTP_423_LOCKED, @@ -192,11 +192,11 @@ def show_blank( user: UserToken = Security(get_user, scopes=["journal"]), ): if request.scope.get("path") == "/api/payment": - type_ = "Payment" + type_ = VoucherType.PAYMENT elif request.scope.get("path") == "/api/receipt": - type_ = "Receipt" + type_ = VoucherType.RECEIPT else: - type_ = "Journal" + type_ = VoucherType.JOURNAL additional_info = {"date": get_date(request.session), "type": type_} if a: diff --git a/brewman/brewman/routers/lock_information.py b/brewman/brewman/routers/lock_information.py index 9232351e..66518794 100644 --- a/brewman/brewman/routers/lock_information.py +++ b/brewman/brewman/routers/lock_information.py @@ -78,14 +78,14 @@ def get_info(db: Session) -> List[LockInformation]: voucherTypes=[ VoucherTypesSelected( id=vt["id_"], - name=VoucherType.by_id(vt["id_"]).name, + name=VoucherType(vt["id_"]).name.replace("_", " ").title(), ) for vt in item["voucher_types"] ], accountTypes=[ AccountTypesSelected( id=at["id_"], - name=AccountType.by_id(at["id_"]).name, + name=db.execute(select(AccountType.name).where(AccountType.id == at["id_"])).scalar_one(), ) for at in item["account_types"] ], diff --git a/brewman/brewman/routers/purchase.py b/brewman/brewman/routers/purchase.py index 2f8f3978..414eca1f 100644 --- a/brewman/brewman/routers/purchase.py +++ b/brewman/brewman/routers/purchase.py @@ -2,7 +2,7 @@ import uuid from datetime import date, datetime from decimal import Decimal -from typing import List, Optional +from typing import Dict, List, Optional import brewman.schemas.input as schema_in import brewman.schemas.voucher as output @@ -77,7 +77,7 @@ def save(data: schema_in.PurchaseIn, user: UserToken, db: Session) -> Voucher: ) account_types = ( db.execute( - select(distinct(AccountBase.type)).where( + select(distinct(AccountBase.type_id)).where( or_( AccountBase.id == data.vendor.id_, AccountBase.id.in_(product_accounts), @@ -87,7 +87,7 @@ def save(data: schema_in.PurchaseIn, user: UserToken, db: Session) -> Voucher: .scalars() .all() ) - allowed, message = get_lock_info([data.date_], VoucherType.by_name(data.type_).id, account_types, db) + allowed, message = get_lock_info([data.date_], VoucherType[data.type_.replace(" ", "_").upper()], account_types, db) if not allowed: raise HTTPException( status_code=status.HTTP_423_LOCKED, @@ -98,7 +98,7 @@ def save(data: schema_in.PurchaseIn, user: UserToken, db: Session) -> Voucher: narration=data.narration, is_starred=data.is_starred, user_id=user.id_, - type_=VoucherType.by_name(data.type_), + voucher_type=VoucherType[data.type_.replace(" ", "_").upper()], ) db.add(voucher) return voucher @@ -116,8 +116,8 @@ def save_inventories(voucher: Voucher, vendor_id: uuid.UUID, inventories: List[I detail="Product price does not match the Rate Contract price", ) if rc_price is not None: - item.tax = 0 - item.discount = 0 + item.tax = Decimal(0) + item.discount = Decimal(0) batch = Batch( name=voucher.date, sku=sku, @@ -142,7 +142,7 @@ def save_inventories(voucher: Voucher, vendor_id: uuid.UUID, inventories: List[I def save_journals(voucher: Voucher, ven: schema_in.AccountLink, db: Session): vendor = db.execute(select(AccountBase).where(AccountBase.id == ven.id_)).scalar_one() - journals = {} + journals: Dict[uuid.UUID, Journal] = {} amount = 0 for item in voucher.inventories: account_id, cc_id = db.execute( @@ -208,7 +208,7 @@ def update_voucher(id_: uuid.UUID, data: schema_in.PurchaseIn, user: UserToken, ) account_types = ( db.execute( - select(distinct(AccountBase.type)).where( + select(distinct(AccountBase.type_id)).where( or_( AccountBase.id.in_([data.vendor.id_] + [vj.account_id for vj in voucher.journals]), AccountBase.id.in_(product_accounts), @@ -218,7 +218,7 @@ def update_voucher(id_: uuid.UUID, data: schema_in.PurchaseIn, user: UserToken, .scalars() .all() ) - allowed, message = get_lock_info([voucher.date, data.date_], voucher.type, account_types, db) + allowed, message = get_lock_info([voucher.date, data.date_], voucher.voucher_type, account_types, db) if not allowed: raise HTTPException( status_code=status.HTTP_423_LOCKED, @@ -264,8 +264,8 @@ def update_inventory(voucher: Voucher, vendor_id: uuid.UUID, inventories: List[I detail="Product price does not match the Rate Contract price", ) if rc_price is not None: - new_inventory.tax = 0 - new_inventory.discount = 0 + new_inventory.tax = Decimal(0) + new_inventory.discount = Decimal(0) if new_inventory.quantity < quantity_consumed: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, @@ -301,7 +301,7 @@ def update_inventory(voucher: Voucher, vendor_id: uuid.UUID, inventories: List[I def update_journals(voucher: Voucher, ven: schema_in.AccountLink, db: Session): vendor = db.execute(select(AccountBase).where(AccountBase.id == ven.id_)).scalar_one() - journals = {} + journals: Dict[uuid.UUID, Journal] = {} amount = 0 for item in voucher.inventories: account_id, cc_id = db.execute( @@ -363,7 +363,7 @@ def show_blank( request: Request, user: UserToken = Security(get_user, scopes=["purchase"]), ): - additional_info = {"date": get_date(request.session), "type": "Purchase"} + additional_info = {"date": get_date(request.session), "type": VoucherType.PURCHASE} with SessionFuture() as db: return blank_voucher(additional_info, db) diff --git a/brewman/brewman/routers/purchase_return.py b/brewman/brewman/routers/purchase_return.py index cd6feae4..0da00ef7 100644 --- a/brewman/brewman/routers/purchase_return.py +++ b/brewman/brewman/routers/purchase_return.py @@ -73,7 +73,7 @@ def save(data: schema_in.PurchaseIn, user: UserToken, db: Session) -> Voucher: ) account_types = ( db.execute( - select(distinct(AccountBase.type)).where( + select(distinct(AccountBase.type_id)).where( or_( AccountBase.id == data.vendor.id_, AccountBase.id.in_(product_accounts), @@ -83,7 +83,7 @@ def save(data: schema_in.PurchaseIn, user: UserToken, db: Session) -> Voucher: .scalars() .all() ) - allowed, message = get_lock_info([data.date_], VoucherType.by_name(data.type_).id, account_types, db) + allowed, message = get_lock_info([data.date_], VoucherType[data.type_.replace(" ", "_").upper()], account_types, db) if not allowed: raise HTTPException( status_code=status.HTTP_423_LOCKED, @@ -94,7 +94,7 @@ def save(data: schema_in.PurchaseIn, user: UserToken, db: Session) -> Voucher: narration=data.narration, is_starred=data.is_starred, user_id=user.id_, - type_=VoucherType.by_name(data.type_), + voucher_type=VoucherType[data.type_.replace(" ", "_").upper()], ) db.add(voucher) return voucher @@ -201,7 +201,7 @@ def update_voucher(id_: uuid.UUID, data: schema_in.PurchaseIn, user: UserToken, ) account_types = ( db.execute( - select(distinct(AccountBase.type)).where( + select(distinct(AccountBase.type_id)).where( or_( AccountBase.id.in_([data.vendor.id_] + [vj.account_id for vj in voucher.journals]), AccountBase.id.in_(product_accounts), @@ -211,7 +211,7 @@ def update_voucher(id_: uuid.UUID, data: schema_in.PurchaseIn, user: UserToken, .scalars() .all() ) - allowed, message = get_lock_info([voucher.date, data.date_], voucher.type, account_types, db) + allowed, message = get_lock_info([voucher.date, data.date_], voucher.voucher_type, account_types, db) if not allowed: raise HTTPException( status_code=status.HTTP_423_LOCKED, @@ -329,6 +329,6 @@ def show_blank( request: Request, user: UserToken = Security(get_user, scopes=["purchase-return"]), ): - additional_info = {"date": get_date(request.session), "type": "Purchase Return"} + additional_info = {"date": get_date(request.session), "type": VoucherType.PURCHASE_RETURN} with SessionFuture() as db: return blank_voucher(additional_info, db) diff --git a/brewman/brewman/routers/rebase.py b/brewman/brewman/routers/rebase.py index cb85f9f2..6378db15 100644 --- a/brewman/brewman/routers/rebase.py +++ b/brewman/brewman/routers/rebase.py @@ -92,14 +92,14 @@ def save_starred(date_: date, db: Session): db.add(journal) for other in others: - if voucher.type != VoucherType.by_name("Opening Accounts").id: + if voucher.voucher_type != VoucherType.OPENING_ACCOUNTS: voucher.narration += f"\nSuspense \u20B9{other.amount:,.2f} is {other.account.name}" if other.employee_benefit: db.delete(other.employee_benefit) if other.incentive: db.delete(other.incentive) db.delete(other) - voucher.type = VoucherType.by_name("Journal") + voucher.voucher_type = VoucherType.JOURNAL if len(voucher.narration) >= 1000: voucher.narration = voucher.narration[:1000] return vouchers @@ -116,7 +116,7 @@ def opening_accounts(date_: date, user_id: uuid.UUID, db: Session): AccountBase.is_starred == False, # noqa: E712 AccountBase.id != AccountBase.suspense(), Voucher.date < date_, - Voucher.type != VoucherType.by_name("Issue").id, + Voucher.voucher_type != VoucherType.ISSUE, ) .having(sum_func != 0) .group_by(AccountBase) @@ -128,11 +128,11 @@ def opening_accounts(date_: date, user_id: uuid.UUID, db: Session): narration="Opening Accounts", user_id=user_id, posted=True, - type_=VoucherType.by_name("Opening Accounts"), + voucher_type=VoucherType.OPENING_ACCOUNTS, ) for account, amount in query: amount = round(Decimal(amount), 2) - if account.type_object.balance_sheet and amount != 0: + if account.type_.balance_sheet and amount != 0: running_total += amount journal = Journal( amount=abs(amount), @@ -171,7 +171,7 @@ def opening_batches(date_: date, user_id: uuid.UUID, db: Session): narration="Opening Batches", user_id=user_id, posted=True, - type_=VoucherType.by_name("Opening Batches"), + voucher_type=VoucherType.OPENING_BATCHES, ) for batch, quantity in query: diff --git a/brewman/brewman/routers/recipe.py b/brewman/brewman/routers/recipe.py index d530670a..81812804 100644 --- a/brewman/brewman/routers/recipe.py +++ b/brewman/brewman/routers/recipe.py @@ -4,6 +4,7 @@ from datetime import date, timedelta from typing import List import brewman.schemas.recipe as schemas +import brewman.schemas.recipe_item from fastapi import APIRouter, Depends, HTTPException, Request, Security, status from sqlalchemy import desc, func, or_, select @@ -107,7 +108,7 @@ def get_purchased_product_cost(product_id: uuid.UUID, start_date: date, finish_d Inventory.product_id == product_id, Voucher.date >= start_date, Voucher.date <= finish_date, - Voucher.type == VoucherType.by_name("Issue").id, + Voucher.voucher_type == VoucherType.ISSUE, Journal.cost_centre_id == CostCentre.cost_centre_purchase(), ) .group_by(Product) @@ -265,13 +266,9 @@ async def show_list( user: UserToken = Depends(get_user), ) -> List[schemas.Recipe]: with SessionFuture() as db: - list_ = ( + list_: List[Recipe] = ( db.execute( - select(Recipe) - .join(Recipe.product) - .where(Recipe.effective_to == None) - .order_by(Recipe.product_id) - .order_by(desc(Recipe.valid_till)) + select(Recipe).join(Recipe.product).order_by(Recipe.product_id).order_by(desc(Recipe.valid_till)) ) .scalars() .all() @@ -283,6 +280,7 @@ async def show_list( quantity=item.quantity, costPrice=item.cost_price, salePrice=item.sale_price, + isLocked=item.is_locked, notes="", validFrom=item.valid_from, validTill=item.valid_till, @@ -310,48 +308,35 @@ def recipe_info(recipe: Recipe) -> schemas.Recipe: product=schemas.ProductLink( id=recipe.product_id, name=recipe.product.name, - units=recipe.product.units, - salePrice=recipe.product.sale_price, - isSold=recipe.product.is_sold, ), quantity=recipe.quantity, salePrice=recipe.sale_price, costPrice=recipe.cost_price, validFrom=recipe.valid_from, validTill=recipe.valid_till, + isLocked=recipe.is_locked, notes="", items=[ - schemas.RecipeItem( + brewman.schemas.recipe_item.RecipeItem( id=item.id, product=schemas.ProductLink( id=item.product.id, name=item.product.name, - units=item.product.units, - fractionUnits=item.product.fraction_units, - fraction=item.product.fraction, - productYield=item.product.product_yield, ), quantity=item.quantity, price=item.price, ) - for item in recipe.recipe_items + for item in recipe.items ], ) def recipe_blank() -> schemas.RecipeBlank: - # if id_ is None: - # info = { - # "Quantity": 1, - # "ValidFrom": get_start_date(request), - # "ValidTo": get_finish_date(request), - # "RecipeItems": [], - # } - # else: return schemas.RecipeBlank( quantity=0, costPrice=0, salePrice=0, - items=[], + isLocked=False, notes="", + items=[], ) diff --git a/brewman/brewman/routers/reports/balance_sheet.py b/brewman/brewman/routers/reports/balance_sheet.py index 38b5c642..7418cdef 100644 --- a/brewman/brewman/routers/reports/balance_sheet.py +++ b/brewman/brewman/routers/reports/balance_sheet.py @@ -1,9 +1,10 @@ from datetime import date, datetime -from typing import List +from typing import List, Tuple import brewman.schemas.balance_sheet as schemas from fastapi import APIRouter, Request, Security +from sqlalchemy import not_ from sqlalchemy.orm import Session from sqlalchemy.sql.expression import desc, func, select @@ -43,8 +44,10 @@ def report_data( return schemas.BalanceSheet(date=date_, body=body, footer=footer) -def build_balance_sheet(date_: date, db: Session) -> (List[schemas.BalanceSheetItem], schemas.BalanceSheetItem): - type_list = [i.id for i in AccountType.list() if i.balance_sheet] +def build_balance_sheet(date_: date, db: Session) -> Tuple[List[schemas.BalanceSheetItem], schemas.BalanceSheetItem]: + type_list = ( + db.execute(select(AccountType.id).where(AccountType.balance_sheet == True)).scalars().all() # noqa: E712 + ) report = [] groups = dict() # Add Net Profit / Loss @@ -59,7 +62,7 @@ def build_balance_sheet(date_: date, db: Session) -> (List[schemas.BalanceSheetI } ) - capital_group = AccountType.by_id(5) + capital_group = db.execute(select(AccountType).where(AccountType.name == "Capital")).scalar_one() groups[capital_group.id] = { "group": capital_group.name, "amount": round(total_amount, 2), @@ -69,7 +72,7 @@ def build_balance_sheet(date_: date, db: Session) -> (List[schemas.BalanceSheetI total_amount += closing_stock report.append({"name": "Closing Stock", "subAmount": round(closing_stock, 2), "order": 20001}) - asset_group = AccountType.by_id(4) + asset_group = db.execute(select(AccountType).where(AccountType.name == "Assets")).scalar_one() groups[asset_group.id] = { "group": asset_group.name, "amount": round(closing_stock, 2), @@ -81,15 +84,18 @@ def build_balance_sheet(date_: date, db: Session) -> (List[schemas.BalanceSheetI select(AccountBase, amount_sum) .join(Journal.voucher) .join(Journal.account) - .where(Voucher.date <= date_, Voucher.type != VoucherType.by_name("Issue").id, AccountBase.type.in_(type_list)) + .where( + Voucher.date <= date_, + not_(Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK])), + AccountBase.type_id.in_(type_list), + ) .group_by(AccountBase) - .order_by(AccountBase.type, desc(func.abs(amount_sum))) + .order_by(AccountBase.type_id, desc(func.abs(amount_sum))) ).all() counter = 0 for account, amount in query: # Add Items - account_type = AccountType.by_id(account.type) total_amount += amount if amount != 0: counter += 1 @@ -97,16 +103,16 @@ def build_balance_sheet(date_: date, db: Session) -> (List[schemas.BalanceSheetI { "name": account.name, "subAmount": round(amount, 2), - "order": account_type.order + counter, + "order": account.type_.order + counter, } ) - if account_type.id in groups: - groups[account_type.id]["amount"] = round(groups[account_type.id]["amount"] + amount, 2) + if account.type_.id in groups: + groups[account.type_.id]["amount"] = round(groups[account.type_.id]["amount"] + amount, 2) else: - groups[account_type.id] = { - "group": account_type.name, + groups[account.type_.id] = { + "group": account.type_.name, "amount": round(amount, 2), - "order": account_type.order, + "order": account.type_.order, } # Add Subtotals diff --git a/brewman/brewman/routers/reports/cash_flow.py b/brewman/brewman/routers/reports/cash_flow.py index c429c9ec..862dd2b1 100644 --- a/brewman/brewman/routers/reports/cash_flow.py +++ b/brewman/brewman/routers/reports/cash_flow.py @@ -1,9 +1,10 @@ -import datetime +from datetime import date, datetime import brewman.schemas.cash_flow as schemas from fastapi import APIRouter, Request, Security -from sqlalchemy.orm.util import aliased +from sqlalchemy import not_ +from sqlalchemy.orm import Session from sqlalchemy.sql.expression import desc, func, select from ...core.security import get_current_active_user as get_user @@ -41,7 +42,9 @@ def report_data( user: UserToken = Security(get_user, scopes=["cash-flow"]), ): with SessionFuture() as db: - body, footer = build_report(s, f, db) + start_date = datetime.strptime(s, "%d-%b-%Y") + finish_date = datetime.strptime(f, "%d-%b-%Y") + body, footer = build_report(start_date, finish_date, db) set_period(s, f, request.session) return { "startDate": s, @@ -60,7 +63,9 @@ def report_id( user: UserToken = Security(get_user, scopes=["cash-flow"]), ): with SessionFuture() as db: - details, footer = build_report_id(id_, s, f, db) + start_date = datetime.strptime(s, "%d-%b-%Y") + finish_date = datetime.strptime(f, "%d-%b-%Y") + details, footer = build_report_id(id_, start_date, finish_date, db) set_period(s, f, request.session) return { "startDate": s, @@ -70,41 +75,39 @@ def report_id( } -def build_report(start_date, finish_date, db): - sub_voucher = aliased(Voucher) - sub_journal = aliased(Journal) - sub_account = aliased(AccountBase) - +def build_report(start_date: date, finish_date: date, db: Session): sub_query = ( - select(sub_voucher.id) - .join(sub_journal, sub_voucher.journals) - .join(sub_account, sub_journal.account) + select(Voucher.id) + .join(Voucher.journals) + .join(Journal.account) .where( - sub_account.type == AccountType.by_name("Cash").id, - sub_voucher.date >= datetime.datetime.strptime(start_date, "%d-%b-%Y"), - sub_voucher.date <= datetime.datetime.strptime(finish_date, "%d-%b-%Y"), + AccountBase.type_id == select(AccountType.id).where(AccountType.name == "Cash").scalar_subquery(), + Voucher.date >= start_date, + Voucher.date <= finish_date, ) - .subquery() ) query = db.execute( - select(AccountBase.type, func.sum(Journal.signed_amount)) - .join(Journal, Voucher.journals) - .join(AccountBase, Journal.account) - .where(Voucher.id.in_(sub_query), AccountBase.type != AccountType.by_name("Cash").id) - .group_by(AccountBase.type) + select(AccountType, func.sum(Journal.signed_amount)) + .join(Journal.voucher) + .join(Journal.account) + .join(AccountBase.type_) + .where( + Voucher.id.in_(sub_query), + AccountBase.type_id != select(AccountType.id).where(AccountType.name == "Cash").scalar_subquery(), + ) + .group_by(AccountType) .order_by(func.sum(Journal.signed_amount)) ).all() total_amount = 0 cf = {"operating": [], "investing": [], "financing": []} for account_type, amount in query: - lt = AccountType.by_id(account_type) total_amount += amount * -1 - cf[lt.cash_flow_classification.lower()].append( + cf[account_type.cash_flow_classification.lower()].append( { - "name": lt.name, - "url": ["/", "cash-flow", str(lt.id)], + "name": account_type.name, + "url": ["/", "cash-flow", str(account_type.id)], "amount": amount * -1, } ) @@ -115,8 +118,8 @@ def build_report(start_date, finish_date, db): .join(Journal.account) .where( Voucher.date < start_date, - Voucher.type != VoucherType.by_name("Issue").id, - AccountBase.type == AccountType.by_name("Cash").id, + not_(Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK])), + AccountBase.type_id == select(AccountType.id).where(AccountType.name == "Cash"), ) ).scalar() @@ -126,8 +129,8 @@ def build_report(start_date, finish_date, db): .join(Journal.account) .where( Voucher.date <= finish_date, - Voucher.type != VoucherType.by_name("Issue").id, - AccountBase.type == AccountType.by_name("Cash").id, + not_(Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK])), + AccountBase.type_id == select(AccountType.id).where(AccountType.name == "Cash"), ) ).scalar() @@ -147,29 +150,24 @@ def build_report(start_date, finish_date, db): ) -def build_report_id(account_type, start_date, finish_date, db): +def build_report_id(account_type: int, start_date: date, finish_date: date, db: Session): details = [] - sub_voucher = aliased(Voucher) - sub_journal = aliased(Journal) - sub_account = aliased(AccountBase) - sub_query = ( - select(sub_voucher.id) - .join(sub_journal, sub_voucher.journals) - .join(sub_account, sub_journal.account) + select(Voucher.id) + .join(Voucher.journals) + .join(Journal.account) .where( - sub_account.type == AccountType.by_name("Cash").id, - sub_voucher.date >= datetime.datetime.strptime(start_date, "%d-%b-%Y"), - sub_voucher.date <= datetime.datetime.strptime(finish_date, "%d-%b-%Y"), + AccountBase.type_id == select(AccountType.id).where(AccountType.name == "Cash").scalar_subquery(), + Voucher.date >= start_date, + Voucher.date <= finish_date, ) - .subquery() ) query = db.execute( select(AccountBase, func.sum(Journal.signed_amount)) .join(Journal, Voucher.journals) .join(AccountBase, Journal.account) - .where(Voucher.id.in_(sub_query), AccountBase.type == account_type) + .where(Voucher.id.in_(sub_query), AccountBase.type_id == account_type) .group_by(AccountBase) .order_by(desc(func.sum(Journal.amount))) ).all() diff --git a/brewman/brewman/routers/reports/closing_stock.py b/brewman/brewman/routers/reports/closing_stock.py index a00e003a..2bb95f39 100644 --- a/brewman/brewman/routers/reports/closing_stock.py +++ b/brewman/brewman/routers/reports/closing_stock.py @@ -1,25 +1,33 @@ +import uuid + from datetime import date, datetime from decimal import Decimal -from typing import List +from typing import List, Optional import brewman.schemas.closing_stock as schemas -from fastapi import APIRouter, Request, Security +from fastapi import APIRouter, HTTPException, Request, Security, status +from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import Session, contains_eager -from sqlalchemy.sql.expression import func, select +from sqlalchemy.sql.expression import delete, distinct, func, or_, select, update from ...core.security import get_current_active_user as get_user -from ...core.session import get_finish_date, get_start_date, set_period +from ...core.session import get_finish_date, get_start_date, set_date, set_period from ...db.session import SessionFuture +from ...models.account_base import AccountBase from ...models.batch import Batch +from ...models.closing_stock import ClosingStock from ...models.cost_centre import CostCentre from ...models.inventory import Inventory from ...models.journal import Journal from ...models.product import Product from ...models.product_group import ProductGroup from ...models.stock_keeping_unit import StockKeepingUnit +from ...models.validations import check_journals_are_valid from ...models.voucher import Voucher +from ...models.voucher_type import VoucherType from ...schemas.user import UserToken +from .. import get_lock_info router = APIRouter() @@ -30,24 +38,105 @@ def report_blank( request: Request, user: UserToken = Security(get_user, scopes=["closing-stock"]), ) -> schemas.ClosingStock: - return schemas.ClosingStock(date=get_finish_date(request.session), body=[]) + return schemas.ClosingStock( + date=get_finish_date(request.session), + posted=False, + costCentre=schemas.CostCentreLink(id=CostCentre.cost_centre_purchase()), + items=[], + ) @router.get("/{date_}", response_model=schemas.ClosingStock) def report_data( date_: str, request: Request, + d: Optional[uuid.UUID] = None, user: UserToken = Security(get_user, scopes=["closing-stock"]), ) -> schemas.ClosingStock: set_period(get_start_date(request.session), date_, request.session) with SessionFuture() as db: - return schemas.ClosingStock( - date=date_, - body=build_report(datetime.strptime(date_, "%d-%b-%Y").date(), db), + return full_report(datetime.strptime(date_, "%d-%b-%Y").date(), d or CostCentre.cost_centre_purchase(), db) + + +@router.delete("/{date_}") +def delete_voucher( + date_: str, + d: Optional[uuid.UUID] = None, + user: UserToken = Security(get_user, scopes=["closing-stock"]), +): + try: + cost_centre_id = d or CostCentre.cost_centre_purchase() + with SessionFuture() as db: + db.execute( + delete(ClosingStock) + .where(ClosingStock.date == date_, ClosingStock.cost_centre_id == cost_centre_id) + .execution_options(synchronize_session=False) + ) + delete_old_vouchers(datetime.strptime(date_, "%d-%b-%Y").date(), cost_centre_id, db) + db.commit() + except SQLAlchemyError as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=str(e), ) -def build_report(date_: date, db: Session) -> List[schemas.ClosingStockItem]: +@router.post("/{date_}", response_model=schemas.ClosingStock) +def post_voucher( + date_: str, + d: Optional[uuid.UUID], + user: UserToken = Security(get_user, scopes=["post-vouchers"]), +) -> schemas.ClosingStock: + try: + with SessionFuture() as db: + date_obj = datetime.strptime(date_, "%d-%b-%Y").date() + vouchers = ( + db.execute( + select(Voucher.id).where( + Voucher.date == date_obj, Voucher.voucher_type == VoucherType.CLOSING_STOCK + ) + ) + .scalars() + .all() + ) + vouchers = ( + db.execute( + select(Journal.voucher_id).where(Journal.cost_centre_id == d, Journal.voucher_id.in_(vouchers)) + ) + .scalars() + .all() + ) + db.execute(update(Voucher).where(Voucher.id.in_(vouchers)).values(posted=True, poster_id=user.id_)) + db.commit() + return full_report(date_obj, d, db) + except SQLAlchemyError as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=str(e), + ) + + +def full_report(date_: date, cost_centre_id: uuid.UUID, db: Session) -> schemas.ClosingStock: + voucher = ( + db.execute(select(Voucher).where(Voucher.date == date_, Voucher.voucher_type == VoucherType.CLOSING_STOCK)) + .scalars() + .first() + ) + return schemas.ClosingStock( + date=date_, + costCentre=schemas.CostCentreLink(id=cost_centre_id), + items=build_report(date_, cost_centre_id, db), + creationDate=voucher.creation_date if voucher is not None else None, + lastEditDate=voucher.last_edit_date if voucher is not None else None, + user=schemas.UserLink(id=voucher.user.id, name=voucher.user.name) if voucher is not None else None, + posted=voucher.posted if voucher is not None else False, + poster=None + if voucher is None or voucher.poster is None + else schemas.UserLink(id=voucher.poster.id, name=voucher.poster.name), + ) + + +def build_report(date_: date, cost_centre_id: uuid.UUID, db: Session) -> List[schemas.ClosingStockItem]: amount_sum = func.sum(Journal.debit * Inventory.quantity * Inventory.rate * (1 + Inventory.tax)).label("amount") quantity_sum = func.sum(Journal.debit * Inventory.quantity).label("quantity") query = db.execute( @@ -59,20 +148,52 @@ def build_report(date_: date, db: Session) -> List[schemas.ClosingStockItem]: .join(Inventory.voucher) .join(Voucher.journals) .options(contains_eager(StockKeepingUnit.product).contains_eager(Product.product_group)) - .where(Voucher.date <= date_, Journal.cost_centre_id == CostCentre.cost_centre_purchase()) + .where( + Voucher.date <= date_, + Journal.cost_centre_id == cost_centre_id, + or_(Voucher.voucher_type != VoucherType.CLOSING_STOCK, Voucher.date != date_), + ) .group_by(StockKeepingUnit, Product, ProductGroup) .order_by(amount_sum.desc()) ).all() + physical_list = ( + db.execute( + select(ClosingStock).where(ClosingStock.date == date_, ClosingStock.cost_centre_id == cost_centre_id) + ) + .scalars() + .all() + ) + + ccs = db.execute( + select(Journal.cost_centre_id, Batch.sku_id) + .join(Journal.voucher) + .join(Voucher.inventories) + .join(Inventory.batch) + .where( + Voucher.voucher_type == VoucherType.CLOSING_STOCK, + Voucher.date == date_, + Journal.cost_centre_id != cost_centre_id, + Voucher.id.in_(select(Journal.voucher_id).where(Journal.cost_centre_id == cost_centre_id)), + ) + .group_by(Journal.cost_centre_id, Batch.sku_id) + ).all() + body = [] for sku, quantity, amount in query: if quantity != 0 and amount != 0: + id_ = next((p.id for p in physical_list if p.sku_id == sku.id), None) + physical = next((p.quantity for p in physical_list if p.sku_id == sku.id), quantity) + cc = next((schemas.CostCentreLink(id=c) for (c, s) in ccs if s == sku.id), None) body.append( schemas.ClosingStockItem( - product=f"{sku.product.name} ({sku.units})", + id=id_, + product=schemas.ProductLink(id=sku.id, name=f"{sku.product.name} ({sku.units})"), group=sku.product.product_group.name, quantity=quantity, amount=amount, + physical=physical, + costCentre=cc, ) ) return body @@ -98,3 +219,196 @@ def get_closing_stock(date_, db: Session) -> Decimal: .where(Voucher.date <= date_, Journal.cost_centre_id == CostCentre.cost_centre_purchase()) ).scalar() return 0 if closing_stock is None else closing_stock + + +@router.post("", response_model=schemas.ClosingStock) +def save_route( + request: Request, + data: schemas.ClosingStock, + user: UserToken = Security(get_user, scopes=["closing-stock"]), +): + try: + departments = set(i.cost_centre.id_ for i in data.items if i.cost_centre is not None) + if data.cost_centre.id_ in departments: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Issue department cannot be same as main department", + ) + if CostCentre.cost_centre_purchase() in departments: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Products cannot be issued to purchase", + ) + if CostCentre.cost_centre_overall() in departments: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Products cannot be issued to overall", + ) + if len([i for i in data.items if i.physical > i.quantity]) > 0: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Physical Quantity cannot be more than computed quality.", + ) + if len([i for i in data.items if i.physical != i.quantity and i.cost_centre is None]) > 0: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Products with change in quantity need a department", + ) + with SessionFuture() as db: + count_ = db.execute(select(func.count()).where(ClosingStock.date == data.date_)).scalar_one() + if count_ == 0: + save_cs(data, db) + else: + update_cs(data.date_, data.cost_centre.id_, data.items[:], db) + delete_old_vouchers(data.date_, data.cost_centre.id_, db) + for dep in departments: + items = [d for d in data.items if d.cost_centre is not None and d.cost_centre.id_ == dep] + item = save(data.date_, items, user, db) + save_inventories(item, items, db) + amount = sum(i.amount for i in item.inventories) + save_journals(item, data.cost_centre.id_, dep, amount, db) + check_journals_are_valid(item) + db.commit() + set_date(data.date_.strftime("%d-%b-%Y"), request.session) + return full_report(data.date_, data.cost_centre.id_, db) + except SQLAlchemyError as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=str(e), + ) + + +def save_cs(data: schemas.ClosingStock, db: Session): + for item in data.items: + cs = ClosingStock( + date_=data.date_, cost_centre_id=data.cost_centre.id_, sku_id=item.product.id_, quantity=item.physical + ) + db.add(cs) + + +def save(date_: date, items: List[schemas.ClosingStockItem], user: UserToken, db: Session) -> Voucher: + product_accounts = ( + select(Product.account_id).join(Product.skus).where(StockKeepingUnit.id.in_([i.product.id_ for i in items])) + ) + account_types = ( + db.execute(select(distinct(AccountBase.type_id)).where(AccountBase.id.in_(product_accounts))).scalars().all() + ) + allowed, message = get_lock_info([date_], VoucherType.CLOSING_STOCK, account_types, db) + if not allowed: + raise HTTPException( + status_code=status.HTTP_423_LOCKED, + detail=message, + ) + voucher = Voucher( + date=date_, + narration="", + is_starred=False, + user_id=user.id_, + voucher_type=VoucherType.CLOSING_STOCK, + ) + db.add(voucher) + return voucher + + +def save_inventories(voucher: Voucher, items: List[schemas.ClosingStockItem], db: Session) -> (Voucher, Optional[bool]): + for item in items: + batches = ( + db.execute( + select(Batch) + .where(Batch.sku_id == item.product.id_, Batch.quantity_remaining != 0, Batch.name <= voucher.date) + .order_by(Batch.name) + ) + .scalars() + .all() + ) + to_issue = item.quantity - item.physical + while to_issue > 0: + batch = batches.pop(0) + quantity_issued = min(to_issue, batch.quantity_remaining) + inv = Inventory( + quantity=quantity_issued, + rate=batch.rate, + tax=batch.tax, + discount=batch.discount, + batch=batch, + ) + batch.quantity_remaining -= quantity_issued + to_issue -= quantity_issued + voucher.inventories.append(inv) + db.add(inv) + + +def save_journals( + voucher: Voucher, + source: uuid.UUID, + destination: uuid.UUID, + amount: Decimal, + db: Session, +): + s = Journal(debit=-1, account_id=AccountBase.all_purchases(), amount=amount, cost_centre_id=source) + d = Journal(debit=1, account_id=AccountBase.all_purchases(), amount=amount, cost_centre_id=destination) + voucher.journals.append(s) + db.add(s) + voucher.journals.append(d) + db.add(d) + + +def update_cs(date_: date, cost_centre_id: uuid.UUID, items: List[schemas.ClosingStockItem], db: Session): + old = ( + db.execute( + select(ClosingStock).where(ClosingStock.date == date_, ClosingStock.cost_centre_id == cost_centre_id) + ) + .scalars() + .all() + ) + for i in range(len(old), 0, -1): + item = old[i - 1] + index = next((idx for (idx, d) in enumerate(items) if d.id_ == item.id), None) + if index is not None: + new_item = items.pop(index) + if item.quantity != new_item.physical: + db.execute(update(ClosingStock).where(ClosingStock.id == item.id).values(quantity=new_item.physical)) + else: + db.execute(delete(ClosingStock).where(ClosingStock.id == item.id)) + for new_item in items: + cs = ClosingStock( + date_=date_, + cost_centre_id=cost_centre_id, + sku_id=new_item.product.id_, + quantity=new_item.physical, + ) + db.add(cs) + + +def delete_old_vouchers(date_: date, cost_centre_id: uuid.UUID, db: Session): + vouchers = ( + db.execute(select(Voucher.id).where(Voucher.date == date_, Voucher.voucher_type == VoucherType.CLOSING_STOCK)) + .scalars() + .all() + ) + vouchers = ( + db.execute( + select(func.distinct(Journal.voucher_id)).where( + Journal.cost_centre_id == cost_centre_id, Journal.voucher_id.in_(vouchers) + ) + ) + .scalars() + .all() + ) + + batches = db.execute(select(Inventory.batch_id).where(Inventory.voucher_id.in_(vouchers))).scalars().all() + db.execute(delete(Journal).where(Journal.voucher_id.in_(vouchers)).execution_options(synchronize_session=False)) + db.execute(delete(Inventory).where(Inventory.voucher_id.in_(vouchers)).execution_options(synchronize_session=False)) + db.execute(delete(Voucher).where(Voucher.id.in_(vouchers)).execution_options(synchronize_session=False)) + for batch in batches: + db.execute(update(Batch).where(Batch.id == batch).values(quantity_remaining=get_batch_quantity(batch, db))) + + +def get_batch_quantity(id_: uuid.UUID, db: Session) -> Decimal: + query = ( + select(func.sum(Inventory.quantity * Journal.debit)) + .join(Inventory.voucher) + .join(Voucher.journals) + .where(Inventory.batch_id == id_, Journal.cost_centre_id == CostCentre.cost_centre_purchase()) + ) + return db.execute(query).scalar_one() or 0 diff --git a/brewman/brewman/routers/reports/daybook.py b/brewman/brewman/routers/reports/daybook.py index 597136eb..de58bc49 100644 --- a/brewman/brewman/routers/reports/daybook.py +++ b/brewman/brewman/routers/reports/daybook.py @@ -4,7 +4,7 @@ from typing import List import brewman.schemas.daybook as schemas from fastapi import APIRouter, Request, Security -from sqlalchemy import select +from sqlalchemy import not_, select from sqlalchemy.orm import Session from ...core.security import get_current_active_user as get_user @@ -55,7 +55,9 @@ def build_report(start_date: date, finish_date: date, db: Session) -> List[schem .join(Voucher.journals) .join(Journal.account) .where( - Voucher.date >= start_date, Voucher.date <= finish_date, Voucher.type != VoucherType.by_name("Issue").id + Voucher.date >= start_date, + Voucher.date <= finish_date, + not_(Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK])), ) .order_by(Voucher.date, Voucher.last_edit_date) ) @@ -85,10 +87,10 @@ def build_report(start_date: date, finish_date: date, db: Session) -> List[schem date=voucher.date.strftime("%d-%b-%Y"), url=[ "/", - VoucherType.by_id(voucher.type).name.replace(" ", "-").lower(), + voucher.voucher_type.name.replace("_", "-").lower(), str(voucher.id), ], - type=VoucherType.by_id(voucher.type).name, + type=voucher.voucher_type.name.replace("_", " ").title(), narration=voucher.narration, posted=voucher.posted, debitText=name_debit, diff --git a/brewman/brewman/routers/reports/entries.py b/brewman/brewman/routers/reports/entries.py index 7764989e..4f38a1d1 100644 --- a/brewman/brewman/routers/reports/entries.py +++ b/brewman/brewman/routers/reports/entries.py @@ -4,7 +4,7 @@ from typing import List, Optional import brewman.schemas.entries as schemas from fastapi import APIRouter, Depends, Security -from sqlalchemy import desc, or_, select +from sqlalchemy import desc, not_, or_, select from sqlalchemy.orm import Session, contains_eager, joinedload from sqlalchemy.sql.functions import count @@ -56,8 +56,10 @@ def build_report( sq = sq.where(Voucher.posted == posted) counts = counts.where(Voucher.posted == posted) if issue is False: - sq = sq.where(Voucher.type != VoucherType.by_name("Issue").id) - counts = counts.where(Voucher.type != VoucherType.by_name("Issue").id) + sq = sq.where(not_(Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK]))) + counts = counts.where( + not_(Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK])), + ) if active_sort == "date": if sort_direction == "desc": sq = sq.order_by(desc(Voucher.date), desc(Voucher.last_edit_date)) @@ -101,10 +103,10 @@ def report_data( date=voucher.date, url=[ "/", - VoucherType.by_id(voucher.type).name.replace(" ", "-").lower(), + voucher.voucher_type.name.replace("_", "-").lower(), str(voucher.id), ], - type=VoucherType.by_id(voucher.type).name, + type=voucher.voucher_type.name.replace("_", " ").title(), posted=voucher.posted, narration=voucher.narration, debitNames=[x.account.name for x in voucher.journals if x.debit == 1], diff --git a/brewman/brewman/routers/reports/ledger.py b/brewman/brewman/routers/reports/ledger.py index 02a5eb53..0be75076 100644 --- a/brewman/brewman/routers/reports/ledger.py +++ b/brewman/brewman/routers/reports/ledger.py @@ -6,6 +6,7 @@ from typing import List import brewman.schemas.ledger as schemas from fastapi import APIRouter, Request, Security +from sqlalchemy import not_ from sqlalchemy.orm import Session from sqlalchemy.sql.expression import func, select @@ -71,7 +72,7 @@ def build_report(account_id: uuid.UUID, start_date: str, finish_date: str, db: S Voucher.journals.any(Journal.account_id == account_id), Voucher.date >= datetime.datetime.strptime(start_date, "%d-%b-%Y"), Voucher.date <= datetime.datetime.strptime(finish_date, "%d-%b-%Y"), - Voucher.type != VoucherType.by_name("Issue").id, + not_(Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK])), ) .order_by(Voucher.date, Voucher.last_edit_date) ) @@ -105,10 +106,10 @@ def build_report(account_id: uuid.UUID, start_date: str, finish_date: str, db: S name=name, url=[ "/", - VoucherType.by_id(voucher.type).name.replace(" ", "-").lower(), + voucher.voucher_type.name.replace("_", "-").lower(), str(voucher.id), ], - type=VoucherType.by_id(voucher.type).name, + type=voucher.voucher_type.name.replace("_", " ").title(), narration=voucher.narration, debit=debit, credit=credit, @@ -124,7 +125,7 @@ def opening_balance(account_id: uuid.UUID, start_date: str, db: Session) -> sche .join(Journal.voucher) .where( Voucher.date < datetime.datetime.strptime(start_date, "%d-%b-%Y"), - Voucher.type != VoucherType.by_name("Issue").id, + not_(Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK])), Journal.account_id == account_id, ) ).scalar() diff --git a/brewman/brewman/routers/reports/net_transactions.py b/brewman/brewman/routers/reports/net_transactions.py index 97e79e2f..cf22924f 100644 --- a/brewman/brewman/routers/reports/net_transactions.py +++ b/brewman/brewman/routers/reports/net_transactions.py @@ -4,6 +4,7 @@ from typing import List import brewman.schemas.net_transactions as schemas from fastapi import APIRouter, Request, Security +from sqlalchemy import not_ from sqlalchemy.orm import Session from sqlalchemy.sql.expression import desc, func, select @@ -58,15 +59,19 @@ def build_report(start_date: date, finish_date: date, db: Session) -> List[schem select(AccountBase, amount_sum) .join(Journal.voucher) .join(Journal.account) - .where(Voucher.date >= start_date, Voucher.date <= finish_date, Voucher.type != VoucherType.by_name("Issue").id) + .where( + Voucher.date >= start_date, + Voucher.date <= finish_date, + not_(Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK])), + ) .group_by(AccountBase) - .order_by(AccountBase.type, desc(func.abs(amount_sum))) + .order_by(AccountBase.type_id, desc(func.abs(amount_sum))) ).all() body = [] for account, amount in query: if amount != 0: - item = schemas.NetTransactionsItem(type=account.type_object.name, name=account.name) + item = schemas.NetTransactionsItem(type=account.type_.name, name=account.name) if amount > 0: item.debit = amount else: diff --git a/brewman/brewman/routers/reports/product_ledger.py b/brewman/brewman/routers/reports/product_ledger.py index e2e0598c..af802406 100644 --- a/brewman/brewman/routers/reports/product_ledger.py +++ b/brewman/brewman/routers/reports/product_ledger.py @@ -95,7 +95,11 @@ def build_report( for voucher, inventory, journal, stockKeepingUnit in query: journal_debit = journal.debit * -1 - name = journal.cost_centre.name if voucher.type == VoucherType.by_name("Issue").id else journal.account.name + name = ( + journal.cost_centre.name + if voucher.voucher_type in [VoucherType.ISSUE, VoucherType.CLOSING_STOCK] + else journal.account.name + ) debit_q = inventory.quantity if journal_debit == 1 else None debit_a = inventory.amount if journal_debit == 1 else None debit_u = stockKeepingUnit.units if journal_debit == 1 else None @@ -109,16 +113,18 @@ def build_report( body.append( schemas.ProductLedgerItem( id=voucher.id, - date=voucher.date.strftime("%d-%b-%Y"), + date=voucher.date, name=name, url=[ "/", - VoucherType.by_id(voucher.type).name.replace(" ", "-").lower(), - str(voucher.id), + voucher.voucher_type.name.replace("_", "-").lower(), + str(voucher.id) + if voucher.voucher_type != VoucherType.CLOSING_STOCK + else voucher.date.strftime("%d-%b-%Y"), ], - type=VoucherType.by_id(voucher.type).name, + type=voucher.voucher_type.name.replace("_", " ").title(), narration=voucher.narration, - posted=voucher.posted or VoucherType.by_id(voucher.type).name == "Issue", + posted=voucher.posted or voucher.voucher_type in [VoucherType.ISSUE, VoucherType.CLOSING_STOCK], debitQuantity=debit_q, debitAmount=debit_a, debitUnit=debit_u, diff --git a/brewman/brewman/routers/reports/profit_loss.py b/brewman/brewman/routers/reports/profit_loss.py index 70a64113..9fb0bfa2 100644 --- a/brewman/brewman/routers/reports/profit_loss.py +++ b/brewman/brewman/routers/reports/profit_loss.py @@ -5,6 +5,7 @@ from typing import List import brewman.schemas.profit_loss as schemas from fastapi import APIRouter, Request, Security +from sqlalchemy import not_ from sqlalchemy.orm import Session from sqlalchemy.sql.expression import desc, func, select @@ -59,7 +60,9 @@ def report_data( def build_profit_loss( start_date: date, finish_date: date, db: Session ) -> (List[schemas.ProfitLossItem], schemas.ProfitLossItem): - profit_type_list = [i.id for i in AccountType.list() if i.balance_sheet is False] + profit_type_list = ( + db.execute(select(AccountType.id).where(AccountType.balance_sheet == False)).scalars().all() # noqa: E712 + ) report: List[schemas.ProfitLossItem] = [] groups = {} @@ -71,11 +74,11 @@ def build_profit_loss( .where( Voucher.date >= start_date, Voucher.date <= finish_date, - Voucher.type != VoucherType.by_name("Issue").id, - AccountBase.type.in_(profit_type_list), + not_(Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK])), + AccountBase.type_id.in_(profit_type_list), ) .group_by(AccountBase) - .order_by(AccountBase.type, desc(func.abs(amount_sum))) + .order_by(AccountBase.type_id, desc(func.abs(amount_sum))) ).all() # Get opening / closing stock @@ -85,7 +88,8 @@ def build_profit_loss( report.append(schemas.ProfitLossItem(name="Opening Stock", amount=opening_stock * -1, order=200001)) report.append(schemas.ProfitLossItem(name="Closing Stock", amount=closing_stock, order=290000)) - purchase_group = AccountType.by_id(2) + + purchase_group = db.execute(select(AccountType).where(AccountType.name == "Purchase")).scalar_one() groups[purchase_group.id] = schemas.ProfitLossItem( group=purchase_group.name, total=total_amount, @@ -96,7 +100,6 @@ def build_profit_loss( for account, amount in query: # Add Items amount *= -1 - account_type = AccountType.by_id(account.type) total_amount += amount if amount != 0: @@ -105,16 +108,16 @@ def build_profit_loss( schemas.ProfitLossItem( name=account.name, amount=amount, - order=account_type.order + counter, + order=account.type_.order + counter, ) ) - if account_type.id in groups: - groups[account_type.id].total += amount + if account.type_.id in groups: + groups[account.type_.id].total += amount else: - groups[account_type.id] = schemas.ProfitLossItem( - group=account_type.name, + groups[account.type_.id] = schemas.ProfitLossItem( + group=account.type_.name, total=amount, - order=account_type.order, + order=account.type_.order, ) # Add Subtotals @@ -132,7 +135,9 @@ def build_profit_loss( def get_accumulated_profit(finish_date: date, db: Session) -> Decimal: - type_list = [i.id for i in AccountType.list() if i.balance_sheet is False] + type_list = ( + db.execute(select(AccountType.id).where(AccountType.balance_sheet == False)).scalars().all() # noqa: E712 + ) accumulated_profit = db.execute( select(func.sum(Journal.amount * Journal.debit)) @@ -140,8 +145,8 @@ def get_accumulated_profit(finish_date: date, db: Session) -> Decimal: .join(Journal.account) .where( Voucher.date <= finish_date, - Voucher.type != VoucherType.by_name("Issue").id, - AccountBase.type.in_(type_list), + not_(Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK])), + AccountBase.type_id.in_(type_list), ) ).scalar() return 0 if accumulated_profit is None else accumulated_profit diff --git a/brewman/brewman/routers/reports/purchase_entries.py b/brewman/brewman/routers/reports/purchase_entries.py index 470bc64c..495921e1 100644 --- a/brewman/brewman/routers/reports/purchase_entries.py +++ b/brewman/brewman/routers/reports/purchase_entries.py @@ -70,7 +70,7 @@ def build_report(start_date: date, finish_date: date, db: Session) -> List[schem .where( Voucher.date >= start_date, Voucher.date <= finish_date, - Voucher.type == VoucherType.by_name("Purchase").id, + Voucher.voucher_type == VoucherType.PURCHASE, ) .order_by(Voucher.date) ) @@ -88,7 +88,7 @@ def build_report(start_date: date, finish_date: date, db: Session) -> List[schem supplier=journal.account.name, url=[ "/", - VoucherType.by_id(voucher.type).name.replace(" ", "-").lower(), + voucher.voucher_type.name.replace("_", "-").lower(), str(voucher.id), ], product=item.batch.sku.product.name, diff --git a/brewman/brewman/routers/reports/purchases.py b/brewman/brewman/routers/reports/purchases.py index fc723b24..64c65389 100644 --- a/brewman/brewman/routers/reports/purchases.py +++ b/brewman/brewman/routers/reports/purchases.py @@ -4,6 +4,7 @@ from typing import List import brewman.schemas.purchases as schemas from fastapi import APIRouter, Request, Security +from sqlalchemy import not_ from sqlalchemy.orm import Session from sqlalchemy.sql.expression import desc, func, select @@ -71,7 +72,7 @@ def build_report( .where( Voucher.date >= datetime.strptime(start_date, "%d-%b-%Y"), Voucher.date <= datetime.strptime(finish_date, "%d-%b-%Y"), - Voucher.type != VoucherType.by_name("Issue").id, + not_(Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK])), Journal.cost_centre_id == CostCentre.cost_centre_purchase(), ) .group_by(Product) diff --git a/brewman/brewman/routers/reports/raw_material_cost.py b/brewman/brewman/routers/reports/raw_material_cost.py index dd265324..252ff628 100644 --- a/brewman/brewman/routers/reports/raw_material_cost.py +++ b/brewman/brewman/routers/reports/raw_material_cost.py @@ -21,6 +21,7 @@ from ...models.product import Product from ...models.product_group import ProductGroup from ...models.stock_keeping_unit import StockKeepingUnit from ...models.voucher import Voucher +from ...models.voucher_type import VoucherType from ...schemas.user import UserToken @@ -81,8 +82,8 @@ def build_report( start_date: date, finish_date: date, db: Session ) -> (List[schemas.RawMaterialCostItem], schemas.RawMaterialCostItem): body = [] - sum_issue = func.sum(case([(AccountBase.type == 2, Journal.signed_amount)], else_=0)).label("issue") - sum_sale = func.sum(case([(AccountBase.type == 3, Journal.signed_amount * -1)], else_=0)).label("sale") + sum_issue = func.sum(case([(AccountBase.type_id == 2, Journal.signed_amount)], else_=0)).label("issue") + sum_sale = func.sum(case([(AccountBase.type_id == 3, Journal.signed_amount * -1)], else_=0)).label("sale") query = db.execute( select(CostCentre, sum_issue, sum_sale) @@ -93,7 +94,7 @@ def build_report( Voucher.date >= start_date, Voucher.date <= finish_date, Journal.cost_centre_id != CostCentre.cost_centre_purchase(), - AccountBase.type.in_([2, 3]), + AccountBase.type_id.in_([2, 3]), ) .group_by(CostCentre) .order_by(sum_sale.desc()) @@ -137,7 +138,7 @@ def build_report_id( .where( Voucher.date >= start_date, Voucher.date <= finish_date, - Voucher.type == 3, + Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK]), Journal.cost_centre_id == cost_centre_id, ) .group_by(Product, ProductGroup, Journal.debit, ProductGroup.name) diff --git a/brewman/brewman/routers/reports/reconcile.py b/brewman/brewman/routers/reports/reconcile.py index 244e36f5..4acbdcd5 100644 --- a/brewman/brewman/routers/reports/reconcile.py +++ b/brewman/brewman/routers/reports/reconcile.py @@ -2,6 +2,7 @@ import datetime import uuid from fastapi import APIRouter, Depends, Request, Security +from sqlalchemy import not_ from sqlalchemy.orm import Session, joinedload from sqlalchemy.sql.expression import and_, func, or_, select @@ -63,13 +64,13 @@ def build_report(account_id, start_date, finish_date, db): .where( Voucher.journals.any(Journal.account_id == account_id), or_( - Voucher.is_reconciled == False, + Voucher.is_reconciled == False, # noqa: E712 and_( Voucher.reconcile_date >= datetime.datetime.strptime(start_date, "%d-%b-%Y"), Voucher.reconcile_date <= datetime.datetime.strptime(finish_date, "%d-%b-%Y"), ), ), - Voucher.type != VoucherType.by_name("Issue").id, + not_(Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK])), ) .order_by(Voucher.is_reconciled, Voucher.reconcile_date, Voucher.last_edit_date) ).all() @@ -97,7 +98,7 @@ def build_report(account_id, start_date, finish_date, db): "id": voucher.id, "date": voucher.date.strftime("%d-%b-%Y"), "name": name, - "type": VoucherType.by_id(voucher.type).name, + "type": voucher.voucher_type.name.replace("_", " ").title(), "narration": voucher.narration, "debit": debit, "credit": credit, @@ -114,8 +115,8 @@ def opening_balance(account_id: uuid.UUID, start_date, db: Session): .join(Journal.voucher) .where( Voucher.reconcile_date < datetime.datetime.strptime(start_date, "%d-%b-%Y"), - Voucher.is_reconciled == True, - Voucher.type != VoucherType.by_name("Issue").id, + Voucher.is_reconciled == True, # noqa: E712 + not_(Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK])), Journal.account_id == account_id, ) ).scalar() diff --git a/brewman/brewman/routers/reports/stock_movement.py b/brewman/brewman/routers/reports/stock_movement.py index 56a0eb7f..4571b856 100644 --- a/brewman/brewman/routers/reports/stock_movement.py +++ b/brewman/brewman/routers/reports/stock_movement.py @@ -5,6 +5,7 @@ from typing import List import brewman.schemas.stock_movement as schemas from fastapi import APIRouter, Request, Security +from sqlalchemy import not_ from sqlalchemy.orm import Session, contains_eager from sqlalchemy.sql.expression import func, select @@ -89,7 +90,7 @@ def build_stock_movement(start_date: date, finish_date: date, db: Session) -> Li .where( Voucher.date >= start_date, Voucher.date <= finish_date, - Voucher.type != VoucherType.by_name("Issue").id, + not_(Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK])), Journal.cost_centre_id == CostCentre.cost_centre_purchase(), ) .group_by(StockKeepingUnit, Product, ProductGroup) @@ -120,7 +121,7 @@ def build_stock_movement(start_date: date, finish_date: date, db: Session) -> Li .where( Voucher.date >= start_date, Voucher.date <= finish_date, - Voucher.type == VoucherType.by_name("Issue").id, + Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK]), Journal.cost_centre_id == CostCentre.cost_centre_purchase(), ) .group_by(StockKeepingUnit, Product, ProductGroup) diff --git a/brewman/brewman/routers/reports/trial_balance.py b/brewman/brewman/routers/reports/trial_balance.py index e987383d..1a90f7b3 100644 --- a/brewman/brewman/routers/reports/trial_balance.py +++ b/brewman/brewman/routers/reports/trial_balance.py @@ -4,13 +4,15 @@ from typing import List import brewman.schemas.trial_balance as schemas from fastapi import APIRouter, Request, Security -from sqlalchemy.orm import Session +from sqlalchemy import not_ +from sqlalchemy.orm import Session, contains_eager, joinedload from sqlalchemy.sql.expression import func, select from ...core.security import get_current_active_user as get_user from ...core.session import get_finish_date, get_start_date, set_period from ...db.session import SessionFuture from ...models.account_base import AccountBase +from ...models.account_type import AccountType from ...models.journal import Journal from ...models.voucher import Voucher from ...models.voucher_type import VoucherType @@ -48,15 +50,23 @@ def build_report(date_: date, db: Session) -> List[schemas.TrialBalanceItem]: select(AccountBase, amount_sum) .join(Journal.voucher) .join(Journal.account) - .where(Voucher.date <= date_, Voucher.type != VoucherType.by_name("Issue").id) - .group_by(AccountBase) - .order_by(AccountBase.type, func.abs(amount_sum).desc()) + .join(AccountBase.type_) + .where( + Voucher.date <= date_, + not_(Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK])), + ) + .group_by(AccountBase, AccountType) + .order_by(AccountBase.type_id, func.abs(amount_sum).desc()) + .options( + joinedload(AccountBase.type_, innerjoin=True), + contains_eager(AccountBase.type_), + ) ).all() body = [] for account, amount in query: if amount > 0: - body.append(schemas.TrialBalanceItem(type=account.type_object.name, name=account.name, debit=amount)) + body.append(schemas.TrialBalanceItem(type=account.type_.name, name=account.name, debit=amount)) if amount < 0: - body.append(schemas.TrialBalanceItem(type=account.type_object.name, name=account.name, credit=amount)) + body.append(schemas.TrialBalanceItem(type=account.type_.name, name=account.name, credit=amount)) return body diff --git a/brewman/brewman/routers/role.py b/brewman/brewman/routers/role.py index 86182e3e..0e700455 100644 --- a/brewman/brewman/routers/role.py +++ b/brewman/brewman/routers/role.py @@ -63,8 +63,7 @@ def update_route( def add_permissions(role: Role, permissions: List[schemas.PermissionItem], db: Session) -> None: for permission in permissions: - gp = [p for p in role.permissions if p.id == permission.id_] - gp = None if len(gp) == 0 else gp[0] + gp = next((p for p in role.permissions if p.id == permission.id_), None) if permission.enabled and gp is None: role.permissions.append(db.execute(select(Permission).where(Permission.id == permission.id_)).scalar_one()) elif not permission.enabled and gp: diff --git a/brewman/brewman/routers/user.py b/brewman/brewman/routers/user.py index 5d1d05f2..d4da86b4 100644 --- a/brewman/brewman/routers/user.py +++ b/brewman/brewman/routers/user.py @@ -99,8 +99,7 @@ def update_route( def add_roles(user: User, roles: List[schemas.RoleItem], db: Session) -> None: for role in roles: - ug = [g for g in user.roles if g.id == role.id_] - ug = None if len(ug) == 0 else ug[0] + ug = next((g for g in user.roles if g.id == role.id_), None) if role.enabled and ug is None: user.roles.append(db.execute(select(Role).where(Role.id == role.id_)).scalar_one()) elif not role.enabled and ug: diff --git a/brewman/brewman/routers/voucher.py b/brewman/brewman/routers/voucher.py index 4da1f196..42bc2b5b 100644 --- a/brewman/brewman/routers/voucher.py +++ b/brewman/brewman/routers/voucher.py @@ -42,14 +42,14 @@ def post_voucher( voucher: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one() account_types = ( db.execute( - select(distinct(AccountBase.type)).where( + select(distinct(AccountBase.type_id)).where( AccountBase.id.in_([vj.account_id for vj in voucher.journals]) ) ) .scalars() .all() ) - allowed, message = get_lock_info([voucher.date], voucher.type, account_types, db) + allowed, message = get_lock_info([voucher.date], voucher.voucher_type, account_types, db) if not allowed: raise HTTPException( status_code=status.HTTP_423_LOCKED, @@ -67,7 +67,7 @@ def post_voucher( def check_delete_permissions(voucher: Voucher, user: UserToken): - voucher_type = VoucherType.by_id(voucher.type).name.replace(" ", "-").lower() + voucher_type = voucher.voucher_type.name.replace("_", "-").lower() if voucher_type in ["payment", "receipt"]: voucher_type = "journal" if voucher.posted and "edit-posted-vouchers" not in user.permissions: @@ -83,7 +83,7 @@ def check_delete_permissions(voucher: Voucher, user: UserToken): elif voucher_type not in user.permissions: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail=f"You are not allowed ({VoucherType.by_id(voucher.type).name}) vouchers", + detail=f"You are not allowed ({voucher.voucher_type.name.replace('_', ' ').title()}) vouchers", ) @@ -98,12 +98,14 @@ def delete_voucher( check_delete_permissions(voucher, user) account_types = ( db.execute( - select(distinct(AccountBase.type)).where(AccountBase.id.in_([vj.account_id for vj in voucher.journals])) + select(distinct(AccountBase.type_id)).where( + AccountBase.id.in_([vj.account_id for vj in voucher.journals]) + ) ) .scalars() .all() ) - allowed, message = get_lock_info([voucher.date], voucher.type, account_types, db) + allowed, message = get_lock_info([voucher.date], voucher.voucher_type, account_types, db) if not allowed: raise HTTPException( status_code=status.HTTP_423_LOCKED, @@ -111,7 +113,7 @@ def delete_voucher( ) json_voucher = voucher_info(voucher, db) batches_to_delete = [] - if voucher.type == VoucherType.by_name("Issue").id: + if voucher.voucher_type == VoucherType.ISSUE: for item in voucher.journals: if item.debit == 1: destination = item.cost_centre_id @@ -138,7 +140,7 @@ def delete_voucher( f"So it cannot be deleted", ) item.batch.quantity_remaining -= item.quantity - elif voucher.type == VoucherType.by_name("Purchase").id: + elif voucher.voucher_type == VoucherType.PURCHASE: for item in voucher.inventories: uses = db.execute( select(func.count(Inventory.id)).where(Inventory.batch_id == item.batch.id, Inventory.id != item.id) @@ -149,7 +151,7 @@ def delete_voucher( detail=f"{item.product.name} has been issued and cannot be deleted", ) batches_to_delete.append(item.batch) - elif voucher.type == VoucherType.by_name("Purchase Return").id: + elif voucher.voucher_type == VoucherType.PURCHASE_RETURN: for item in voucher.inventories: item.batch.quantity_remaining += item.quantity for b in batches_to_delete: @@ -158,15 +160,16 @@ def delete_voucher( for image in images: db.delete(image) db.commit() + json_voucher["type"] = VoucherType[json_voucher["type"].replace(" ", "_").upper()] return blank_voucher(info=json_voucher, db=db) -def voucher_info(voucher, db): +def voucher_info(voucher, db: Session): json_voucher = { "id": voucher.id, "date": voucher.date.strftime("%d-%b-%Y"), "isStarred": voucher.is_starred, - "type": VoucherType.by_id(voucher.type).name, + "type": voucher.voucher_type.name, "posted": voucher.posted, "narration": voucher.narration, "journals": [], @@ -182,15 +185,15 @@ def voucher_info(voucher, db): } if voucher.reconcile_date is not None: json_voucher["reconcileDate"] = voucher.reconcile_date.strftime("%d-%b-%Y") - if voucher.type == 2: # "Purchase" + if voucher.voucher_type == VoucherType.PURCHASE: item = [j for j in voucher.journals if j.debit == -1][0] json_voucher["vendor"] = {"id": item.account.id, "name": item.account.name} - elif voucher.type == 3: # "Issue" + elif voucher.voucher_type == VoucherType.ISSUE: item = [j for j in voucher.journals if j.debit == -1][0] json_voucher["source"] = {"id": item.cost_centre_id, "name": ""} item = [j for j in voucher.journals if j.debit == 1][0] json_voucher["destination"] = {"id": item.cost_centre_id, "name": ""} - if voucher.type == 6: # "Purchase Return" + if voucher.voucher_type == VoucherType.PURCHASE_RETURN: item = [j for j in voucher.journals if j.debit == 1][0] json_voucher["vendor"] = {"id": item.account.id, "name": item.account.name} else: @@ -279,7 +282,7 @@ def voucher_info(voucher, db): return json_voucher -def blank_voucher(info, db): +def blank_voucher(info, db: Session): if "type" not in info: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -292,7 +295,7 @@ def blank_voucher(info, db): detail="Date cannot be null", ) json_voucher = { - "type": type_, + "type": type_.name, "date": info["date"], "isStarred": False, "posted": False, @@ -301,9 +304,9 @@ def blank_voucher(info, db): "inventories": [], "employeeBenefits": [], } - if type_ == "Journal": + if type_ == VoucherType.JOURNAL: pass - elif type_ == "Payment": + elif type_ == VoucherType.PAYMENT: account = None if info and "account" in info and info["account"]: account = ( @@ -316,7 +319,7 @@ def blank_voucher(info, db): else: account = AccountBase.cash_in_hand() json_voucher["journals"].append({"account": account, "amount": 0, "debit": -1}) - elif type_ == "Receipt": + elif type_ == VoucherType.RECEIPT: account = None if info and "account" in info and info["account"]: account = ( @@ -329,12 +332,12 @@ def blank_voucher(info, db): else: account = AccountBase.cash_in_hand() json_voucher["journals"].append({"account": account, "amount": 0, "debit": 1}) - elif type_ == "Purchase": + elif type_ == VoucherType.PURCHASE: json_voucher["vendor"] = AccountBase.local_purchase() - elif type_ == "Purchase Return": + elif type_ == VoucherType.PURCHASE_RETURN: json_voucher["vendor"] = AccountBase.local_purchase() - elif type_ == "Issue": + elif type_ == VoucherType.ISSUE: if "source" in info: json_voucher["source"] = {"id": info["source"]} else: @@ -343,9 +346,9 @@ def blank_voucher(info, db): json_voucher["destination"] = {"id": info["destination"]} else: json_voucher["destination"] = {"id": CostCentre.cost_centre_kitchen()} - elif type_ == "Employee Benefit": + elif type_ == VoucherType.EMPLOYEE_BENEFIT: json_voucher["employeeBenefits"] = [] - elif type_ == "Incentive": + elif type_ == VoucherType.INCENTIVE: json_voucher["incentives"], json_voucher["incentive"] = incentive_employees(info["date"], db) else: raise HTTPException( @@ -400,12 +403,12 @@ def incentive_employees(date_, db: Session): .join(Journal.voucher) .where( Journal.account_id == Account.incentive_id(), - Voucher.type != VoucherType.by_name("Issue").id, + Voucher.voucher_type != VoucherType.ISSUE, or_( Voucher.date <= finish_date, and_( Voucher.date == finish_date, - Voucher.type != VoucherType.by_name("Incentive").id, + Voucher.voucher_type != VoucherType.INCENTIVE, ), ), ) @@ -436,4 +439,4 @@ def get_batch_quantity(id_: uuid.UUID, voucher_id: Optional[uuid.UUID], db: Sess ) if voucher_id is not None: query = query.where(Voucher.id != voucher_id) - return db.execute(query).scalar_one() or 0 + return db.execute(query).scalar_one() or Decimal(0) diff --git a/brewman/brewman/routers/voucher_types.py b/brewman/brewman/routers/voucher_types.py index efafee93..361c24c4 100644 --- a/brewman/brewman/routers/voucher_types.py +++ b/brewman/brewman/routers/voucher_types.py @@ -14,4 +14,4 @@ router = APIRouter() @router.get("", response_model=List[schemas.AccountType]) def account_type_list(user: UserToken = Depends(get_user)) -> List[schemas.AccountType]: - return [schemas.AccountType(id=item.id, name=item.name) for item in VoucherType.list()] + return [schemas.AccountType(id=item.value, name=item.name) for item in list(VoucherType)] diff --git a/brewman/brewman/schemas/account.py b/brewman/brewman/schemas/account.py index 019993d0..8cbb5941 100644 --- a/brewman/brewman/schemas/account.py +++ b/brewman/brewman/schemas/account.py @@ -28,9 +28,12 @@ class AccountBase(BaseModel): class AccountIn(AccountBase): - type: int + type_: int is_reconcilable: bool + class Config: + alias_generator = to_camel + class Account(AccountIn): id_: uuid.UUID diff --git a/brewman/brewman/schemas/closing_stock.py b/brewman/brewman/schemas/closing_stock.py index ec5e847e..356ecbdc 100644 --- a/brewman/brewman/schemas/closing_stock.py +++ b/brewman/brewman/schemas/closing_stock.py @@ -1,18 +1,26 @@ +import uuid + from datetime import date, datetime from decimal import Decimal -from typing import List +from typing import List, Optional from pydantic import validator from pydantic.main import BaseModel from . import to_camel +from .cost_centre import CostCentreLink +from .product import ProductLink +from .user_link import UserLink class ClosingStockItem(BaseModel): - product: str + id_: Optional[uuid.UUID] + product: ProductLink group: str quantity: Decimal amount: Decimal + physical: Decimal + cost_centre: Optional[CostCentreLink] class Config: anystr_strip_whitespace = True @@ -21,7 +29,13 @@ class ClosingStockItem(BaseModel): class ClosingStock(BaseModel): date_: date - body: List[ClosingStockItem] + cost_centre: Optional[CostCentreLink] + items: List[ClosingStockItem] + creation_date: Optional[datetime] + last_edit_date: Optional[datetime] + user: Optional[UserLink] + posted: bool + poster: Optional[UserLink] class Config: anystr_strip_whitespace = True diff --git a/brewman/brewman/schemas/product.py b/brewman/brewman/schemas/product.py index a3a835d9..089a948e 100644 --- a/brewman/brewman/schemas/product.py +++ b/brewman/brewman/schemas/product.py @@ -40,7 +40,7 @@ class Product(ProductIn): class ProductBlank(ProductIn): name: str skus: List[StockKeepingUnit] - product_group: Optional[ProductGroupLink] + product_group: Optional[ProductGroupLink] # type: ignore[assignment] is_fixture: bool class Config: diff --git a/brewman/brewman/schemas/user.py b/brewman/brewman/schemas/user.py index b63652e8..6eaa179c 100644 --- a/brewman/brewman/schemas/user.py +++ b/brewman/brewman/schemas/user.py @@ -45,7 +45,7 @@ class UserList(BaseModel): class UserToken(BaseModel): id_: uuid.UUID name: str - locked_out: bool = None + locked_out: bool password: str permissions: List[str] diff --git a/brewman/pyproject.toml b/brewman/pyproject.toml index b04cff23..9270d80e 100644 --- a/brewman/pyproject.toml +++ b/brewman/pyproject.toml @@ -25,6 +25,7 @@ flake8 = "^3.9.2" black = "^21.8b0" isort = {extras = ["toml"], version = "^5.9.3"} pre-commit = "^2.15.0" +mypy = "^0.910" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/overlord/src/app/closing-stock/closing-stock-datasource.ts b/overlord/src/app/closing-stock/closing-stock-datasource.ts index e5b5e960..d8bbc30b 100644 --- a/overlord/src/app/closing-stock/closing-stock-datasource.ts +++ b/overlord/src/app/closing-stock/closing-stock-datasource.ts @@ -65,9 +65,7 @@ export class ClosingStockDataSource extends DataSource { const isAsc = sort.direction === 'asc'; switch (sort.active) { case 'product': - return compare(a.product, b.product, isAsc); - case 'group': - return compare(a.group, b.group, isAsc); + return compare(`${a.group} - ${a.product}`, `${b.group} - ${b.product}`, isAsc); case 'quantity': return compare(+a.quantity, +b.quantity, isAsc); case 'amount': diff --git a/overlord/src/app/closing-stock/closing-stock-item.ts b/overlord/src/app/closing-stock/closing-stock-item.ts index 54c3bb5b..219ef3c6 100644 --- a/overlord/src/app/closing-stock/closing-stock-item.ts +++ b/overlord/src/app/closing-stock/closing-stock-item.ts @@ -1,14 +1,24 @@ +import { CostCentre } from '../core/cost-centre'; +import { Product } from '../core/product'; + export class ClosingStockItem { - product: string; + id: string | null; + product: Product; group: string; quantity: number; amount: number; + physical: number; + variance: number; + costCentre?: CostCentre; public constructor(init?: Partial) { - this.product = ''; + this.id = null; + this.product = new Product(); this.group = ''; this.quantity = 0; this.amount = 0; + this.variance = 0; + this.physical = 0; Object.assign(this, init); } } diff --git a/overlord/src/app/closing-stock/closing-stock-resolver.service.ts b/overlord/src/app/closing-stock/closing-stock-resolver.service.ts index 1301e2d2..f1afef9e 100644 --- a/overlord/src/app/closing-stock/closing-stock-resolver.service.ts +++ b/overlord/src/app/closing-stock/closing-stock-resolver.service.ts @@ -13,6 +13,7 @@ export class ClosingStockResolver implements Resolve { resolve(route: ActivatedRouteSnapshot): Observable { const date = route.paramMap.get('date'); - return this.ser.list(date); + const costCentre = route.queryParamMap.get('d') || null; + return this.ser.list(date, costCentre); } } diff --git a/overlord/src/app/closing-stock/closing-stock-routing.module.ts b/overlord/src/app/closing-stock/closing-stock-routing.module.ts index 88490196..38403d78 100644 --- a/overlord/src/app/closing-stock/closing-stock-routing.module.ts +++ b/overlord/src/app/closing-stock/closing-stock-routing.module.ts @@ -3,6 +3,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { AuthGuard } from '../auth/auth-guard.service'; +import { CostCentreListResolver } from '../cost-centre/cost-centre-list-resolver.service'; import { ClosingStockResolver } from './closing-stock-resolver.service'; import { ClosingStockComponent } from './closing-stock.component'; @@ -17,7 +18,9 @@ const closingStockRoutes: Routes = [ }, resolve: { info: ClosingStockResolver, + costCentres: CostCentreListResolver, }, + runGuardsAndResolvers: 'always', }, { path: ':date', @@ -28,7 +31,9 @@ const closingStockRoutes: Routes = [ }, resolve: { info: ClosingStockResolver, + costCentres: CostCentreListResolver, }, + runGuardsAndResolvers: 'always', }, ]; diff --git a/overlord/src/app/closing-stock/closing-stock.component.css b/overlord/src/app/closing-stock/closing-stock.component.css index a9626b3c..bfd7f853 100644 --- a/overlord/src/app/closing-stock/closing-stock.component.css +++ b/overlord/src/app/closing-stock/closing-stock.component.css @@ -2,3 +2,16 @@ display: flex; justify-content: flex-end; } + +.first { + margin-right: 4px; +} + +.middle { + margin-left: 4px; + margin-right: 4px; +} + +.last { + margin-left: 4px; +} diff --git a/overlord/src/app/closing-stock/closing-stock.component.html b/overlord/src/app/closing-stock/closing-stock.component.html index c8fd811d..6efe31b6 100644 --- a/overlord/src/app/closing-stock/closing-stock.component.html +++ b/overlord/src/app/closing-stock/closing-stock.component.html @@ -14,7 +14,15 @@ fxLayoutGap.lt-md="0px" fxLayoutAlign="space-around start" > - + + Department + + + {{ at.name }} + + + + + + + + Product + {{ row.product.name }} + + + + + Group + {{ row.group }} + + + + + Closing Stock + {{ + row.quantity | number: '0.2-2' + }} + + + + + Physical Stock + + + Physical + + + + + + + + Variance + {{ + row.quantity - row.physical | number: '0.2-2' + }} + + + + + Department + + + Department + + + {{ at.name }} + + + + + + + + + Amount + {{ + row.amount | currency: 'INR' + }} + + + + + + + + - - - - Product - {{ row.product }} - - - - - Group - {{ row.group }} - - - - - Quantity - {{ - row.quantity | number: '0.2-2' - }} - - - - - Amount - {{ row.amount | currency: 'INR' }} - - - - - - - - + + + + + diff --git a/overlord/src/app/closing-stock/closing-stock.component.ts b/overlord/src/app/closing-stock/closing-stock.component.ts index 6a299d48..0b130aec 100644 --- a/overlord/src/app/closing-stock/closing-stock.component.ts +++ b/overlord/src/app/closing-stock/closing-stock.component.ts @@ -1,14 +1,23 @@ import { Component, OnInit, ViewChild } from '@angular/core'; -import { FormBuilder, FormGroup } from '@angular/forms'; +import { FormArray, FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; import { MatPaginator } from '@angular/material/paginator'; +import { MatSelectChange } from '@angular/material/select'; import { MatSort } from '@angular/material/sort'; import { ActivatedRoute, Router } from '@angular/router'; import * as moment from 'moment'; +import { AuthService } from '../auth/auth.service'; +import { CostCentre } from '../core/cost-centre'; +import { ToasterService } from '../core/toaster.service'; +import { User } from '../core/user'; +import { ConfirmDialogComponent } from '../shared/confirm-dialog/confirm-dialog.component'; import { ToCsvService } from '../shared/to-csv.service'; import { ClosingStock } from './closing-stock'; import { ClosingStockDataSource } from './closing-stock-datasource'; +import { ClosingStockItem } from './closing-stock-item'; +import { ClosingStockService } from './closing-stock.service'; @Component({ selector: 'app-closing-stock', @@ -19,37 +28,96 @@ export class ClosingStockComponent implements OnInit { @ViewChild(MatPaginator, { static: true }) paginator?: MatPaginator; @ViewChild(MatSort, { static: true }) sort?: MatSort; info: ClosingStock = new ClosingStock(); - dataSource: ClosingStockDataSource = new ClosingStockDataSource(this.info.body); + dataSource: ClosingStockDataSource = new ClosingStockDataSource(this.info.items); form: FormGroup; /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ - displayedColumns = ['product', 'group', 'quantity', 'amount']; + displayedColumns = [ + 'product', + 'group', + 'quantity', + 'physical', + 'variance', + 'department', + 'amount', + ]; + + costCentres: CostCentre[]; constructor( private route: ActivatedRoute, private router: Router, private fb: FormBuilder, private toCsv: ToCsvService, + private dialog: MatDialog, + private toaster: ToasterService, + public auth: AuthService, + private ser: ClosingStockService, ) { + this.costCentres = []; this.form = this.fb.group({ date: '', + costCentre: '', + stocks: this.fb.array([]), }); } ngOnInit() { this.route.data.subscribe((value) => { - const data = value as { info: ClosingStock }; - + const data = value as { info: ClosingStock; costCentres: CostCentre[] }; this.info = data.info; - this.form.setValue({ + this.costCentres = data.costCentres; + this.form.patchValue({ date: moment(this.info.date, 'DD-MMM-YYYY').toDate(), + costCentre: this.info.costCentre.id, }); + this.form.setControl( + 'stocks', + this.fb.array( + this.info.items.map((x) => + this.fb.group({ + physical: '' + x.physical, + costCentre: x.costCentre?.id, + }), + ), + ), + ); + this.dataSource = new ClosingStockDataSource(this.info.items, this.paginator, this.sort); }); - this.dataSource = new ClosingStockDataSource(this.info.body, this.paginator, this.sort); } show() { const info = this.getInfo(); - this.router.navigate(['closing-stock', info.date]); + this.router.navigate(['closing-stock', info.date], { + queryParams: { + d: info.costCentre.id, + }, + }); + } + + save() { + this.ser.save(this.getClosingStock()).subscribe( + () => { + this.toaster.show('Success', ''); + }, + (error) => { + this.toaster.show('Danger', error); + }, + ); + } + + getClosingStock(): ClosingStock { + const formModel = this.form.value; + this.info.date = moment(formModel.date).format('DD-MMM-YYYY'); + const array = this.form.get('stocks') as FormArray; + this.info.items.forEach((item, index) => { + item.physical = +array.controls[index].value.physical; + item.costCentre = + array.controls[index].value.costCentre == null + ? undefined + : new CostCentre({ id: array.controls[index].value.costCentre }); + }); + console.log('getClosingStock', this.info); + return this.info; } getInfo(): ClosingStock { @@ -57,6 +125,7 @@ export class ClosingStockComponent implements OnInit { return new ClosingStock({ date: moment(formModel.date).format('DD-MMM-YYYY'), + costCentre: new CostCentre({ id: formModel.costCentre }), }); } @@ -78,4 +147,69 @@ export class ClosingStockComponent implements OnInit { link.click(); document.body.removeChild(link); } + + updatePhysical($event: Event, row: ClosingStockItem) { + row.physical = +($event.target as HTMLInputElement).value; + } + + updateDepartment($event: MatSelectChange, row: ClosingStockItem) { + row.costCentre = new CostCentre({ id: $event.value }); + } + + canDelete() { + return this.info.items.find((x) => !!x.id) !== undefined; + } + + canSave() { + if (this.info.items.find((x) => !!x.id) !== undefined) { + return true; + } + if (this.info.posted && this.auth.allowed('edit-posted-vouchers')) { + return true; + } + return ( + this.info.user.id === (this.auth.user as User).id || + this.auth.allowed("edit-other-user's-vouchers") + ); + } + + post() { + this.ser.post(this.info.date, this.info.costCentre.id as string).subscribe( + (result) => { + // this.loadVoucher(result); + this.toaster.show('Success', 'Voucher Posted'); + }, + (error) => { + this.toaster.show('Danger', error); + }, + ); + } + + delete() { + this.ser.delete(this.info.date, this.info.costCentre.id as string).subscribe( + () => { + this.toaster.show('Success', ''); + this.router.navigate(['/closing-stock'], { replaceUrl: true }); + }, + (error) => { + this.toaster.show('Danger', error); + }, + ); + } + + confirmDelete(): void { + const dialogRef = this.dialog.open(ConfirmDialogComponent, { + width: '250px', + data: { + title: 'Delete Closing Stock information?', + content: 'Are you sure? This cannot be undone.', + }, + }); + + dialogRef.afterClosed().subscribe((result: boolean) => { + if (result) { + this.delete(); + } + }); + } } diff --git a/overlord/src/app/closing-stock/closing-stock.module.ts b/overlord/src/app/closing-stock/closing-stock.module.ts index 838b8a03..e6602a6e 100644 --- a/overlord/src/app/closing-stock/closing-stock.module.ts +++ b/overlord/src/app/closing-stock/closing-stock.module.ts @@ -19,6 +19,7 @@ import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatPaginatorModule } from '@angular/material/paginator'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatSelectModule } from '@angular/material/select'; import { MatSortModule } from '@angular/material/sort'; import { MatTableModule } from '@angular/material/table'; @@ -59,6 +60,7 @@ export const MY_FORMATS = { ReactiveFormsModule, SharedModule, ClosingStockRoutingModule, + MatSelectModule, ], declarations: [ClosingStockComponent], providers: [ diff --git a/overlord/src/app/closing-stock/closing-stock.service.ts b/overlord/src/app/closing-stock/closing-stock.service.ts index feb05d6d..dd76fd3a 100644 --- a/overlord/src/app/closing-stock/closing-stock.service.ts +++ b/overlord/src/app/closing-stock/closing-stock.service.ts @@ -1,9 +1,10 @@ -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/internal/Observable'; import { catchError } from 'rxjs/operators'; import { ErrorLoggerService } from '../core/error-logger.service'; +import { Voucher } from '../core/voucher'; import { ClosingStock } from './closing-stock'; @@ -16,10 +17,38 @@ const serviceName = 'ClosingStockService'; export class ClosingStockService { constructor(private http: HttpClient, private log: ErrorLoggerService) {} - list(date: string | null): Observable { + list(date: string | null, costCentre: string | null): Observable { const listUrl = date === null ? url : `${url}/${date}`; + const options = { params: new HttpParams() }; + if (costCentre !== null) { + options.params = options.params.set('d', costCentre); + } return this.http - .get(listUrl) + .get(listUrl, options) .pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable; } + + save(closingStock: ClosingStock): Observable { + return this.http + .post(url, closingStock) + .pipe(catchError(this.log.handleError(serviceName, 'save'))) as Observable; + } + + post(date: string, costCentre: string): Observable { + const options = { params: new HttpParams().set('d', costCentre) }; + return this.http + .post(`${url}/${date}`, {}, options) + .pipe( + catchError(this.log.handleError(serviceName, 'Post Voucher')), + ) as Observable; + } + + delete(date: string, costCentre: string): Observable { + const options = { params: new HttpParams().set('d', costCentre) }; + return this.http + .delete(`${url}/${date}`, options) + .pipe( + catchError(this.log.handleError(serviceName, 'Delete Voucher')), + ) as Observable; + } } diff --git a/overlord/src/app/closing-stock/closing-stock.ts b/overlord/src/app/closing-stock/closing-stock.ts index 682643e4..69de8c00 100644 --- a/overlord/src/app/closing-stock/closing-stock.ts +++ b/overlord/src/app/closing-stock/closing-stock.ts @@ -1,12 +1,27 @@ +import { CostCentre } from '../core/cost-centre'; +import { User } from '../core/user'; + import { ClosingStockItem } from './closing-stock-item'; export class ClosingStock { date: string; - body: ClosingStockItem[]; + costCentre: CostCentre; + items: ClosingStockItem[]; + creationDate: string; + lastEditDate: string; + user: User; + posted: boolean; + poster: User; public constructor(init?: Partial) { this.date = ''; - this.body = []; + this.costCentre = new CostCentre(); + this.items = []; + this.creationDate = ''; + this.lastEditDate = ''; + this.user = new User(); + this.posted = false; + this.poster = new User(); Object.assign(this, init); } } diff --git a/overlord/src/app/core/product.ts b/overlord/src/app/core/product.ts index 4b9d8118..793fece9 100644 --- a/overlord/src/app/core/product.ts +++ b/overlord/src/app/core/product.ts @@ -27,8 +27,7 @@ export class Product { name: string; skus: StockKeepingUnit[]; price: number | undefined; - tax: number | undefined; - discount: number | undefined; + fractionUnits: string | undefined; isActive: boolean; isFixture: boolean; diff --git a/overlord/src/app/core/voucher.service.ts b/overlord/src/app/core/voucher.service.ts index 7b353d82..724d4f85 100644 --- a/overlord/src/app/core/voucher.service.ts +++ b/overlord/src/app/core/voucher.service.ts @@ -71,7 +71,7 @@ export class VoucherService { } saveOrUpdate(voucher: Voucher): Observable { - const endpoint = voucher.type.replace(/ /g, '-').toLowerCase(); + const endpoint = voucher.type.replace(/_/g, '-').toLowerCase(); if (!voucher.id) { return this.save(voucher, endpoint); } diff --git a/overlord/src/app/product-ledger/product-ledger.component.ts b/overlord/src/app/product-ledger/product-ledger.component.ts index 61154162..52c147fc 100644 --- a/overlord/src/app/product-ledger/product-ledger.component.ts +++ b/overlord/src/app/product-ledger/product-ledger.component.ts @@ -71,7 +71,7 @@ export class ProductLedgerComponent implements OnInit, AfterViewInit { debounceTime(150), distinctUntilChanged(), switchMap((x) => - x === null ? observableOf([]) : this.productSer.autocomplete(x, false, false), + x === null ? observableOf([]) : this.productSer.autocomplete(x, null, false, false), ), ); } diff --git a/overlord/src/app/product/product-resolver.service.ts b/overlord/src/app/product/product-resolver.service.ts index 2145068a..2d7916d2 100644 --- a/overlord/src/app/product/product-resolver.service.ts +++ b/overlord/src/app/product/product-resolver.service.ts @@ -10,7 +10,7 @@ import { ProductService } from './product.service'; providedIn: 'root', }) export class ProductResolver implements Resolve { - constructor(private ser: ProductService, private router: Router) {} + constructor(private ser: ProductService) {} resolve(route: ActivatedRouteSnapshot): Observable { const id = route.paramMap.get('id'); diff --git a/overlord/src/app/product/product.service.ts b/overlord/src/app/product/product.service.ts index 43e0e2f6..6e5d15b5 100644 --- a/overlord/src/app/product/product.service.ts +++ b/overlord/src/app/product/product.service.ts @@ -53,6 +53,7 @@ export class ProductService { autocomplete( query: string, + isPurchased: boolean | null, extended: boolean = false, skus: boolean = true, date?: string, @@ -61,6 +62,9 @@ export class ProductService { const options = { params: new HttpParams().set('q', query).set('e', extended.toString()).set('s', skus), }; + if (isPurchased !== null) { + options.params = options.params.set('p', isPurchased.toString()); + } if (!!vendorId && !!date) { options.params = options.params.set('v', vendorId as string).set('d', date as string); } diff --git a/overlord/src/app/purchase/purchase-dialog.component.ts b/overlord/src/app/purchase/purchase-dialog.component.ts index 9d500e9c..9ea6ede5 100644 --- a/overlord/src/app/purchase/purchase-dialog.component.ts +++ b/overlord/src/app/purchase/purchase-dialog.component.ts @@ -42,7 +42,7 @@ export class PurchaseDialogComponent implements OnInit { map((x) => (x !== null && x.length >= 1 ? x : null)), debounceTime(150), distinctUntilChanged(), - switchMap((x) => (x === null ? observableOf([]) : this.productSer.autocomplete(x))), + switchMap((x) => (x === null ? observableOf([]) : this.productSer.autocomplete(x, true))), ); } diff --git a/overlord/src/app/purchase/purchase.component.ts b/overlord/src/app/purchase/purchase.component.ts index f5c4f5a2..56bb0e84 100644 --- a/overlord/src/app/purchase/purchase.component.ts +++ b/overlord/src/app/purchase/purchase.component.ts @@ -100,6 +100,7 @@ export class PurchaseComponent implements OnInit, AfterViewInit, OnDestroy { ? observableOf([]) : this.productSer.autocomplete( x, + true, false, true, moment(this.form.value.date).format('DD-MMM-YYYY'), diff --git a/overlord/src/app/rate-contract/rate-contract-detail/rate-contract-detail.component.ts b/overlord/src/app/rate-contract/rate-contract-detail/rate-contract-detail.component.ts index 94f38f05..ef172cf0 100644 --- a/overlord/src/app/rate-contract/rate-contract-detail/rate-contract-detail.component.ts +++ b/overlord/src/app/rate-contract/rate-contract-detail/rate-contract-detail.component.ts @@ -77,7 +77,7 @@ export class RateContractDetailComponent implements OnInit, AfterViewInit { map((x) => (x !== null && x.length >= 1 ? x : null)), debounceTime(150), distinctUntilChanged(), - switchMap((x) => (x === null ? observableOf([]) : this.productSer.autocomplete(x))), + switchMap((x) => (x === null ? observableOf([]) : this.productSer.autocomplete(x, true))), ); }