diff --git a/brewman/routers/reports/product_ledger.py b/brewman/routers/reports/product_ledger.py index 08b7d25e..354dc66e 100644 --- a/brewman/routers/reports/product_ledger.py +++ b/brewman/routers/reports/product_ledger.py @@ -1,4 +1,4 @@ -import datetime +from datetime import datetime, date import uuid 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 brewman.models.master import Product, CostCentre from brewman.models.voucher import Voucher, Journal, VoucherType, Inventory +import brewman.schemas.reports as schemas from ...core.session import ( set_period, get_start_date, @@ -28,10 +29,9 @@ def get_db() -> Session: db.close() -@router.get("") +@router.get("", response_model=schemas.ProductLedger) def show_blank( - request: Request, - user: UserToken = Security(get_user, scopes=["product-ledger"]), + request: Request, user: UserToken = Security(get_user, scopes=["product-ledger"]), ): return { "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( id_: uuid.UUID, request: Request, @@ -53,7 +53,12 @@ def show_data( product = db.query(Product).filter(Product.id == id_).first() 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) - 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) return { "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 = [] running_total_q, running_total_a, opening = opening_balance( 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(Inventory.product_id == product_id) .filter(Journal.cost_centre_id != CostCentre.cost_centre_purchase()) - .filter(Voucher.date >= datetime.datetime.strptime(start_date, "%d-%b-%Y")) - .filter(Voucher.date <= datetime.datetime.strptime(finish_date, "%d-%b-%Y")) + .filter(Voucher.date >= start_date) + .filter(Voucher.date <= finish_date) .order_by(Voucher.date) .order_by(Voucher.last_edit_date) .all() @@ -94,10 +101,10 @@ def build_report(product_id, start_date, finish_date, db): if row.Voucher.type == VoucherType.by_name("Issue").id else row.Journal.account.name ) - debit_q = row.Inventory.quantity if journal_debit == 1 else 0 - debit_a = row.Inventory.amount if journal_debit == 1 else 0 - credit_q = row.Inventory.quantity if journal_debit != 1 else 0 - credit_a = row.Inventory.amount 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 None + credit_q = row.Inventory.quantity if journal_debit != 1 else None + credit_a = row.Inventory.amount if journal_debit != 1 else None running_total_q += row.Inventory.quantity * 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, "date": row.Voucher.date.strftime("%d-%b-%Y"), "name": name, + "url": [ + "/", + VoucherType.by_id(row.Voucher.type).name.replace(" ", "-").lower(), + str(row.Voucher.id), + ], "type": VoucherType.by_id(row.Voucher.type).name, "narration": row.Voucher.narration, "posted": row.Voucher.posted @@ -123,7 +135,7 @@ def build_report(product_id, start_date, finish_date, db): return body -def opening_balance(product_id, start_date, db): +def opening_balance(product_id: uuid.UUID, start_date: date, db: Session): quantity, amount = ( db.query( 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(Inventory.product_id == product_id) .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() ) @@ -143,8 +155,8 @@ def opening_balance(product_id, start_date, db): debit_quantity = quantity debit_amount = amount else: - debit_quantity = "" - debit_amount = "" + debit_quantity = None + debit_amount = None if quantity is None: quantity = 0 @@ -154,8 +166,10 @@ def opening_balance(product_id, start_date, db): quantity, amount, { - "date": start_date, + "id": None, + "date": start_date.strftime("%d-%b-%Y"), "name": "Opening Balance", + "url": [], "type": "Opening Balance", "narration": "", "posted": True, diff --git a/brewman/schemas/master.py b/brewman/schemas/master.py index 6e31c33f..2c72f9b2 100644 --- a/brewman/schemas/master.py +++ b/brewman/schemas/master.py @@ -40,6 +40,14 @@ class ProductGroupLink(BaseModel): fields = {'id_': 'id'} +class ProductLink(BaseModel): + id_: uuid.UUID = Field(...) + name: Optional[str] + + class Config: + fields = {'id_': 'id'} + + class ProductIn(BaseModel): name: str = Field(..., min_length=1) units: str diff --git a/brewman/schemas/reports.py b/brewman/schemas/reports.py index 80e40bbe..be31b9d6 100644 --- a/brewman/schemas/reports.py +++ b/brewman/schemas/reports.py @@ -4,7 +4,7 @@ from typing import List, Optional from datetime import datetime, date 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: @@ -270,3 +270,61 @@ class NetTransactions(BaseModel): value, "%d-%b-%Y" ).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() diff --git a/overlord/src/app/product-ledger/product-ledger.component.html b/overlord/src/app/product-ledger/product-ledger.component.html index 519325ef..a19377c6 100644 --- a/overlord/src/app/product-ledger/product-ledger.component.html +++ b/overlord/src/app/product-ledger/product-ledger.component.html @@ -1,6 +1,9 @@ Product Ledger +
@@ -22,8 +25,8 @@
- + {{product.name}} @@ -44,7 +47,7 @@ Particulars - {{row.name}} + {{row.name}} Closing Balance diff --git a/overlord/src/app/product-ledger/product-ledger.component.ts b/overlord/src/app/product-ledger/product-ledger.component.ts index e6f9cdcc..25a2a1ee 100644 --- a/overlord/src/app/product-ledger/product-ledger.component.ts +++ b/overlord/src/app/product-ledger/product-ledger.component.ts @@ -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 { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatPaginator } from '@angular/material/paginator'; @@ -12,13 +12,15 @@ import {ProductLedger} from './product-ledger'; import {ProductLedgerService} from './product-ledger.service'; import {Product} from '../core/product'; import {ProductService} from '../product/product.service'; +import {ToCsvService} from '../shared/to-csv.service'; @Component({ selector: 'app-product-ledger', templateUrl: './product-ledger.component.html', 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(MatSort, { static: true }) sort: MatSort; dataSource: ProductLedgerDataSource; @@ -45,6 +47,7 @@ export class ProductLedgerComponent implements OnInit { private route: ActivatedRoute, private router: Router, private fb: FormBuilder, + private toCsv: ToCsvService, private ser: ProductLedgerService, private productSer: ProductService ) { @@ -61,12 +64,7 @@ export class ProductLedgerComponent implements OnInit { } ngOnInit() { - this.route.data.pipe(map( - (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.route.data.subscribe((data: { info: ProductLedger }) => { this.info = data.info; this.calculateTotals(); this.form.setValue({ @@ -75,9 +73,16 @@ export class ProductLedgerComponent implements OnInit { finishDate: moment(this.info.finishDate, 'DD-MMM-YYYY').toDate() }); this.dataSource = new ProductLedgerDataSource(this.paginator, this.sort, this.info.body); + }); } + ngAfterViewInit() { + setTimeout(() => { + this.productElement.nativeElement.focus(); + }, 0); + } + displayFn(product?: Product): string | undefined { return product ? product.name : undefined; } @@ -111,11 +116,11 @@ export class ProductLedgerComponent implements OnInit { } show() { - const l = this.prepareSubmit(); - this.router.navigate(['product-ledger', l.product.id], { + const info = this.getInfo(); + this.router.navigate(['product-ledger', info.product.id], { queryParams: { - startDate: l.startDate, - finishDate: l.finishDate + startDate: info.startDate, + finishDate: info.finishDate } }); } @@ -128,14 +133,33 @@ export class ProductLedgerComponent implements OnInit { }); } - prepareSubmit(): ProductLedger { + getInfo(): ProductLedger { const formModel = this.form.value; return { product: formModel.product, startDate: moment(formModel.startDate).format('DD-MMM-YYYY'), - finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'), - body: this.info.body + finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY') }; } + + 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); + } } diff --git a/overlord/src/app/product-ledger/product-ledger.ts b/overlord/src/app/product-ledger/product-ledger.ts index 0ceee66c..c588f69b 100644 --- a/overlord/src/app/product-ledger/product-ledger.ts +++ b/overlord/src/app/product-ledger/product-ledger.ts @@ -13,12 +13,12 @@ export class ProductLedgerItem { runningQuantity: number; runningAmount: number; posted: boolean; - url: string; + url: string[]; } export class ProductLedger { startDate: string; finishDate: string; - product: Product; - body: ProductLedgerItem[]; + product?: Product; + body?: ProductLedgerItem[]; }