21 Commits

Author SHA1 Message Date
05fb3c1677 disable description at payment entry form 2021-12-12 14:07:25 +01:00
cbc96036d9 adjusted gitignore file 2021-11-09 10:39:37 +01:00
44202ef9ed Merge branch 'overhead_account' of ssh://repo.hottis.de:2922/hv2/hv2-all-in-one into overhead_account 2021-11-09 10:22:12 +01:00
34f8e8ecd4 note on betriebskosten at ledger page 2021-11-09 10:21:47 +01:00
6f3248f03c more fix premise account stuff 2021-11-09 10:14:16 +01:00
3c97fb3582 fix premise account stuff 2021-11-09 10:02:53 +01:00
3ad019b374 schema changes 2021-11-08 21:05:24 +01:00
28e505f570 overhead account stuff 2021-11-08 21:04:21 +01:00
ba63874a18 change order 2021-11-02 13:11:02 +01:00
0e1e03f1a9 error dialog introduced 2021-11-02 13:06:34 +01:00
272500df8c one-way-binding in account entry form works now 2021-11-02 12:36:35 +01:00
0ab106d021 switch to one-way-binding in form 2021-11-01 21:17:20 +01:00
125af5a206 viewchild, das hat es noch nicht gebracht 2021-11-01 09:40:19 +01:00
419997cea5 not yet working correctly 2021-10-31 23:06:02 +01:00
797151d547 more ledger stuff 2021-10-31 22:27:00 +01:00
2b883aee02 ledger 2021-10-31 21:47:53 +01:00
8c4dbe7d71 add ledger, Buchfuehrung 2021-10-31 16:06:18 +01:00
d297eb60b3 option to disable colors in output 2021-09-15 17:36:57 +02:00
5744e84842 Merge branch 'master' of https://home.hottis.de/gitlab/hv2/hv2-all-in-one 2021-09-15 17:32:16 +02:00
b8083ec41e outer join for tenant-saldo-query 2021-09-15 17:31:55 +02:00
f559aba317 default today to monthly payment 2021-09-14 22:36:25 +02:00
36 changed files with 569 additions and 80 deletions

2
.gitignore vendored
View File

@ -3,4 +3,6 @@ ENV
api/config/dbconfig.ini api/config/dbconfig.ini
api/config/authservice.pub api/config/authservice.pub
cli/config/dbconfig.ini cli/config/dbconfig.ini
*~
.*~

View File

@ -88,4 +88,43 @@
$ref: '#/components/schemas/tenant_with_saldo' $ref: '#/components/schemas/tenant_with_saldo'
security: security:
- jwt: ['secret'] - jwt: ['secret']
/v1/accounts/bydescription/{description}:
get:
tags: [ "account" ]
summary: Return the normalized account with given description
operationId: additional_methods.get_account_by_description
parameters:
- name: description
in: path
required: true
schema:
type: string
responses:
'200':
description: account response
content:
'application/json':
schema:
type: array
items:
$ref: '#/components/schemas/account'
security:
- jwt: ['secret']
/v1/uniquenumber:
get:
tags: [ "uniquenumber" ]
summary: Returns a unique number
operationId: additional_methods.get_unique_number
responses:
'200':
description: get_unique_number
content:
'application/json':
schema:
type: object
properties:
number:
type: number
security:
- jwt: ['secret']

View File

@ -37,10 +37,27 @@ def get_tenant_with_saldo(user, token_info):
return dbGetMany(user, token_info, { return dbGetMany(user, token_info, {
"statement": """ "statement": """
SELECT t.id, t.firstname, t.lastname, t.address1, sum(a.amount) AS saldo SELECT t.id, t.firstname, t.lastname, t.address1, sum(a.amount) AS saldo
FROM tenant_t t, account_entry_t a FROM tenant_t t LEFT OUTER JOIN account_entry_t a ON a.account = t.account
WHERE a.account = t.account
GROUP BY t.id, t.firstname, t.lastname, t.address1 GROUP BY t.id, t.firstname, t.lastname, t.address1
""", """,
"params": () "params": ()
} }
) )
def get_account_by_description(user, token_info, description=None):
return dbGetOne(user, token_info, {
"statement": """
SELECT a.id ,a.description
FROM account_t a
WHERE a.description = %s""",
"params": (description, )
}
)
def get_unique_number(user, token_info):
return dbGetOne(user, token_info, {
"statement": """
SELECT nextval('unique_number_s') AS "number"
""",
"params": ()
})

View File

@ -283,6 +283,7 @@ SELECT
,street ,street
,zip ,zip
,city ,city
,account
FROM premise_t FROM premise_t
ORDER BY ORDER BY
description description
@ -298,6 +299,7 @@ def insert_premise(user, token_info, **args):
v_street = body["street"] v_street = body["street"]
v_zip = body["zip"] v_zip = body["zip"]
v_city = body["city"] v_city = body["city"]
v_account = body["account"]
return dbInsert(user, token_info, { return dbInsert(user, token_info, {
"statement": """ "statement": """
INSERT INTO premise_t INSERT INTO premise_t
@ -306,11 +308,13 @@ INSERT INTO premise_t
,street ,street
,zip ,zip
,city ,city
,account
) VALUES ( ) VALUES (
%s %s
,%s ,%s
,%s ,%s
,%s ,%s
,%s
) )
RETURNING * RETURNING *
""", """,
@ -319,6 +323,7 @@ INSERT INTO premise_t
,v_street ,v_street
,v_zip ,v_zip
,v_city ,v_city
,v_account
] ]
}) })
except KeyError as e: except KeyError as e:
@ -335,6 +340,7 @@ SELECT
,street ,street
,zip ,zip
,city ,city
,account
FROM premise_t FROM premise_t
WHERE id = %s WHERE id = %s
""", """,
@ -373,6 +379,23 @@ UPDATE premise_t
raise werkzeug.exceptions.UnprocessableEntity("parameter missing: {}".format(e)) raise werkzeug.exceptions.UnprocessableEntity("parameter missing: {}".format(e))
def get_premise_by_account(user, token_info, accountId=None):
return dbGetMany(user, token_info, {
"statement": """
SELECT
id
,description
,street
,zip
,city
,account
FROM premise_t
WHERE account = %s
""",
"params": (accountId, )
}
)
def get_flats(user, token_info): def get_flats(user, token_info):
return dbGetMany(user, token_info, { return dbGetMany(user, token_info, {
"statement": """ "statement": """
@ -1301,6 +1324,7 @@ SELECT
,account ,account
,created_at ,created_at
,amount ,amount
,document_no
,account_entry_category ,account_entry_category
FROM account_entry_t FROM account_entry_t
ORDER BY ORDER BY
@ -1317,6 +1341,7 @@ def insert_account_entry(user, token_info, **args):
v_account = body["account"] v_account = body["account"]
v_created_at = body["created_at"] v_created_at = body["created_at"]
v_amount = body["amount"] v_amount = body["amount"]
v_document_no = body["document_no"]
v_account_entry_category = body["account_entry_category"] v_account_entry_category = body["account_entry_category"]
return dbInsert(user, token_info, { return dbInsert(user, token_info, {
"statement": """ "statement": """
@ -1326,6 +1351,7 @@ INSERT INTO account_entry_t
,account ,account
,created_at ,created_at
,amount ,amount
,document_no
,account_entry_category ,account_entry_category
) VALUES ( ) VALUES (
%s %s
@ -1333,6 +1359,7 @@ INSERT INTO account_entry_t
,%s ,%s
,%s ,%s
,%s ,%s
,%s
) )
RETURNING * RETURNING *
""", """,
@ -1341,6 +1368,7 @@ INSERT INTO account_entry_t
,v_account ,v_account
,v_created_at ,v_created_at
,v_amount ,v_amount
,v_document_no
,v_account_entry_category ,v_account_entry_category
] ]
}) })
@ -1358,6 +1386,7 @@ SELECT
,account ,account
,created_at ,created_at
,amount ,amount
,document_no
,account_entry_category ,account_entry_category
FROM account_entry_t FROM account_entry_t
WHERE id = %s WHERE id = %s
@ -1377,6 +1406,7 @@ SELECT
,account ,account
,created_at ,created_at
,amount ,amount
,document_no
,account_entry_category ,account_entry_category
FROM account_entry_t FROM account_entry_t
WHERE account = %s WHERE account = %s
@ -1394,6 +1424,7 @@ SELECT
,account ,account
,created_at ,created_at
,amount ,amount
,document_no
,account_entry_category ,account_entry_category
FROM account_entry_t FROM account_entry_t
WHERE account_entry_category = %s WHERE account_entry_category = %s

View File

@ -299,6 +299,28 @@ paths:
$ref: '#/components/schemas/premise' $ref: '#/components/schemas/premise'
security: security:
- jwt: ['secret'] - jwt: ['secret']
/v1/premises/account/{accountId}:
get:
tags: [ "premise", "account" ]
summary: Return premise by $account
operationId: methods.get_premise_by_account
parameters:
- name: accountId
in: path
required: true
schema:
type: integer
responses:
'200':
description: premise response
content:
'application/json':
schema:
type: array
items:
$ref: '#/components/schemas/premise'
security:
- jwt: ['secret']
/v1/flats: /v1/flats:
get: get:
tags: [ "flat" ] tags: [ "flat" ]
@ -1509,6 +1531,45 @@ paths:
$ref: '#/components/schemas/tenant_with_saldo' $ref: '#/components/schemas/tenant_with_saldo'
security: security:
- jwt: ['secret'] - jwt: ['secret']
/v1/accounts/bydescription/{description}:
get:
tags: [ "account" ]
summary: Return the normalized account with given description
operationId: additional_methods.get_account_by_description
parameters:
- name: description
in: path
required: true
schema:
type: string
responses:
'200':
description: account response
content:
'application/json':
schema:
type: array
items:
$ref: '#/components/schemas/account'
security:
- jwt: ['secret']
/v1/uniquenumber:
get:
tags: [ "uniquenumber" ]
summary: Returns a unique number
operationId: additional_methods.get_unique_number
responses:
'200':
description: get_unique_number
content:
'application/json':
schema:
type: object
properties:
number:
type: number
security:
- jwt: ['secret']
components: components:
@ -1583,6 +1644,8 @@ components:
type: string type: string
city: city:
type: string type: string
account:
type: integer
flat: flat:
description: flat description: flat
type: object type: object
@ -1729,6 +1792,9 @@ components:
type: string type: string
amount: amount:
type: number type: number
document_no:
type: integer
nullable: true
account_entry_category: account_entry_category:
type: integer type: integer
note: note:

View File

@ -1,10 +1,13 @@
from db import dbGetMany, dbGetOne from db import dbGetMany, dbGetOne
from loguru import logger from loguru import logger
from decimal import Decimal from decimal import Decimal
import datetime
def perform(dbh, params): def perform(dbh, params):
createdAt = params['created_at'] try:
createdAt = params['created_at']
except KeyError:
createdAt = datetime.datetime.today().strftime("%Y-%m-%d")
tenants = dbGetMany(dbh, { "statement": "SELECT * FROM tenant_t", "params": () }) tenants = dbGetMany(dbh, { "statement": "SELECT * FROM tenant_t", "params": () })
for tenant in tenants: for tenant in tenants:

View File

@ -38,14 +38,21 @@ parser.add_argument('--verbosity', '-v',
help='Minimal log level for output: DEBUG, INFO, WARNING, ..., default: DEBUG', help='Minimal log level for output: DEBUG, INFO, WARNING, ..., default: DEBUG',
required=False, required=False,
default="DEBUG") default="DEBUG")
parser.add_argument('--nocolorize', '-n',
help='disable colored output (for cron)',
required=False,
action='store_true',
default=False)
args = parser.parse_args() args = parser.parse_args()
operation = args.operation operation = args.operation
params = json.loads(args.params) params = json.loads(args.params)
logLevel = args.verbosity logLevel = args.verbosity
noColorize = args.nocolorize
logger.remove() logger.remove()
logger.add(sys.stderr, colorize=True, level=logLevel) logger.add(sys.stderr, colorize=(not noColorize), level=logLevel)
dbh = None dbh = None

View File

@ -32,7 +32,8 @@
{ "name": "description", "sqltype": "varchar(128)", "selector": 0, "unique": true }, { "name": "description", "sqltype": "varchar(128)", "selector": 0, "unique": true },
{ "name": "street", "sqltype": "varchar(128)", "notnull": true }, { "name": "street", "sqltype": "varchar(128)", "notnull": true },
{ "name": "zip", "sqltype": "varchar(10)", "notnull": true }, { "name": "zip", "sqltype": "varchar(10)", "notnull": true },
{ "name": "city", "sqltype": "varchar(128)", "notnull": true } { "name": "city", "sqltype": "varchar(128)", "notnull": true },
{ "name": "account", "sqltype": "integer", "notnull": true, "foreignkey": true, "immutable": true, "unique": true }
] ]
}, },
{ {
@ -137,6 +138,7 @@
{ "name": "account", "sqltype": "integer", "notnull": true, "foreignkey": true }, { "name": "account", "sqltype": "integer", "notnull": true, "foreignkey": true },
{ "name": "created_at", "sqltype": "timestamp", "notnull": true, "default": "now()" }, { "name": "created_at", "sqltype": "timestamp", "notnull": true, "default": "now()" },
{ "name": "amount", "sqltype": "numeric(10,2)", "notnull": true, "selector": 0 }, { "name": "amount", "sqltype": "numeric(10,2)", "notnull": true, "selector": 0 },
{ "name": "document_no", "sqltype": "integer", "unique": true },
{ "name": "account_entry_category", "sqltype": "integer", "notnull": true, "foreignkey": true } { "name": "account_entry_category", "sqltype": "integer", "notnull": true, "foreignkey": true }
], ],
"tableConstraints": [ "tableConstraints": [

6
schema/changes01.sql Normal file
View File

@ -0,0 +1,6 @@
alter table premise_t add column account integer;
alter table premise_t add constraint fk_premise_t_account foreign key (account) references account_t(id);
alter table premise_t add constraint uk_premise_t_account unique(account);
alter table account_entry_t add column document_no integer unique;
create sequence unique_number_s start with 1 increment by 1;

View File

@ -39,6 +39,7 @@ CREATE TABLE premise_t (
,street varchar(128) not null ,street varchar(128) not null
,zip varchar(10) not null ,zip varchar(10) not null
,city varchar(128) not null ,city varchar(128) not null
,account integer not null references account_t (id) unique
); );
GRANT SELECT, INSERT, UPDATE ON premise_t TO hv2; GRANT SELECT, INSERT, UPDATE ON premise_t TO hv2;
@ -145,10 +146,11 @@ GRANT SELECT, UPDATE ON account_entry_category_t_id_seq TO hv2;
CREATE TABLE account_entry_t ( CREATE TABLE account_entry_t (
id serial not null primary key id serial not null primary key
,description varchar(128) not null ,description varchar(1024) not null
,account integer not null references account_t (id) ,account integer not null references account_t (id)
,created_at timestamp not null default now() ,created_at timestamp not null default now()
,amount numeric(10,2) not null ,amount numeric(10,2) not null
,document_no integer unique
,account_entry_category integer not null references account_entry_category_t (id) ,account_entry_category integer not null references account_entry_category_t (id)
,unique(description, account, created_at) ,unique(description, account, created_at)
); );

View File

@ -1,24 +1,24 @@
<div id="firstBlock"> <div id="firstBlock">
<form (ngSubmit)="addAccountEntry()"> <form (ngSubmit)="addAccountEntry(accountEntryForm)" #accountEntryForm="ngForm">
<mat-form-field appearance="outline" id="addEntryfield"> <mat-form-field appearance="outline" id="addEntryfield">
<mat-label>Datum</mat-label> <mat-label>Datum</mat-label>
<input matInput name="createdAt" [(ngModel)]="newAccountEntry.created_at" [matDatepicker]="createdAtPicker"/> <input matInput ngModel name="createdAt" [matDatepicker]="createdAtPicker"/>
<mat-datepicker-toggle matSuffix [for]="createdAtPicker"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="createdAtPicker"></mat-datepicker-toggle>
<mat-datepicker #createdAtPicker></mat-datepicker> <mat-datepicker #createdAtPicker></mat-datepicker>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" *ngIf="!shallBeRentPayment">
<mat-label>Kategorie</mat-label> <mat-label>Kategorie</mat-label>
<mat-select [(ngModel)]="newAccountEntry.account_entry_category" name="category" disabled="shallBeRentPayment"> <mat-select ngModel name="category" [disabled]="shallBeRentPayment">
<mat-option *ngFor="let p of accountEntryCategories" [value]="p.id">{{p.description}}</mat-option> <mat-option *ngFor="let p of accountEntryCategories" [value]="p.id">{{p.description}}</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Betrag (€)</mat-label> <mat-label>Betrag (€)</mat-label>
<input matInput type="number" name="amount" [(ngModel)]="newAccountEntry.amount"/> <input matInput type="number" name="amount" ngModel/>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" *ngIf="!shallBeRentPayment">
<mat-label>Beschreibung</mat-label> <mat-label>Beschreibung</mat-label>
<input matInput name="description" [(ngModel)]="newAccountEntry.description"/> <input matInput name="description" [disabled]="shallBeRentPayment" ngModel/>
</mat-form-field> </mat-form-field>
<button #addAccountEntryButton type="submit" mat-raised-button color="primary">Buchung speichern</button> <button #addAccountEntryButton type="submit" mat-raised-button color="primary">Buchung speichern</button>
</form> </form>
@ -36,6 +36,10 @@ Saldo: {{saldo?.saldo | number:'1.2-2'}} €
<th mat-header-cell *matHeaderCellDef>Beschreibung</th> <th mat-header-cell *matHeaderCellDef>Beschreibung</th>
<td mat-cell *matCellDef="let element">{{element.rawAccountEntry.description}}</td> <td mat-cell *matCellDef="let element">{{element.rawAccountEntry.description}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="document_no">
<th mat-header-cell *matHeaderCellDef>Belegnummer</th>
<td mat-cell *matCellDef="let element">{{element.rawAccountEntry.document_no}}</td>
</ng-container>
<ng-container matColumnDef="amount"> <ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>Betrag</th> <th mat-header-cell *matHeaderCellDef>Betrag</th>
<td mat-cell *matCellDef="let element" class="rightaligned">{{element.rawAccountEntry.amount | number:'1.2-2'}} €</td> <td mat-cell *matCellDef="let element" class="rightaligned">{{element.rawAccountEntry.amount | number:'1.2-2'}} €</td>

View File

@ -5,8 +5,9 @@ import { MatExpansionPanel } from '@angular/material/expansion';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { AccountEntryCategoryService, AccountEntryService, AccountService } from '../data-object-service'; import { AccountEntryCategoryService, AccountEntryService, AccountService } from '../data-object-service';
import { Account, AccountEntry, AccountEntryCategory, NULL_AccountEntry } from '../data-objects'; import { Account, AccountEntry, AccountEntryCategory, NULL_AccountEntry } from '../data-objects';
import { ErrorDialogService } from '../error-dialog.service';
import { ExtApiService } from '../ext-data-object-service'; import { ExtApiService } from '../ext-data-object-service';
import { Saldo } from '../ext-data-objects'; import { Saldo, UniqueNumber } from '../ext-data-objects';
import { MessageService } from '../message.service'; import { MessageService } from '../message.service';
@ -32,14 +33,13 @@ export class AccountComponent implements OnInit {
account: Account account: Account
accountEntries: DN_AccountEntry[] accountEntries: DN_AccountEntry[]
accountEntriesDataSource: MatTableDataSource<DN_AccountEntry> accountEntriesDataSource: MatTableDataSource<DN_AccountEntry>
accountEntriesDisplayedColumns: string[] = [ "description", "amount", "createdAt", "category", "overhead_relevant" ] accountEntriesDisplayedColumns: string[] = [ "description", "document_no", "amount", "createdAt", "category", "overhead_relevant" ]
saldo: Saldo saldo: Saldo
accountEntryCategories: AccountEntryCategory[] accountEntryCategories: AccountEntryCategory[]
accountEntryCategoriesMap: Map<number, AccountEntryCategory> accountEntryCategoriesMap: Map<number, AccountEntryCategory>
accountEntryCategoriesInverseMap: Map<string, AccountEntryCategory> accountEntryCategoriesInverseMap: Map<string, AccountEntryCategory>
newAccountEntry: AccountEntry = NULL_AccountEntry
constructor( constructor(
@ -47,7 +47,8 @@ export class AccountComponent implements OnInit {
private accountEntryService: AccountEntryService, private accountEntryService: AccountEntryService,
private extApiService: ExtApiService, private extApiService: ExtApiService,
private accountEntryCategoryService: AccountEntryCategoryService, private accountEntryCategoryService: AccountEntryCategoryService,
private messageService: MessageService private messageService: MessageService,
private errorDialogService: ErrorDialogService
) { } ) { }
@ -87,20 +88,43 @@ export class AccountComponent implements OnInit {
} }
} }
async addAccountEntry(): Promise<void> { async addAccountEntry(formData: any): Promise<void> {
try { try {
this.addAccountEntryButton.disabled = true this.addAccountEntryButton.disabled = true
this.newAccountEntry.account = this.account.id this.messageService.add(`${JSON.stringify(formData.value, undefined, 4)}`)
this.messageService.add(`addAccountEntry: ${ JSON.stringify(this.newAccountEntry, undefined, 4) }`) let uniquenumber: UniqueNumber = await this.extApiService.getUniqueNumber();
this.newAccountEntry = await this.accountEntryService.postAccountEntry(this.newAccountEntry) this.messageService.add(`Got unique number as document_no: ${uniquenumber.number}`)
this.messageService.add(`New accountEntry created: ${this.newAccountEntry.id}`) let newAccountEntry: AccountEntry = {
this.newAccountEntry = { 'account': this.account.id, 'amount': undefined, 'created_at': '', 'description': '', 'id': 0, 'account_entry_category': 0 } description: formData.value.description,
this.getAccountEntries() account: this.account.id,
} catch (err) { created_at: formData.value.createdAt,
this.messageService.add(`Error in addAccountEntry: ${JSON.stringify(err, undefined, 4)}`) amount: formData.value.amount,
} finally { id: 0,
this.addAccountEntryButton.disabled = false document_no: uniquenumber.number,
account_entry_category: 0
} }
if (this.shallBeRentPayment) {
newAccountEntry.account_entry_category = this.accountEntryCategoriesInverseMap.get('Mietzahlung').id
newAccountEntry.description = "Miete"
this.messageService.add(`shall be rentpayment, category is ${newAccountEntry.account_entry_category}`)
} else {
newAccountEntry.account_entry_category = formData.value.category
this.messageService.add(`category is ${newAccountEntry.account_entry_category}`)
}
this.messageService.add(`addAccountEntry: ${ JSON.stringify(newAccountEntry, undefined, 4) }`)
newAccountEntry = await this.accountEntryService.postAccountEntry(newAccountEntry)
this.messageService.add(`New accountEntry created: ${newAccountEntry.id}`)
formData.reset()
this.getAccountEntries()
} catch (err) {
this.messageService.add(`Error in addAccountEntry: ${JSON.stringify(err, undefined, 4)}`)
this.errorDialogService.openDialog('AccountComponent', 'addAccountEntry', JSON.stringify(err, undefined, 4))
} finally {
this.addAccountEntryButton.disabled = false
}
} }
async getAccountEntryCategories(): Promise<void> { async getAccountEntryCategories(): Promise<void> {
@ -123,10 +147,6 @@ export class AccountComponent implements OnInit {
this.messageService.add(`AccountComponent.init, account: ${this.selectedAccountId}`) this.messageService.add(`AccountComponent.init, account: ${this.selectedAccountId}`)
this.getAccount() this.getAccount()
await this.getAccountEntryCategories() await this.getAccountEntryCategories()
if (this.shallBeRentPayment) {
this.messageService.add('shall be rentpayment')
this.newAccountEntry.account_entry_category = this.accountEntryCategoriesInverseMap.get('Mietzahlung').id
}
} }
ngOnInit(): void { ngOnInit(): void {
@ -137,4 +157,5 @@ export class AccountComponent implements OnInit {
this.init() this.init()
} }
} }

View File

@ -19,6 +19,7 @@ import { FeeListComponent } from './fee-list/fee-list.component';
import { FeeDetailsComponent } from './fee-details/fee-details.component'; import { FeeDetailsComponent } from './fee-details/fee-details.component';
import { EnterPaymentComponent } from './enter-payment/enter-payment.component'; import { EnterPaymentComponent } from './enter-payment/enter-payment.component';
import { HomeComponent } from './home/home.component'; import { HomeComponent } from './home/home.component';
import { LedgerComponent } from './ledger/ledger.component';
const routes: Routes = [ const routes: Routes = [
@ -44,6 +45,7 @@ const routes: Routes = [
{ path: 'fee/:id', component: FeeDetailsComponent, canActivate: [ AuthGuardService ] }, { path: 'fee/:id', component: FeeDetailsComponent, canActivate: [ AuthGuardService ] },
{ path: 'fee', component: FeeDetailsComponent, canActivate: [ AuthGuardService ] }, { path: 'fee', component: FeeDetailsComponent, canActivate: [ AuthGuardService ] },
{ path: 'enterPayment', component: EnterPaymentComponent, canActivate: [ AuthGuardService ] }, { path: 'enterPayment', component: EnterPaymentComponent, canActivate: [ AuthGuardService ] },
{ path: 'ledger', component: LedgerComponent, canActivate: [ AuthGuardService ] },
{ path: 'home', component: HomeComponent }, { path: 'home', component: HomeComponent },
{ path: 'logout', component: LogoutComponent }, { path: 'logout', component: LogoutComponent },
{ path: 'login', component: LoginComponent }, { path: 'login', component: LoginComponent },

View File

@ -47,7 +47,9 @@ import { AccountComponent } from './account/account.component';
import { NoteComponent } from './note/note.component' import { NoteComponent } from './note/note.component'
import { MatMomentDateModule, MAT_MOMENT_DATE_ADAPTER_OPTIONS } from '@angular/material-moment-adapter'; import { MatMomentDateModule, MAT_MOMENT_DATE_ADAPTER_OPTIONS } from '@angular/material-moment-adapter';
import { EnterPaymentComponent } from './enter-payment/enter-payment.component'; import { EnterPaymentComponent } from './enter-payment/enter-payment.component';
import { HomeComponent } from './home/home.component' import { HomeComponent } from './home/home.component';
import { LedgerComponent } from './ledger/ledger.component';
import { ErrorDialogComponent } from './error-dialog/error-dialog.component'
registerLocaleData(localeDe) registerLocaleData(localeDe)
@ -76,7 +78,9 @@ registerLocaleData(localeDe)
AccountComponent, AccountComponent,
NoteComponent, NoteComponent,
EnterPaymentComponent, EnterPaymentComponent,
HomeComponent HomeComponent,
LedgerComponent,
ErrorDialogComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

View File

@ -130,6 +130,11 @@ export class PremiseService {
} }
async getPremisesByAccount(id: number): Promise<Premise[]> {
this.messageService.add(`PremiseService: get data by Account ${id}`);
return this.http.get<Premise[]>(`${serviceBaseUrl}/v1/premises/account/${id}`).toPromise()
}
} }

View File

@ -52,6 +52,7 @@ export interface Premise {
street: string street: string
zip: string zip: string
city: string city: string
account: number
} }
export const NULL_Premise: Premise = { export const NULL_Premise: Premise = {
id: 0 id: 0
@ -59,6 +60,7 @@ export const NULL_Premise: Premise = {
,street: '' ,street: ''
,zip: '' ,zip: ''
,city: '' ,city: ''
,account: undefined
} }
export interface Flat { export interface Flat {
@ -190,6 +192,7 @@ export interface AccountEntry {
account: number account: number
created_at: string created_at: string
amount: number amount: number
document_no: number
account_entry_category: number account_entry_category: number
} }
export const NULL_AccountEntry: AccountEntry = { export const NULL_AccountEntry: AccountEntry = {
@ -198,6 +201,7 @@ export const NULL_AccountEntry: AccountEntry = {
,account: undefined ,account: undefined
,created_at: '' ,created_at: ''
,amount: undefined ,amount: undefined
,document_no: undefined
,account_entry_category: undefined ,account_entry_category: undefined
} }

View File

@ -0,0 +1,6 @@
export interface ErrorDialogData {
module: string,
func: string,
msg: string
}

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { ErrorDialogService } from './error-dialog.service';
describe('ErrorDialogService', () => {
let service: ErrorDialogService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ErrorDialogService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,18 @@
import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ErrorDialogComponent } from './error-dialog/error-dialog.component';
@Injectable({
providedIn: 'root'
})
export class ErrorDialogService {
constructor(public dialog: MatDialog) { }
openDialog(module: string, func: string, msg: string): void {
const dialogRef = this.dialog.open(ErrorDialogComponent, {
width: '450px',
data: { module: module, func: func, msg: msg }
})
}
}

View File

@ -0,0 +1,11 @@
<h1>Fehler</h1>
<h2>Module</h2>
<p>{{data.module}}</p>
<h2>Function</h2>
<p>{{data.func}}</p>
<h2>Message</h2>
<p>{{data.msg}}</p>

View File

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

View File

@ -0,0 +1,24 @@
import { Component, Inject, OnInit } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { ErrorDialogData } from '../error-dialog-data';
@Component({
selector: 'app-error-dialog',
templateUrl: './error-dialog.component.html',
styleUrls: ['./error-dialog.component.css']
})
export class ErrorDialogComponent implements OnInit {
constructor(
public dialogRef: MatDialogRef<ErrorDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: ErrorDialogData
) { }
onNoClick(): void {
this.dialogRef.close()
}
ngOnInit(): void {
}
}

View File

@ -6,8 +6,8 @@ import { MessageService } from './message.service';
import { serviceBaseUrl } from './config'; import { serviceBaseUrl } from './config';
import { Fee, OverheadAdvance } from './data-objects'; import { Account, Fee, OverheadAdvance } from './data-objects';
import { Saldo, Tenant_with_Saldo } from './ext-data-objects'; import { Saldo, Tenant_with_Saldo, UniqueNumber } from './ext-data-objects';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
@ -19,6 +19,11 @@ export class ExtApiService {
return this.http.get<OverheadAdvance[]>(`${serviceBaseUrl}/v1/overhead_advances/flat/${id}`).toPromise() return this.http.get<OverheadAdvance[]>(`${serviceBaseUrl}/v1/overhead_advances/flat/${id}`).toPromise()
} }
async getAccountByDescription(description: string): Promise<Account> {
this.messageService.add(`ExtApiService: get account by description ${description}`);
return this.http.get<Account>(`${serviceBaseUrl}/v1/accounts/bydescription/${description}`).toPromise()
}
async getFeeByTenancies(id: number): Promise<Fee[]> { async getFeeByTenancies(id: number): Promise<Fee[]> {
this.messageService.add(`ExtApiService: get fees by flat ${id}`); this.messageService.add(`ExtApiService: get fees by flat ${id}`);
return this.http.get<Fee[]>(`${serviceBaseUrl}/v1/fees/tenancy/${id}`).toPromise() return this.http.get<Fee[]>(`${serviceBaseUrl}/v1/fees/tenancy/${id}`).toPromise()
@ -33,4 +38,9 @@ export class ExtApiService {
this.messageService.add("ExtApiService: get tenants with saldo"); this.messageService.add("ExtApiService: get tenants with saldo");
return this.http.get<Tenant_with_Saldo[]>(`${serviceBaseUrl}/v1/tenants/saldo`).toPromise() return this.http.get<Tenant_with_Saldo[]>(`${serviceBaseUrl}/v1/tenants/saldo`).toPromise()
} }
async getUniqueNumber(): Promise<UniqueNumber> {
this.messageService.add("ExtApiService: get unique number");
return this.http.get<UniqueNumber>(`${serviceBaseUrl}/v1/uniquenumber`).toPromise()
}
} }

View File

@ -9,4 +9,8 @@ export interface Tenant_with_Saldo {
lastname: string lastname: string
address1: string address1: string
saldo: number saldo: number
}
export interface UniqueNumber {
number: number
} }

View File

@ -0,0 +1,34 @@
<mat-card class="defaultCard">
<mat-card-header>
<mat-card-title>
Buchführung
</mat-card-title>
</mat-card-header>
<mat-card-content>
<mat-accordion>
<mat-expansion-panel (opened)="collapseExpenseDetails = true"
(closed)="collapseExpenseDetails = false">
<mat-expansion-panel-header>
<mat-panel-title>
Ausgaben
</mat-panel-title>
<mat-panel-description>
<div>Betriebskosten-relevante Ausgaben nicht hier sondern im Betriebskostenkonto unter "Meine Häuser" erfassen.</div>
</mat-panel-description>
</mat-expansion-panel-header>
<app-account #expenseAccountComponent [selectedAccountId]="expenseAccountId" [shallBeRentPayment]="false"></app-account>
</mat-expansion-panel>
<mat-expansion-panel (opened)="collapseIncomeDetails = true"
(closed)="collapseIncomeDetails = false">
<mat-expansion-panel-header>
<mat-panel-title>
Einnahmen
</mat-panel-title>
<mat-panel-description>
</mat-panel-description>
</mat-expansion-panel-header>
<app-account #incomeAccountComponent [selectedAccountId]="incomeAccountId" [shallBeRentPayment]="false"></app-account>
</mat-expansion-panel>
</mat-accordion>
</mat-card-content>
</mat-card>

View File

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

View File

@ -0,0 +1,48 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { AccountComponent } from '../account/account.component';
import { Account } from '../data-objects';
import { ExtApiService } from '../ext-data-object-service';
import { MessageService } from '../message.service';
@Component({
selector: 'app-ledger',
templateUrl: './ledger.component.html',
styleUrls: ['./ledger.component.css']
})
export class LedgerComponent implements OnInit {
incomeAccount: Account
incomeAccountId: number
expenseAccount: Account
expenseAccountId: number
collapseIncomeDetails: boolean = false
collapseExpenseDetails: boolean = false
@ViewChild('incomeAccountComponent') incomeAccountComponent: AccountComponent
@ViewChild('expenseAccountComponent') expenseAccountComponent: AccountComponent
constructor(
private extApiService: ExtApiService,
private messageService: MessageService
) { }
async getAccount(): Promise<void> {
try {
this.messageService.add("Trying to load ledger account")
this.incomeAccount = await this.extApiService.getAccountByDescription('LedgerIncome')
this.expenseAccount = await this.extApiService.getAccountByDescription('LedgerExpense')
this.messageService.add("Account loaded")
} catch (err) {
this.messageService.add(JSON.stringify(err, undefined, 4))
}
}
async ngOnInit(): Promise<void> {
await this.getAccount()
this.incomeAccountId = this.incomeAccount.id
this.expenseAccountId = this.expenseAccount.id
}
}

View File

@ -26,6 +26,10 @@
<th mat-header-cell *matHeaderCellDef>Ort</th> <th mat-header-cell *matHeaderCellDef>Ort</th>
<td mat-cell *matCellDef="let element">{{element.city}}</td> <td mat-cell *matCellDef="let element">{{element.city}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="account">
<th mat-header-cell *matHeaderCellDef>Betriebskostenkonto</th>
<td mat-cell *matCellDef="let element">{{element.account}}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" [routerLink]="['/premise/', row.id]"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;" [routerLink]="['/premise/', row.id]"></tr>
</table> </table>

View File

@ -13,7 +13,8 @@ export class MyPremisesComponent implements OnInit {
premises: Premise[] premises: Premise[]
dataSource: MatTableDataSource<Premise> dataSource: MatTableDataSource<Premise>
displayedColumns: string[] = [ "description", "street", "zip", "city" ] displayedColumns: string[] = [ "description", "street", "zip", "city", "account" ]
constructor(private premiseService: PremiseService, private messageService: MessageService) { } constructor(private premiseService: PremiseService, private messageService: MessageService) { }

View File

@ -20,6 +20,8 @@
<a mat-list-item href="/fees">Mietsätze</a> <a mat-list-item href="/fees">Mietsätze</a>
</mat-nav-list><mat-divider *ngIf="authenticated"></mat-divider><mat-nav-list *ngIf="authenticated"> </mat-nav-list><mat-divider *ngIf="authenticated"></mat-divider><mat-nav-list *ngIf="authenticated">
<a mat-list-item href="/premises">Meine Häuser</a> <a mat-list-item href="/premises">Meine Häuser</a>
</mat-nav-list><mat-divider *ngIf="authenticated"></mat-divider><mat-nav-list *ngIf="authenticated">
<a mat-list-item href="/ledger">Buchführung</a>
</mat-nav-list><mat-divider *ngIf="authenticated"></mat-divider><mat-nav-list *ngIf="authenticated"> </mat-nav-list><mat-divider *ngIf="authenticated"></mat-divider><mat-nav-list *ngIf="authenticated">
<a mat-list-item href="/logout">Abmelden</a> <a mat-list-item href="/logout">Abmelden</a>
</mat-nav-list> </mat-nav-list>

View File

@ -9,31 +9,59 @@
</mat-card-subtitle> </mat-card-subtitle>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<div> <mat-accordion>
<form (ngSubmit)="savePremise()"> <mat-expansion-panel (opened)="collapseDetails = true"
<div> (closed)="collapseDetails = false"
<mat-form-field appearance="outline"> [expanded]="premise.id == 0">
<mat-label>Beschreibung</mat-label> <mat-expansion-panel-header>
<input matInput name="description" [(ngModel)]="premise.description"/> <mat-panel-title>
</mat-form-field> Details
</div><div> </mat-panel-title>
<mat-form-field appearance="outline"> <mat-panel-description>
<mat-label>Strasse</mat-label> </mat-panel-description>
<input matInput name="street" [(ngModel)]="premise.street"/> </mat-expansion-panel-header>
</mat-form-field> <form (ngSubmit)="savePremise()">
</div><div> <div>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>PLZ</mat-label> <mat-label>Beschreibung</mat-label>
<input matInput name="zip" [(ngModel)]="premise.zip"/> <input matInput name="description" [(ngModel)]="premise.description"/>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> </div><div>
<mat-label>Ort</mat-label> <mat-form-field appearance="outline">
<input matInput name="city" [(ngModel)]="premise.city"/> <mat-label>Strasse</mat-label>
</mat-form-field> <input matInput name="street" [(ngModel)]="premise.street"/>
</div> </mat-form-field>
<button #submitButton type="submit" mat-raised-button color="primary">Speichern</button> </div><div>
</form> <mat-form-field appearance="outline">
</div> <mat-label>PLZ</mat-label>
<input matInput name="zip" [(ngModel)]="premise.zip"/>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Ort</mat-label>
<input matInput name="city" [(ngModel)]="premise.city"/>
</mat-form-field>
</div><div>
<mat-form-field appearance="outline" *ngIf="premise.account">
<mat-label>Betriebskostenkonto</mat-label>
<input matInput name="account" [readonly]="true" [(ngModel)]="premise.account"/>
</mat-form-field>
</div>
<button #submitButton type="submit" mat-raised-button color="primary">Speichern</button>
</form>
</mat-expansion-panel>
<mat-expansion-panel (opened)="collapseOverheadAccount = true"
(closed)="collapseOverheadAccount = false">
<mat-expansion-panel-header>
<mat-panel-title>
Betriebskostenkonto
</mat-panel-title>
<mat-panel-description>
</mat-panel-description>
</mat-expansion-panel-header>
<app-account #incomeAccountComponent [selectedAccountId]="overheadAccountId" [shallBeRentPayment]="false"></app-account>
</mat-expansion-panel>
</mat-accordion>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</section> </section>

View File

@ -1,8 +1,9 @@
import { Component, OnInit, ViewChild } from '@angular/core'; import { Component, OnInit, ViewChild } from '@angular/core';
import { MatButton } from '@angular/material/button'; import { MatButton } from '@angular/material/button';
import { MatExpansionPanel } from '@angular/material/expansion';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { PremiseService } from '../data-object-service'; import { AccountService, PremiseService } from '../data-object-service';
import { Premise } from '../data-objects'; import { Account, NULL_Premise, Premise } from '../data-objects';
import { MessageService } from '../message.service'; import { MessageService } from '../message.service';
@Component({ @Component({
@ -12,18 +13,19 @@ import { MessageService } from '../message.service';
}) })
export class PremiseDetailsComponent implements OnInit { export class PremiseDetailsComponent implements OnInit {
collapseDetails: boolean = false
collapseOverheadAccount: boolean = false
@ViewChild('submitButton') submitButton: MatButton @ViewChild('submitButton') submitButton: MatButton
premise: Premise = { premise: Premise = NULL_Premise
id: 0,
description: '', overheadAccount: Account
street: '', overheadAccountId: number
zip: '',
city: ''
}
constructor( constructor(
private premiseService: PremiseService, private premiseService: PremiseService,
private accountService: AccountService,
private messageService: MessageService, private messageService: MessageService,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router private router: Router
@ -34,6 +36,8 @@ export class PremiseDetailsComponent implements OnInit {
const id = +this.route.snapshot.paramMap.get('id') const id = +this.route.snapshot.paramMap.get('id')
if (id != 0) { if (id != 0) {
this.premise = await this.premiseService.getPremise(id) this.premise = await this.premiseService.getPremise(id)
this.overheadAccount = await this.accountService.getAccount(this.premise.account)
this.overheadAccountId = this.overheadAccount.id
} }
} catch (err) { } catch (err) {
this.messageService.add(JSON.stringify(err, undefined, 4)) this.messageService.add(JSON.stringify(err, undefined, 4))
@ -47,6 +51,12 @@ export class PremiseDetailsComponent implements OnInit {
this.messageService.add(JSON.stringify(this.premise, undefined, 4)) this.messageService.add(JSON.stringify(this.premise, undefined, 4))
if (this.premise.id == 0) { if (this.premise.id == 0) {
this.messageService.add("about to insert new premise") this.messageService.add("about to insert new premise")
this.overheadAccount = {
"id": 0,
"description": `overhead_account_${this.premise.description}`
}
this.overheadAccount = await this.accountService.postAccount(this.overheadAccount)
this.premise.account = this.overheadAccount.id
this.premise = await this.premiseService.postPremise(this.premise) this.premise = await this.premiseService.postPremise(this.premise)
this.messageService.add(`Successfully added premises with id ${this.premise.id}`) this.messageService.add(`Successfully added premises with id ${this.premise.id}`)
} else { } else {

View File

@ -8,7 +8,8 @@
<mat-card-content> <mat-card-content>
<mat-accordion> <mat-accordion>
<mat-expansion-panel (opened)="collapseTenantDetails = true" <mat-expansion-panel (opened)="collapseTenantDetails = true"
(closed)="collapseTenantDetails = false"> (closed)="collapseTenantDetails = false"
[expanded]="tenant.id == 0">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title *ngIf="!collapseTenantDetails"> <mat-panel-title *ngIf="!collapseTenantDetails">
Details Details
@ -68,18 +69,18 @@
<input matInput name="iban" [(ngModel)]="tenant.iban"/> <input matInput name="iban" [(ngModel)]="tenant.iban"/>
</mat-form-field> </mat-form-field>
</div> </div>
<!--
<div> <div>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline" *ngIf="tenant.account">
<mat-label>Account ID</mat-label> <mat-label>Account ID</mat-label>
<input matInput name="account_id" [readonly]="true" [ngModel]="account.id"/> <input matInput name="account_id" [readonly]="true" [ngModel]="tenant.account"/>
</mat-form-field> </mat-form-field>
<!--
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Account Description</mat-label> <mat-label>Account Description</mat-label>
<input matInput name="account_desc" [readonly]="true" [ngModel]="account.description"/> <input matInput name="account_desc" [readonly]="true" [ngModel]="account.description"/>
</mat-form-field> </mat-form-field>
</div> -->
--> </div>
<button #submitButton type="submit" mat-raised-button color="primary">Speichern</button> <button #submitButton type="submit" mat-raised-button color="primary">Speichern</button>
</form> </form>
</div> </div>

View File

@ -90,11 +90,18 @@ export class TenantDetailsComponent implements OnInit {
async getTenant(): Promise<void> { async getTenant(): Promise<void> {
try { try {
const id = +this.route.snapshot.paramMap.get('id') const id = +this.route.snapshot.paramMap.get('id')
this.messageService.add(`getTenant, id=${id}`)
if (id != 0) { if (id != 0) {
this.messageService.add("getTenant, not-0-branch")
this.tenantId = id this.tenantId = id
this.tenant = await this.tenantService.getTenant(id) this.tenant = await this.tenantService.getTenant(id)
this.account = await this.accountService.getAccount(this.tenant.account) this.account = await this.accountService.getAccount(this.tenant.account)
this.getTenancies() this.getTenancies()
} else {
this.messageService.add("getTenant, 0-branch")
this.tenant = NULL_Tenant
this.account = NULL_Account
this.tenancies = []
} }
} catch (err) { } catch (err) {
this.messageService.add(JSON.stringify(err, undefined, 4)) this.messageService.add(JSON.stringify(err, undefined, 4))