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:
parent
b3075577e6
commit
30e3288b1e
brewman
alembic/versions
brewman
docker
overlord/src/app
core
product-ledger
product
product-detail
product-detail-dialog.component.htmlproduct-detail-dialog.component.tsproduct-detail.component.htmlproduct-detail.component.ts
product-list
product.service.tspurchase
rate-contract/rate-contract-detail
50
brewman/alembic/versions/c39eb451a683_fraction_units.py
Normal file
50
brewman/alembic/versions/c39eb451a683_fraction_units.py
Normal file
@ -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
|
||||
|
20
brewman/brewman/schemas/product_sku.py
Normal file
20
brewman/brewman/schemas/product_sku.py
Normal file
@ -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
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);
|
||||
}
|
||||
}
|
||||
|
24
overlord/src/app/core/product-sku.ts
Normal file
24
overlord/src/app/core/product-sku.ts
Normal file
@ -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))),
|
||||
);
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user