Product Ledger Done!!
This commit is contained in:
parent
6ec4068ed0
commit
48d03ab832
brewman
overlord/src/app/product-ledger
@ -1,4 +1,4 @@
|
|||||||
import datetime
|
from datetime import datetime, date
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Security, Request
|
from fastapi import APIRouter, Depends, Security, Request
|
||||||
@ -10,6 +10,7 @@ from ...core.security import get_current_active_user as get_user
|
|||||||
from ...db.session import SessionLocal
|
from ...db.session import SessionLocal
|
||||||
from brewman.models.master import Product, CostCentre
|
from brewman.models.master import Product, CostCentre
|
||||||
from brewman.models.voucher import Voucher, Journal, VoucherType, Inventory
|
from brewman.models.voucher import Voucher, Journal, VoucherType, Inventory
|
||||||
|
import brewman.schemas.reports as schemas
|
||||||
from ...core.session import (
|
from ...core.session import (
|
||||||
set_period,
|
set_period,
|
||||||
get_start_date,
|
get_start_date,
|
||||||
@ -28,10 +29,9 @@ def get_db() -> Session:
|
|||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("", response_model=schemas.ProductLedger)
|
||||||
def show_blank(
|
def show_blank(
|
||||||
request: Request,
|
request: Request, user: UserToken = Security(get_user, scopes=["product-ledger"]),
|
||||||
user: UserToken = Security(get_user, scopes=["product-ledger"]),
|
|
||||||
):
|
):
|
||||||
return {
|
return {
|
||||||
"startDate": get_start_date(request.session),
|
"startDate": get_start_date(request.session),
|
||||||
@ -41,7 +41,7 @@ def show_blank(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{id}")
|
@router.get("/{id_}", response_model=schemas.ProductLedger)
|
||||||
def show_data(
|
def show_data(
|
||||||
id_: uuid.UUID,
|
id_: uuid.UUID,
|
||||||
request: Request,
|
request: Request,
|
||||||
@ -53,7 +53,12 @@ def show_data(
|
|||||||
product = db.query(Product).filter(Product.id == id_).first()
|
product = db.query(Product).filter(Product.id == id_).first()
|
||||||
start_date = s if s is not None else get_start_date(request.session)
|
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)
|
finish_date = f if f is not None else get_finish_date(request.session)
|
||||||
body = build_report(product.id, start_date, finish_date, db)
|
body = build_report(
|
||||||
|
product.id,
|
||||||
|
datetime.strptime(start_date, "%d-%b-%Y"),
|
||||||
|
datetime.strptime(finish_date, "%d-%b-%Y"),
|
||||||
|
db,
|
||||||
|
)
|
||||||
set_period(start_date, finish_date, request.session)
|
set_period(start_date, finish_date, request.session)
|
||||||
return {
|
return {
|
||||||
"startDate": start_date,
|
"startDate": start_date,
|
||||||
@ -63,7 +68,9 @@ def show_data(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def build_report(product_id, start_date, finish_date, db):
|
def build_report(
|
||||||
|
product_id: uuid.UUID, start_date: date, finish_date: date, db: Session
|
||||||
|
):
|
||||||
body = []
|
body = []
|
||||||
running_total_q, running_total_a, opening = opening_balance(
|
running_total_q, running_total_a, opening = opening_balance(
|
||||||
product_id, start_date, db
|
product_id, start_date, db
|
||||||
@ -80,8 +87,8 @@ def build_report(product_id, start_date, finish_date, db):
|
|||||||
.filter(Voucher.id == Journal.voucher_id)
|
.filter(Voucher.id == Journal.voucher_id)
|
||||||
.filter(Inventory.product_id == product_id)
|
.filter(Inventory.product_id == product_id)
|
||||||
.filter(Journal.cost_centre_id != CostCentre.cost_centre_purchase())
|
.filter(Journal.cost_centre_id != CostCentre.cost_centre_purchase())
|
||||||
.filter(Voucher.date >= datetime.datetime.strptime(start_date, "%d-%b-%Y"))
|
.filter(Voucher.date >= start_date)
|
||||||
.filter(Voucher.date <= datetime.datetime.strptime(finish_date, "%d-%b-%Y"))
|
.filter(Voucher.date <= finish_date)
|
||||||
.order_by(Voucher.date)
|
.order_by(Voucher.date)
|
||||||
.order_by(Voucher.last_edit_date)
|
.order_by(Voucher.last_edit_date)
|
||||||
.all()
|
.all()
|
||||||
@ -94,10 +101,10 @@ def build_report(product_id, start_date, finish_date, db):
|
|||||||
if row.Voucher.type == VoucherType.by_name("Issue").id
|
if row.Voucher.type == VoucherType.by_name("Issue").id
|
||||||
else row.Journal.account.name
|
else row.Journal.account.name
|
||||||
)
|
)
|
||||||
debit_q = row.Inventory.quantity if journal_debit == 1 else 0
|
debit_q = row.Inventory.quantity if journal_debit == 1 else None
|
||||||
debit_a = row.Inventory.amount if journal_debit == 1 else 0
|
debit_a = row.Inventory.amount if journal_debit == 1 else None
|
||||||
credit_q = row.Inventory.quantity if journal_debit != 1 else 0
|
credit_q = row.Inventory.quantity if journal_debit != 1 else None
|
||||||
credit_a = row.Inventory.amount if journal_debit != 1 else 0
|
credit_a = row.Inventory.amount if journal_debit != 1 else None
|
||||||
|
|
||||||
running_total_q += row.Inventory.quantity * journal_debit
|
running_total_q += row.Inventory.quantity * journal_debit
|
||||||
running_total_a += row.Inventory.amount * journal_debit
|
running_total_a += row.Inventory.amount * journal_debit
|
||||||
@ -107,6 +114,11 @@ def build_report(product_id, start_date, finish_date, db):
|
|||||||
"id": row.Voucher.id,
|
"id": row.Voucher.id,
|
||||||
"date": row.Voucher.date.strftime("%d-%b-%Y"),
|
"date": row.Voucher.date.strftime("%d-%b-%Y"),
|
||||||
"name": name,
|
"name": name,
|
||||||
|
"url": [
|
||||||
|
"/",
|
||||||
|
VoucherType.by_id(row.Voucher.type).name.replace(" ", "-").lower(),
|
||||||
|
str(row.Voucher.id),
|
||||||
|
],
|
||||||
"type": VoucherType.by_id(row.Voucher.type).name,
|
"type": VoucherType.by_id(row.Voucher.type).name,
|
||||||
"narration": row.Voucher.narration,
|
"narration": row.Voucher.narration,
|
||||||
"posted": row.Voucher.posted
|
"posted": row.Voucher.posted
|
||||||
@ -123,7 +135,7 @@ def build_report(product_id, start_date, finish_date, db):
|
|||||||
return body
|
return body
|
||||||
|
|
||||||
|
|
||||||
def opening_balance(product_id, start_date, db):
|
def opening_balance(product_id: uuid.UUID, start_date: date, db: Session):
|
||||||
quantity, amount = (
|
quantity, amount = (
|
||||||
db.query(
|
db.query(
|
||||||
func.sum(Inventory.quantity * Journal.debit),
|
func.sum(Inventory.quantity * Journal.debit),
|
||||||
@ -135,7 +147,7 @@ def opening_balance(product_id, start_date, db):
|
|||||||
.filter(Voucher.id == Journal.voucher_id)
|
.filter(Voucher.id == Journal.voucher_id)
|
||||||
.filter(Inventory.product_id == product_id)
|
.filter(Inventory.product_id == product_id)
|
||||||
.filter(Journal.cost_centre_id == CostCentre.cost_centre_purchase())
|
.filter(Journal.cost_centre_id == CostCentre.cost_centre_purchase())
|
||||||
.filter(Voucher.date < datetime.datetime.strptime(start_date, "%d-%b-%Y"))
|
.filter(Voucher.date < start_date)
|
||||||
.one()
|
.one()
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -143,8 +155,8 @@ def opening_balance(product_id, start_date, db):
|
|||||||
debit_quantity = quantity
|
debit_quantity = quantity
|
||||||
debit_amount = amount
|
debit_amount = amount
|
||||||
else:
|
else:
|
||||||
debit_quantity = ""
|
debit_quantity = None
|
||||||
debit_amount = ""
|
debit_amount = None
|
||||||
|
|
||||||
if quantity is None:
|
if quantity is None:
|
||||||
quantity = 0
|
quantity = 0
|
||||||
@ -154,8 +166,10 @@ def opening_balance(product_id, start_date, db):
|
|||||||
quantity,
|
quantity,
|
||||||
amount,
|
amount,
|
||||||
{
|
{
|
||||||
"date": start_date,
|
"id": None,
|
||||||
|
"date": start_date.strftime("%d-%b-%Y"),
|
||||||
"name": "Opening Balance",
|
"name": "Opening Balance",
|
||||||
|
"url": [],
|
||||||
"type": "Opening Balance",
|
"type": "Opening Balance",
|
||||||
"narration": "",
|
"narration": "",
|
||||||
"posted": True,
|
"posted": True,
|
||||||
|
@ -40,6 +40,14 @@ class ProductGroupLink(BaseModel):
|
|||||||
fields = {'id_': 'id'}
|
fields = {'id_': 'id'}
|
||||||
|
|
||||||
|
|
||||||
|
class ProductLink(BaseModel):
|
||||||
|
id_: uuid.UUID = Field(...)
|
||||||
|
name: Optional[str]
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
fields = {'id_': 'id'}
|
||||||
|
|
||||||
|
|
||||||
class ProductIn(BaseModel):
|
class ProductIn(BaseModel):
|
||||||
name: str = Field(..., min_length=1)
|
name: str = Field(..., min_length=1)
|
||||||
units: str
|
units: str
|
||||||
|
@ -4,7 +4,7 @@ from typing import List, Optional
|
|||||||
from datetime import datetime, date
|
from datetime import datetime, date
|
||||||
from pydantic import BaseModel, Field, validator
|
from pydantic import BaseModel, Field, validator
|
||||||
|
|
||||||
from brewman.schemas.master import AccountLink
|
from brewman.schemas.master import AccountLink, ProductLink
|
||||||
|
|
||||||
|
|
||||||
def to_camel(string: str) -> str:
|
def to_camel(string: str) -> str:
|
||||||
@ -270,3 +270,61 @@ class NetTransactions(BaseModel):
|
|||||||
value,
|
value,
|
||||||
"%d-%b-%Y"
|
"%d-%b-%Y"
|
||||||
).date()
|
).date()
|
||||||
|
|
||||||
|
|
||||||
|
class ProductLedgerItem(BaseModel):
|
||||||
|
id_: Optional[uuid.UUID]
|
||||||
|
date_: date
|
||||||
|
name: str
|
||||||
|
url: List[str]
|
||||||
|
type_: str
|
||||||
|
narration: str
|
||||||
|
debit_quantity: Optional[Decimal] = Field(multiple_of=0.01)
|
||||||
|
debit_amount: Optional[Decimal] = Field(multiple_of=0.01)
|
||||||
|
credit_quantity: Optional[Decimal] = Field(multiple_of=0.01)
|
||||||
|
credit_amount: Optional[Decimal] = Field(multiple_of=0.01)
|
||||||
|
running_quantity: Decimal = Field(multiple_of=0.01)
|
||||||
|
running_amount: Decimal = Field(multiple_of=0.01)
|
||||||
|
posted: bool
|
||||||
|
|
||||||
|
@validator("date_", pre=True)
|
||||||
|
def parse_date(cls, value):
|
||||||
|
return datetime.strptime(
|
||||||
|
value,
|
||||||
|
"%d-%b-%Y"
|
||||||
|
).date()
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
anystr_strip_whitespace = True
|
||||||
|
alias_generator = to_camel
|
||||||
|
json_encoders = {
|
||||||
|
date: lambda v: v.strftime("%d-%b-%Y")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ProductLedger(BaseModel):
|
||||||
|
start_date: date
|
||||||
|
finish_date: date
|
||||||
|
product: Optional[ProductLink]
|
||||||
|
body: List[ProductLedgerItem]
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
anystr_strip_whitespace = True
|
||||||
|
alias_generator = to_camel
|
||||||
|
json_encoders = {
|
||||||
|
date: lambda v: v.strftime("%d-%b-%Y")
|
||||||
|
}
|
||||||
|
|
||||||
|
@validator("start_date", pre=True)
|
||||||
|
def parse_start_date(cls, value):
|
||||||
|
return datetime.strptime(
|
||||||
|
value,
|
||||||
|
"%d-%b-%Y"
|
||||||
|
).date()
|
||||||
|
|
||||||
|
@validator("finish_date", pre=True)
|
||||||
|
def parse_finish_date(cls, value):
|
||||||
|
return datetime.strptime(
|
||||||
|
value,
|
||||||
|
"%d-%b-%Y"
|
||||||
|
).date()
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
<mat-card>
|
<mat-card>
|
||||||
<mat-card-title-group>
|
<mat-card-title-group>
|
||||||
<mat-card-title>Product Ledger</mat-card-title>
|
<mat-card-title>Product Ledger</mat-card-title>
|
||||||
|
<button mat-button mat-icon-button *ngIf="dataSource.data.length" (click)="exportCsv()">
|
||||||
|
<mat-icon>save_alt</mat-icon>
|
||||||
|
</button>
|
||||||
</mat-card-title-group>
|
</mat-card-title-group>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<form [formGroup]="form" fxLayout="column">
|
<form [formGroup]="form" fxLayout="column">
|
||||||
@ -22,8 +25,8 @@
|
|||||||
<div fxLayout="row" fxLayout.lt-md="column" fxLayoutGap="20px" fxLayoutGap.lt-md="0px"
|
<div fxLayout="row" fxLayout.lt-md="column" fxLayoutGap="20px" fxLayoutGap.lt-md="0px"
|
||||||
fxLayoutAlign="space-around start">
|
fxLayoutAlign="space-around start">
|
||||||
<mat-form-field fxFlex="80">
|
<mat-form-field fxFlex="80">
|
||||||
<input type="text" matInput placeholder="Product" [matAutocomplete]="auto" formControlName="product"
|
<input type="text" matInput #productElement placeholder="Product" [matAutocomplete]="auto"
|
||||||
autocomplete="off">
|
formControlName="product" autocomplete="off">
|
||||||
<mat-autocomplete #auto="matAutocomplete" autoActiveFirstOption [displayWith]="displayFn"
|
<mat-autocomplete #auto="matAutocomplete" autoActiveFirstOption [displayWith]="displayFn"
|
||||||
(optionSelected)="selected($event)">
|
(optionSelected)="selected($event)">
|
||||||
<mat-option *ngFor="let product of products | async" [value]="product">{{product.name}}</mat-option>
|
<mat-option *ngFor="let product of products | async" [value]="product">{{product.name}}</mat-option>
|
||||||
@ -44,7 +47,7 @@
|
|||||||
<!-- Particulars Column -->
|
<!-- Particulars Column -->
|
||||||
<ng-container matColumnDef="particulars">
|
<ng-container matColumnDef="particulars">
|
||||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Particulars</mat-header-cell>
|
<mat-header-cell *matHeaderCellDef mat-sort-header>Particulars</mat-header-cell>
|
||||||
<mat-cell *matCellDef="let row"><a [routerLink]="['/', row.url, row.id]">{{row.name}}</a></mat-cell>
|
<mat-cell *matCellDef="let row"><a [routerLink]="row.url">{{row.name}}</a></mat-cell>
|
||||||
<mat-footer-cell *matFooterCellDef>Closing Balance</mat-footer-cell>
|
<mat-footer-cell *matFooterCellDef>Closing Balance</mat-footer-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {Component, OnInit, ViewChild} from '@angular/core';
|
import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core';
|
||||||
import {FormBuilder, FormGroup} from '@angular/forms';
|
import {FormBuilder, FormGroup} from '@angular/forms';
|
||||||
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||||
import { MatPaginator } from '@angular/material/paginator';
|
import { MatPaginator } from '@angular/material/paginator';
|
||||||
@ -12,13 +12,15 @@ import {ProductLedger} from './product-ledger';
|
|||||||
import {ProductLedgerService} from './product-ledger.service';
|
import {ProductLedgerService} from './product-ledger.service';
|
||||||
import {Product} from '../core/product';
|
import {Product} from '../core/product';
|
||||||
import {ProductService} from '../product/product.service';
|
import {ProductService} from '../product/product.service';
|
||||||
|
import {ToCsvService} from '../shared/to-csv.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-product-ledger',
|
selector: 'app-product-ledger',
|
||||||
templateUrl: './product-ledger.component.html',
|
templateUrl: './product-ledger.component.html',
|
||||||
styleUrls: ['./product-ledger.component.css']
|
styleUrls: ['./product-ledger.component.css']
|
||||||
})
|
})
|
||||||
export class ProductLedgerComponent implements OnInit {
|
export class ProductLedgerComponent implements OnInit, AfterViewInit {
|
||||||
|
@ViewChild('productElement', { static: true }) productElement: ElementRef;
|
||||||
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
|
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
|
||||||
@ViewChild(MatSort, { static: true }) sort: MatSort;
|
@ViewChild(MatSort, { static: true }) sort: MatSort;
|
||||||
dataSource: ProductLedgerDataSource;
|
dataSource: ProductLedgerDataSource;
|
||||||
@ -45,6 +47,7 @@ export class ProductLedgerComponent implements OnInit {
|
|||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private fb: FormBuilder,
|
private fb: FormBuilder,
|
||||||
|
private toCsv: ToCsvService,
|
||||||
private ser: ProductLedgerService,
|
private ser: ProductLedgerService,
|
||||||
private productSer: ProductService
|
private productSer: ProductService
|
||||||
) {
|
) {
|
||||||
@ -61,12 +64,7 @@ export class ProductLedgerComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.route.data.pipe(map(
|
this.route.data.subscribe((data: { info: ProductLedger }) => {
|
||||||
(data: { info: ProductLedger }) => {
|
|
||||||
data.info.body = data.info.body.map(x => ({...x, url: x['type'].replace(/ /g, '-').toLowerCase()}));
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
)).subscribe((data: { info: ProductLedger }) => {
|
|
||||||
this.info = data.info;
|
this.info = data.info;
|
||||||
this.calculateTotals();
|
this.calculateTotals();
|
||||||
this.form.setValue({
|
this.form.setValue({
|
||||||
@ -75,9 +73,16 @@ export class ProductLedgerComponent implements OnInit {
|
|||||||
finishDate: moment(this.info.finishDate, 'DD-MMM-YYYY').toDate()
|
finishDate: moment(this.info.finishDate, 'DD-MMM-YYYY').toDate()
|
||||||
});
|
});
|
||||||
this.dataSource = new ProductLedgerDataSource(this.paginator, this.sort, this.info.body);
|
this.dataSource = new ProductLedgerDataSource(this.paginator, this.sort, this.info.body);
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.productElement.nativeElement.focus();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
displayFn(product?: Product): string | undefined {
|
displayFn(product?: Product): string | undefined {
|
||||||
return product ? product.name : undefined;
|
return product ? product.name : undefined;
|
||||||
}
|
}
|
||||||
@ -111,11 +116,11 @@ export class ProductLedgerComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
show() {
|
show() {
|
||||||
const l = this.prepareSubmit();
|
const info = this.getInfo();
|
||||||
this.router.navigate(['product-ledger', l.product.id], {
|
this.router.navigate(['product-ledger', info.product.id], {
|
||||||
queryParams: {
|
queryParams: {
|
||||||
startDate: l.startDate,
|
startDate: info.startDate,
|
||||||
finishDate: l.finishDate
|
finishDate: info.finishDate
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -128,14 +133,33 @@ export class ProductLedgerComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareSubmit(): ProductLedger {
|
getInfo(): ProductLedger {
|
||||||
const formModel = this.form.value;
|
const formModel = this.form.value;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
product: formModel.product,
|
product: formModel.product,
|
||||||
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
|
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
|
||||||
finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'),
|
finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY')
|
||||||
body: this.info.body
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exportCsv() {
|
||||||
|
const headers = {
|
||||||
|
Date: 'date',
|
||||||
|
Name: 'name',
|
||||||
|
Type: 'type',
|
||||||
|
Narration: 'narration',
|
||||||
|
Debit: 'debit',
|
||||||
|
Credit: 'credit',
|
||||||
|
Running: 'running',
|
||||||
|
Posted: 'posted'
|
||||||
|
};
|
||||||
|
const csvData = new Blob([this.toCsv.toCsv(headers, this.dataSource.data)], {type: 'text/csv;charset=utf-8;'});
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = window.URL.createObjectURL(csvData);
|
||||||
|
link.setAttribute('download', 'ledger.csv');
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,12 +13,12 @@ export class ProductLedgerItem {
|
|||||||
runningQuantity: number;
|
runningQuantity: number;
|
||||||
runningAmount: number;
|
runningAmount: number;
|
||||||
posted: boolean;
|
posted: boolean;
|
||||||
url: string;
|
url: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ProductLedger {
|
export class ProductLedger {
|
||||||
startDate: string;
|
startDate: string;
|
||||||
finishDate: string;
|
finishDate: string;
|
||||||
product: Product;
|
product?: Product;
|
||||||
body: ProductLedgerItem[];
|
body?: ProductLedgerItem[];
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user