Feature: Changed the unposted report to entries report with paging, sorting, etc.

This commit is contained in:
2021-09-14 11:36:40 +05:30
parent d34c8ea0a4
commit 176559466a
35 changed files with 776 additions and 518 deletions

View File

@ -57,7 +57,7 @@ from .routers.reports import (
reconcile, reconcile,
stock_movement, stock_movement,
trial_balance, trial_balance,
unposted, entries,
) )
@ -104,7 +104,7 @@ app.include_router(raw_material_cost.router, prefix="/api/raw-material-cost", ta
app.include_router(reconcile.router, prefix="/api/reconcile", tags=["reports"]) app.include_router(reconcile.router, prefix="/api/reconcile", tags=["reports"])
app.include_router(stock_movement.router, prefix="/api/stock-movement", tags=["reports"]) app.include_router(stock_movement.router, prefix="/api/stock-movement", tags=["reports"])
app.include_router(trial_balance.router, prefix="/api/trial-balance", tags=["reports"]) app.include_router(trial_balance.router, prefix="/api/trial-balance", tags=["reports"])
app.include_router(unposted.router, prefix="/api/unposted", tags=["reports"]) app.include_router(entries.router, prefix="/api/entries", tags=["reports"])
app.include_router(issue_grid.router, prefix="/api/issue-grid", tags=["vouchers"]) app.include_router(issue_grid.router, prefix="/api/issue-grid", tags=["vouchers"])
app.include_router(batch.router, prefix="/api/batch", tags=["vouchers"]) app.include_router(batch.router, prefix="/api/batch", tags=["vouchers"])

View File

@ -0,0 +1,10 @@
from datetime import date, datetime
from typing import Optional
def report_start_date(s: str = None) -> Optional[date]:
return None if s is None else datetime.strptime(s, "%d-%b-%Y").date()
def report_finish_date(f: str = None) -> Optional[date]:
return None if f is None else datetime.strptime(f, "%d-%b-%Y").date()

View File

@ -0,0 +1,117 @@
from datetime import date
from typing import List, Optional
import brewman.schemas.entries as schemas
from fastapi import APIRouter, Depends, Security
from sqlalchemy import desc, or_, select
from sqlalchemy.orm import Session, contains_eager, joinedload
from sqlalchemy.sql.functions import count
from ...core.security import get_current_active_user as get_user
from ...db.session import SessionFuture
from ...models.journal import Journal
from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
from ...schemas.user import UserToken
from ...schemas.user_link import UserLink
from . import report_finish_date, report_start_date
router = APIRouter()
def build_report(
start_date: Optional[date],
finish_date: date,
posted: Optional[bool],
issue: bool,
page_size: int,
page_index: int,
active_sort: str,
sort_direction: str,
db: Session,
) -> (int, List[Voucher]):
query = (
select(Voucher)
.join(Voucher.user)
.options(
joinedload(Voucher.user, innerjoin=True),
joinedload(Voucher.journals, innerjoin=True).joinedload(Journal.account, innerjoin=True),
contains_eager(Voucher.user),
contains_eager(Voucher.journals, Journal.account),
)
)
sq = select(Voucher.id)
counts = select(count(Voucher.id))
if start_date is not None:
sq = sq.where(or_(Voucher.creation_date >= start_date, Voucher.last_edit_date >= start_date))
counts = counts.where(or_(Voucher.creation_date >= start_date, Voucher.last_edit_date >= start_date))
if finish_date is not None:
sq = sq.where(or_(Voucher.creation_date <= finish_date, Voucher.last_edit_date <= finish_date))
counts = counts.where(or_(Voucher.creation_date <= finish_date, Voucher.last_edit_date <= finish_date))
if posted is not None:
sq = sq.where(Voucher.posted == posted)
counts = counts.where(Voucher.posted == posted)
if issue is False:
sq = sq.where(Voucher.type != VoucherType.by_name("Issue").id)
counts = counts.where(Voucher.type != VoucherType.by_name("Issue").id)
if active_sort == "date":
if sort_direction == "desc":
sq = sq.order_by(desc(Voucher.date), desc(Voucher.last_edit_date))
query = query.order_by(desc(Voucher.date), desc(Voucher.last_edit_date))
else:
sq = sq.order_by(Voucher.date, Voucher.last_edit_date)
query = query.order_by(Voucher.date, Voucher.last_edit_date)
if active_sort == "user":
if sort_direction == "desc":
sq = sq.order_by(desc(Voucher.last_edit_date))
query = query.order_by(desc(Voucher.last_edit_date))
else:
sq = sq.order_by(Voucher.last_edit_date)
query = query.order_by(Voucher.last_edit_date)
if page_size:
sq = sq.limit(page_size)
if page_index:
sq = sq.offset(page_size * page_index)
return db.execute(counts).scalar_one(), db.execute(query.where(Voucher.id.in_(sq))).unique().scalars().all()
@router.get("", response_model=schemas.Report)
def report_data(
start_date: Optional[date] = Depends(report_start_date),
finish_date: Optional[date] = Depends(report_finish_date),
p: Optional[bool] = None, # Posted
ps: Optional[int] = 50, # Page Size
pi: Optional[int] = 0, # Page Index
a: Optional[str] = "date", # Active Sort
d: Optional[str] = "desc", # Sort Direction
i: Optional[bool] = False, # Show issue vouchers
user: UserToken = Security(get_user, scopes=["ledger"]),
) -> schemas.Report:
with SessionFuture() as db:
counts, report = build_report(start_date, finish_date, p, i, ps, pi, a, d, db)
return schemas.Report(
counts=counts,
report=[
schemas.Entries(
id=voucher.id,
date=voucher.date,
url=[
"/",
VoucherType.by_id(voucher.type).name.replace(" ", "-").lower(),
str(voucher.id),
],
type=VoucherType.by_id(voucher.type).name,
posted=voucher.posted,
narration=voucher.narration,
debitNames=[x.account.name for x in voucher.journals if x.debit == 1],
creditNames=[x.account.name for x in voucher.journals if x.debit != 1],
amount=sum(x.amount for x in voucher.journals if x.debit != 1),
creationDate=voucher.creation_date,
lastEditDate=voucher.last_edit_date,
user=UserLink(id=voucher.user.id, name=voucher.user.name),
)
for voucher in report
],
)

View File

@ -1,77 +0,0 @@
from typing import List
import brewman.schemas.unposted as schemas
from fastapi import APIRouter, Security
from sqlalchemy import select
from sqlalchemy.orm import Session
from ...core.security import get_current_active_user as get_user
from ...db.session import SessionFuture
from ...models.journal import Journal
from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
from ...schemas.user import UserToken
router = APIRouter()
@router.get("", response_model=List[schemas.Unposted])
def report_data(
user: UserToken = Security(get_user, scopes=["post-vouchers"]),
) -> List[schemas.Unposted]:
with SessionFuture() as db:
return build_report(db)
def build_report(db: Session) -> List[schemas.Unposted]:
body = []
query = (
db.execute(
select(Voucher)
.join(Voucher.journals)
.join(Journal.account)
.where(Voucher.posted == False, Voucher.type != VoucherType.by_name("Issue").id) # noqa: E712
.order_by(Voucher.date, Voucher.last_edit_date)
)
.unique()
.scalars()
.all()
)
for voucher in query:
debit = 0
credit = 0
name_debit = ""
name_credit = ""
for journal in voucher.journals:
if journal.debit == 1:
debit += journal.amount
name_debit += "{0} / ".format(journal.account.name)
else:
credit += journal.amount
name_credit += "{0} / ".format(journal.account.name)
name_debit = name_debit[:-3]
name_credit = name_credit[:-3]
body.append(
schemas.Unposted(
id=voucher.id,
date=voucher.date.strftime("%d-%b-%Y"),
url=[
"/",
VoucherType.by_id(voucher.type).name.replace(" ", "-").lower(),
str(voucher.id),
],
type=VoucherType.by_id(voucher.type).name,
narration=voucher.narration,
debitName=name_debit,
debitAmount=debit,
creditName=name_credit,
creditAmount=credit,
)
)
return body

View File

@ -0,0 +1,64 @@
import uuid
from datetime import date, datetime
from decimal import Decimal
from typing import List
from pydantic import BaseModel, validator
from . import to_camel
from .user_link import UserLink
class Entries(BaseModel):
id_: uuid.UUID
date_: date
url: List[str]
type_: str
posted: bool
narration: str
debit_names: List[str]
credit_names: List[str]
amount: Decimal
creation_date: datetime
last_edit_date: datetime
user: UserLink
class Config:
anystr_strip_whitespace = True
alias_generator = to_camel
json_encoders = {
date: lambda v: v.strftime("%d-%b-%Y"),
datetime: lambda v: v.strftime("%d-%b-%Y %H:%M"),
}
@validator("date_", pre=True)
def parse_date(cls, value):
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@validator("creation_date", pre=True)
def parse_creation_date(cls, value):
if isinstance(value, datetime):
return value
return datetime.strptime(value, "%d-%b-%Y %H:%M")
@validator("last_edit_date", pre=True)
def parse_last_edit_date(cls, value):
if isinstance(value, datetime):
return value
return datetime.strptime(value, "%d-%b-%Y %H:%M")
class Report(BaseModel):
counts: int
report: List[Entries]
class Config:
anystr_strip_whitespace = True
alias_generator = to_camel
json_encoders = {
date: lambda v: v.strftime("%d-%b-%Y"),
datetime: lambda v: v.strftime("%d-%b-%Y %H:%M"),
}

View File

@ -1,32 +0,0 @@
import uuid
from datetime import date, datetime
from decimal import Decimal
from typing import List
from pydantic import BaseModel, validator
from . import to_camel
class Unposted(BaseModel):
id_: uuid.UUID
date_: date
url: List[str]
type_: str
narration: str
debit_name: str
debit_amount: Decimal
credit_name: str
credit_amount: Decimal
@validator("date_", pre=True)
def parse_date(cls, value):
if isinstance(value, date):
return 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")}

View File

@ -165,8 +165,8 @@ const appRoutes: Routes = [
import('./trial-balance/trial-balance.module').then((mod) => mod.TrialBalanceModule), import('./trial-balance/trial-balance.module').then((mod) => mod.TrialBalanceModule),
}, },
{ {
path: 'unposted', path: 'entries',
loadChildren: () => import('./unposted/unposted.module').then((mod) => mod.UnpostedModule), loadChildren: () => import('./entries/entries.module').then((mod) => mod.EntriesModule),
}, },
{ {
path: 'users', path: 'users',

View File

@ -35,7 +35,7 @@
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
<mat-datepicker #finishDate></mat-datepicker> <mat-datepicker #finishDate></mat-datepicker>
</mat-form-field> </mat-form-field>
<button mat-raised-button color="primary" (click)="show()" fxFlex="20c">Show</button> <button mat-raised-button color="primary" (click)="show()" fxFlex="20">Show</button>
</div> </div>
</form> </form>
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements"> <mat-table #table [dataSource]="dataSource" matSort aria-label="Elements">

View File

@ -19,7 +19,9 @@
<a mat-menu-item routerLink="/profit-loss">Profit and Loss</a> <a mat-menu-item routerLink="/profit-loss">Profit and Loss</a>
<a mat-menu-item routerLink="/balance-sheet">Balance Sheet</a> <a mat-menu-item routerLink="/balance-sheet">Balance Sheet</a>
<a mat-menu-item routerLink="/net-transactions">Net Transactions</a> <a mat-menu-item routerLink="/net-transactions">Net Transactions</a>
<a mat-menu-item routerLink="/unposted">UnPosted Entries</a> <a mat-menu-item routerLink="/entries" [queryParams]="{ p: 'false', a: 'date', d: 'desc' }"
>All Entries</a
>
</mat-menu> </mat-menu>
<button mat-button [matMenuTriggerFor]="reportMenu">Reports</button> <button mat-button [matMenuTriggerFor]="reportMenu">Reports</button>

View File

@ -0,0 +1,45 @@
import { DataSource } from '@angular/cdk/collections';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { SortDirection } from '@angular/material/sort/sort-direction';
import { Observable, of as observableOf } from 'rxjs';
import { Entries } from './entries';
export class EntriesDatasource extends DataSource<Entries> {
constructor(
public data: Entries[],
public length: number,
private paginator?: MatPaginator,
private sort?: MatSort,
private pageSize?: number,
private pageIndex?: number,
private activeSort?: string,
private sortDirection?: SortDirection,
) {
super();
}
connect(): Observable<Entries[]> {
if (this.paginator) {
this.paginator.length = this.length;
if (this.pageSize !== undefined) {
this.paginator.pageSize = this.pageSize;
}
if (this.pageIndex !== undefined) {
this.paginator.pageIndex = this.pageIndex;
}
}
if (this.sort) {
if (this.activeSort !== undefined) {
this.sort.active = this.activeSort;
}
if (this.sortDirection !== undefined) {
this.sort.direction = this.sortDirection;
}
}
return observableOf(this.data);
}
disconnect() {}
}

View File

@ -1,17 +1,17 @@
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http';
import { inject, TestBed } from '@angular/core/testing'; import { inject, TestBed } from '@angular/core/testing';
import { UnpostedResolver } from './unposted-resolver.service'; import { EntriesResolver } from './entries-resolver.service';
describe('UnpostedResolver', () => { describe('EntriesResolver', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [HttpClientModule], imports: [HttpClientModule],
providers: [UnpostedResolver], providers: [EntriesResolver],
}); });
}); });
it('should be created', inject([UnpostedResolver], (service: UnpostedResolver) => { it('should be created', inject([EntriesResolver], (service: EntriesResolver) => {
expect(service).toBeTruthy(); expect(service).toBeTruthy();
})); }));
}); });

View File

@ -0,0 +1,38 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve } from '@angular/router';
import { Observable } from 'rxjs/internal/Observable';
import { EntriesService } from './entries.service';
import { Report } from './report';
@Injectable({
providedIn: 'root',
})
export class EntriesResolver implements Resolve<Report> {
constructor(private ser: EntriesService) {}
resolve(route: ActivatedRouteSnapshot): Observable<Report> {
const start_date = route.queryParamMap.get('s');
const finish_date = route.queryParamMap.get('f');
const posted =
route.queryParamMap.get('p') === null ? null : route.queryParamMap.get('p') === 'true';
const page_size =
route.queryParamMap.get('ps') === null ? 50 : +(route.queryParamMap.get('ps') as string);
const page_index =
route.queryParamMap.get('pi') === null ? 0 : +(route.queryParamMap.get('pi') as string);
const sort = route.queryParamMap.get('a') ?? 'date';
const direction = route.queryParamMap.get('d') ?? 'desc';
const issue_vouchers =
route.queryParamMap.get('i') === null ? false : route.queryParamMap.get('i') === 'true';
return this.ser.list(
start_date,
finish_date,
posted,
page_size,
page_index,
sort,
direction,
issue_vouchers,
);
}
}

View File

@ -0,0 +1,13 @@
import { EntriesRoutingModule } from './entries-routing.module';
describe('EntriesRoutingModule', () => {
let entriesRoutingModule: EntriesRoutingModule;
beforeEach(() => {
entriesRoutingModule = new EntriesRoutingModule();
});
it('should create an instance', () => {
expect(entriesRoutingModule).toBeTruthy();
});
});

View File

@ -0,0 +1,30 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from '../auth/auth-guard.service';
import { EntriesResolver } from './entries-resolver.service';
import { EntriesComponent } from './entries.component';
const entriesRoutes: Routes = [
{
path: '',
component: EntriesComponent,
canActivate: [AuthGuard],
data: {
permission: 'Ledger',
},
resolve: {
info: EntriesResolver,
},
runGuardsAndResolvers: 'always',
},
];
@NgModule({
imports: [CommonModule, RouterModule.forChild(entriesRoutes)],
exports: [RouterModule],
providers: [EntriesResolver],
})
export class EntriesRoutingModule {}

View File

@ -0,0 +1,19 @@
.right {
display: flex;
justify-content: flex-end;
}
.my-margin {
margin: 0 12px;
}
.selected {
background: #fff3cd;
}
.unposted {
background: #f8d7da;
}
.mat-column-date,
.mat-column-voucherType {
max-width: 100px;
}

View File

@ -0,0 +1,138 @@
<mat-card>
<mat-card-title-group>
<mat-card-title>Entries</mat-card-title>
</mat-card-title-group>
<form [formGroup]="form" fxLayout="column">
<div
fxLayout="row"
fxLayout.lt-md="column"
fxLayoutGap="20px"
fxLayoutGap.lt-md="0px"
fxLayoutAlign="space-around start"
>
<mat-form-field fxFlex="40%">
<input
matInput
[matDatepicker]="startDate"
placeholder="Create / Last Edit Date From"
formControlName="startDate"
autocomplete="off"
/>
<mat-datepicker-toggle matSuffix [for]="startDate"></mat-datepicker-toggle>
<mat-datepicker #startDate></mat-datepicker>
</mat-form-field>
<mat-form-field fxFlex="40%">
<input
matInput
[matDatepicker]="finishDate"
placeholder="Create / Last Edit Date Till"
formControlName="finishDate"
autocomplete="off"
/>
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
<mat-datepicker #finishDate></mat-datepicker>
</mat-form-field>
<button mat-raised-button (click)="refresh()" fxFlex="20%">
<mat-icon>refresh</mat-icon>Refresh
</button>
</div>
<div
fxLayout="row"
fxLayout.lt-md="column"
fxLayoutGap="20px"
fxLayoutGap.lt-md="0px"
fxLayoutAlign="space-around start"
>
<mat-radio-group fxFlex="40%" formControlName="posted" (change)="togglePosted($event)">
<mat-radio-button class="my-margin" value="true"> Posted </mat-radio-button>
<mat-radio-button class="my-margin" value="false"> Not Posted </mat-radio-button>
<mat-radio-button class="my-margin" value="null"> All </mat-radio-button>
</mat-radio-group>
<mat-checkbox
fxFlex="40%"
formControlName="issue"
class="my-margin"
[checked]="issue"
(change)="toggleIssue()"
>
Include Issue Entries
</mat-checkbox>
</div>
</form>
<mat-card-content>
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements">
<!-- Date Column -->
<ng-container matColumnDef="date">
<mat-header-cell *matHeaderCellDef mat-sort-header>Date</mat-header-cell>
<mat-cell *matCellDef="let row"
><a [routerLink]="row.url">{{ row.date }}</a></mat-cell
>
</ng-container>
<!-- Type Column -->
<ng-container matColumnDef="voucherType">
<mat-header-cell *matHeaderCellDef>Type</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.type }}</mat-cell>
</ng-container>
<!-- Narration Column -->
<ng-container matColumnDef="narration">
<mat-header-cell *matHeaderCellDef>Narration</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.narration }}</mat-cell>
</ng-container>
<!-- DebitName Column -->
<ng-container matColumnDef="debitNames">
<mat-header-cell *matHeaderCellDef>Debit</mat-header-cell>
<mat-cell *matCellDef="let row">
<ul>
<li *ngFor="let item of row.debitNames">{{ item }}</li>
</ul>
</mat-cell>
</ng-container>
<!-- CreditName Column -->
<ng-container matColumnDef="creditNames">
<mat-header-cell *matHeaderCellDef>Credit</mat-header-cell>
<mat-cell *matCellDef="let row">
<ul>
<li *ngFor="let item of row.creditNames">{{ item }}</li>
</ul>
</mat-cell>
</ng-container>
<!-- Amount Column -->
<ng-container matColumnDef="amount">
<mat-header-cell *matHeaderCellDef class="right">Amount</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">{{ row.amount | currency: 'INR' }}</mat-cell>
</ng-container>
<!-- User Column -->
<ng-container matColumnDef="user">
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">User</mat-header-cell>
<mat-cell *matCellDef="let row">
<ul>
<li>{{ row.creationDate | localTime }}</li>
<li>{{ row.lastEditDate | localTime }}</li>
<li>{{ row.user.name }}</li>
</ul>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row
*matRowDef="let row; columns: displayedColumns"
[class.unposted]="!row.posted && posted !== false && row.type !== 'Issue'"
></mat-row>
</mat-table>
<mat-paginator
#paginator
[length]="dataSource.data.length"
[pageIndex]="0"
[pageSize]="50"
[pageSizeOptions]="[25, 50, 100, 250, 300]"
>
</mat-paginator>
</mat-card-content>
</mat-card>

View File

@ -2,23 +2,23 @@ import { HttpClientModule } from '@angular/common/http';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { UnpostedComponent } from './unposted.component'; import { EntriesComponent } from './entries.component';
describe('UnpostedComponent', () => { describe('EntriesComponent', () => {
let component: UnpostedComponent; let component: EntriesComponent;
let fixture: ComponentFixture<UnpostedComponent>; let fixture: ComponentFixture<EntriesComponent>;
beforeEach( beforeEach(
waitForAsync(() => { waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [HttpClientModule, RouterTestingModule], imports: [HttpClientModule, RouterTestingModule],
declarations: [UnpostedComponent], declarations: [EntriesComponent],
}).compileComponents(); }).compileComponents();
}), }),
); );
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(UnpostedComponent); fixture = TestBed.createComponent(EntriesComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@ -0,0 +1,143 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
import { MatPaginator } from '@angular/material/paginator';
import { MatRadioChange } from '@angular/material/radio';
import { MatSort } from '@angular/material/sort';
import { SortDirection } from '@angular/material/sort/sort-direction';
import { ActivatedRoute, Router } from '@angular/router';
import * as moment from 'moment';
import { EntriesDatasource } from './entries-datasource';
import { Report } from './report';
@Component({
selector: 'app-entries',
templateUrl: './entries.component.html',
styleUrls: ['./entries.component.css'],
})
export class EntriesComponent implements OnInit {
@ViewChild(MatPaginator, { static: true }) paginator?: MatPaginator;
@ViewChild(MatSort, { static: true }) sort?: MatSort;
info: Report = new Report();
dataSource: EntriesDatasource = new EntriesDatasource(this.info.report, 0);
form: FormGroup;
displayedColumns = [
'date',
'voucherType',
'narration',
'debitNames',
'creditNames',
'amount',
'user',
];
posted: boolean | null = null;
issue: boolean = false;
constructor(private route: ActivatedRoute, private router: Router, private fb: FormBuilder) {
this.form = this.fb.group({
startDate: '',
finishDate: '',
posted: '',
issue: '',
});
}
ngOnInit() {
this.route.data.subscribe((value) => {
const data = value as { info: Report };
this.info = data.info;
const queryMap = this.route.snapshot.queryParamMap;
const startDateO = queryMap.get('s');
const finishDateO = queryMap.get('f');
const startDate = startDateO !== null ? moment(startDateO, 'DD-MMM-YYYY').toDate() : '';
const finishDate = finishDateO !== null ? moment(finishDateO, 'DD-MMM-YYYY').toDate() : '';
const postedO = queryMap.get('p');
this.posted = postedO !== null ? postedO === 'true' : null;
const pageSizeO = queryMap.get('ps');
const pageSize = pageSizeO !== null ? +pageSizeO : 50;
const pageIndexO = queryMap.get('pi');
const pageIndex = pageIndexO !== null ? +pageIndexO : 0;
const activeSortO = queryMap.get('a');
const activeSort = activeSortO !== null ? (activeSortO as string) : 'date';
const sortDirectionO = queryMap.get('d');
const sortDirection = sortDirectionO !== null ? (sortDirectionO as SortDirection) : 'desc';
const issueO = queryMap.get('i');
this.issue = issueO !== null ? issueO === 'true' : false;
this.form.setValue({
startDate: startDate,
finishDate: finishDate,
posted: postedO ?? 'null',
issue: this.issue,
});
if (this.posted === null || this.posted) {
(this.form.get('issue') as FormControl).enable();
} else {
(this.form.get('issue') as FormControl).disable();
}
this.dataSource = new EntriesDatasource(
this.info.report,
this.info.counts,
this.paginator,
this.sort,
pageSize,
pageIndex,
activeSort,
sortDirection,
);
});
this.sort?.sortChange.subscribe((x) =>
this.router.navigate([], {
relativeTo: this.route,
queryParams: { a: this.sort?.active ?? 'date', d: this.sort?.direction },
queryParamsHandling: 'merge',
}),
);
this.paginator?.page.subscribe((x) =>
this.router.navigate([], {
relativeTo: this.route,
queryParams: { ps: this.paginator?.pageSize, pi: this.paginator?.pageIndex },
queryParamsHandling: 'merge',
}),
);
}
refresh() {
const formModel = this.form.value;
const startDate = formModel.startDate
? moment(formModel.startDate).format('DD-MMM-YYYY')
: null;
const finishDate = formModel.finishDate
? moment(formModel.finishDate).format('DD-MMM-YYYY')
: null;
this.router.navigate([], {
relativeTo: this.route,
queryParams: { s: startDate, f: finishDate },
queryParamsHandling: 'merge',
});
}
togglePosted($event: MatRadioChange) {
if ($event.value === 'true') {
this.posted = true;
} else if ($event.value === 'false') {
this.posted = false;
this.issue = false;
} else {
this.posted = null;
}
this.router.navigate([], {
relativeTo: this.route,
queryParams: { p: this.posted, i: this.issue || null },
queryParamsHandling: 'merge',
});
}
toggleIssue() {
this.issue = !this.issue;
this.router.navigate([], {
relativeTo: this.route,
queryParams: { i: this.issue || null },
queryParamsHandling: 'merge',
});
}
}

View File

@ -0,0 +1,13 @@
import { EntriesModule } from './entries.module';
describe('EntriesModule', () => {
let entriesModule: EntriesModule;
beforeEach(() => {
entriesModule = new EntriesModule();
});
it('should create an instance', () => {
expect(entriesModule).toBeTruthy();
});
});

View File

@ -2,22 +2,32 @@ import { A11yModule } from '@angular/cdk/a11y';
import { CdkTableModule } from '@angular/cdk/table'; import { CdkTableModule } from '@angular/cdk/table';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { ReactiveFormsModule } from '@angular/forms';
import { MomentDateAdapter } from '@angular/material-moment-adapter';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatNativeDateModule } from '@angular/material/core'; import {
DateAdapter,
MAT_DATE_FORMATS,
MAT_DATE_LOCALE,
MatNativeDateModule,
} from '@angular/material/core';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatExpansionModule } from '@angular/material/expansion'; import { MatExpansionModule } from '@angular/material/expansion';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatPaginatorModule } from '@angular/material/paginator'; import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatRadioModule } from '@angular/material/radio';
import { MatSortModule } from '@angular/material/sort'; import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { SharedModule } from '../shared/shared.module'; import { SharedModule } from '../shared/shared.module';
import { UnpostedRoutingModule } from './unposted-routing.module'; import { EntriesRoutingModule } from './entries-routing.module';
import { UnpostedComponent } from './unposted.component'; import { EntriesComponent } from './entries.component';
export const MY_FORMATS = { export const MY_FORMATS = {
parse: { parse: {
@ -44,12 +54,20 @@ export const MY_FORMATS = {
MatNativeDateModule, MatNativeDateModule,
MatPaginatorModule, MatPaginatorModule,
MatProgressSpinnerModule, MatProgressSpinnerModule,
MatRadioModule,
MatSortModule, MatSortModule,
MatTableModule, MatTableModule,
SharedModule, SharedModule,
UnpostedRoutingModule, EntriesRoutingModule,
MatExpansionModule, MatExpansionModule,
MatDatepickerModule,
ReactiveFormsModule,
FlexLayoutModule,
],
declarations: [EntriesComponent],
providers: [
{ provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] },
{ provide: MAT_DATE_FORMATS, useValue: MY_FORMATS },
], ],
declarations: [UnpostedComponent],
}) })
export class UnpostedModule {} export class EntriesModule {}

View File

@ -1,17 +1,17 @@
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http';
import { inject, TestBed } from '@angular/core/testing'; import { inject, TestBed } from '@angular/core/testing';
import { UnpostedService } from './unposted.service'; import { EntriesService } from './entries.service';
describe('UnpostedService', () => { describe('EntriesService', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [HttpClientModule], imports: [HttpClientModule],
providers: [UnpostedService], providers: [EntriesService],
}); });
}); });
it('should be created', inject([UnpostedService], (service: UnpostedService) => { it('should be created', inject([EntriesService], (service: EntriesService) => {
expect(service).toBeTruthy(); expect(service).toBeTruthy();
})); }));
}); });

View File

@ -0,0 +1,44 @@
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 { Report } from './report';
const url = '/api/entries';
const serviceName = 'EntriesService';
@Injectable({
providedIn: 'root',
})
export class EntriesService {
constructor(private http: HttpClient, private log: ErrorLoggerService) {}
list(
start_date: string | null,
finish_date: string | null,
posted: boolean | null,
page_size: number,
page_index: number,
sort: string,
direction: string,
issue: boolean,
): Observable<Report> {
let params = new HttpParams();
if (start_date !== null) params = params.set('s', start_date);
if (finish_date !== null) params = params.set('f', finish_date);
if (posted !== null) params = params.set('p', posted.toString());
params = params
.set('ps', page_size)
.set('pi', page_index)
.set('a', sort)
.set('d', direction)
.set('i', issue.toString());
return this.http
.get<Report>(url, { params })
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<Report>;
}
}

View File

@ -0,0 +1,32 @@
import { User } from '../core/user';
export class Entries {
id: string;
date: string;
url: string[];
type: string;
posted: boolean;
narration: string;
debitNames: string[];
creditNames: string[];
amount: number;
creationDate: string;
lastEditDate: string;
user: User;
public constructor(init?: Partial<Entries>) {
this.id = '';
this.date = '';
this.url = [];
this.type = '';
this.posted = false;
this.narration = '';
this.debitNames = [];
this.creditNames = [];
this.amount = 0;
this.creationDate = '';
this.lastEditDate = '';
this.user = new User();
Object.assign(this, init);
}
}

View File

@ -0,0 +1,12 @@
import { Entries } from './entries';
export class Report {
counts: number;
report: Entries[];
public constructor(init?: Partial<Report>) {
this.counts = 0;
this.report = [];
Object.assign(this, init);
}
}

View File

@ -364,20 +364,21 @@ export class PurchaseComponent implements OnInit, AfterViewInit, OnDestroy {
productSelected(event: MatAutocompleteSelectedEvent): void { productSelected(event: MatAutocompleteSelectedEvent): void {
const product: Product = event.option.value; const product: Product = event.option.value;
const addRowForm: FormControl = this.form.get('addRow') as FormControl;
this.product = product; this.product = product;
((this.form.get('addRow') as FormControl).get('price') as FormControl).setValue(product.price); (addRowForm.get('price') as FormControl).setValue(product.price);
if (product.isRateContracted) { if (product.isRateContracted) {
((this.form.get('addRow') as FormControl).get('price') as FormControl).disable(); (addRowForm.get('price') as FormControl).disable();
((this.form.get('addRow') as FormControl).get('tax') as FormControl).disable(); (addRowForm.get('tax') as FormControl).disable();
((this.form.get('addRow') as FormControl).get('discount') as FormControl).disable(); (addRowForm.get('discount') as FormControl).disable();
((this.form.get('addRow') as FormControl).get('tax') as FormControl).setValue('RC'); (addRowForm.get('tax') as FormControl).setValue('RC');
((this.form.get('addRow') as FormControl).get('discount') as FormControl).setValue('RC'); (addRowForm.get('discount') as FormControl).setValue('RC');
} else { } else {
((this.form.get('addRow') as FormControl).get('price') as FormControl).enable(); (addRowForm.get('price') as FormControl).enable();
((this.form.get('addRow') as FormControl).get('tax') as FormControl).enable(); (addRowForm.get('tax') as FormControl).enable();
((this.form.get('addRow') as FormControl).get('discount') as FormControl).enable(); (addRowForm.get('discount') as FormControl).enable();
((this.form.get('addRow') as FormControl).get('tax') as FormControl).setValue(''); (addRowForm.get('tax') as FormControl).setValue('');
((this.form.get('addRow') as FormControl).get('discount') as FormControl).setValue(''); (addRowForm.get('discount') as FormControl).setValue('');
} }
} }

View File

@ -1,74 +0,0 @@
import { DataSource } from '@angular/cdk/collections';
import { EventEmitter } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSort, Sort } from '@angular/material/sort';
import { merge, Observable, of as observableOf } from 'rxjs';
import { map } from 'rxjs/operators';
import { Unposted } from './unposted';
/** Simple sort comparator for example ID/Name columns (for client-side sorting). */
const compare = (a: string | number, b: string | number, isAsc: boolean) =>
(a < b ? -1 : 1) * (isAsc ? 1 : -1);
export class UnpostedDataSource extends DataSource<Unposted> {
constructor(public data: Unposted[], private paginator?: MatPaginator, private sort?: MatSort) {
super();
}
connect(): Observable<Unposted[]> {
const dataMutations: (Observable<Unposted[]> | EventEmitter<PageEvent> | EventEmitter<Sort>)[] =
[observableOf(this.data)];
if (this.paginator) {
dataMutations.push((this.paginator as MatPaginator).page);
}
if (this.sort) {
dataMutations.push((this.sort as MatSort).sortChange);
}
// Set the paginators length
if (this.paginator) {
this.paginator.length = this.data.length;
}
return merge(...dataMutations).pipe(
map(() => this.getPagedData(this.getSortedData([...this.data]))),
);
}
disconnect() {}
private getPagedData(data: Unposted[]) {
if (this.paginator === undefined) {
return data;
}
const startIndex = this.paginator.pageIndex * this.paginator.pageSize;
return data.splice(startIndex, this.paginator.pageSize);
}
private getSortedData(data: Unposted[]) {
if (this.sort === undefined) {
return data;
}
if (!this.sort.active || this.sort.direction === '') {
return data;
}
const sort = this.sort as MatSort;
return data.sort((a: Unposted, b: Unposted) => {
const isAsc = sort.direction === 'asc';
switch (sort.active) {
case 'date':
return compare(a.date, b.date, isAsc);
case 'type':
return compare(a.type, b.type, isAsc);
case 'debitAmount':
return compare(+a.debitAmount, +b.debitAmount, isAsc);
case 'creditAmount':
return compare(+a.creditAmount, +b.creditAmount, isAsc);
default:
return 0;
}
});
}
}

View File

@ -1,17 +0,0 @@
import { Injectable } from '@angular/core';
import { Resolve } from '@angular/router';
import { Observable } from 'rxjs/internal/Observable';
import { Unposted } from './unposted';
import { UnpostedService } from './unposted.service';
@Injectable({
providedIn: 'root',
})
export class UnpostedResolver implements Resolve<Unposted[]> {
constructor(private ser: UnpostedService) {}
resolve(): Observable<Unposted[]> {
return this.ser.list();
}
}

View File

@ -1,13 +0,0 @@
import { UnpostedRoutingModule } from './unposted-routing.module';
describe('UnpostedRoutingModule', () => {
let unpostedRoutingModule: UnpostedRoutingModule;
beforeEach(() => {
unpostedRoutingModule = new UnpostedRoutingModule();
});
it('should create an instance', () => {
expect(unpostedRoutingModule).toBeTruthy();
});
});

View File

@ -1,29 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from '../auth/auth-guard.service';
import { UnpostedResolver } from './unposted-resolver.service';
import { UnpostedComponent } from './unposted.component';
const unpostedRoutes: Routes = [
{
path: '',
component: UnpostedComponent,
canActivate: [AuthGuard],
data: {
permission: 'post-vouchers',
},
resolve: {
info: UnpostedResolver,
},
},
];
@NgModule({
imports: [CommonModule, RouterModule.forChild(unpostedRoutes)],
exports: [RouterModule],
providers: [UnpostedResolver],
})
export class UnpostedRoutingModule {}

View File

@ -1,7 +0,0 @@
.right {
display: flex;
justify-content: flex-end;
}
.my-margin {
margin: 0 12px;
}

View File

@ -1,108 +0,0 @@
<mat-card>
<mat-card-title-group>
<mat-card-title>Unposted</mat-card-title>
<a mat-button (click)="refresh()">
<mat-icon>refresh</mat-icon>
Refresh
</a>
</mat-card-title-group>
<mat-expansion-panel (opened)="panelOpenState = true" (closed)="panelOpenState = false">
<mat-expansion-panel-header>
<mat-panel-title> Self aware panel </mat-panel-title>
<mat-panel-description>
Currently I am {{ panelOpenState ? 'open' : 'closed' }}
</mat-panel-description>
</mat-expansion-panel-header>
<mat-checkbox
class="my-margin"
[checked]="!!unposted"
[indeterminate]="unposted === undefined"
(change)="toggleUnposted()"
>
Unposted
</mat-checkbox>
<p></p>
<mat-checkbox
class="my-margin"
[checked]="!!unposted"
[indeterminate]="unposted === undefined"
(change)="toggleUnposted()"
>
All userscv
</mat-checkbox>
<mat-checkbox
class="my-margin"
[checked]="!!unposted"
[indeterminate]="unposted === undefined"
(change)="toggleUnposted()"
>
Unposted
</mat-checkbox>
</mat-expansion-panel>
<mat-card-content>
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements">
<!-- Date Column -->
<ng-container matColumnDef="date">
<mat-header-cell *matHeaderCellDef mat-sort-header>Date</mat-header-cell>
<mat-cell *matCellDef="let row"
><a [routerLink]="row.url">{{ row.date }}</a></mat-cell
>
</ng-container>
<!-- Type Column -->
<ng-container matColumnDef="voucherType">
<mat-header-cell *matHeaderCellDef mat-sort-header>Type</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.type }}</mat-cell>
</ng-container>
<!-- Narration Column -->
<ng-container matColumnDef="narration">
<mat-header-cell *matHeaderCellDef mat-sort-header>Narration</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.narration }}</mat-cell>
</ng-container>
<!-- DebitName Column -->
<ng-container matColumnDef="debitName">
<mat-header-cell *matHeaderCellDef mat-sort-header>Debit</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.debitName }}</mat-cell>
</ng-container>
<!-- DebitAmount Column -->
<ng-container matColumnDef="debitAmount">
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Amount</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">{{
row.debitAmount | currency: 'INR' | accounting
}}</mat-cell>
</ng-container>
<!-- CreditName Column -->
<ng-container matColumnDef="creditName">
<mat-header-cell *matHeaderCellDef mat-sort-header>Credit</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.creditName }}</mat-cell>
</ng-container>
<!-- CreditAmount Column -->
<ng-container matColumnDef="creditAmount">
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Amount</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">{{
row.creditAmount | currency: 'INR' | accounting
}}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row
*matRowDef="let row; columns: displayedColumns"
[class.posted]="row.isPosted"
></mat-row>
</mat-table>
<mat-paginator
#paginator
[length]="dataSource.data.length"
[pageIndex]="0"
[pageSize]="50"
[pageSizeOptions]="[25, 50, 100, 250, 300]"
>
</mat-paginator>
</mat-card-content>
</mat-card>

View File

@ -1,63 +0,0 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { ActivatedRoute, Router } from '@angular/router';
import { Unposted } from './unposted';
import { UnpostedDataSource } from './unposted-datasource';
import { UnpostedService } from './unposted.service';
@Component({
selector: 'app-unposted',
templateUrl: './unposted.component.html',
styleUrls: ['./unposted.component.css'],
})
export class UnpostedComponent implements OnInit {
@ViewChild(MatPaginator, { static: true }) paginator?: MatPaginator;
@ViewChild(MatSort, { static: true }) sort?: MatSort;
info: Unposted[] = [];
dataSource: UnpostedDataSource = new UnpostedDataSource(this.info);
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = [
'date',
'voucherType',
'narration',
'debitName',
'debitAmount',
'creditName',
'creditAmount',
];
panelOpenState = false;
unposted: boolean | undefined = undefined;
constructor(
private route: ActivatedRoute,
private router: Router,
private ser: UnpostedService,
) {}
ngOnInit() {
this.route.data.subscribe((value) => {
const data = value as { info: Unposted[] };
this.info = data.info;
});
this.dataSource = new UnpostedDataSource(this.info, this.paginator, this.sort);
}
refresh() {
this.ser.list().subscribe((info: Unposted[]) => {
this.info = info;
});
}
toggleUnposted() {
if (this.unposted === undefined) {
this.unposted = false;
} else if (!this.unposted) {
this.unposted = true;
} else {
this.unposted = undefined;
}
}
}

View File

@ -1,13 +0,0 @@
import { UnpostedModule } from './unposted.module';
describe('UnpostedModule', () => {
let unpostedModule: UnpostedModule;
beforeEach(() => {
unpostedModule = new UnpostedModule();
});
it('should create an instance', () => {
expect(unpostedModule).toBeTruthy();
});
});

View File

@ -1,24 +0,0 @@
import { HttpClient } 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 { Unposted } from './unposted';
const url = '/api/unposted';
const serviceName = 'UnpostedService';
@Injectable({
providedIn: 'root',
})
export class UnpostedService {
constructor(private http: HttpClient, private log: ErrorLoggerService) {}
list(): Observable<Unposted[]> {
return this.http
.get<Unposted[]>(url)
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<Unposted[]>;
}
}

View File

@ -1,24 +0,0 @@
export class Unposted {
id: string;
date: string;
url: string[];
type: string;
narration: string;
debitName: string;
debitAmount: number;
creditName: string;
creditAmount: number;
public constructor(init?: Partial<Unposted>) {
this.id = '';
this.date = '';
this.url = [];
this.type = '';
this.narration = '';
this.debitName = '';
this.debitAmount = 0;
this.creditName = '';
this.creditAmount = 0;
Object.assign(this, init);
}
}