Feature: Mozimo Product Register

This commit is contained in:
Amritanshu Agrawal 2024-08-16 10:23:50 +05:30
parent bd0ee4facd
commit 3b46ac97bc
20 changed files with 1107 additions and 0 deletions

@ -0,0 +1,46 @@
"""mozimo product ledger
Revision ID: 82e5c8d18382
Revises: fab52fb911e4
Create Date: 2024-08-16 06:57:58.336202
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "82e5c8d18382"
down_revision = "fab52fb911e4"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"mozimo_stock_register",
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("date", sa.Date(), nullable=False),
sa.Column("sku_id", sa.Uuid(), nullable=False),
sa.Column("received", sa.Numeric(precision=15, scale=2), nullable=False),
sa.Column("sale", sa.Numeric(precision=15, scale=2), nullable=False),
sa.Column("nc", sa.Numeric(precision=15, scale=2), nullable=False),
sa.Column("display", sa.Numeric(precision=15, scale=2), nullable=True),
sa.Column("ageing", sa.Numeric(precision=15, scale=2), nullable=True),
sa.Column("last_edit_date", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(
["sku_id"], ["stock_keeping_units.id"], name=op.f("fk_mozimo_stock_register_sku_id_stock_keeping_units")
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_mozimo_stock_register")),
sa.UniqueConstraint("date", "sku_id", name=op.f("uq_mozimo_stock_register_date")),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("mozimo_stock_register")
# ### end Alembic commands ###

@ -17,6 +17,7 @@ from ..models.incentive import Incentive # noqa: F401
from ..models.inventory import Inventory # noqa: F401
from ..models.journal import Journal # noqa: F401
from ..models.login_history import LoginHistory # noqa: F401
from ..models.mozimo_stock_register import MozimoStockRegister # noqa: F401
from ..models.period import Period # noqa: F401
from ..models.permission import Permission # noqa: F401
from ..models.price import Price # noqa: F401

@ -53,6 +53,7 @@ from .routers.reports import (
daybook,
entries,
ledger,
mozimo_product_register,
net_transactions,
non_contract_purchase,
product_ledger,
@ -114,6 +115,8 @@ app.include_router(trial_balance.router, prefix="/api/trial-balance", tags=["rep
app.include_router(entries.router, prefix="/api/entries", tags=["reports"])
app.include_router(batch_integrity.router, prefix="/api/batch-integrity", tags=["reports"])
app.include_router(non_contract_purchase.router, prefix="/api/non-contract-purchase", tags=["reports"])
app.include_router(mozimo_product_register.router, prefix="/api/mozimo-product-register", tags=["mozimo"])
app.include_router(issue_grid.router, prefix="/api/issue-grid", tags=["vouchers"])
app.include_router(batch.router, prefix="/api/batch", tags=["vouchers"])

@ -0,0 +1,63 @@
from __future__ import annotations
import uuid
from datetime import UTC, date, datetime
from decimal import Decimal
from typing import TYPE_CHECKING
from sqlalchemy import Date, DateTime, ForeignKey, Numeric, UniqueConstraint, Uuid
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ..db.base_class import reg
if TYPE_CHECKING:
from .stock_keeping_unit import StockKeepingUnit
@reg.mapped_as_dataclass(unsafe_hash=True)
class MozimoStockRegister:
__tablename__ = "mozimo_stock_register"
__table_args__ = (UniqueConstraint("date", "sku_id"),)
id_: Mapped[uuid.UUID] = mapped_column("id", Uuid, primary_key=True, insert_default=uuid.uuid4)
date_: Mapped[date] = mapped_column("date", Date, nullable=False)
sku_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("stock_keeping_units.id"), nullable=False)
received: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=2), nullable=False)
sale: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=2), nullable=False)
nc: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=2), nullable=False)
# The physical stock in the display area
display: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=2), nullable=True)
# The physical stock in the ageing room
ageing: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=2), nullable=True)
last_edit_date: Mapped[datetime] = mapped_column(DateTime(), nullable=False)
sku: Mapped[StockKeepingUnit] = relationship("StockKeepingUnit")
def __init__(
self,
date_: datetime.date,
received: Decimal,
sale: Decimal,
nc: Decimal,
display: Decimal,
ageing: Decimal,
sku_id: uuid.UUID | None = None,
sku: StockKeepingUnit | None = None,
id_: uuid.UUID | None = None,
last_edit_date: datetime | None = None,
):
self.date_ = date_
self.received = received
self.sale = sale
self.nc = nc
self.display = display
self.ageing = ageing
if sku_id is not None:
self.sku_id = sku_id
if sku is not None:
self.sku = sku
self.sku_id = sku.id
if id_ is not None:
self.id_ = id_
self.last_edit_date = last_edit_date or datetime.now(UTC).replace(tzinfo=None)

@ -0,0 +1,203 @@
import uuid
from datetime import UTC, date, datetime
from decimal import Decimal
from fastapi import APIRouter, HTTPException, Request, Security, status
from sqlalchemy import desc
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from sqlalchemy.sql.expression import select
import brewman.schemas.mozimo_product_register as schemas
from ...core.security import get_current_active_user as get_user
from ...core.session import get_finish_date, get_start_date, set_period
from ...db.session import SessionFuture
from ...models.mozimo_stock_register import MozimoStockRegister
from ...models.stock_keeping_unit import StockKeepingUnit
from ...schemas.user import UserToken
from ..attendance import date_range
router = APIRouter()
@router.get("", response_model=schemas.MozimoProductRegister)
def show_blank(
request: Request,
user: UserToken = Security(get_user, scopes=["ledger"]),
) -> schemas.MozimoProductRegister:
return schemas.MozimoProductRegister(
start_date=get_start_date(request.session),
finish_date=get_finish_date(request.session),
product=None,
body=[],
)
@router.get("/{id_}", response_model=schemas.MozimoProductRegister)
def show_data(
id_: uuid.UUID,
request: Request,
s: str | None = None,
f: str | None = None,
user: UserToken = Security(get_user, scopes=["ledger"]),
) -> schemas.MozimoProductRegister:
with SessionFuture() as db:
sku = db.execute(select(StockKeepingUnit).where(StockKeepingUnit.id == id_)).scalar_one()
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()
body = build_report(sku.id, start_date, finish_date, db)
set_period(start_date, finish_date, request.session)
return schemas.MozimoProductRegister(
start_date=start_date,
finish_date=finish_date,
product=schemas.ProductLink(id_=sku.id, name=f"{sku.product.name} ({sku.units})"),
body=body,
)
def build_report(
product_id: uuid.UUID, start_date: date, finish_date: date, db: Session
) -> list[schemas.MozimoProductRegisterItem]:
body = []
ob = opening_balance(product_id, start_date, db)
for date_ in date_range(start_date, finish_date, inclusive=True):
item = (
db.execute(
select(MozimoStockRegister).where(
MozimoStockRegister.sku_id == product_id,
MozimoStockRegister.date_ == date_,
)
)
.scalars()
.one_or_none()
)
body.append(
schemas.MozimoProductRegisterItem(
id_=None if item is None else item.id_,
date_=date_,
opening=ob,
received=Decimal(0) if item is None else item.received,
sale=Decimal(0) if item is None else item.sale,
nc=Decimal(0) if item is None else item.nc,
display=None if item is None else item.display,
ageing=None if item is None else item.ageing,
last_edit_date=None if item is None else item.last_edit_date,
)
)
if item is not None:
if item.ageing is not None or item.display is not None:
closing = (item.ageing or 0) + (item.display or 0)
else:
closing = ob + item.received - item.sale - item.nc
ob = closing # Setting the cb as ob for next iteration
return body
def opening_balance(product_id: uuid.UUID, start_date: date, db: Session) -> Decimal:
opening = db.execute(
select(MozimoStockRegister.display + MozimoStockRegister.ageing)
.order_by(desc(MozimoStockRegister.date_))
.where(
MozimoStockRegister.sku_id == product_id,
MozimoStockRegister.date_ < start_date,
)
).scalar()
return Decimal(0) if opening is None else opening
@router.post("", response_model=schemas.MozimoProductRegister)
def save_route(
request: Request,
data: schemas.MozimoProductRegister,
user: UserToken = Security(get_user, scopes=["ledger"]),
) -> schemas.MozimoProductRegister:
try:
if any(i for i in data.body if i.received < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Received cannot be less than 0.",
)
if any(i for i in data.body if i.sale < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Sale cannot be less than 0.",
)
if any(i for i in data.body if i.nc < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="No Charge cannot be less than 0.",
)
if any(i for i in data.body if i.display is not None and i.display < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Quantity on display can be Blank, but cannot be less than 0.",
)
if any(i for i in data.body if i.ageing is not None and i.ageing < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Quantity in ageing room can be Blank, but cannot be less than 0.",
)
with SessionFuture() as db:
now = datetime.now(UTC).replace(tzinfo=None)
ob = opening_balance(data.product.id_, data.start_date, db)
for item in data.body:
if item.ageing is not None or item.display is not None:
closing = (item.ageing or 0) + (item.display or 0)
else:
closing = ob + item.received - item.sale - item.nc
ob = closing # Setting the cb as ob for next iteration
if closing < 0:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Closing Stock cannot be less than 0.",
)
for item in data.body:
old = (
db.execute(
select(MozimoStockRegister).where(
MozimoStockRegister.sku_id == data.product.id_,
MozimoStockRegister.date_ == item.date_,
)
)
.scalars()
.one_or_none()
)
if old is not None:
if (
old.received != item.received
or old.sale != item.sale
or old.nc != item.nc
or old.display != item.display
or old.ageing != item.ageing
):
old.received = item.received
old.sale = item.sale
old.nc = item.nc
old.display = item.display
old.ageing = item.ageing
old.last_edit_date = now
else:
entry = MozimoStockRegister(
item.date_, item.received, item.sale, item.nc, item.display, item.ageing, data.product.id_
)
db.add(entry)
body = build_report(data.product.id_, data.start_date, data.finish_date, db)
db.commit()
return schemas.MozimoProductRegister(
start_date=data.start_date,
finish_date=data.finish_date,
product=data.product,
body=body,
)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)

@ -0,0 +1,85 @@
import uuid
from datetime import date, datetime
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from . import Daf, to_camel
from .product import ProductLink
class MozimoProductRegisterItem(BaseModel):
id_: uuid.UUID | None = None
date_: date
opening: Daf
received: Daf
sale: Daf
nc: Daf
display: Daf | None
ageing: Daf | None
last_edit_date: datetime | None = None
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: date | str) -> date:
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("date_")
def serialize_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
@field_validator("last_edit_date", mode="before")
@classmethod
def parse_last_edit_date(cls, value: None | datetime | str) -> datetime | None:
if value is None or value == "":
return None
if isinstance(value, datetime):
return value
return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("last_edit_date")
def serialize_last_edit_date(self, value: datetime, info: FieldSerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y %H:%M")
class MozimoProductRegister(BaseModel):
start_date: date | None = None
finish_date: date | None = None
product: ProductLink | None = None
body: list[MozimoProductRegisterItem]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("start_date", mode="before")
@classmethod
def parse_start_date(cls, value: date | str | None) -> date | None:
if value is None:
return None
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("start_date")
def serialize_start_date(self, value: date, info: FieldSerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y")
@field_validator("finish_date", mode="before")
@classmethod
def parse_finish_date(cls, value: date | str | None) -> date | None:
if value is None:
return None
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("finish_date")
def serialize_finish_date(self, value: date, info: FieldSerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y")

@ -89,6 +89,10 @@ export const routes: Routes = [
path: 'ledger',
loadChildren: () => import('./ledger/ledger.routes').then((mod) => mod.routes),
},
{
path: 'mozimo-product-register',
loadChildren: () => import('./mozimo-product-register/mozimo-product-register.routes').then((mod) => mod.routes),
},
{
path: 'net-transactions',
loadChildren: () => import('./net-transactions/net-transactions.routes').then((mod) => mod.routes),

@ -35,6 +35,7 @@
<a mat-menu-item routerLink="/rate-contracts">Rate Contracts</a>
<a mat-menu-item routerLink="/batch-integrity-report">Batch Integrity</a>
<a mat-menu-item routerLink="/non-contract-purchase">Non Contract Purchases</a>
<a mat-menu-item routerLink="/mozimo-product-register">Mozimo Product Register</a>
</mat-menu>
<button mat-button [matMenuTriggerFor]="productReportMenu">Product Reports</button>

@ -0,0 +1,66 @@
import { DataSource } from '@angular/cdk/collections';
import { EventEmitter } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { merge, Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { MozimoProductRegisterItem } from './mozimo-product-register-item';
export class MozimoProductRegisterDataSource extends DataSource<MozimoProductRegisterItem> {
public data: MozimoProductRegisterItem[] = [];
public paginator?: MatPaginator;
constructor(public dataObs: Observable<MozimoProductRegisterItem[]>) {
super();
}
connect(): Observable<MozimoProductRegisterItem[]> {
const dataMutations: EventEmitter<PageEvent>[] = [];
const d = this.dataObs.pipe(
tap((x) => {
this.data = x;
}),
);
if (this.paginator) {
dataMutations.push((this.paginator as MatPaginator).page);
}
return merge(d, ...dataMutations).pipe(
map(() => this.calculate([...this.data])),
tap(() => {
if (this.paginator) {
this.paginator.length = this.data.length;
}
}),
map((x: MozimoProductRegisterItem[]) => this.getPagedData(x)),
);
}
disconnect() {}
private calculate(data: MozimoProductRegisterItem[]): MozimoProductRegisterItem[] {
if (data.length === 0) {
return data;
}
let ob = data[0].opening;
data.forEach((item) => {
item.opening = ob;
if (item.ageing !== null || item.display !== null) {
item.closing = (item.ageing ?? 0) + (item.display ?? 0);
item.variance = item.opening + item.received - item.sale - item.nc - item.closing;
} else {
item.closing = item.opening + item.received - item.sale - item.nc;
item.variance = 0;
}
ob = item.closing;
});
return data;
}
private getPagedData(data: MozimoProductRegisterItem[]) {
if (this.paginator === undefined) {
return data;
}
const startIndex = this.paginator.pageIndex * this.paginator.pageSize;
return data.splice(startIndex, this.paginator.pageSize);
}
}

@ -0,0 +1,28 @@
export class MozimoProductRegisterItem {
id: string | null;
date: string;
opening: number;
received: number;
sale: number;
nc: number;
display: number | null;
ageing: number | null;
variance: number | null;
closing: number;
lastEditDate: string | null;
public constructor(init?: Partial<MozimoProductRegisterItem>) {
this.id = null;
this.date = '';
this.opening = 0;
this.received = 0;
this.sale = 0;
this.nc = 0;
this.display = null;
this.ageing = null;
this.variance = null;
this.closing = 0;
this.lastEditDate = null;
Object.assign(this, init);
}
}

@ -0,0 +1,17 @@
.right {
display: flex;
justify-content: flex-end;
}
.first {
margin-right: 4px;
}
.middle {
margin-left: 4px;
margin-right: 4px;
}
.last {
margin-left: 4px;
}

@ -0,0 +1,164 @@
<mat-card>
<mat-card-header>
<mat-card-title-group>
<mat-card-title>Mozimo Product Register</mat-card-title>
@if (dataSource.data.length) {
<button mat-icon-button (click)="exportCsv()">
<mat-icon>save_alt</mat-icon>
</button>
}
</mat-card-title-group>
</mat-card-header>
<mat-card-content>
<form [formGroup]="form" class="flex flex-col">
<div class="flex flex-row justify-around content-start items-start sm:max-lg:flex-col">
<mat-form-field class="flex-auto mr-5">
<mat-label>Start Date</mat-label>
<input
matInput
#startDateElement
[matDatepicker]="startDate"
formControlName="startDate"
autocomplete="off"
/>
<mat-datepicker-toggle matSuffix [for]="startDate"></mat-datepicker-toggle>
<mat-datepicker #startDate></mat-datepicker>
</mat-form-field>
<mat-form-field class="flex-auto">
<mat-label>Finish Date</mat-label>
<input matInput [matDatepicker]="finishDate" formControlName="finishDate" autocomplete="off" />
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
<mat-datepicker #finishDate></mat-datepicker>
</mat-form-field>
</div>
<div class="flex flex-row justify-around content-start items-start sm:max-lg:flex-col">
<mat-form-field class="flex-auto basis-4/5 mr-5">
<mat-label>Product</mat-label>
<input
type="text"
matInput
#productElement
[matAutocomplete]="auto"
formControlName="product"
autocomplete="off"
/>
<mat-autocomplete
#auto="matAutocomplete"
autoActiveFirstOption
[displayWith]="displayFn"
(optionSelected)="selected($event)"
>
@for (product of products | async; track product) {
<mat-option [value]="product">{{ product.name }}</mat-option>
}
</mat-autocomplete>
</mat-form-field>
<button mat-raised-button class="flex-auto basis-1/5" color="primary" (click)="show()">Show</button>
</div>
<mat-table #table [dataSource]="dataSource" aria-label="Elements" formArrayName="items">
<!-- Date Column -->
<ng-container matColumnDef="date">
<mat-header-cell *matHeaderCellDef class="center first">Date</mat-header-cell>
<mat-cell *matCellDef="let row" class="center first">
<span
matBadge="1"
matBadgeSize="small"
[matBadgeHidden]="!row.lastEditDate"
matTooltip="{{ row.lastEditDate | localTime }}"
[matTooltipDisabled]="!row.lastEditDate"
>{{ row.date }}</span
>
</mat-cell>
</ng-container>
<!-- Opening Column -->
<ng-container matColumnDef="opening">
<mat-header-cell *matHeaderCellDef class="right middle">Opening</mat-header-cell>
<mat-cell *matCellDef="let row" class="right middle">{{ row.opening | number: '0.2-2' }}</mat-cell>
</ng-container>
<!-- Received Column -->
<ng-container matColumnDef="received">
<mat-header-cell *matHeaderCellDef class="middle">Received</mat-header-cell>
<mat-cell *matCellDef="let row; let i = index" [formGroupName]="i" class="middle">
<mat-form-field class="flex-auto">
<mat-label>Received</mat-label>
<input matInput type="number" formControlName="received" (change)="updateReceived($event, row)" />
</mat-form-field>
</mat-cell>
</ng-container>
<!-- Sale Column -->
<ng-container matColumnDef="sale">
<mat-header-cell *matHeaderCellDef class="middle">Sale</mat-header-cell>
<mat-cell *matCellDef="let row; let i = index" [formGroupName]="i" class="middle">
<mat-form-field class="flex-auto">
<mat-label>Sale</mat-label>
<input matInput type="number" formControlName="sale" (change)="updateSale($event, row)" />
</mat-form-field>
</mat-cell>
</ng-container>
<!-- Nc Column -->
<ng-container matColumnDef="nc">
<mat-header-cell *matHeaderCellDef class="middle">Nc</mat-header-cell>
<mat-cell *matCellDef="let row; let i = index" [formGroupName]="i" class="middle">
<mat-form-field class="flex-auto">
<mat-label>Nc</mat-label>
<input matInput type="number" formControlName="nc" (change)="updateNc($event, row)" />
</mat-form-field>
</mat-cell>
</ng-container>
<!-- Display Column -->
<ng-container matColumnDef="display">
<mat-header-cell *matHeaderCellDef class="middle">Display</mat-header-cell>
<mat-cell *matCellDef="let row; let i = index" [formGroupName]="i" class="middle">
<mat-form-field class="flex-auto">
<mat-label>Display</mat-label>
<input matInput type="number" formControlName="display" (change)="updateDisplay($event, row)" />
</mat-form-field>
</mat-cell>
</ng-container>
<!-- Ageing Column -->
<ng-container matColumnDef="ageing">
<mat-header-cell *matHeaderCellDef class="middle">Ageing</mat-header-cell>
<mat-cell *matCellDef="let row; let i = index" [formGroupName]="i" class="middle">
<mat-form-field class="flex-auto">
<mat-label>Ageing</mat-label>
<input matInput type="number" formControlName="ageing" (change)="updateAgeing($event, row)" />
</mat-form-field>
</mat-cell>
</ng-container>
<!-- Variance Column -->
<ng-container matColumnDef="variance">
<mat-header-cell *matHeaderCellDef class="right middle">Variance</mat-header-cell>
<mat-cell *matCellDef="let row" class="right middle">{{ row.variance | number: '0.2-2' }}</mat-cell>
</ng-container>
<!-- Closing Column -->
<ng-container matColumnDef="closing">
<mat-header-cell *matHeaderCellDef class="right last">Closing</mat-header-cell>
<mat-cell *matCellDef="let row" class="right last">{{ row.closing | number: '0.2-2' }}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>
<mat-paginator
#paginator
[length]="dataSource.data.length"
[pageIndex]="0"
[pageSize]="50"
[pageSizeOptions]="[25, 50, 100, 250, 300, 5000]"
>
</mat-paginator>
</form>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button color="primary" (click)="save()" [disabled]="form.pristine">Save</button>
</mat-card-actions>
</mat-card>

@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MozimoProductRegisterComponent } from './mozimo-product-register.component';
describe('MozimoProductRegisterComponent', () => {
let component: MozimoProductRegisterComponent;
let fixture: ComponentFixture<MozimoProductRegisterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MozimoProductRegisterComponent],
}).compileComponents();
fixture = TestBed.createComponent(MozimoProductRegisterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,250 @@
import { DecimalPipe, CurrencyPipe, AsyncPipe } from '@angular/common';
import { Component, OnInit, ViewChild } from '@angular/core';
import { FormGroup, FormArray, FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialog } from '@angular/material/dialog';
import { MatBadgeModule } from '@angular/material/badge';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
import { MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router';
import moment from 'moment';
import { AuthService } from '../auth/auth.service';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ToCsvService } from '../shared/to-csv.service';
import { MozimoProductRegister } from './mozimo-product-register';
import { MozimoProductRegisterDataSource } from './mozimo-product-register-datasource';
import { MozimoProductRegisterItem } from './mozimo-product-register-item';
import { MozimoProductRegisterService } from './mozimo-product-register.service';
import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { Product } from '../core/product';
import { debounceTime, distinctUntilChanged, Observable, switchMap, of as observableOf, BehaviorSubject } from 'rxjs';
import { ProductSku } from '../core/product-sku';
import { ProductService } from '../product/product.service';
import { LocalTimePipe } from '../shared/local-time.pipe';
import { MatTooltipModule } from '@angular/material/tooltip';
@Component({
selector: 'app-mozimo-product-register',
templateUrl: './mozimo-product-register.component.html',
styleUrls: ['./mozimo-product-register.component.css'],
standalone: true,
imports: [
AsyncPipe,
CurrencyPipe,
DecimalPipe,
LocalTimePipe,
MatAutocompleteModule,
MatBadgeModule,
MatButtonModule,
MatCardModule,
MatDatepickerModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatPaginatorModule,
MatTableModule,
MatTooltipModule,
ReactiveFormsModule,
],
})
export class MozimoProductRegisterComponent implements OnInit {
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
info: MozimoProductRegister = new MozimoProductRegister();
body = new BehaviorSubject<MozimoProductRegisterItem[]>([]);
dataSource: MozimoProductRegisterDataSource = new MozimoProductRegisterDataSource(this.body);
form: FormGroup<{
startDate: FormControl<Date>;
finishDate: FormControl<Date>;
product: FormControl<string | null>;
items: FormArray<
FormGroup<{
received: FormControl<number>;
sale: FormControl<number>;
nc: FormControl<number>;
display: FormControl<number | null>;
ageing: FormControl<number | null>;
}>
>;
}>;
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['date', 'opening', 'received', 'sale', 'nc', 'display', 'ageing', 'variance', 'closing'];
products: Observable<ProductSku[]>;
constructor(
private route: ActivatedRoute,
private router: Router,
private toCsv: ToCsvService,
private dialog: MatDialog,
private snackBar: MatSnackBar,
public auth: AuthService,
private ser: MozimoProductRegisterService,
private productSer: ProductService,
) {
this.form = new FormGroup({
startDate: new FormControl(new Date(), { nonNullable: true }),
finishDate: new FormControl(new Date(), { nonNullable: true }),
product: new FormControl<string | null>(null),
items: new FormArray<
FormGroup<{
received: FormControl<number>;
sale: FormControl<number>;
nc: FormControl<number>;
display: FormControl<number | null>;
ageing: FormControl<number | null>;
}>
>([]),
});
this.products = this.form.controls.product.valueChanges.pipe(
debounceTime(150),
distinctUntilChanged(),
switchMap((x) => (x === null ? observableOf([]) : this.productSer.autocompleteSku(x, null))),
);
}
ngOnInit() {
this.route.data.subscribe((value) => {
const data = value as { info: MozimoProductRegister };
this.info = data.info;
this.form.patchValue({
product: this.info.product?.name ?? '',
startDate: moment(this.info.startDate, 'DD-MMM-YYYY').toDate(),
finishDate: moment(this.info.finishDate, 'DD-MMM-YYYY').toDate(),
});
this.form.controls.items.clear();
this.info.body.forEach((x) =>
this.form.controls.items.push(
new FormGroup({
received: new FormControl(x.received, { nonNullable: true, validators: [Validators.min(0)] }),
sale: new FormControl(x.sale, { nonNullable: true, validators: [Validators.min(0)] }),
nc: new FormControl(x.nc, { nonNullable: true, validators: [Validators.min(0)] }),
display: new FormControl(x.display, { nonNullable: false, validators: [Validators.min(0)] }),
ageing: new FormControl(x.ageing, { nonNullable: false, validators: [Validators.min(0)] }),
}),
),
);
if (!this.dataSource.paginator) {
this.dataSource.paginator = this.paginator;
}
this.body.next(this.info.body);
});
}
displayFn(product?: Product | string): string {
return !product ? '' : typeof product === 'string' ? product : product.name;
}
selected(event: MatAutocompleteSelectedEvent): void {
this.info.product = event.option.value;
}
show() {
const info = this.getInfo();
if (info.product) {
this.router.navigate(['mozimo-product-register', info.product.id], {
queryParams: {
startDate: info.startDate,
finishDate: info.finishDate,
},
});
}
}
save() {
this.ser.save(this.getMozimoProductRegister()).subscribe({
next: () => {
this.snackBar.open('', 'Success');
},
error: (error) => {
this.snackBar.open(error, 'Danger');
},
});
}
getMozimoProductRegister(): MozimoProductRegister {
const formModel = this.form.value;
this.info.startDate = moment(formModel.startDate).format('DD-MMM-YYYY');
this.info.finishDate = moment(formModel.finishDate).format('DD-MMM-YYYY');
const array = this.form.controls.items;
this.info.body.forEach((item, index) => {
item.received = +(array.controls[index].value.received ?? 0);
item.sale = +(array.controls[index].value.sale ?? 0);
item.nc = +(array.controls[index].value.nc ?? 0);
const display = array.controls[index].value.display ?? null;
const ageing = array.controls[index].value.ageing ?? null;
item.display = display == null ? null : +display;
item.ageing = ageing == null ? null : +ageing;
});
return this.info;
}
getInfo(): MozimoProductRegister {
const formModel = this.form.value;
return new MozimoProductRegister({
product: this.info.product,
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'),
});
}
exportCsv() {
const headers = {
Date: 'date',
Opening: 'opening',
Received: 'received',
Sale: 'sale',
};
const d = JSON.parse(JSON.stringify(this.dataSource.data)).map((x: MozimoProductRegisterItem) => ({
x,
}));
const csvData = new Blob([this.toCsv.toCsv(headers, d)], {
type: 'text/csv;charset=utf-8;',
});
const link = document.createElement('a');
link.href = window.URL.createObjectURL(csvData);
link.setAttribute('download', 'mozimo-product-register.csv');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
updateReceived($event: Event, row: MozimoProductRegisterItem) {
row.received = +($event.target as HTMLInputElement).value;
this.body.next(this.info.body);
}
updateSale($event: Event, row: MozimoProductRegisterItem) {
row.sale = +($event.target as HTMLInputElement).value;
this.body.next(this.info.body);
}
updateNc($event: Event, row: MozimoProductRegisterItem) {
row.nc = +($event.target as HTMLInputElement).value;
this.body.next(this.info.body);
}
updateDisplay($event: Event, row: MozimoProductRegisterItem) {
const val = ($event.target as HTMLInputElement).value;
row.display = val === '' ? null : +val;
this.body.next(this.info.body);
}
updateAgeing($event: Event, row: MozimoProductRegisterItem) {
const val = ($event.target as HTMLInputElement).value;
row.ageing = val === '' ? null : +val;
this.body.next(this.info.body);
}
}

@ -0,0 +1,18 @@
import { TestBed } from '@angular/core/testing';
import { ResolveFn } from '@angular/router';
import { MozimoProductRegister } from './mozimo-product-register';
import { mozimoProductRegisterResolver } from './mozimo-product-register.resolver';
describe('mozimoProductRegisterResolver', () => {
const executeResolver: ResolveFn<MozimoProductRegister> = (...resolverParameters) =>
TestBed.runInInjectionContext(() => mozimoProductRegisterResolver(...resolverParameters));
beforeEach(() => {
TestBed.configureTestingModule({});
});
it('should be created', () => {
expect(executeResolver).toBeTruthy();
});
});

@ -0,0 +1,12 @@
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { MozimoProductRegister } from './mozimo-product-register';
import { MozimoProductRegisterService } from './mozimo-product-register.service';
export const mozimoProductRegisterResolver: ResolveFn<MozimoProductRegister> = (route) => {
const id = route.paramMap.get('id');
const startDate = route.queryParamMap.get('startDate') || null;
const finishDate = route.queryParamMap.get('finishDate') || null;
return inject(MozimoProductRegisterService).list(id, startDate, finishDate);
};

@ -0,0 +1,34 @@
import { Routes } from '@angular/router';
import { authGuard } from '../auth/auth-guard.service';
import { MozimoProductRegisterComponent } from './mozimo-product-register.component';
import { mozimoProductRegisterResolver } from './mozimo-product-register.resolver';
export const routes: Routes = [
{
path: '',
component: MozimoProductRegisterComponent,
canActivate: [authGuard],
data: {
permission: 'Ledger',
},
resolve: {
info: mozimoProductRegisterResolver,
},
runGuardsAndResolvers: 'always',
},
{
path: ':id',
component: MozimoProductRegisterComponent,
canActivate: [authGuard],
data: {
// permission: 'Mozimo Product Register',
permission: 'Ledger',
},
resolve: {
info: mozimoProductRegisterResolver,
},
runGuardsAndResolvers: 'always',
},
];

@ -0,0 +1,17 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { inject, TestBed } from '@angular/core/testing';
import { MozimoProductRegisterService } from './mozimo-product-register.service';
describe('MozimoProductRegisterService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: [MozimoProductRegisterService, provideHttpClient(withInterceptorsFromDi())],
});
});
it('should be created', inject([MozimoProductRegisterService], (service: MozimoProductRegisterService) => {
expect(service).toBeTruthy();
}));
});

@ -0,0 +1,55 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
import { catchError } from 'rxjs/operators';
import { ErrorLoggerService } from '../core/error-logger.service';
import { MozimoProductRegister } from './mozimo-product-register';
const url = '/api/mozimo-product-register';
const serviceName = 'MozimoProductRegisterService';
@Injectable({
providedIn: 'root',
})
export class MozimoProductRegisterService {
constructor(
private http: HttpClient,
private log: ErrorLoggerService,
) {}
list(id: string | null, startDate: string | null, finishDate: string | null): Observable<MozimoProductRegister> {
const listUrl = id === null ? url : `${url}/${id}`;
const options = { params: new HttpParams() };
if (startDate !== null) {
options.params = options.params.set('s', startDate);
}
if (finishDate !== null) {
options.params = options.params.set('f', finishDate);
}
return this.http
.get<MozimoProductRegister>(listUrl, options)
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<MozimoProductRegister>;
}
save(mozimoProductRegister: MozimoProductRegister): Observable<MozimoProductRegister> {
return this.http
.post<MozimoProductRegister>(url, mozimoProductRegister)
.pipe(catchError(this.log.handleError(serviceName, 'save'))) as Observable<MozimoProductRegister>;
}
post(date: string, costCentre: string): Observable<MozimoProductRegister> {
const options = { params: new HttpParams().set('d', costCentre) };
return this.http
.post<MozimoProductRegister>(`${url}/${date}`, {}, options)
.pipe(catchError(this.log.handleError(serviceName, 'Post Voucher'))) as Observable<MozimoProductRegister>;
}
delete(date: string, costCentre: string): Observable<MozimoProductRegister> {
const options = { params: new HttpParams().set('d', costCentre) };
return this.http
.delete<MozimoProductRegister>(`${url}/${date}`, options)
.pipe(catchError(this.log.handleError(serviceName, 'Delete Voucher'))) as Observable<MozimoProductRegister>;
}
}

@ -0,0 +1,18 @@
import { Product } from '../core/product';
import { MozimoProductRegisterItem } from './mozimo-product-register-item';
export class MozimoProductRegister {
startDate: string;
finishDate: string;
product: Product;
body: MozimoProductRegisterItem[];
public constructor(init?: Partial<MozimoProductRegister>) {
this.startDate = '';
this.finishDate = '';
this.product = new Product();
this.body = [];
Object.assign(this, init);
}
}