From dee053c115bc2f8db72539be82e720552e1cc35f Mon Sep 17 00:00:00 2001 From: tanshu Date: Sat, 11 Sep 2021 15:43:18 +0530 Subject: [PATCH] Added: Rate Contract Module. To implement: Checking this during purchase. --- .../versions/0363f582ab28_rename_auth.py | 2 - .../versions/071e8f29d257_rate_contracts.py | 71 ++++++ .../versions/0bf3d70ee7de_initial_commit.py | 2 +- .../eed0b382c287_lowercase_fastapi.py | 2 +- brewman/brewman/db/base.py | 2 + brewman/brewman/main.py | 2 + brewman/brewman/models/account_base.py | 2 + brewman/brewman/models/rate_contract.py | 142 +++--------- brewman/brewman/models/rate_contract_item.py | 121 ++-------- brewman/brewman/routers/rate_contract.py | 187 +++++++++++++++ brewman/brewman/routers/voucher.py | 4 +- brewman/brewman/schemas/rate_contract.py | 91 ++++++++ brewman/brewman/schemas/rate_contract_item.py | 17 ++ overlord/src/app/app-routing.module.ts | 5 + .../app/core/nav-bar/nav-bar.component.html | 1 + .../rate-contract/rate-contact.module.spec.ts | 13 ++ .../rate-contract-detail-datasource.ts | 16 ++ .../rate-contract-detail.component.css | 3 + .../rate-contract-detail.component.html | 164 +++++++++++++ .../rate-contract-detail.component.spec.ts | 29 +++ .../rate-contract-detail.component.ts | 219 ++++++++++++++++++ .../app/rate-contract/rate-contract-item.ts | 13 ++ ...ate-contract-list-resolver.service.spec.ts | 21 ++ .../rate-contract-list-resolver.service.ts | 17 ++ .../rate-contract-list-datasource.ts | 74 ++++++ .../rate-contract-list.component.css | 0 .../rate-contract-list.component.html | 52 +++++ .../rate-contract-list.component.spec.ts | 24 ++ .../rate-contract-list.component.ts | 33 +++ .../rate-contract-resolver.service.spec.ts | 18 ++ .../rate-contract-resolver.service.ts | 18 ++ .../rate-contract-routing.module.spec.ts | 13 ++ .../rate-contract-routing.module.ts | 53 +++++ .../app/rate-contract/rate-contract.module.ts | 71 ++++++ .../rate-contract.service.spec.ts | 17 ++ .../rate-contract/rate-contract.service.ts | 58 +++++ .../src/app/rate-contract/rate-contract.ts | 30 +++ 37 files changed, 1391 insertions(+), 216 deletions(-) create mode 100644 brewman/alembic/versions/071e8f29d257_rate_contracts.py create mode 100644 brewman/brewman/routers/rate_contract.py create mode 100644 brewman/brewman/schemas/rate_contract.py create mode 100644 brewman/brewman/schemas/rate_contract_item.py create mode 100644 overlord/src/app/rate-contract/rate-contact.module.spec.ts create mode 100644 overlord/src/app/rate-contract/rate-contract-detail/rate-contract-detail-datasource.ts create mode 100644 overlord/src/app/rate-contract/rate-contract-detail/rate-contract-detail.component.css create mode 100644 overlord/src/app/rate-contract/rate-contract-detail/rate-contract-detail.component.html create mode 100644 overlord/src/app/rate-contract/rate-contract-detail/rate-contract-detail.component.spec.ts create mode 100644 overlord/src/app/rate-contract/rate-contract-detail/rate-contract-detail.component.ts create mode 100644 overlord/src/app/rate-contract/rate-contract-item.ts create mode 100644 overlord/src/app/rate-contract/rate-contract-list-resolver.service.spec.ts create mode 100644 overlord/src/app/rate-contract/rate-contract-list-resolver.service.ts create mode 100644 overlord/src/app/rate-contract/rate-contract-list/rate-contract-list-datasource.ts create mode 100644 overlord/src/app/rate-contract/rate-contract-list/rate-contract-list.component.css create mode 100644 overlord/src/app/rate-contract/rate-contract-list/rate-contract-list.component.html create mode 100644 overlord/src/app/rate-contract/rate-contract-list/rate-contract-list.component.spec.ts create mode 100644 overlord/src/app/rate-contract/rate-contract-list/rate-contract-list.component.ts create mode 100644 overlord/src/app/rate-contract/rate-contract-resolver.service.spec.ts create mode 100644 overlord/src/app/rate-contract/rate-contract-resolver.service.ts create mode 100644 overlord/src/app/rate-contract/rate-contract-routing.module.spec.ts create mode 100644 overlord/src/app/rate-contract/rate-contract-routing.module.ts create mode 100644 overlord/src/app/rate-contract/rate-contract.module.ts create mode 100644 overlord/src/app/rate-contract/rate-contract.service.spec.ts create mode 100644 overlord/src/app/rate-contract/rate-contract.service.ts create mode 100644 overlord/src/app/rate-contract/rate-contract.ts diff --git a/brewman/alembic/versions/0363f582ab28_rename_auth.py b/brewman/alembic/versions/0363f582ab28_rename_auth.py index eb446d00..13e0fa8e 100644 --- a/brewman/alembic/versions/0363f582ab28_rename_auth.py +++ b/brewman/alembic/versions/0363f582ab28_rename_auth.py @@ -5,10 +5,8 @@ Revises: ad8b2d208492 Create Date: 2021-09-11 05:32:56.683107 """ -import sqlalchemy as sa from alembic import op -from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. diff --git a/brewman/alembic/versions/071e8f29d257_rate_contracts.py b/brewman/alembic/versions/071e8f29d257_rate_contracts.py new file mode 100644 index 00000000..9534fccf --- /dev/null +++ b/brewman/alembic/versions/071e8f29d257_rate_contracts.py @@ -0,0 +1,71 @@ +"""rate contracts + +Revision ID: 071e8f29d257 +Revises: 0363f582ab28 +Create Date: 2021-09-11 05:57:49.062468 + +""" +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +from brewman.models.permission import Permission +from sqlalchemy.dialects import postgresql + + +revision = "071e8f29d257" +down_revision = "0363f582ab28" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.execute(Permission.__table__.insert().values(id="f97589c6-df9f-45d3-874d-4245a4d99ade", name="Rate Contracts")) + op.create_table( + "rate_contracts", + sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("date", sa.Date(), nullable=False), + sa.Column("vendor_id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("valid_from", sa.Date(), nullable=True), + sa.Column("valid_till", sa.Date(), nullable=True), + sa.Column("narration", sa.Unicode(length=1000), nullable=False), + sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("creation_date", sa.DateTime(), nullable=False), + sa.Column("last_edit_date", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(["user_id"], ["users.id"], name=op.f("fk_rate_contracts_user_id_users")), + sa.ForeignKeyConstraint(["vendor_id"], ["accounts.id"], name=op.f("fk_rate_contracts_vendor_id_accounts")), + sa.PrimaryKeyConstraint("id", name=op.f("pk_rate_contracts")), + ) + op.create_index(op.f("ix_rate_contracts_date"), "rate_contracts", ["date"], unique=False) + op.create_table( + "rate_contract_items", + sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("rate_contract_id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("product_id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("cost_price", sa.Numeric(precision=15, scale=2), nullable=False), + sa.ForeignKeyConstraint( + ["product_id"], ["products.id"], name=op.f("fk_rate_contract_items_product_id_products") + ), + sa.ForeignKeyConstraint( + ["rate_contract_id"], + ["rate_contracts.id"], + name=op.f("fk_rate_contract_items_rate_contract_id_rate_contracts"), + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_rate_contract_items")), + sa.UniqueConstraint("rate_contract_id", "product_id", name=op.f("uq_rate_contract_items_rate_contract_id")), + ) + op.create_index( + op.f("ix_rate_contract_items_rate_contract_id"), "rate_contract_items", ["rate_contract_id"], unique=False + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f("ix_rate_contract_items_rate_contract_id"), table_name="rate_contract_items") + op.drop_table("rate_contract_items") + op.drop_index(op.f("ix_rate_contracts_date"), table_name="rate_contracts") + op.drop_table("rate_contracts") + # ### end Alembic commands ### diff --git a/brewman/alembic/versions/0bf3d70ee7de_initial_commit.py b/brewman/alembic/versions/0bf3d70ee7de_initial_commit.py index ee835afa..d68aec72 100644 --- a/brewman/alembic/versions/0bf3d70ee7de_initial_commit.py +++ b/brewman/alembic/versions/0bf3d70ee7de_initial_commit.py @@ -1,7 +1,7 @@ """Initial Commit Revision ID: 0bf3d70ee7de -Revises: +Revises: Create Date: 2020-05-10 19:02:57.301086 """ diff --git a/brewman/alembic/versions/eed0b382c287_lowercase_fastapi.py b/brewman/alembic/versions/eed0b382c287_lowercase_fastapi.py index 33863e56..f3fefff1 100644 --- a/brewman/alembic/versions/eed0b382c287_lowercase_fastapi.py +++ b/brewman/alembic/versions/eed0b382c287_lowercase_fastapi.py @@ -273,7 +273,7 @@ def upgrade(): ) op.execute(permission.delete().where(permission.c.name.in_(["Payment", "Receipt"]))) - ### end Alembic commands ### + # ### end Alembic commands ### def downgrade(): diff --git a/brewman/brewman/db/base.py b/brewman/brewman/db/base.py index 7671ef98..521edec3 100644 --- a/brewman/brewman/db/base.py +++ b/brewman/brewman/db/base.py @@ -18,6 +18,8 @@ from ..models.login_history import LoginHistory # noqa: F401 from ..models.permission import Permission # noqa: F401 from ..models.product import Product # noqa: F401 from ..models.product_group import ProductGroup # noqa: F401 +from ..models.rate_contract import RateContract # noqa: F401 +from ..models.rate_contract_item import RateContractItem # noqa: F401 from ..models.recipe import Recipe # noqa: F401 from ..models.recipe_item import RecipeItem # noqa: F401 from ..models.role import Role # noqa: F401 diff --git a/brewman/brewman/main.py b/brewman/brewman/main.py index 649a9228..b6d6e57a 100644 --- a/brewman/brewman/main.py +++ b/brewman/brewman/main.py @@ -33,6 +33,7 @@ from .routers import ( product_group, purchase, purchase_return, + rate_contract, rebase, recipe, reset_stock, @@ -76,6 +77,7 @@ app.include_router(attendance_types.router, prefix="/api/attendance-types", tags app.include_router(employee_attendance.router, prefix="/api/employee-attendance", tags=["attendance"]) app.include_router(employee_attendance.router, prefix="/api/employee-attendance", tags=["attendance"]) app.include_router(attendance_report.router, prefix="/attendance-report", tags=["attendance"]) +app.include_router(rate_contract.router, prefix="/api/rate-contracts", tags=["products"]) app.include_router(cost_centre.router, prefix="/api/cost-centres", tags=["cost-centres"]) app.include_router(employee.router, prefix="/api/employees", tags=["employees"]) diff --git a/brewman/brewman/models/account_base.py b/brewman/brewman/models/account_base.py index 9df3eadf..2217b4e9 100644 --- a/brewman/brewman/models/account_base.py +++ b/brewman/brewman/models/account_base.py @@ -32,6 +32,8 @@ class AccountBase(Base): journals = relationship("Journal", back_populates="account") cost_centre = relationship("CostCentre", back_populates="accounts") + rate_contracts = relationship("RateContract", back_populates="vendor") + @property def __name__(self): return self.name diff --git a/brewman/brewman/models/rate_contract.py b/brewman/brewman/models/rate_contract.py index 1d4f8066..caa16d99 100644 --- a/brewman/brewman/models/rate_contract.py +++ b/brewman/brewman/models/rate_contract.py @@ -1,124 +1,50 @@ import uuid -from sqlalchemy import ( - Boolean, - Column, - ForeignKey, - Integer, - Numeric, - Unicode, - UniqueConstraint, - case, - func, - select, -) +from datetime import datetime + +from sqlalchemy import Column, Date, DateTime, ForeignKey, Unicode from sqlalchemy.dialects.postgresql import UUID -from sqlalchemy.ext.hybrid import hybrid_property -from sqlalchemy.orm import Session, relationship +from sqlalchemy.orm import relationship from .meta import Base -class Product(Base): - __tablename__ = "products" - __table_args__ = (UniqueConstraint("name", "units"),) +class RateContract(Base): + __tablename__ = "rate_contracts" id = Column("id", UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - code = Column("code", Integer, unique=True) - name = Column("name", Unicode(255), nullable=False) - units = Column("units", Unicode(255), nullable=False) - fraction = Column("fraction", Numeric(precision=15, scale=5), nullable=False) - fraction_units = Column("fraction_units", Unicode(255), nullable=False) - product_yield = Column("product_yield", Numeric(precision=15, scale=5), nullable=False) - product_group_id = Column( - "product_group_id", - UUID(as_uuid=True), - ForeignKey("product_groups.id"), - nullable=False, - ) - account_id = Column("account_id", UUID(as_uuid=True), ForeignKey("accounts.id"), nullable=False) - price = Column("cost_price", Numeric(precision=15, scale=2), nullable=False) - sale_price = Column("sale_price", Numeric(precision=15, scale=2), nullable=False) - is_active = Column("is_active", Boolean, nullable=False) - is_fixture = Column("is_fixture", Boolean, nullable=False) - is_purchased = Column("is_purchased", Boolean, nullable=False) - is_sold = Column("is_sold", Boolean, nullable=False) + date = Column("date", Date, nullable=False, index=True) + vendor_id = Column("vendor_id", UUID(as_uuid=True), ForeignKey("accounts.id"), nullable=False) + valid_from = Column("valid_from", Date(), nullable=True) + valid_till = Column("valid_till", Date(), nullable=True) + narration = Column("narration", Unicode(1000), nullable=False) - product_group = relationship("ProductGroup", back_populates="products") - batches = relationship("Batch", back_populates="product") - inventories = relationship("Inventory", back_populates="product") - recipes = relationship("Recipe", back_populates="product") - account = relationship("Account", primaryjoin="Account.id==Product.account_id", back_populates="products") + user_id = Column("user_id", UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) + creation_date = Column("creation_date", DateTime(), nullable=False) + last_edit_date = Column("last_edit_date", DateTime(), nullable=False) + + user = relationship("User") + vendor = relationship("AccountBase", back_populates="rate_contracts") + items = relationship("RateContractItem", back_populates="rate_contract") def __init__( self, - code=None, - name=None, - units=None, - fraction=None, - fraction_units=None, - product_yield=None, - product_group_id=None, - account_id=None, - price=None, - sale_price=None, - is_active=None, - is_purchased=None, - is_sold=None, + date=None, + vendor_id=None, + valid_from=None, + valid_till=None, + narration="", + user_id=None, + creation_date=None, + last_edit_date=None, id_=None, - is_fixture=False, ): - self.code = code - self.name = name - self.units = units - self.fraction = fraction - self.fraction_units = fraction_units - self.product_yield = product_yield - self.product_group_id = product_group_id - self.account_id = account_id - self.price = price - self.sale_price = sale_price - self.is_active = is_active - self.is_purchased = is_purchased - self.is_sold = is_sold + self.date = date + self.vendor_id = vendor_id + self.valid_from = valid_from + self.valid_till = valid_till + self.narration = narration + self.user_id = user_id + self.creation_date = creation_date or datetime.utcnow() + self.last_edit_date = last_edit_date or datetime.utcnow() self.id = id_ - self.is_fixture = is_fixture - - @hybrid_property - def full_name(self): - return f"{self.name} ({self.units})" if self.units else self.name - - @full_name.expression - def full_name(cls): - return cls.name + case([(cls.units != "", " (" + cls.units + ")")], else_="") - - def create(self, db: Session): - self.code = db.execute(select(func.coalesce(func.max(Product.code), 0) + 1)).scalar_one() - db.add(self) - return self - - def can_delete(self, advanced_delete: bool): - if self.is_fixture: - return False, f"{self.name} is a fixture and cannot be edited or deleted." - if self.is_active: - return False, "Product is active" - if len(self.inventories) > 0 and not advanced_delete: - return False, "Product has entries" - return True, "" - - @classmethod - def query(cls, q, is_purchased: bool = None, active: bool = None, db: Session = None): - query_ = select(cls) - if active is not None: - query_ = query_.filter(cls.is_active == active) - if is_purchased is not None: - query_ = query_.filter(cls.is_purchased == is_purchased) - if q is not None: - for item in q.split(): - if item.strip() != "": - query_ = query_.filter(cls.name.ilike(f"%{item}%")) - return db.execute(query_).scalars().all() - - @classmethod - def suspense(cls): - return uuid.UUID("aa79a643-9ddc-4790-ac7f-a41f9efb4c15") diff --git a/brewman/brewman/models/rate_contract_item.py b/brewman/brewman/models/rate_contract_item.py index 441ed14a..3b57cc59 100644 --- a/brewman/brewman/models/rate_contract_item.py +++ b/brewman/brewman/models/rate_contract_item.py @@ -1,119 +1,38 @@ import uuid -from sqlalchemy import ( - Boolean, - Column, - ForeignKey, - Integer, - Numeric, - Unicode, - UniqueConstraint, - case, - func, - select, DateTime, Date, -) +from sqlalchemy import Column, ForeignKey, Numeric, UniqueConstraint from sqlalchemy.dialects.postgresql import UUID -from sqlalchemy.ext.hybrid import hybrid_property -from sqlalchemy.orm import Session, relationship +from sqlalchemy.orm import relationship from .meta import Base -class RateContract(Base): - __tablename__ = "rate_contracts" - __table_args__ = (UniqueConstraint("name", "units"),) +class RateContractItem(Base): + __tablename__ = "rate_contract_items" + __table_args__ = (UniqueConstraint("rate_contract_id", "product_id"),) id = Column("id", UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + rate_contract_id = Column( + "rate_contract_id", + UUID(as_uuid=True), + ForeignKey("rate_contracts.id"), + nullable=False, + index=True, + ) + product_id = Column("product_id", UUID(as_uuid=True), ForeignKey("products.id"), nullable=False) + price = Column("cost_price", Numeric(precision=15, scale=2), nullable=False) - user = relationship("User", primaryjoin="User.id==Voucher.user_id", cascade=None) - creation_date = Column("creation_date", DateTime(), nullable=False) - last_edit_date = Column("last_edit_date", DateTime(), nullable=False) - - vendor_id = Column("vendor_id", UUID(as_uuid=True), ForeignKey("accounts.id"), nullable=False) - - # product_id = Column("product_id", UUID(as_uuid=True), ForeignKey("products.id"), nullable=False) - # price = Column("cost_price", Numeric(precision=15, scale=2), nullable=False) - - valid_from = Column("valid_from", Date(), nullable=True) - valid_till = Column("valid_till", Date(), nullable=True) - - user = relationship("User") - vendor = relationship("Account", back_populates="rate_contracts") - - # batches = relationship("Batch", back_populates="product") - # inventories = relationship("Inventory", back_populates="product") - # recipes = relationship("Recipe", back_populates="product") - # account = relationship("Account", primaryjoin="Account.id==Product.account_id", back_populates="products") + rate_contract = relationship("RateContract", back_populates="items") + product = relationship("Product") def __init__( self, - code=None, - name=None, - units=None, - fraction=None, - fraction_units=None, - product_yield=None, - product_group_id=None, - account_id=None, + rate_contract_id=None, + product_id=None, price=None, - sale_price=None, - is_active=None, - is_purchased=None, - is_sold=None, id_=None, - is_fixture=False, ): - self.code = code - self.name = name - self.units = units - self.fraction = fraction - self.fraction_units = fraction_units - self.product_yield = product_yield - self.product_group_id = product_group_id - self.account_id = account_id + self.rate_contract_id = rate_contract_id + self.product_id = product_id self.price = price - self.sale_price = sale_price - self.is_active = is_active - self.is_purchased = is_purchased - self.is_sold = is_sold self.id = id_ - self.is_fixture = is_fixture - - @hybrid_property - def full_name(self): - return f"{self.name} ({self.units})" if self.units else self.name - - @full_name.expression - def full_name(cls): - return cls.name + case([(cls.units != "", " (" + cls.units + ")")], else_="") - - def create(self, db: Session): - self.code = db.execute(select(func.coalesce(func.max(Product.code), 0) + 1)).scalar_one() - db.add(self) - return self - - def can_delete(self, advanced_delete: bool): - if self.is_fixture: - return False, f"{self.name} is a fixture and cannot be edited or deleted." - if self.is_active: - return False, "Product is active" - if len(self.inventories) > 0 and not advanced_delete: - return False, "Product has entries" - return True, "" - - @classmethod - def query(cls, q, is_purchased: bool = None, active: bool = None, db: Session = None): - query_ = select(cls) - if active is not None: - query_ = query_.filter(cls.is_active == active) - if is_purchased is not None: - query_ = query_.filter(cls.is_purchased == is_purchased) - if q is not None: - for item in q.split(): - if item.strip() != "": - query_ = query_.filter(cls.name.ilike(f"%{item}%")) - return db.execute(query_).scalars().all() - - @classmethod - def suspense(cls): - return uuid.UUID("aa79a643-9ddc-4790-ac7f-a41f9efb4c15") diff --git a/brewman/brewman/routers/rate_contract.py b/brewman/brewman/routers/rate_contract.py new file mode 100644 index 00000000..5f16dc67 --- /dev/null +++ b/brewman/brewman/routers/rate_contract.py @@ -0,0 +1,187 @@ +import uuid + +from typing import List + +from fastapi import APIRouter, HTTPException, Request, Security, status +from sqlalchemy import delete, select +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import Session + +from ..core.security import get_current_active_user as get_user +from ..core.session import ( + get_date, + get_finish_date, + get_start_date, + set_date, + set_period, +) +from ..db.session import SessionFuture +from ..models.rate_contract import RateContract +from ..models.rate_contract_item import RateContractItem +from ..schemas.product import ProductLink +from ..schemas.rate_contract import AccountLink +from ..schemas.rate_contract import RateContract as RateContractSchema +from ..schemas.rate_contract import RateContractIn as RateContractInSchema +from ..schemas.rate_contract import RateContractItem as RateContractItemSchema +from ..schemas.user import UserToken +from ..schemas.user_link import UserLink + + +router = APIRouter() + + +@router.post("", response_model=RateContractSchema) +async def save( + data: RateContractInSchema, + request: Request, + user: UserToken = Security(get_user, scopes=["rate-contracts"]), +) -> RateContractSchema: + try: + with SessionFuture() as db: + item = RateContract( + date=data.date_, + vendor_id=data.vendor.id_, + valid_from=data.valid_from, + valid_till=data.valid_till, + narration=data.narration, + user_id=user.id_, + ) + db.add(item) + add_items(item, data.items, db) + set_date(data.date_.strftime("%d-%b-%Y"), request.session) + set_period(data.valid_from.strftime("%d-%b-%Y"), data.valid_till.strftime("%d-%b-%Y"), request.session) + db.commit() + return rate_contract_info(item) + except SQLAlchemyError as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=str(e), + ) + + +def add_items(rate_contract: RateContract, items: List[RateContractItemSchema], db: Session) -> None: + for item in items: + rci = RateContractItem(rate_contract_id=rate_contract.id, product_id=item.product.id_, price=item.price) + rate_contract.items.append(rci) + db.add(rci) + + +@router.put("/{id_}", response_model=RateContractSchema) +async def update_route( + id_: uuid.UUID, + data: RateContractInSchema, + request: Request, + user: UserToken = Security(get_user, scopes=["rate-contracts"]), +) -> RateContractSchema: + try: + with SessionFuture() as db: + item: RateContract = db.execute(select(RateContract).where(RateContract.id == id_)).scalar_one() + item.date = data.date_ + item.vendor_id = data.vendor.id_ + item.valid_from = data.valid_from + item.valid_till = data.valid_till + item.narration = data.narration + item.user_id = user.id_ + update_items(item, data.items, db) + set_date(data.date_.strftime("%d-%b-%Y"), request.session) + set_period(data.valid_from.strftime("%d-%b-%Y"), data.valid_till.strftime("%d-%b-%Y"), request.session) + db.commit() + return rate_contract_info(item) + except SQLAlchemyError as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=str(e), + ) + + +def update_items(rate_contract: RateContract, items: List[RateContractItemSchema], db: Session): + for it in range(len(rate_contract.items), 0, -1): + item = rate_contract.items[it - 1] + for j in range(len(items), 0, -1): + new_item = items[j - 1] + if new_item.id_ == item.id: + item.product_id = new_item.product.id_ + item.price = new_item.price + items.remove(new_item) + break + else: + db.delete(item) + rate_contract.items.remove(item) + for item in items: + rci = RateContractItem(rate_contract_id=rate_contract.id, product_id=item.product.id_, price=item.price) + rate_contract.items.append(rci) + db.add(rci) + + +@router.delete("/{id_}", response_model=RateContractInSchema) +async def delete_route( + id_: uuid.UUID, + request: Request, + user: UserToken = Security(get_user, scopes=["rate-contracts"]), +) -> RateContractInSchema: + with SessionFuture() as db: + db.execute(delete(RateContractItem).where(RateContractItem.rate_contract_id == id_)) + db.execute(delete(RateContract).where(RateContract.id == id_)) + db.commit() + return rate_contract_blank(request.session) + + +@router.get("", response_model=RateContractInSchema) +async def show_blank( + request: Request, + user: UserToken = Security(get_user, scopes=["rate-contracts"]), +) -> RateContractInSchema: + return rate_contract_blank(request.session) + + +@router.get("/list", response_model=List[RateContractSchema]) +async def show_list( + user: UserToken = Security(get_user), +) -> List[RateContractSchema]: + with SessionFuture() as db: + return [ + rate_contract_info(item) + for item in db.execute(select(RateContract).order_by(RateContract.date)).scalars().all() + ] + + +@router.get("/{id_}", response_model=RateContractSchema) +async def show_id( + id_: uuid.UUID, + user: UserToken = Security(get_user, scopes=["rate-contracts"]), +) -> RateContractSchema: + with SessionFuture() as db: + item: RateContract = db.execute(select(RateContract).where(RateContract.id == id_)).scalar_one() + return rate_contract_info(item) + + +def rate_contract_info(item: RateContract) -> RateContractSchema: + return RateContractSchema( + id=item.id, + date=item.date, + vendor=AccountLink(id=item.vendor_id, name=item.vendor.name), + validFrom=item.valid_from, + validTill=item.valid_till, + narration=item.narration, + creationDate=item.creation_date, + lastEditDate=item.last_edit_date, + user=UserLink(id=item.user_id, name=item.user.name), + items=[ + RateContractItemSchema( + id=i.id, + product=ProductLink(id=i.product_id, name=i.product.full_name), + price=i.price, + ) + for i in item.items + ], + ) + + +def rate_contract_blank(session: dict) -> RateContractInSchema: + return RateContractInSchema( + date=get_date(session), + validFrom=get_start_date(session), + validTill=get_finish_date(session), + narration="", + items=[], + ) diff --git a/brewman/brewman/routers/voucher.py b/brewman/brewman/routers/voucher.py index 716fc09e..cc412b3f 100644 --- a/brewman/brewman/routers/voucher.py +++ b/brewman/brewman/routers/voucher.py @@ -1,8 +1,7 @@ import uuid -from datetime import date, datetime +from datetime import datetime from decimal import Decimal -from typing import List, Optional import brewman.schemas.voucher as output @@ -16,7 +15,6 @@ from ..core.session import get_first_day from ..db.session import SessionFuture from ..models.account import Account from ..models.account_base import AccountBase -from ..models.account_type import AccountType from ..models.attendance import Attendance from ..models.attendance_type import AttendanceType from ..models.cost_centre import CostCentre diff --git a/brewman/brewman/schemas/rate_contract.py b/brewman/brewman/schemas/rate_contract.py new file mode 100644 index 00000000..0770c964 --- /dev/null +++ b/brewman/brewman/schemas/rate_contract.py @@ -0,0 +1,91 @@ +import uuid + +from datetime import date, datetime +from typing import List, Optional + +from pydantic import BaseModel, validator + +from . import to_camel +from .account import AccountLink +from .rate_contract_item import RateContractItem +from .user_link import UserLink + + +class RateContractIn(BaseModel): + date_: date + vendor: Optional[AccountLink] + valid_from: date + valid_till: date + narration: str + items: List[RateContractItem] + + class Config: + anystr_strip_whitespace = True + alias_generator = to_camel + json_encoders = { + date: lambda v: v.strftime("%d-%b-%Y"), + datetime: lambda v: v.strftime("%d-%b-%Y %H:%I"), + } + + @validator("date_", pre=True) + def parse_date(cls, value): + if isinstance(value, date): + return value + return datetime.strptime(value, "%d-%b-%Y").date() + + @validator("valid_from", pre=True) + def parse_creation_date(cls, value): + if isinstance(value, date): + return value + return datetime.strptime(value, "%d-%b-%Y") + + @validator("valid_till", pre=True) + def parse_last_edit_date(cls, value): + if isinstance(value, date): + return value + return datetime.strptime(value, "%d-%b-%Y") + + +class RateContract(RateContractIn): + id_: uuid.UUID + creation_date: Optional[datetime] + last_edit_date: Optional[datetime] + user: Optional[UserLink] + + class Config: + anystr_strip_whitespace = True + alias_generator = to_camel + json_encoders = { + date: lambda v: v.strftime("%d-%b-%Y"), + datetime: lambda v: v.strftime("%d-%b-%Y %H:%I"), + } + + # @validator("date_", pre=True) + # def parse_date(cls, value): + # if isinstance(value, date): + # return value + # return datetime.strptime(value, "%d-%b-%Y").date() + # + # @validator("valid_from", pre=True) + # def parse_creation_date(cls, value): + # if isinstance(value, datetime): + # return value + # return datetime.strptime(value, "%d-%b-%Y %H:%M") + # + # @validator("valid_till", pre=True) + # def parse_last_edit_date(cls, value): + # if isinstance(value, datetime): + # return value + # return datetime.strptime(value, "%d-%b-%Y %H:%M") + + @validator("creation_date", pre=True) + def parse_creation_date(cls, value): + if isinstance(value, datetime): + return value + return datetime.strptime(value, "%d-%b-%Y %H:%M") + + @validator("last_edit_date", pre=True) + def parse_last_edit_date(cls, value): + if isinstance(value, datetime): + return value + return datetime.strptime(value, "%d-%b-%Y %H:%M") diff --git a/brewman/brewman/schemas/rate_contract_item.py b/brewman/brewman/schemas/rate_contract_item.py new file mode 100644 index 00000000..d080bc47 --- /dev/null +++ b/brewman/brewman/schemas/rate_contract_item.py @@ -0,0 +1,17 @@ +import uuid + +from decimal import Decimal +from typing import Optional + +from brewman.schemas import to_camel +from brewman.schemas.product import ProductLink +from pydantic import BaseModel, Field + + +class RateContractItem(BaseModel): + id_: Optional[uuid.UUID] + product: ProductLink + price: Decimal = Field(ge=0, multiple_of=0.01) + + class Config: + alias_generator = to_camel diff --git a/overlord/src/app/app-routing.module.ts b/overlord/src/app/app-routing.module.ts index a50c7cd2..0bc56a3e 100644 --- a/overlord/src/app/app-routing.module.ts +++ b/overlord/src/app/app-routing.module.ts @@ -68,6 +68,11 @@ const appRoutes: Routes = [ (mod) => mod.EmployeeFunctionsModule, ), }, + { + path: 'rate-contracts', + loadChildren: () => + import('./rate-contract/rate-contract.module').then((mod) => mod.RateContractModule), + }, { path: 'roles', loadChildren: () => import('./role/role.module').then((mod) => mod.RoleModule), diff --git a/overlord/src/app/core/nav-bar/nav-bar.component.html b/overlord/src/app/core/nav-bar/nav-bar.component.html index 384b9e4b..9de30bae 100644 --- a/overlord/src/app/core/nav-bar/nav-bar.component.html +++ b/overlord/src/app/core/nav-bar/nav-bar.component.html @@ -30,6 +30,7 @@ Purchases Closing Stock Stock Movement + Rate Contracts diff --git a/overlord/src/app/rate-contract/rate-contact.module.spec.ts b/overlord/src/app/rate-contract/rate-contact.module.spec.ts new file mode 100644 index 00000000..85650484 --- /dev/null +++ b/overlord/src/app/rate-contract/rate-contact.module.spec.ts @@ -0,0 +1,13 @@ +import { RateContractModule } from './rate-contract.module'; + +describe('RateContractModule', () => { + let rateContractModule: RateContractModule; + + beforeEach(() => { + rateContractModule = new RateContractModule(); + }); + + it('should create an instance', () => { + expect(rateContractModule).toBeTruthy(); + }); +}); diff --git a/overlord/src/app/rate-contract/rate-contract-detail/rate-contract-detail-datasource.ts b/overlord/src/app/rate-contract/rate-contract-detail/rate-contract-detail-datasource.ts new file mode 100644 index 00000000..a86126ed --- /dev/null +++ b/overlord/src/app/rate-contract/rate-contract-detail/rate-contract-detail-datasource.ts @@ -0,0 +1,16 @@ +import { DataSource } from '@angular/cdk/collections'; +import { Observable } from 'rxjs'; + +import { RateContractItem } from '../rate-contract-item'; + +export class RateContractDetailDatasource extends DataSource { + constructor(private data: Observable) { + super(); + } + + connect(): Observable { + return this.data; + } + + disconnect() {} +} diff --git a/overlord/src/app/rate-contract/rate-contract-detail/rate-contract-detail.component.css b/overlord/src/app/rate-contract/rate-contract-detail/rate-contract-detail.component.css new file mode 100644 index 00000000..82c7afd6 --- /dev/null +++ b/overlord/src/app/rate-contract/rate-contract-detail/rate-contract-detail.component.css @@ -0,0 +1,3 @@ +.example-card { + max-width: 400px; +} diff --git a/overlord/src/app/rate-contract/rate-contract-detail/rate-contract-detail.component.html b/overlord/src/app/rate-contract/rate-contract-detail/rate-contract-detail.component.html new file mode 100644 index 00000000..c95077fb --- /dev/null +++ b/overlord/src/app/rate-contract/rate-contract-detail/rate-contract-detail.component.html @@ -0,0 +1,164 @@ +
+ + + Rate Contract + + +
+
+ + + + + + + + + {{ + account.name + }} + + +
+
+ + + + + + + + + + +
+
+ + + + {{ + product.name + }} + + + + Price + + + +
+ + + + Product + {{ row.product.name }} + + + + + Price + {{ + row.price | currency: 'INR' + }} + + + + + Action + + + + + + + + + + Narration + + +
+
+ + + + +
+
diff --git a/overlord/src/app/rate-contract/rate-contract-detail/rate-contract-detail.component.spec.ts b/overlord/src/app/rate-contract/rate-contract-detail/rate-contract-detail.component.spec.ts new file mode 100644 index 00000000..d8add806 --- /dev/null +++ b/overlord/src/app/rate-contract/rate-contract-detail/rate-contract-detail.component.spec.ts @@ -0,0 +1,29 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { RateContractDetailComponent } from './rate-contract-detail.component'; + +describe('RateContractDetailComponent', () => { + let component: RateContractDetailComponent; + let fixture: ComponentFixture; + + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ReactiveFormsModule, RouterTestingModule], + declarations: [RateContractDetailComponent], + }).compileComponents(); + }), + ); + + beforeEach(() => { + fixture = TestBed.createComponent(RateContractDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 new file mode 100644 index 00000000..04b53a0f --- /dev/null +++ b/overlord/src/app/rate-contract/rate-contract-detail/rate-contract-detail.component.ts @@ -0,0 +1,219 @@ +import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { MatDialog } from '@angular/material/dialog'; +import { ActivatedRoute, Router } from '@angular/router'; +import * as moment from 'moment'; +import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; +import { debounceTime, distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators'; + +import { Account } from '../../core/account'; +import { AccountService } from '../../core/account.service'; +import { Product } from '../../core/product'; +import { ToasterService } from '../../core/toaster.service'; +import { ProductService } from '../../product/product.service'; +import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component'; +import { MathService } from '../../shared/math.service'; +import { RateContract } from '../rate-contract'; +import { RateContractItem } from '../rate-contract-item'; +import { RateContractService } from '../rate-contract.service'; + +import { RateContractDetailDatasource } from './rate-contract-detail-datasource'; + +@Component({ + selector: 'app-rate-contract-detail', + templateUrl: './rate-contract-detail.component.html', + styleUrls: ['./rate-contract-detail.component.css'], +}) +export class RateContractDetailComponent implements OnInit, AfterViewInit { + @ViewChild('accountElement', { static: true }) accountElement?: ElementRef; + @ViewChild('productElement', { static: true }) productElement?: ElementRef; + public itemsObservable = new BehaviorSubject([]); + dataSource: RateContractDetailDatasource = new RateContractDetailDatasource(this.itemsObservable); + form: FormGroup; + item: RateContract = new RateContract(); + + product: Product | null = null; + + displayedColumns = ['product', 'price', 'action']; + + accounts: Observable; + products: Observable; + + constructor( + private route: ActivatedRoute, + private router: Router, + private fb: FormBuilder, + private toaster: ToasterService, + private dialog: MatDialog, + private math: MathService, + private ser: RateContractService, + private productSer: ProductService, + private accountSer: AccountService, + ) { + this.form = this.fb.group({ + date: '', + account: '', + validFrom: '', + validTill: '', + addRow: this.fb.group({ + product: '', + price: '', + }), + narration: '', + }); + this.accounts = (this.form.get('account') as FormControl).valueChanges.pipe( + startWith(null), + map((x) => (x !== null && x.length >= 1 ? x : null)), + debounceTime(150), + distinctUntilChanged(), + switchMap((x) => (x === null ? observableOf([]) : this.accountSer.autocomplete(x))), + ); + // Listen to Product Autocomplete Change + this.products = ( + (this.form.get('addRow') as FormControl).get('product') as FormControl + ).valueChanges.pipe( + startWith(null), + map((x) => (x !== null && x.length >= 1 ? x : null)), + debounceTime(150), + distinctUntilChanged(), + switchMap((x) => (x === null ? observableOf([]) : this.productSer.autocomplete(x))), + ); + } + + ngOnInit() { + this.route.data.subscribe((value) => { + const data = value as { item: RateContract }; + this.loadItem(data.item); + }); + } + + loadItem(item: RateContract) { + this.item = item; + this.form.setValue({ + date: moment(this.item.date, 'DD-MMM-YYYY').toDate(), + validFrom: moment(this.item.validFrom, 'DD-MMM-YYYY').toDate(), + validTill: moment(this.item.validTill, 'DD-MMM-YYYY').toDate(), + account: this.item.vendor, + addRow: { + product: '', + price: '', + }, + narration: this.item.narration, + }); + this.dataSource = new RateContractDetailDatasource(this.itemsObservable); + this.updateView(); + } + + ngAfterViewInit() { + setTimeout(() => { + if (this.accountElement) { + this.accountElement.nativeElement.focus(); + } + }, 0); + } + + addRow() { + const formValue = (this.form.get('addRow') as FormControl).value; + const price = this.math.parseAmount(formValue.price, 2); + if (this.product === null || price <= 0) { + return; + } + const oldFiltered = this.item.items.filter( + (x) => x.product.id === (this.product as Product).id, + ); + if (oldFiltered.length) { + this.toaster.show('Danger', 'Product already added'); + return; + } + this.item.items.push( + new RateContractItem({ + price, + product: this.product, + }), + ); + this.resetAddRow(); + this.updateView(); + } + + resetAddRow() { + (this.form.get('addRow') as FormControl).reset({ + product: null, + price: '', + }); + this.product = null; + setTimeout(() => { + if (this.productElement) { + this.productElement.nativeElement.focus(); + } + }, 0); + } + + displayFn(item?: Account | Product): string { + return item ? item.name : ''; + } + + updateView() { + this.itemsObservable.next(this.item.items); + } + + accountSelected(event: MatAutocompleteSelectedEvent): void { + (this.form.get('account') as FormControl).setValue(event.option.value); + } + + productSelected(event: MatAutocompleteSelectedEvent): void { + this.product = event.option.value; + } + + deleteRow(row: RateContractItem) { + this.item.items.splice(this.item.items.indexOf(row), 1); + this.updateView(); + } + + save() { + this.ser.saveOrUpdate(this.getItem()).subscribe( + () => { + this.toaster.show('Success', ''); + this.router.navigateByUrl('/rate-contracts'); + }, + (error) => { + this.toaster.show('Danger', error); + }, + ); + } + + delete() { + this.ser.delete(this.item.id as string).subscribe( + () => { + this.toaster.show('Success', ''); + this.router.navigateByUrl('/rate-contracts'); + }, + (error) => { + this.toaster.show('Danger', error); + }, + ); + } + + confirmDelete(): void { + const dialogRef = this.dialog.open(ConfirmDialogComponent, { + width: '250px', + data: { title: 'Delete RateContract?', content: 'Are you sure? This cannot be undone.' }, + }); + + dialogRef.afterClosed().subscribe((result: boolean) => { + if (result) { + this.delete(); + } + }); + } + + getItem(): RateContract { + const formModel = this.form.value; + this.item.date = moment(formModel.date).format('DD-MMM-YYYY'); + this.item.validFrom = moment(formModel.validFrom).format('DD-MMM-YYYY'); + this.item.validTill = moment(formModel.validTill).format('DD-MMM-YYYY'); + this.item.vendor = formModel.account; + this.item.narration = formModel.narration; + return this.item; + } +} diff --git a/overlord/src/app/rate-contract/rate-contract-item.ts b/overlord/src/app/rate-contract/rate-contract-item.ts new file mode 100644 index 00000000..67a08bc6 --- /dev/null +++ b/overlord/src/app/rate-contract/rate-contract-item.ts @@ -0,0 +1,13 @@ +import { Product } from '../core/product'; + +export class RateContractItem { + id: string | undefined; + product: Product; + price: number; + + public constructor(init?: Partial) { + this.product = new Product(); + this.price = 0; + Object.assign(this, init); + } +} diff --git a/overlord/src/app/rate-contract/rate-contract-list-resolver.service.spec.ts b/overlord/src/app/rate-contract/rate-contract-list-resolver.service.spec.ts new file mode 100644 index 00000000..a4b1175b --- /dev/null +++ b/overlord/src/app/rate-contract/rate-contract-list-resolver.service.spec.ts @@ -0,0 +1,21 @@ +import { HttpClientModule } from '@angular/common/http'; +import { inject, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { RateContractListResolver } from './rate-contract-list-resolver.service'; + +describe('RateContractListResolver', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientModule, RouterTestingModule], + providers: [RateContractListResolver], + }); + }); + + it('should be created', inject( + [RateContractListResolver], + (service: RateContractListResolver) => { + expect(service).toBeTruthy(); + }, + )); +}); diff --git a/overlord/src/app/rate-contract/rate-contract-list-resolver.service.ts b/overlord/src/app/rate-contract/rate-contract-list-resolver.service.ts new file mode 100644 index 00000000..00ef5188 --- /dev/null +++ b/overlord/src/app/rate-contract/rate-contract-list-resolver.service.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@angular/core'; +import { Resolve } from '@angular/router'; +import { Observable } from 'rxjs/internal/Observable'; + +import { RateContract } from './rate-contract'; +import { RateContractService } from './rate-contract.service'; + +@Injectable({ + providedIn: 'root', +}) +export class RateContractListResolver implements Resolve { + constructor(private ser: RateContractService) {} + + resolve(): Observable { + return this.ser.list(); + } +} diff --git a/overlord/src/app/rate-contract/rate-contract-list/rate-contract-list-datasource.ts b/overlord/src/app/rate-contract/rate-contract-list/rate-contract-list-datasource.ts new file mode 100644 index 00000000..7e29edf7 --- /dev/null +++ b/overlord/src/app/rate-contract/rate-contract-list/rate-contract-list-datasource.ts @@ -0,0 +1,74 @@ +import { DataSource } from '@angular/cdk/collections'; +import { EventEmitter } from '@angular/core'; +import { MatPaginator, PageEvent } from '@angular/material/paginator'; +import { MatSort, Sort } from '@angular/material/sort'; +import { merge, Observable, of as observableOf } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { RateContract } from '../rate-contract'; + +/** Simple sort comparator for example ID/Name columns (for client-side sorting). */ +const compare = (a: string | number, b: string | number, isAsc: boolean) => + (a < b ? -1 : 1) * (isAsc ? 1 : -1); +export class RateContractListDatasource extends DataSource { + constructor( + public data: RateContract[], + private paginator?: MatPaginator, + private sort?: MatSort, + ) { + super(); + } + + connect(): Observable { + const dataMutations: ( + | Observable + | EventEmitter + | EventEmitter + )[] = [observableOf(this.data)]; + if (this.paginator) { + dataMutations.push((this.paginator as MatPaginator).page); + } + if (this.sort) { + dataMutations.push((this.sort as MatSort).sortChange); + } + + // Set the paginators length + if (this.paginator) { + this.paginator.length = this.data.length; + } + + return merge(...dataMutations).pipe( + map(() => this.getPagedData(this.getSortedData([...this.data]))), + ); + } + + disconnect() {} + + private getPagedData(data: RateContract[]) { + if (this.paginator === undefined) { + return data; + } + const startIndex = this.paginator.pageIndex * this.paginator.pageSize; + return data.splice(startIndex, this.paginator.pageSize); + } + + private getSortedData(data: RateContract[]) { + if (this.sort === undefined) { + return data; + } + if (!this.sort.active || this.sort.direction === '') { + return data; + } + + const sort = this.sort as MatSort; + return data.sort((a, b) => { + const isAsc = sort.direction === 'asc'; + switch (sort.active) { + case 'vendor': + return compare(a.vendor.name, b.vendor.name, isAsc); + default: + return 0; + } + }); + } +} diff --git a/overlord/src/app/rate-contract/rate-contract-list/rate-contract-list.component.css b/overlord/src/app/rate-contract/rate-contract-list/rate-contract-list.component.css new file mode 100644 index 00000000..e69de29b diff --git a/overlord/src/app/rate-contract/rate-contract-list/rate-contract-list.component.html b/overlord/src/app/rate-contract/rate-contract-list/rate-contract-list.component.html new file mode 100644 index 00000000..ebc20eb2 --- /dev/null +++ b/overlord/src/app/rate-contract/rate-contract-list/rate-contract-list.component.html @@ -0,0 +1,52 @@ + + + Rate Contracts + + add_box + Add + + + + + + + Vendor + {{ row.vendor.name }} // {{ row.date }} + + + + + Validity + {{ row.validFrom }} - {{ row.validTill }} + + + + + Products + +
    +
  • + {{ item.product.name }} @ {{ item.price | currency: 'INR' }} +
  • +
+
+
+ + + +
+ + + +
+
diff --git a/overlord/src/app/rate-contract/rate-contract-list/rate-contract-list.component.spec.ts b/overlord/src/app/rate-contract/rate-contract-list/rate-contract-list.component.spec.ts new file mode 100644 index 00000000..c096ca46 --- /dev/null +++ b/overlord/src/app/rate-contract/rate-contract-list/rate-contract-list.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { RateContractListComponent } from './rate-contract-list.component'; + +describe('RateContractListComponent', () => { + let component: RateContractListComponent; + let fixture: ComponentFixture; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [RouterTestingModule], + declarations: [RateContractListComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(RateContractListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + })); + + it('should compile', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/overlord/src/app/rate-contract/rate-contract-list/rate-contract-list.component.ts b/overlord/src/app/rate-contract/rate-contract-list/rate-contract-list.component.ts new file mode 100644 index 00000000..75cfa916 --- /dev/null +++ b/overlord/src/app/rate-contract/rate-contract-list/rate-contract-list.component.ts @@ -0,0 +1,33 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import { ActivatedRoute } from '@angular/router'; + +import { RateContract } from '../rate-contract'; + +import { RateContractListDatasource } from './rate-contract-list-datasource'; + +@Component({ + selector: 'app-rate-contract-list', + templateUrl: './rate-contract-list.component.html', + styleUrls: ['./rate-contract-list.component.css'], +}) +export class RateContractListComponent implements OnInit { + @ViewChild(MatPaginator, { static: true }) paginator?: MatPaginator; + @ViewChild(MatSort, { static: true }) sort?: MatSort; + list: RateContract[] = []; + dataSource: RateContractListDatasource = new RateContractListDatasource(this.list); + /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ + displayedColumns = ['vendor', 'validity', 'products']; + + constructor(private route: ActivatedRoute) {} + + ngOnInit() { + this.route.data.subscribe((value) => { + const data = value as { list: RateContract[] }; + + this.list = data.list; + }); + this.dataSource = new RateContractListDatasource(this.list, this.paginator, this.sort); + } +} diff --git a/overlord/src/app/rate-contract/rate-contract-resolver.service.spec.ts b/overlord/src/app/rate-contract/rate-contract-resolver.service.spec.ts new file mode 100644 index 00000000..8542b384 --- /dev/null +++ b/overlord/src/app/rate-contract/rate-contract-resolver.service.spec.ts @@ -0,0 +1,18 @@ +import { HttpClientModule } from '@angular/common/http'; +import { inject, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { RateContractResolver } from './rate-contract-resolver.service'; + +describe('RateContractResolver', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientModule, RouterTestingModule], + providers: [RateContractResolver], + }); + }); + + it('should be created', inject([RateContractResolver], (service: RateContractResolver) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/overlord/src/app/rate-contract/rate-contract-resolver.service.ts b/overlord/src/app/rate-contract/rate-contract-resolver.service.ts new file mode 100644 index 00000000..c2320cda --- /dev/null +++ b/overlord/src/app/rate-contract/rate-contract-resolver.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; +import { Observable } from 'rxjs/internal/Observable'; + +import { RateContract } from './rate-contract'; +import { RateContractService } from './rate-contract.service'; + +@Injectable({ + providedIn: 'root', +}) +export class RateContractResolver implements Resolve { + constructor(private ser: RateContractService) {} + + resolve(route: ActivatedRouteSnapshot): Observable { + const id = route.paramMap.get('id'); + return this.ser.get(id); + } +} diff --git a/overlord/src/app/rate-contract/rate-contract-routing.module.spec.ts b/overlord/src/app/rate-contract/rate-contract-routing.module.spec.ts new file mode 100644 index 00000000..b09cb4cd --- /dev/null +++ b/overlord/src/app/rate-contract/rate-contract-routing.module.spec.ts @@ -0,0 +1,13 @@ +import { RateContractRoutingModule } from './rate-contract-routing.module'; + +describe('RateContractRoutingModule', () => { + let rateContractRoutingModule: RateContractRoutingModule; + + beforeEach(() => { + rateContractRoutingModule = new RateContractRoutingModule(); + }); + + it('should create an instance', () => { + expect(rateContractRoutingModule).toBeTruthy(); + }); +}); diff --git a/overlord/src/app/rate-contract/rate-contract-routing.module.ts b/overlord/src/app/rate-contract/rate-contract-routing.module.ts new file mode 100644 index 00000000..5e1396ac --- /dev/null +++ b/overlord/src/app/rate-contract/rate-contract-routing.module.ts @@ -0,0 +1,53 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { AuthGuard } from '../auth/auth-guard.service'; + +import { RateContractDetailComponent } from './rate-contract-detail/rate-contract-detail.component'; +import { RateContractListResolver } from './rate-contract-list-resolver.service'; +import { RateContractListComponent } from './rate-contract-list/rate-contract-list.component'; +import { RateContractResolver } from './rate-contract-resolver.service'; + +const rateContractRoutes: Routes = [ + { + path: '', + component: RateContractListComponent, + canActivate: [AuthGuard], + data: { + permission: 'Rate Contracts', + }, + resolve: { + list: RateContractListResolver, + }, + }, + { + path: 'new', + component: RateContractDetailComponent, + canActivate: [AuthGuard], + data: { + permission: 'Rate Contracts', + }, + resolve: { + item: RateContractResolver, + }, + }, + { + path: ':id', + component: RateContractDetailComponent, + canActivate: [AuthGuard], + data: { + permission: 'Rate Contracts', + }, + resolve: { + item: RateContractResolver, + }, + }, +]; + +@NgModule({ + imports: [CommonModule, RouterModule.forChild(rateContractRoutes)], + exports: [RouterModule], + providers: [RateContractListResolver, RateContractResolver], +}) +export class RateContractRoutingModule {} diff --git a/overlord/src/app/rate-contract/rate-contract.module.ts b/overlord/src/app/rate-contract/rate-contract.module.ts new file mode 100644 index 00000000..cc5e2468 --- /dev/null +++ b/overlord/src/app/rate-contract/rate-contract.module.ts @@ -0,0 +1,71 @@ +import { CdkTableModule } from '@angular/cdk/table'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FlexLayoutModule } from '@angular/flex-layout'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MomentDateAdapter } from '@angular/material-moment-adapter'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { + DateAdapter, + MAT_DATE_FORMATS, + MAT_DATE_LOCALE, + MatNativeDateModule, +} from '@angular/material/core'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { MatDividerModule } from '@angular/material/divider'; +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 { MatSortModule } from '@angular/material/sort'; +import { MatTableModule } from '@angular/material/table'; + +import { SharedModule } from '../shared/shared.module'; + +import { RateContractDetailComponent } from './rate-contract-detail/rate-contract-detail.component'; +import { RateContractListComponent } from './rate-contract-list/rate-contract-list.component'; +import { RateContractRoutingModule } from './rate-contract-routing.module'; + +export const MY_FORMATS = { + parse: { + dateInput: 'DD-MMM-YYYY', + }, + display: { + dateInput: 'DD-MMM-YYYY', + monthYearLabel: 'MMM YYYY', + dateA11yLabel: 'DD-MMM-YYYY', + monthYearA11yLabel: 'MMM YYYY', + }, +}; + +@NgModule({ + imports: [ + CommonModule, + CdkTableModule, + FlexLayoutModule, + MatButtonModule, + MatCardModule, + MatCheckboxModule, + MatDividerModule, + MatIconModule, + MatInputModule, + MatPaginatorModule, + MatProgressSpinnerModule, + MatSortModule, + MatTableModule, + ReactiveFormsModule, + SharedModule, + RateContractRoutingModule, + MatDatepickerModule, + MatAutocompleteModule, + ], + declarations: [RateContractListComponent, RateContractDetailComponent], + providers: [ + { provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] }, + { provide: MAT_DATE_FORMATS, useValue: MY_FORMATS }, + ], +}) +export class RateContractModule {} diff --git a/overlord/src/app/rate-contract/rate-contract.service.spec.ts b/overlord/src/app/rate-contract/rate-contract.service.spec.ts new file mode 100644 index 00000000..5a4897bc --- /dev/null +++ b/overlord/src/app/rate-contract/rate-contract.service.spec.ts @@ -0,0 +1,17 @@ +import { HttpClientModule } from '@angular/common/http'; +import { inject, TestBed } from '@angular/core/testing'; + +import { RateContractService } from './rate-contract.service'; + +describe('RateContractService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientModule], + providers: [RateContractService], + }); + }); + + it('should be created', inject([RateContractService], (service: RateContractService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/overlord/src/app/rate-contract/rate-contract.service.ts b/overlord/src/app/rate-contract/rate-contract.service.ts new file mode 100644 index 00000000..3d76be1e --- /dev/null +++ b/overlord/src/app/rate-contract/rate-contract.service.ts @@ -0,0 +1,58 @@ +import { HttpClient } 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 { RateContract } from './rate-contract'; + +const url = '/api/rate-contracts'; +const serviceName = 'RateContractService'; + +@Injectable({ + providedIn: 'root', +}) +export class RateContractService { + constructor(private http: HttpClient, private log: ErrorLoggerService) {} + + get(id: string | null): Observable { + const getUrl: string = id === null ? `${url}` : `${url}/${id}`; + return this.http + .get(getUrl) + .pipe( + catchError(this.log.handleError(serviceName, `get id=${id}`)), + ) as Observable; + } + + list(): Observable { + return this.http + .get(`${url}/list`) + .pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable; + } + + save(rateContract: RateContract): Observable { + return this.http + .post(`${url}`, rateContract) + .pipe(catchError(this.log.handleError(serviceName, 'save'))) as Observable; + } + + update(rateContract: RateContract): Observable { + return this.http + .put(`${url}/${rateContract.id}`, rateContract) + .pipe(catchError(this.log.handleError(serviceName, 'update'))) as Observable; + } + + saveOrUpdate(rateContract: RateContract): Observable { + if (!rateContract.id) { + return this.save(rateContract); + } + return this.update(rateContract); + } + + delete(id: string): Observable { + return this.http + .delete(`${url}/${id}`) + .pipe(catchError(this.log.handleError(serviceName, 'delete'))) as Observable; + } +} diff --git a/overlord/src/app/rate-contract/rate-contract.ts b/overlord/src/app/rate-contract/rate-contract.ts new file mode 100644 index 00000000..3917adf5 --- /dev/null +++ b/overlord/src/app/rate-contract/rate-contract.ts @@ -0,0 +1,30 @@ +import { Account } from '../core/account'; +import { User } from '../core/user'; + +import { RateContractItem } from './rate-contract-item'; + +export class RateContract { + id: string | undefined; + date: string; + vendor: Account; + validFrom: string; + validTill: string; + narration: string; + creationDate: string; + lastEditDate: string; + user: User; + items: RateContractItem[]; + + public constructor(init?: Partial) { + this.date = ''; + this.vendor = new Account(); + this.validFrom = ''; + this.validTill = ''; + this.narration = ''; + this.creationDate = ''; + this.lastEditDate = ''; + this.user = new User(); + this.items = []; + Object.assign(this, init); + } +}