27 Commits

Author SHA1 Message Date
cb632e9e8e due at fix 2 2022-01-06 21:52:17 +01:00
2fc7922707 due_at fix 2022-01-06 21:06:16 +01:00
422a8d37ab get flat tenants 2021-12-19 14:44:27 +01:00
006b488c63 start overhead accounts 2021-12-19 14:17:44 +01:00
1d36d99462 option to select config file 2021-12-19 12:51:35 +01:00
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
97dfcbe2fb optimize tenant with saldo query, fix 2021-09-14 14:31:42 +02:00
40 changed files with 828 additions and 108 deletions

4
.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
*~
.*~
.vscode/

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

@ -36,11 +36,28 @@ def get_account_saldo(user, token_info, accountId=None):
def get_tenant_with_saldo(user, token_info): def get_tenant_with_saldo(user, token_info):
return dbGetMany(user, token_info, { return dbGetMany(user, token_info, {
"statement": """ "statement": """
SELECT 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.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

@ -269,6 +269,9 @@ SELECT
,account ,account
FROM tenant_t FROM tenant_t
WHERE account = %s WHERE account = %s
ORDER BY
lastname
,firstname
""", """,
"params": (accountId, ) "params": (accountId, )
} }
@ -283,6 +286,7 @@ SELECT
,street ,street
,zip ,zip
,city ,city
,account
FROM premise_t FROM premise_t
ORDER BY ORDER BY
description description
@ -298,6 +302,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 +311,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 +326,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 +343,7 @@ SELECT
,street ,street
,zip ,zip
,city ,city
,account
FROM premise_t FROM premise_t
WHERE id = %s WHERE id = %s
""", """,
@ -373,6 +382,25 @@ 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
ORDER BY
description
""",
"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": """
@ -484,6 +512,9 @@ SELECT
,flat_no ,flat_no
FROM flat_t FROM flat_t
WHERE premise = %s WHERE premise = %s
ORDER BY
premise
,description
""", """,
"params": (premiseId, ) "params": (premiseId, )
} }
@ -651,6 +682,9 @@ SELECT
,flat ,flat
FROM overhead_advance_flat_mapping_t FROM overhead_advance_flat_mapping_t
WHERE overhead_advance = %s WHERE overhead_advance = %s
ORDER BY
overhead_advance
,flat
""", """,
"params": (overhead_advanceId, ) "params": (overhead_advanceId, )
} }
@ -665,6 +699,9 @@ SELECT
,flat ,flat
FROM overhead_advance_flat_mapping_t FROM overhead_advance_flat_mapping_t
WHERE flat = %s WHERE flat = %s
ORDER BY
overhead_advance
,flat
""", """,
"params": (flatId, ) "params": (flatId, )
} }
@ -761,6 +798,9 @@ SELECT
,premise ,premise
FROM parking_t FROM parking_t
WHERE premise = %s WHERE premise = %s
ORDER BY
premise
,description
""", """,
"params": (premiseId, ) "params": (premiseId, )
} }
@ -857,6 +897,9 @@ SELECT
,premise ,premise
FROM commercial_premise_t FROM commercial_premise_t
WHERE premise = %s WHERE premise = %s
ORDER BY
premise
,description
""", """,
"params": (premiseId, ) "params": (premiseId, )
} }
@ -988,6 +1031,9 @@ SELECT
,enddate ,enddate
FROM tenancy_t FROM tenancy_t
WHERE tenant = %s WHERE tenant = %s
ORDER BY
description
,startdate
""", """,
"params": (tenantId, ) "params": (tenantId, )
} }
@ -1007,6 +1053,9 @@ SELECT
,enddate ,enddate
FROM tenancy_t FROM tenancy_t
WHERE flat = %s WHERE flat = %s
ORDER BY
description
,startdate
""", """,
"params": (flatId, ) "params": (flatId, )
} }
@ -1026,6 +1075,9 @@ SELECT
,enddate ,enddate
FROM tenancy_t FROM tenancy_t
WHERE parking = %s WHERE parking = %s
ORDER BY
description
,startdate
""", """,
"params": (parkingId, ) "params": (parkingId, )
} }
@ -1045,6 +1097,9 @@ SELECT
,enddate ,enddate
FROM tenancy_t FROM tenancy_t
WHERE commercial_premise = %s WHERE commercial_premise = %s
ORDER BY
description
,startdate
""", """,
"params": (commercial_premiseId, ) "params": (commercial_premiseId, )
} }
@ -1300,11 +1355,13 @@ SELECT
,description ,description
,account ,account
,created_at ,created_at
,due_at
,amount ,amount
,document_no
,account_entry_category ,account_entry_category
FROM account_entry_t FROM account_entry_t
ORDER BY ORDER BY
amount created_at
""", """,
"params": () "params": ()
} }
@ -1316,7 +1373,9 @@ def insert_account_entry(user, token_info, **args):
v_description = body["description"] v_description = body["description"]
v_account = body["account"] v_account = body["account"]
v_created_at = body["created_at"] v_created_at = body["created_at"]
v_due_at = body["due_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": """
@ -1325,7 +1384,9 @@ INSERT INTO account_entry_t
description description
,account ,account
,created_at ,created_at
,due_at
,amount ,amount
,document_no
,account_entry_category ,account_entry_category
) VALUES ( ) VALUES (
%s %s
@ -1333,6 +1394,8 @@ INSERT INTO account_entry_t
,%s ,%s
,%s ,%s
,%s ,%s
,%s
,%s
) )
RETURNING * RETURNING *
""", """,
@ -1340,7 +1403,9 @@ INSERT INTO account_entry_t
v_description v_description
,v_account ,v_account
,v_created_at ,v_created_at
,v_due_at
,v_amount ,v_amount
,v_document_no
,v_account_entry_category ,v_account_entry_category
] ]
}) })
@ -1357,7 +1422,9 @@ SELECT
,description ,description
,account ,account
,created_at ,created_at
,due_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
@ -1376,10 +1443,14 @@ SELECT
,description ,description
,account ,account
,created_at ,created_at
,due_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
ORDER BY
created_at
""", """,
"params": (accountId, ) "params": (accountId, )
} }
@ -1393,10 +1464,14 @@ SELECT
,description ,description
,account ,account
,created_at ,created_at
,due_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
ORDER BY
created_at
""", """,
"params": (account_entry_categoryId, ) "params": (account_entry_categoryId, )
} }
@ -1411,6 +1486,8 @@ SELECT
,tenant ,tenant
,note ,note
FROM note_t FROM note_t
ORDER BY
created_at
""", """,
"params": () "params": ()
} }
@ -1474,6 +1551,8 @@ SELECT
,note ,note
FROM note_t FROM note_t
WHERE tenant = %s WHERE tenant = %s
ORDER BY
created_at
""", """,
"params": (tenantId, ) "params": (tenantId, )
} }

View File

@ -129,6 +129,14 @@ SELECT
#end for #end for
FROM ${table.name}_t FROM ${table.name}_t
WHERE ${column.name} = %s WHERE ${column.name} = %s
#if $table.selectors
ORDER BY
#set $sep = ""
#for $selector in $table.selectors
$sep$selector
#set $sep = ","
#end for
#end if
""", """,
"params": (${column.name}Id, ) "params": (${column.name}Id, )
} }

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
@ -1727,8 +1790,14 @@ components:
type: integer type: integer
created_at: created_at:
type: string type: string
due_at:
type: string
nullable: true
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,15 @@
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']
dueAt = createdAt.replace(day=1)
except KeyError:
createdAt = datetime.datetime.today().strftime("%Y-%m-%d")
dueAt = createdAt.replace(day=1)
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:

102
cli/OverheadAccounts.py Normal file
View File

@ -0,0 +1,102 @@
from db import dbGetMany
import datetime
from loguru import logger
def perform(dbh, params):
try:
year = params['year']
except KeyError:
year = datetime.datetime.today().year
startDate = datetime.datetime(year, 1, 1, 0, 0, 0)
endDate = datetime.datetime(year, 12, 31, 23, 59, 59)
premises = (1, 2)
# get flat tenants by object and timespan with paid overhead and due overhead
tenants = dbGetMany(
dbh,
{
"statement":
"""
select t.id as tenant_id,
t.firstname as tenant_firstname,
t.lastname as tenant_lastname,
f.id as flat_id,
f.description as flat,
p.id as house_id,
p.description as house,
ty.startdate as startdate,
ty.enddate as enddate
from tenant_t t,
premise_t p,
flat_t f,
tenancy_t ty
where ty.tenant = t.id and
ty.flat = f.id and
ty.startdate >= %(startDate)s and
(ty.enddate <= %(endDate)s or ty.enddate is null) and
f.premise = p.id and
p.id in %(premises)s
order by house_id, tenant_id
""",
"params": {
"startDate": startDate,
"endDate": endDate,
"premises": premises
}
}
)
logger.info(f"{tenants=}")
# get overhead sums by object, category and timespan
overheadSums = dbGetMany(
dbh,
{
"statement":
"""
select sum(ae.amount) as sum,
aec.description as category,
p.id as house_id,
p.description as house
from account_t a,
premise_t p,
account_entry_t ae,
account_entry_category_t aec
where p.account = a.id and
ae.account = a.id and
aec.overhead_relevant = 't' and
ae.account_entry_category = aec.id and
created_at between %(startDate)s and %(endDate)s and
p.id in %(premises)s
group by house_id, house, category
union
select 0 as sum,
aec.description as category,
p.id as house_id,
p.description as house
from account_t a,
premise_t p,
account_entry_t ae,
account_entry_category_t aec
where p.account = a.id and
ae.account = a.id and
aec.overhead_relevant = 't' and
aec.id not in (select distinct account_entry_category from account_entry_t) and
created_at between %(startDate)s and %(endDate)s and
p.id in %(premises)s
group by house_id, house, category
order by house_id, category
""",
"params": {
"startDate": startDate,
"endDate": endDate,
"premises": premises
}
}
)
logger.info(f"{overheadSums=}")

View File

@ -12,7 +12,7 @@ def execDatabaseOperation(dbh, func, params):
cur = None cur = None
try: try:
with dbh.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur: with dbh.cursor(cursor_factory = psycopg2.extras.RealDictCursor) as cur:
params["params"] = [ v if not v=='' else None for v in params["params"] ] # params["params"] = [ v if not v=='' else None for v in params["params"] ]
logger.debug("edo: {}".format(str(params))) logger.debug("edo: {}".format(str(params)))
return func(cur, params) return func(cur, params)
except psycopg2.Error as err: except psycopg2.Error as err:
@ -21,6 +21,7 @@ def execDatabaseOperation(dbh, func, params):
def _opGetMany(cursor, params): def _opGetMany(cursor, params):
logger.warning(f"{params=}")
items = [] items = []
cursor.execute(params["statement"], params["params"]) cursor.execute(params["statement"], params["params"])
for itemObj in cursor: for itemObj in cursor:

38
cli/fixDueDate.py Normal file
View File

@ -0,0 +1,38 @@
from loguru import logger
from db import dbGetMany, dbGetOne
def perform(dbh, params):
accountEntries = dbGetMany(
dbh,
{
"statement":
"""
select id, due_at from account_entry_t where due_at is not null
""",
"params": {}
}
)
for accountEntry in accountEntries:
id = accountEntry['id']
oldDueAt = accountEntry['due_at']
newDueAt = oldDueAt.replace(day=1)
logger.info(f"id: {id}, due_at: {oldDueAt} -> {newDueAt}")
fixedEntry = dbGetOne(
dbh,
{
"statement":
"""
UPDATE account_entry_t
SET due_at = %(dueAt)s
WHERE id = %(id)s
RETURNING *
""",
"params": {
"id": id,
"dueAt": newDueAt
}
}
)
logger.info("fixed")

View File

@ -8,6 +8,36 @@ import argparse
import importlib import importlib
import sys import sys
parser = argparse.ArgumentParser(description="hv2cli.py")
parser.add_argument('--config', '-c',
help="Config file, default is ./config/dbconfig.ini",
required=False,
default="./config/dbconfig.ini")
parser.add_argument('--operation', '-o',
help='Operation to perform.',
required=True)
parser.add_argument('--params', '-p',
help='JSON string with parameter for the selected operation, default: {}',
required=False,
default="{}")
parser.add_argument('--verbosity', '-v',
help='Minimal log level for output: DEBUG, INFO, WARNING, ..., default: DEBUG',
required=False,
default="DEBUG")
parser.add_argument('--nocolorize', '-n',
help='disable colored output (for cron)',
required=False,
action='store_true',
default=False)
args = parser.parse_args()
operation = args.operation
params = json.loads(args.params)
logLevel = args.verbosity
noColorize = args.nocolorize
DB_USER = "" DB_USER = ""
DB_PASS = "" DB_PASS = ""
DB_HOST = "" DB_HOST = ""
@ -19,33 +49,15 @@ try:
DB_NAME = os.environ["DB_NAME"] DB_NAME = os.environ["DB_NAME"]
except KeyError: except KeyError:
config = configparser.ConfigParser() config = configparser.ConfigParser()
config.read('./config/dbconfig.ini') config.read(args.config)
DB_USER = config["database"]["user"] DB_USER = config["database"]["user"]
DB_PASS = config["database"]["pass"] DB_PASS = config["database"]["pass"]
DB_HOST = config["database"]["host"] DB_HOST = config["database"]["host"]
DB_NAME = config["database"]["name"] DB_NAME = config["database"]["name"]
parser = argparse.ArgumentParser(description="hv2cli.py")
parser.add_argument('--operation', '-o',
help='Operation to perform.',
required=True)
parser.add_argument('--params', '-p',
help='JSON string with parameter for the selected operation, default: {}',
required=False,
default="{}")
parser.add_argument('--verbosity', '-v',
help='Minimal log level for output: DEBUG, INFO, WARNING, ..., default: DEBUG',
required=False,
default="DEBUG")
args = parser.parse_args()
operation = args.operation
params = json.loads(args.params)
logLevel = args.verbosity
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 }
] ]
}, },
{ {
@ -135,8 +136,10 @@
"columns": [ "columns": [
{ "name": "description", "sqltype": "varchar(1024)", "notnull": true }, { "name": "description", "sqltype": "varchar(1024)", "notnull": true },
{ "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()", "selector": 0 },
{ "name": "amount", "sqltype": "numeric(10,2)", "notnull": true, "selector": 0 }, { "name": "due_at", "sqltype": "timestamp", "notnull": false },
{ "name": "amount", "sqltype": "numeric(10,2)", "notnull": true },
{ "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": [
@ -147,7 +150,7 @@
"name": "note", "name": "note",
"immutable": true, "immutable": true,
"columns": [ "columns": [
{ "name": "created_at", "sqltype": "timestamp", "notnull": true, "default": "now()" }, { "name": "created_at", "sqltype": "timestamp", "notnull": true, "default": "now()", "selector": 0 },
{ "name": "tenant", "sqltype": "integer", "notnull": true, "foreignkey": true }, { "name": "tenant", "sqltype": "integer", "notnull": true, "foreignkey": true },
{ "name": "note", "sqltype": "varchar(4096)", "notnull": true } { "name": "note", "sqltype": "varchar(4096)", "notnull": true }
] ]

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,12 @@ 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()
,due_at timestamp
,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,30 @@
<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" id="addEntryfield">
<mat-label>Fälligkeit</mat-label>
<input matInput ngModel name="dueAt" [matDatepicker]="dueAtPicker" [formControl]="presetDueAt"/>
<mat-datepicker-toggle matSuffix [for]="dueAtPicker"></mat-datepicker-toggle>
<mat-datepicker #dueAtPicker></mat-datepicker>
</mat-form-field>
<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>
@ -32,10 +38,18 @@ Saldo: {{saldo?.saldo | number:'1.2-2'}} €
<th mat-header-cell *matHeaderCellDef>Datum</th> <th mat-header-cell *matHeaderCellDef>Datum</th>
<td mat-cell *matCellDef="let element">{{element.rawAccountEntry.created_at | date}}</td> <td mat-cell *matCellDef="let element">{{element.rawAccountEntry.created_at | date}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="dueAt">
<th mat-header-cell *matHeaderCellDef>Fälligkeit</th>
<td mat-cell *matCellDef="let element">{{element.rawAccountEntry.due_at | date}}</td>
</ng-container>
<ng-container matColumnDef="description"> <ng-container matColumnDef="description">
<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

@ -1,12 +1,14 @@
import { ViewFlags } from '@angular/compiler/src/core'; import { ViewFlags } from '@angular/compiler/src/core';
import { Component, Input, OnInit, OnChanges, ViewChild } from '@angular/core'; import { Component, Input, OnInit, OnChanges, ViewChild } from '@angular/core';
import { FormControl } from '@angular/forms';
import { MatButton } from '@angular/material/button'; import { MatButton } from '@angular/material/button';
import { MatExpansionPanel } from '@angular/material/expansion'; 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,22 +34,22 @@ 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", "dueAt", "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 presetDueAt: FormControl
constructor( constructor(
private accountService: AccountService, private accountService: AccountService,
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 +89,44 @@ 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)}`) due_at: formData.value.dueAt,
} finally { amount: formData.value.amount,
this.addAccountEntryButton.disabled = false id: 0,
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> {
@ -120,13 +146,17 @@ export class AccountComponent implements OnInit {
} }
private async init(): Promise<void> { private async init(): Promise<void> {
let currentDate = new Date()
let y = currentDate.getFullYear().toString()
let m = (currentDate.getMonth()+1).toString()
if (m.length == 1) {
m = `0${m}`
}
this.presetDueAt = new FormControl(`${y}-${m}-01`)
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 +167,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 {
@ -189,7 +191,9 @@ export interface AccountEntry {
description: string description: string
account: number account: number
created_at: string created_at: string
due_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 = {
@ -197,7 +201,9 @@ export const NULL_AccountEntry: AccountEntry = {
,description: '' ,description: ''
,account: undefined ,account: undefined
,created_at: '' ,created_at: ''
,due_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))