DB Normalization: Moved fractionUnits back to Product from SKU as it is better suited there.

Feature: Created the ProductSku schema for the product/sku autocomplete
This commit is contained in:
Amritanshu Agrawal 2021-11-02 13:50:35 +05:30
parent b3075577e6
commit 30e3288b1e
26 changed files with 295 additions and 244 deletions

@ -0,0 +1,50 @@
"""fraction units
Revision ID: c39eb451a683
Revises: 7ba0aff64237
Create Date: 2021-11-01 10:05:46.057929
"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy import Unicode, column, select, table
# revision identifiers, used by Alembic.
from sqlalchemy.dialects.postgresql import UUID
revision = "c39eb451a683"
down_revision = "7ba0aff64237"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"products",
sa.Column("fraction_units", sa.Unicode(length=255), nullable=False, server_default=""),
)
product = table(
"products",
column("id", UUID(as_uuid=True)),
column("fraction_units", Unicode(255)),
)
sku = table(
"stock_keeping_units",
column("product_id", UUID(as_uuid=True)),
column("fraction_units", Unicode(255)),
)
op.execute(
product.update().values(fraction_units=select(sku.c.fraction_units).where(sku.c.product_id == product.c.id))
)
op.drop_index("unique_true_is_default", table_name="stock_keeping_units")
op.drop_column("stock_keeping_units", "is_default")
op.drop_column("stock_keeping_units", "fraction_units")
# ### end Alembic commands ###
def downgrade():
pass

@ -1,6 +1,7 @@
import uuid
from datetime import date
from typing import Optional
from sqlalchemy import Column, Date, ForeignKey, Numeric, select
from sqlalchemy.dialects.postgresql import UUID
@ -51,7 +52,7 @@ class Batch(Base):
return self.quantity_remaining * self.rate * (1 + self.tax) * (1 - self.discount)
@classmethod
def list(cls, q: str, include_nil: bool, date_: date, db: Session):
def list(cls, q: str, include_nil: bool, date_: Optional[date], db: Session):
query = (
select(cls)
.join(cls.sku)

@ -1,11 +1,10 @@
import uuid
from sqlalchemy import Boolean, Column, ForeignKey, Integer, Unicode, desc, func, select
from sqlalchemy import Boolean, Column, ForeignKey, Integer, Unicode
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Session, relationship
from sqlalchemy.orm import relationship
from .meta import Base
from .stock_keeping_unit import StockKeepingUnit
class Product(Base):
@ -14,6 +13,7 @@ class Product(Base):
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, unique=True)
fraction_units = Column("fraction_units", Unicode(255), nullable=False)
product_group_id = Column(
"product_group_id",
UUID(as_uuid=True),
@ -26,15 +26,15 @@ class Product(Base):
is_purchased = Column("is_purchased", Boolean, nullable=False)
is_sold = Column("is_sold", Boolean, nullable=False)
skus = relationship("StockKeepingUnit", back_populates="product", order_by=desc(StockKeepingUnit.is_default))
skus = relationship("StockKeepingUnit", back_populates="product")
product_group = relationship("ProductGroup", back_populates="products")
recipes = relationship("Recipe", back_populates="product")
account = relationship("Account", primaryjoin="Account.id==Product.account_id", back_populates="products")
def __init__(
self,
code=None,
name=None,
fraction_units=None,
product_group_id=None,
account_id=None,
is_active=None,
@ -45,6 +45,7 @@ class Product(Base):
):
self.code = code
self.name = name
self.fraction_units = fraction_units
self.product_group_id = product_group_id
self.account_id = account_id
self.is_active = is_active
@ -53,11 +54,6 @@ class Product(Base):
self.id = id_
self.is_fixture = is_fixture
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
@classmethod
def suspense(cls):
return uuid.UUID("aa79a643-9ddc-4790-ac7f-a41f9efb4c15")

@ -1,15 +1,6 @@
import uuid
from sqlalchemy import (
Boolean,
Column,
ForeignKey,
Index,
Numeric,
Unicode,
UniqueConstraint,
text,
)
from sqlalchemy import Column, ForeignKey, Numeric, Unicode, UniqueConstraint
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
@ -18,50 +9,37 @@ from .meta import Base
class StockKeepingUnit(Base):
__tablename__ = "stock_keeping_units"
__table_args__ = (
UniqueConstraint("product_id", "units"),
Index(
"unique_true_is_default",
"product_id",
unique=True,
postgresql_where=text("is_default = true"),
),
)
__table_args__ = (UniqueConstraint("product_id", "units"),)
id = Column("id", UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
product_id = Column("product_id", UUID(as_uuid=True), ForeignKey("products.id"), nullable=False)
is_default = Column("is_default", Boolean, 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)
price = Column("cost_price", Numeric(precision=15, scale=2), nullable=False)
cost_price = Column("cost_price", Numeric(precision=15, scale=2), nullable=False)
sale_price = Column("sale_price", Numeric(precision=15, scale=2), nullable=False)
product = relationship("Product", back_populates="skus")
batches = relationship("Batch", back_populates="sku")
recipes = relationship("Recipe", back_populates="sku")
def __init__(
self,
product_id=None,
is_default=None,
units=None,
fraction=None,
fraction_units=None,
product_yield=None,
price=None,
cost_price=None,
sale_price=None,
id_=None,
product=None,
):
if product_id is not None:
self.product_id = product_id
self.is_default = is_default
self.units = units
self.fraction = fraction
self.fraction_units = fraction_units
self.product_yield = product_yield
self.price = price
self.cost_price = cost_price
self.sale_price = sale_price
self.id = id_
if product is not None:

@ -17,10 +17,10 @@ def batch_term(
d: str = None,
current_user: UserToken = Depends(get_user),
):
date = None if not d else datetime.datetime.strptime(d, "%d-%b-%Y")
date_ = None if not d else datetime.datetime.strptime(d, "%d-%b-%Y")
list_ = []
with SessionFuture() as db:
for item in Batch.list(q, include_nil=False, date_=date, db=db):
for item in Batch.list(q, include_nil=False, date_=date_, db=db):
text = (
f"{item.sku.product.name} ({item.sku.units}) {item.quantity_remaining:.2f}@"
f"{item.rate:.2f} from {item.name.strftime('%d-%b-%Y')}"

@ -1,6 +1,7 @@
import uuid
from datetime import date, datetime
from typing import List
import brewman.schemas.employee_attendance as schemas
@ -41,27 +42,27 @@ def employee_attendance_report(
s: str = None,
f: str = None,
user: UserToken = Security(get_user, scopes=["attendance"]),
):
) -> schemas.EmployeeAttendance:
with SessionFuture() as db:
employee: Employee = db.execute(select(Employee).where(Employee.id == id_)).scalar_one()
start_date = s if s is not None else get_start_date(request.session)
finish_date = f if f is not None else get_finish_date(request.session)
info = {
"startDate": start_date,
"finishDate": finish_date,
"employee": {"id": employee.id, "name": employee.name},
}
start_date = datetime.strptime(start_date, "%d-%b-%Y").date()
finish_date = datetime.strptime(finish_date, "%d-%b-%Y").date()
start_date = datetime.strptime(s or get_start_date(request.session), "%d-%b-%Y").date()
finish_date = datetime.strptime(f or get_finish_date(request.session), "%d-%b-%Y").date()
info = schemas.EmployeeAttendance(
startDate=start_date,
finishDate=finish_date,
employee=schemas.AccountLink(id=employee.id, name=employee.name),
)
start_date = employee.joining_date if employee.joining_date > start_date else start_date
finish_date = (
employee.leaving_date if not employee.is_active and employee.leaving_date < finish_date else finish_date
)
info["body"] = employee_attendance(employee, start_date, finish_date, db)
info.body = employee_attendance(employee, start_date, finish_date, db)
return info
def employee_attendance(employee: Employee, start_date: date, finish_date: date, db: Session):
def employee_attendance(
employee: Employee, start_date: date, finish_date: date, db: Session
) -> List[schemas.EmployeeAttendanceItem]:
list_ = []
for item in date_range(start_date, finish_date, inclusive=True):
att = (
@ -95,16 +96,10 @@ def save_employee_attendance(
id_: uuid.UUID,
data: schemas.EmployeeAttendance,
user: UserToken = Security(get_user, scopes=["attendance"]),
):
start_date = None
finish_date = None
) -> schemas.EmployeeAttendance:
with SessionFuture() as db:
employee: Employee = db.execute(select(Employee).where(Employee.id == id_)).scalar_one()
for item in data.body:
if start_date is None:
start_date = item.date_
finish_date = item.date_
attendance_type = item.attendance_type.id_
if attendance_type != 0:
attendance = Attendance(
@ -115,9 +110,11 @@ def save_employee_attendance(
)
attendance.create(db)
db.commit()
return {
"startDate": start_date.strftime("%d-%b-%Y"),
"finishDate": finish_date.strftime("%d-%b-%Y"),
"employee": {"id": employee.id, "name": employee.name},
"body": employee_attendance(employee, start_date, finish_date, db),
}
start_date = min(i.date_ for i in data.body)
finish_date = max(i.date_ for i in data.body)
return schemas.EmployeeAttendance(
startDate=start_date,
finishDate=finish_date,
employee={"id": employee.id, "name": employee.name},
body=employee_attendance(employee, start_date, finish_date, db),
)

@ -2,7 +2,7 @@ import uuid
from datetime import datetime
from decimal import Decimal
from typing import List, Optional
from typing import List, Optional, Tuple
import brewman.schemas.input as schema_in
import brewman.schemas.voucher as output
@ -67,7 +67,7 @@ def save_route(
)
def save(data: schema_in.IssueIn, user: UserToken, db: Session) -> (Voucher, Optional[bool]):
def save(data: schema_in.IssueIn, user: UserToken, db: Session) -> Tuple[Voucher, Optional[bool]]:
product_accounts = (
select(Product.account_id)
.join(Product.skus)
@ -198,7 +198,9 @@ def update_route(
)
def update_voucher(id_: uuid.UUID, data: schema_in.IssueIn, user: UserToken, db: Session) -> (Voucher, Optional[bool]):
def update_voucher(
id_: uuid.UUID, data: schema_in.IssueIn, user: UserToken, db: Session
) -> Tuple[Voucher, Optional[bool]]:
voucher: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one()
product_accounts = (
select(Product.account_id)

@ -9,7 +9,7 @@ import brewman.schemas.product as schemas
from fastapi import APIRouter, Depends, HTTPException, Security, status
from sqlalchemy import delete, desc, func, or_, select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session, contains_eager
from sqlalchemy.orm import Session, contains_eager, joinedload
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
@ -19,6 +19,7 @@ from ..models.product import Product
from ..models.rate_contract import RateContract
from ..models.rate_contract_item import RateContractItem
from ..models.stock_keeping_unit import StockKeepingUnit
from ..schemas.product_sku import ProductSku
from ..schemas.user import UserToken
@ -34,30 +35,23 @@ def save(
with SessionFuture() as db:
item = Product(
name=data.name,
fraction_units=data.fraction_units,
product_group_id=data.product_group.id_,
account_id=Account.all_purchases(),
is_active=data.is_active,
is_purchased=data.is_purchased,
is_sold=data.is_sold,
).create(db)
if len([s for s in data.skus if s.is_default is True]) != 1:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Exactly one default sku is needed"
)
if len(set([s.fraction_units for s in data.skus])) != 1:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="All skus need to have the same fraction unit",
)
)
item.code = db.execute(select(func.coalesce(func.max(Product.code), 0) + 1)).scalar_one()
db.add(item)
for sku in data.skus:
db.add(
StockKeepingUnit(
is_default=sku.is_default,
units=sku.units,
fraction=round(sku.fraction, 5),
fraction_units=sku.fraction_units,
product_yield=round(sku.product_yield, 5),
price=round(sku.price, 2),
cost_price=round(sku.cost_price, 2),
sale_price=round(sku.sale_price, 2),
product=item,
)
@ -86,33 +80,21 @@ def update_route(
detail=f"{item.name} is a fixture and cannot be edited or deleted.",
)
item.name = data.name
item.fraction_units = data.fraction_units
item.product_group_id = data.product_group.id_
item.account_id = Account.all_purchases()
item.is_active = data.is_active
item.is_purchased = data.is_purchased
item.is_sold = data.is_sold
if len([sku for sku in data.skus if sku.is_default is True]) != 1:
raise HTTPException(
status_code=status.HTTP_423_LOCKED,
detail="There needs to be exactly 1 default SKU",
)
if len(set([s.fraction_units for s in data.skus])) != 1:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="All skus need to have the same fraction unit",
)
default_sku = next(s.units for s in data.skus if s.is_default is True)
for i in range(len(item.skus), 0, -1):
sku = item.skus[i - 1]
index = next((idx for (idx, d) in enumerate(data.skus) if d.id_ == sku.id), None)
if index is not None:
new_sku = data.skus.pop(index)
sku.is_default = False
sku.units = new_sku.units
sku.fraction = round(new_sku.fraction, 5)
sku.fraction_units = new_sku.fraction_units
sku.product_yield = round(new_sku.product_yield, 5)
sku.price = round(new_sku.price, 2)
sku.cost_price = round(new_sku.cost_price, 2)
sku.sale_price = round(new_sku.sale_price, 2)
else:
count: Decimal = db.execute(select(func.count()).where(Batch.sku_id == sku.id)).scalar_one()
@ -125,20 +107,15 @@ def update_route(
db.delete(sku)
for sku in data.skus:
new_sku = StockKeepingUnit(
is_default=False,
units=sku.units,
fraction=round(sku.fraction, 5),
fraction_units=sku.fraction_units,
product_yield=round(sku.product_yield, 5),
price=round(sku.price, 2),
cost_price=round(sku.cost_price, 2),
sale_price=round(sku.sale_price, 2),
product=item,
)
db.add(new_sku)
item.skus.append(new_sku)
db.flush()
default_sku = next(s for s in item.skus if s.units == default_sku)
default_sku.is_default = True
db.commit()
return product_info(item)
except SQLAlchemyError as e:
@ -187,26 +164,33 @@ def show_list(user: UserToken = Depends(get_user)) -> List[schemas.Product]:
product_info(item)
for item in db.execute(
select(Product)
.join(Product.product_group)
.join(Product.skus)
.order_by(desc(Product.is_active))
.order_by(Product.product_group_id)
.order_by(Product.name)
.options(
joinedload(Product.skus, innerjoin=True),
joinedload(Product.product_group, innerjoin=True),
contains_eager(Product.skus),
contains_eager(Product.product_group),
)
)
.unique()
.scalars()
.all()
]
@router.get("/query")
async def show_term(
@router.get("/q-sku", response_model=List[ProductSku])
async def show_term_sku(
q: str = None, # Query
a: bool = None, # Active
p: bool = None, # Is Purchased?
e: bool = False, # Extended
s: bool = False, # List separate SKUs
v: Optional[uuid.UUID] = None, # Vendor
d: Optional[str] = None, # Date
current_user: UserToken = Depends(get_user),
):
) -> List[ProductSku]:
list_ = []
with SessionFuture() as db:
query_ = select(Product).join(Product.skus).options(contains_eager(Product.skus))
@ -223,33 +207,59 @@ async def show_term(
query_ = query_.order_by(Product.name)
for item in db.execute(query_).unique().scalars().all():
skus = item.skus if s else item.skus[:1]
for sku in skus:
for sku in item.skus: # type: StockKeepingUnit
rc_price = get_rc_price(item.id, d, v, db)
list_.append(
{
"id": sku.id if s else item.id,
"name": item.name,
"price": sku.price if rc_price is None else rc_price,
"units": sku.units if s else "",
"fraction": sku.fraction,
"fractionUnits": sku.fraction_units,
"productYield": sku.product_yield,
"isSold": item.is_sold,
"salePrice": sku.sale_price,
"isRateContracted": False if rc_price is None else True,
}
if e
else {
"id": sku.id if s else item.id,
"name": f"{item.name} ({sku.units})" if s else item.name,
"price": sku.price if rc_price is None else rc_price,
"isRateContracted": False if rc_price is None else True,
}
ProductSku(
id=sku.id,
name=f"{item.name} ({sku.units})",
fractionUnits=item.fraction_units,
costPrice=sku.cost_price if rc_price is None else rc_price,
salePrice=sku.sale_price,
fraction=sku.fraction,
productYield=sku.product_yield,
isRateContracted=False if rc_price is None else True,
)
)
return list_
@router.get("/q-product", response_model=List[ProductSku])
async def show_term_product(
q: str = None, # Query
a: bool = None, # Active
p: bool = None, # Is Purchased?
current_user: UserToken = Depends(get_user),
) -> List[ProductSku]:
list_ = []
with SessionFuture() as db:
query_ = select(Product)
if a is not None:
query_ = query_.filter(Product.is_active == a)
if p is not None:
query_ = query_.filter(Product.is_purchased == p)
if q is not None:
for item in q.split():
if item.strip() != "":
query_ = query_.filter(Product.name.ilike(f"%{item}%"))
query_ = query_.order_by(Product.name)
for item in db.execute(query_).unique().scalars().all():
list_.append(
ProductSku(
id=item.id,
name=item.name,
fractionUnits=item.fraction_units,
costPrice=0,
salePrice=0,
fraction=1,
productYield=1,
isRateContracted=False,
)
)
return list_
@router.get("/{id_}", response_model=schemas.Product)
def show_id(
id_: uuid.UUID,
@ -261,19 +271,18 @@ def show_id(
def product_info(product: Product) -> schemas.Product:
product = schemas.Product(
return schemas.Product(
id=product.id,
code=product.code,
name=product.name,
fractionUnits=product.fraction_units,
skus=[
schemas.StockKeepingUnit(
id=sku.id,
isDefault=sku.is_default,
units=sku.units,
fraction=sku.fraction,
fractionUnits=sku.fraction_units,
productYield=sku.product_yield,
price=sku.price,
costPrice=sku.cost_price,
salePrice=sku.sale_price,
)
for sku in product.skus
@ -284,12 +293,12 @@ def product_info(product: Product) -> schemas.Product:
isSold=product.is_sold,
productGroup=schemas.ProductGroupLink(id=product.product_group.id, name=product.product_group.name),
)
return product
def product_blank() -> schemas.ProductBlank:
return schemas.ProductBlank(
name="",
fractionUnits="",
skus=[],
isActive=True,
isPurchased=True,

@ -1,6 +1,6 @@
from datetime import date, datetime
from decimal import Decimal
from typing import List
from typing import List, Tuple
import brewman.schemas.profit_loss as schemas
@ -59,7 +59,7 @@ def report_data(
def build_profit_loss(
start_date: date, finish_date: date, db: Session
) -> (List[schemas.ProfitLossItem], schemas.ProfitLossItem):
) -> Tuple[List[schemas.ProfitLossItem], schemas.ProfitLossItem]:
profit_type_list = (
db.execute(select(AccountType.id).where(AccountType.balance_sheet == False)).scalars().all() # noqa: E712
)

@ -19,6 +19,7 @@ class ProductLink(BaseModel):
class ProductIn(BaseModel):
name: str = Field(..., min_length=1)
fraction_units: str = Field(..., min_length=1)
skus: List[StockKeepingUnit]
product_group: ProductGroupLink = Field(...)
is_active: bool
@ -26,7 +27,6 @@ class ProductIn(BaseModel):
is_sold: bool
class Config:
fields = {"id_": "id"}
anystr_strip_whitespace = True
alias_generator = to_camel
@ -39,6 +39,7 @@ class Product(ProductIn):
class ProductBlank(ProductIn):
name: str
fraction_units: str
skus: List[StockKeepingUnit]
product_group: Optional[ProductGroupLink] # type: ignore[assignment]
is_fixture: bool

@ -0,0 +1,20 @@
import uuid
from decimal import Decimal
from brewman.schemas import to_camel
from pydantic import BaseModel, Field
class ProductSku(BaseModel):
id_: uuid.UUID = Field(...)
name: str
fraction_units: str
cost_price: Decimal
sale_price: Decimal
fraction: Decimal
product_yield: Decimal
is_rate_contracted: bool
class Config:
alias_generator = to_camel

@ -10,12 +10,10 @@ from . import to_camel
class StockKeepingUnit(BaseModel):
id_: Optional[uuid.UUID]
is_default: bool
units: str = Field(..., min_length=1)
fraction: Decimal = Field(ge=1, default=1)
fraction_units: str
product_yield: Decimal = Field(gt=0, le=1, default=1)
price: Decimal = Field(ge=0, multiple_of=0.01, default=0)
cost_price: Decimal = Field(ge=0, multiple_of=0.01, default=0)
sale_price: Decimal = Field(ge=0, multiple_of=0.01, default=0)
class Config:

2
docker/.gitignore vendored Normal file

@ -0,0 +1,2 @@
app/package.json
app/pyproject.toml

@ -1,4 +1,5 @@
import { Product } from './product';
import { ProductSku } from './product-sku';
export class Batch {
id: string | null;
@ -7,7 +8,7 @@ export class Batch {
tax: number;
discount: number;
rate: number;
sku: Product;
sku: ProductSku;
public constructor(init?: Partial<Batch>) {
this.id = null;
@ -16,7 +17,7 @@ export class Batch {
this.tax = 0;
this.discount = 0;
this.rate = 0;
this.sku = new Product();
this.sku = new ProductSku();
Object.assign(this, init);
}
}

@ -0,0 +1,24 @@
export class ProductSku {
id: string;
name: string;
costPrice: number;
salePrice: number;
fraction: number;
productYield: number;
fractionUnits: string;
isRateContracted: boolean;
public constructor(init?: Partial<ProductSku>) {
this.id = '';
this.name = '';
this.costPrice = 0;
this.salePrice = 0;
this.fraction = 0;
this.productYield = 0;
this.fractionUnits = '';
this.isRateContracted = false;
Object.assign(this, init);
}
}

@ -1,21 +1,17 @@
import { ProductGroup } from './product-group';
export class StockKeepingUnit {
isDefault: boolean;
units: string;
fraction: number;
fractionUnits: string;
productYield: number;
price: number;
costPrice: number;
salePrice: number;
public constructor(init?: Partial<StockKeepingUnit>) {
this.isDefault = false;
this.units = '';
this.fraction = 1;
this.fractionUnits = '';
this.productYield = 1;
this.price = 0;
this.costPrice = 0;
this.salePrice = 0;
Object.assign(this, init);
}
@ -26,7 +22,6 @@ export class Product {
code: number;
name: string;
skus: StockKeepingUnit[];
price: number | undefined;
fractionUnits: string | undefined;
isActive: boolean;
@ -34,7 +29,6 @@ export class Product {
isPurchased: boolean;
isSold: boolean;
productGroup?: ProductGroup;
isRateContracted?: boolean;
public constructor(init?: Partial<Product>) {
this.code = 0;

@ -9,6 +9,7 @@ import { Observable, of as observableOf } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators';
import { Product } from '../core/product';
import { ProductSku } from '../core/product-sku';
import { ProductService } from '../product/product.service';
import { ToCsvService } from '../shared/to-csv.service';
@ -49,7 +50,7 @@ export class ProductLedgerComponent implements OnInit, AfterViewInit {
'runningAmount',
];
products: Observable<Product[]>;
products: Observable<ProductSku[]>;
constructor(
private route: ActivatedRoute,
@ -71,7 +72,7 @@ export class ProductLedgerComponent implements OnInit, AfterViewInit {
debounceTime(150),
distinctUntilChanged(),
switchMap((x) =>
x === null ? observableOf([]) : this.productSer.autocomplete(x, null, false, false),
x === null ? observableOf([]) : this.productSer.autocompleteProduct(x, null),
),
);
}

@ -16,10 +16,6 @@
<mat-label>Fraction</mat-label>
<input matInput type="number" placeholder="Fraction" formControlName="fraction" />
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>Fraction Units</mat-label>
<input matInput placeholder="Fraction Units" formControlName="fractionUnits" />
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>Yield</mat-label>
<input matInput type="number" placeholder="Yield" formControlName="productYield" />
@ -30,7 +26,7 @@
matInput
type="number"
placeholder="{{ data.isPurchased ? 'Purchase Price' : 'Cost Price' }}"
formControlName="price"
formControlName="costPrice"
/>
</mat-form-field>
<mat-form-field fxFlex [hidden]="!data.isSold">

@ -21,9 +21,8 @@ export class ProductDetailDialogComponent implements OnInit {
this.form = this.fb.group({
units: '',
fraction: '',
fractionUnits: '',
productYield: '',
price: '',
costPrice: '',
salePrice: '',
});
}
@ -32,9 +31,8 @@ export class ProductDetailDialogComponent implements OnInit {
this.form.setValue({
units: this.data.item.units,
fraction: '' + this.data.item.fraction,
fractionUnits: this.data.item.fractionUnits,
productYield: '' + this.data.item.productYield,
price: '' + this.data.item.price,
costPrice: '' + this.data.item.costPrice,
salePrice: '' + this.data.item.salePrice,
});
}
@ -49,8 +47,8 @@ export class ProductDetailDialogComponent implements OnInit {
if (productYield < 0 || productYield > 1) {
return;
}
const price = +formValue.price;
if (price < 0) {
const costPrice = +formValue.costPrice;
if (costPrice < 0) {
return;
}
const salePrice = +formValue.salePrice;
@ -59,9 +57,8 @@ export class ProductDetailDialogComponent implements OnInit {
}
this.data.item.units = formValue.units;
this.data.item.fraction = fraction;
this.data.item.fractionUnits = formValue.fractionUnits;
this.data.item.productYield = productYield;
this.data.item.price = price;
this.data.item.costPrice = costPrice;
this.data.item.salePrice = salePrice;
this.dialogRef.close(this.data.item);
}

@ -24,10 +24,14 @@
fxLayoutGap="20px"
fxLayoutGap.lt-md="0px"
>
<mat-form-field fxFlex>
<mat-form-field fxFlex="80">
<mat-label>Name</mat-label>
<input matInput #nameElement placeholder="Name" formControlName="name" />
</mat-form-field>
<mat-form-field fxFlex="20">
<mat-label>Fraction Units</mat-label>
<input matInput placeholder="Fraction Units" formControlName="fractionUnits" />
</mat-form-field>
</div>
<div
fxLayout="row"
@ -73,10 +77,6 @@
<mat-label>Fraction</mat-label>
<input matInput type="number" placeholder="Fraction" formControlName="fraction" />
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>Fraction Units</mat-label>
<input matInput placeholder="Fraction Units" formControlName="fractionUnits" />
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>Yield</mat-label>
<input matInput type="number" placeholder="Yield" formControlName="productYield" />
@ -87,7 +87,7 @@
matInput
type="number"
placeholder="{{ item.isPurchased ? 'Purchase Price' : 'Cost Price' }}"
formControlName="price"
formControlName="costPrice"
/>
</mat-form-field>
<mat-form-field fxFlex>
@ -98,18 +98,6 @@
</div>
</form>
<mat-table [dataSource]="dataSource" aria-label="Elements">
<!-- Checkbox Column -->
<ng-container matColumnDef="isDefault">
<mat-header-cell *matHeaderCellDef>Default</mat-header-cell>
<mat-cell *matCellDef="let row">
<mat-checkbox
(click)="$event.stopPropagation()"
(change)="changeDefault($event, row)"
[checked]="row.isDefault"
>
</mat-checkbox>
</mat-cell>
</ng-container>
<!-- Units Column -->
<ng-container matColumnDef="units">
<mat-header-cell *matHeaderCellDef>Units</mat-header-cell>
@ -122,22 +110,18 @@
<mat-cell *matCellDef="let row">{{ row.fraction }}</mat-cell>
</ng-container>
<!-- Fraction Units Column -->
<ng-container matColumnDef="fractionUnits">
<mat-header-cell *matHeaderCellDef>Fraction Units</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.fractionUnits }}</mat-cell>
</ng-container>
<!-- Yield Column -->
<ng-container matColumnDef="yield">
<mat-header-cell *matHeaderCellDef class="right">Yield</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">{{ row.productYield }}</mat-cell>
</ng-container>
<!-- Price Column -->
<ng-container matColumnDef="price">
<mat-header-cell *matHeaderCellDef class="right">Price</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">{{ row.price | currency: 'INR' }}</mat-cell>
<!-- Cost Price Column -->
<ng-container matColumnDef="costPrice">
<mat-header-cell *matHeaderCellDef class="right">Cost Price</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">{{
row.costPrice | currency: 'INR'
}}</mat-cell>
</ng-container>
<!-- Sale Price Column -->

@ -1,6 +1,5 @@
import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject } from 'rxjs';
@ -27,16 +26,7 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
dataSource: ProductDetailDatasource = new ProductDetailDatasource(this.skus);
item: Product = new Product();
displayedColumns = [
'isDefault',
'units',
'fraction',
'fractionUnits',
'yield',
'price',
'salePrice',
'action',
];
displayedColumns = ['units', 'fraction', 'yield', 'costPrice', 'salePrice', 'action'];
constructor(
private route: ActivatedRoute,
@ -49,12 +39,12 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
this.form = this.fb.group({
code: { value: '', disabled: true },
name: '',
fractionUnits: '',
addRow: this.fb.group({
units: '',
fraction: '',
fractionUnits: '',
productYield: '',
price: '',
costPrice: '',
salePrice: '',
}),
isPurchased: '',
@ -79,13 +69,13 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
this.item = item;
this.form.setValue({
code: this.item.code || '(Auto)',
name: this.item.name || '',
name: this.item.name,
fractionUnits: this.item.fractionUnits,
addRow: {
units: '',
fraction: '',
fractionUnits: '',
productYield: '',
price: '',
costPrice: '',
salePrice: '',
},
isPurchased: this.item.isPurchased,
@ -115,8 +105,8 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
this.toaster.show('Danger', 'Product Yield has to be > 0 and <= 1');
return;
}
const price = +formValue.price;
if (price < 0) {
const costPrice = +formValue.costPrice;
if (costPrice < 0) {
this.toaster.show('Danger', 'Price has to be >= 0');
return;
}
@ -129,9 +119,8 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
new StockKeepingUnit({
units: formValue.units,
fraction,
fractionUnits: formValue.fractionUnits,
productYield,
price,
costPrice,
salePrice,
}),
);
@ -143,9 +132,8 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
(this.form.get('addRow') as FormControl).reset({
units: '',
fraction: '',
fractionUnits: '',
productYield: '',
price: '',
costPrice: '',
salePrice: '',
});
}
@ -216,6 +204,7 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
getItem(): Product {
const formModel = this.form.value;
this.item.name = formModel.name;
this.item.fractionUnits = formModel.fractionUnits;
this.item.isPurchased = formModel.isPurchased;
this.item.isSold = formModel.isSold;
this.item.isActive = formModel.isActive;
@ -225,9 +214,4 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
this.item.productGroup.id = formModel.productGroup;
return this.item;
}
changeDefault($event: MatCheckboxChange, row: StockKeepingUnit) {
this.item.skus.forEach((x) => (x.isDefault = false));
row.isDefault = true;
}
}

@ -39,7 +39,7 @@
<li *ngFor="let sku of row.skus">
<a [routerLink]="['/products', row.id]"
>{{ row.name }} ({{
showExtended ? sku.fraction + ' ' + sku.fractionUnits + ' = 1 ' : ''
showExtended ? sku.fraction + ' ' + row.fractionUnits + ' = 1 ' : ''
}}{{ sku.units }})</a
>
</li>
@ -53,7 +53,7 @@
<mat-cell *matCellDef="let row">
<ul>
<li *ngFor="let sku of row.skus">
{{ sku.price | currency: 'INR' }}
{{ sku.costPrice | currency: 'INR' }}
</li>
</ul>
</mat-cell>

@ -5,6 +5,7 @@ import { catchError } from 'rxjs/operators';
import { ErrorLoggerService } from '../core/error-logger.service';
import { Product } from '../core/product';
import { ProductSku } from '../core/product-sku';
const url = '/api/products';
const serviceName = 'ProductService';
@ -51,16 +52,28 @@ export class ProductService {
.pipe(catchError(this.log.handleError(serviceName, 'delete'))) as Observable<Product>;
}
autocomplete(
autocompleteProduct(query: string, isPurchased: boolean | null): Observable<ProductSku[]> {
const options = {
params: new HttpParams().set('q', query),
};
if (isPurchased !== null) {
options.params = options.params.set('p', isPurchased.toString());
}
return this.http
.get<ProductSku[]>(`${url}/q-product`, options)
.pipe(catchError(this.log.handleError(serviceName, 'autocomplete'))) as Observable<
ProductSku[]
>;
}
autocompleteSku(
query: string,
isPurchased: boolean | null,
extended: boolean = false,
skus: boolean = true,
date?: string,
vendorId?: string,
): Observable<Product[]> {
): Observable<ProductSku[]> {
const options = {
params: new HttpParams().set('q', query).set('e', extended.toString()).set('s', skus),
params: new HttpParams().set('q', query),
};
if (isPurchased !== null) {
options.params = options.params.set('p', isPurchased.toString());
@ -69,7 +82,9 @@ export class ProductService {
options.params = options.params.set('v', vendorId as string).set('d', date as string);
}
return this.http
.get<Product[]>(`${url}/query`, options)
.pipe(catchError(this.log.handleError(serviceName, 'autocomplete'))) as Observable<Product[]>;
.get<ProductSku[]>(`${url}/q-sku`, options)
.pipe(catchError(this.log.handleError(serviceName, 'autocomplete'))) as Observable<
ProductSku[]
>;
}
}

@ -9,6 +9,7 @@ import { debounceTime, distinctUntilChanged, map, startWith, switchMap } from 'r
import { Batch } from '../core/batch';
import { Inventory } from '../core/inventory';
import { Product } from '../core/product';
import { ProductSku } from '../core/product-sku';
import { ProductService } from '../product/product.service';
import { MathService } from '../shared/math.service';
@ -18,9 +19,9 @@ import { MathService } from '../shared/math.service';
styleUrls: ['./purchase-dialog.component.css'],
})
export class PurchaseDialogComponent implements OnInit {
products: Observable<Product[]>;
products: Observable<ProductSku[]>;
form: FormGroup;
product: Product = new Product();
product: ProductSku = new ProductSku();
constructor(
public dialogRef: MatDialogRef<PurchaseDialogComponent>,
@ -42,7 +43,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, true))),
switchMap((x) => (x === null ? observableOf([]) : this.productSer.autocompleteSku(x, true))),
);
}
@ -63,7 +64,7 @@ export class PurchaseDialogComponent implements OnInit {
productSelected(event: MatAutocompleteSelectedEvent): void {
this.product = event.option.value;
(this.form.get('price') as FormControl).setValue(this.product.price);
(this.form.get('price') as FormControl).setValue(this.product.costPrice);
}
accept(): void {

@ -17,6 +17,7 @@ import { Batch } from '../core/batch';
import { DbFile } from '../core/db-file';
import { Inventory } from '../core/inventory';
import { Product } from '../core/product';
import { ProductSku } from '../core/product-sku';
import { ToasterService } from '../core/toaster.service';
import { User } from '../core/user';
import { Voucher } from '../core/voucher';
@ -43,13 +44,13 @@ export class PurchaseComponent implements OnInit, AfterViewInit, OnDestroy {
dataSource: PurchaseDataSource = new PurchaseDataSource(this.inventoryObservable);
form: FormGroup;
voucher: Voucher = new Voucher();
product: Product | null = null;
product: ProductSku | null = null;
accBal: AccountBalance | null = null;
displayedColumns = ['product', 'quantity', 'rate', 'tax', 'discount', 'amount', 'action'];
accounts: Observable<Account[]>;
products: Observable<Product[]>;
products: Observable<ProductSku[]>;
constructor(
private route: ActivatedRoute,
@ -98,11 +99,9 @@ export class PurchaseComponent implements OnInit, AfterViewInit, OnDestroy {
switchMap((x) =>
x === null
? observableOf([])
: this.productSer.autocomplete(
: this.productSer.autocompleteSku(
x,
true,
false,
true,
moment(this.form.value.date).format('DD-MMM-YYYY'),
this.form.value.account.id,
),
@ -198,7 +197,7 @@ export class PurchaseComponent implements OnInit, AfterViewInit, OnDestroy {
return;
}
const price = this.product.isRateContracted
? this.product.price
? (this.product.costPrice as number)
: this.math.parseAmount(formValue.price, 2);
const tax = this.product.isRateContracted ? 0 : this.math.parseAmount(formValue.tax, 5);
const discount = this.product.isRateContracted
@ -208,7 +207,7 @@ export class PurchaseComponent implements OnInit, AfterViewInit, OnDestroy {
return;
}
const oldFiltered = this.voucher.inventories.filter(
(x) => x.batch?.sku.id === (this.product as Product).id,
(x) => x.batch?.sku.id === (this.product as ProductSku).id,
);
if (oldFiltered.length) {
this.toaster.show('Danger', 'Product already added');
@ -365,10 +364,10 @@ export class PurchaseComponent implements OnInit, AfterViewInit, OnDestroy {
}
productSelected(event: MatAutocompleteSelectedEvent): void {
const product: Product = event.option.value;
const product: ProductSku = event.option.value;
const addRowForm: FormControl = this.form.get('addRow') as FormControl;
this.product = product;
(addRowForm.get('price') as FormControl).setValue(product.price);
(addRowForm.get('price') as FormControl).setValue(product.costPrice);
if (product.isRateContracted) {
(addRowForm.get('price') as FormControl).disable();
(addRowForm.get('tax') as FormControl).disable();

@ -10,6 +10,7 @@ import { debounceTime, distinctUntilChanged, map, startWith, switchMap } from 'r
import { Account } from '../../core/account';
import { AccountService } from '../../core/account.service';
import { Product } from '../../core/product';
import { ProductSku } from '../../core/product-sku';
import { ToasterService } from '../../core/toaster.service';
import { ProductService } from '../../product/product.service';
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component';
@ -38,7 +39,7 @@ export class RateContractDetailComponent implements OnInit, AfterViewInit {
displayedColumns = ['product', 'price', 'action'];
accounts: Observable<Account[]>;
products: Observable<Product[]>;
products: Observable<ProductSku[]>;
constructor(
private route: ActivatedRoute,
@ -77,7 +78,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, true))),
switchMap((x) => (x === null ? observableOf([]) : this.productSer.autocompleteSku(x, true))),
);
}