38 Commits

Author SHA1 Message Date
af7b388118 fix 2022-01-29 19:37:09 +01:00
b561ec8a48 small fix 2022-01-29 19:18:50 +01:00
9cb599e2a6 ready so far 2022-01-29 18:35:58 +01:00
d7c713404d overview tmpl 2022-01-29 15:43:50 +01:00
7d8755aab9 overview calculated 2022-01-29 14:40:14 +01:00
f2f2100b8c tenant letter nearly done 2022-01-29 13:42:58 +01:00
b3a49b0fb6 minus_area in premise, calculation of areas and factors 2022-01-24 18:06:10 +01:00
e1ebfe254a consider year in creating fee and overhead requests 2022-01-24 16:36:28 +01:00
b748ba7a77 fiscal year 2022-01-24 16:11:02 +01:00
ab365c65a1 add fiscal_year, drop due_to 2022-01-21 20:46:28 +01:00
0e4fd12238 commercial unit area and overhead stuff 2022-01-06 23:16:34 +01:00
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
50 changed files with 1235 additions and 264 deletions

8
.gitignore vendored
View File

@ -3,4 +3,10 @@ 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/
cli/*.tex
cli/*.log
cli/*.pdf
cli/*.aux

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,8 @@ SELECT
,street ,street
,zip ,zip
,city ,city
,minus_area
,account
FROM premise_t FROM premise_t
ORDER BY ORDER BY
description description
@ -298,6 +303,8 @@ 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_minus_area = body["minus_area"]
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 +313,15 @@ INSERT INTO premise_t
,street ,street
,zip ,zip
,city ,city
,minus_area
,account
) VALUES ( ) VALUES (
%s %s
,%s ,%s
,%s ,%s
,%s ,%s
,%s
,%s
) )
RETURNING * RETURNING *
""", """,
@ -319,6 +330,8 @@ INSERT INTO premise_t
,v_street ,v_street
,v_zip ,v_zip
,v_city ,v_city
,v_minus_area
,v_account
] ]
}) })
except KeyError as e: except KeyError as e:
@ -335,6 +348,8 @@ SELECT
,street ,street
,zip ,zip
,city ,city
,minus_area
,account
FROM premise_t FROM premise_t
WHERE id = %s WHERE id = %s
""", """,
@ -349,6 +364,7 @@ def update_premise(user, token_info, premiseId=None, **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_minus_area = body["minus_area"]
return dbUpdate(user, token_info, { return dbUpdate(user, token_info, {
"statement": """ "statement": """
UPDATE premise_t UPDATE premise_t
@ -357,6 +373,7 @@ UPDATE premise_t
,street = %s ,street = %s
,zip = %s ,zip = %s
,city = %s ,city = %s
,minus_area = %s
WHERE id = %s WHERE id = %s
RETURNING * RETURNING *
""", """,
@ -365,6 +382,7 @@ UPDATE premise_t
v_street, v_street,
v_zip, v_zip,
v_city, v_city,
v_minus_area,
premiseId premiseId
] ]
}) })
@ -373,6 +391,26 @@ 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
,minus_area
,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 +522,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 +692,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 +709,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 +808,9 @@ SELECT
,premise ,premise
FROM parking_t FROM parking_t
WHERE premise = %s WHERE premise = %s
ORDER BY
premise
,description
""", """,
"params": (premiseId, ) "params": (premiseId, )
} }
@ -773,6 +823,7 @@ SELECT
id id
,description ,description
,premise ,premise
,area
FROM commercial_premise_t FROM commercial_premise_t
ORDER BY ORDER BY
premise premise
@ -787,21 +838,25 @@ def insert_commercial_premise(user, token_info, **args):
body = args["body"] body = args["body"]
v_description = body["description"] v_description = body["description"]
v_premise = body["premise"] v_premise = body["premise"]
v_area = body["area"]
return dbInsert(user, token_info, { return dbInsert(user, token_info, {
"statement": """ "statement": """
INSERT INTO commercial_premise_t INSERT INTO commercial_premise_t
( (
description description
,premise ,premise
,area
) VALUES ( ) VALUES (
%s %s
,%s ,%s
,%s
) )
RETURNING * RETURNING *
""", """,
"params": [ "params": [
v_description v_description
,v_premise ,v_premise
,v_area
] ]
}) })
except KeyError as e: except KeyError as e:
@ -816,6 +871,7 @@ SELECT
id id
,description ,description
,premise ,premise
,area
FROM commercial_premise_t FROM commercial_premise_t
WHERE id = %s WHERE id = %s
""", """,
@ -828,18 +884,21 @@ def update_commercial_premise(user, token_info, commercial_premiseId=None, **arg
body = args["body"] body = args["body"]
v_description = body["description"] v_description = body["description"]
v_premise = body["premise"] v_premise = body["premise"]
v_area = body["area"]
return dbUpdate(user, token_info, { return dbUpdate(user, token_info, {
"statement": """ "statement": """
UPDATE commercial_premise_t UPDATE commercial_premise_t
SET SET
description = %s description = %s
,premise = %s ,premise = %s
,area = %s
WHERE id = %s WHERE id = %s
RETURNING * RETURNING *
""", """,
"params": [ "params": [
v_description, v_description,
v_premise, v_premise,
v_area,
commercial_premiseId commercial_premiseId
] ]
}) })
@ -855,8 +914,12 @@ SELECT
id id
,description ,description
,premise ,premise
,area
FROM commercial_premise_t FROM commercial_premise_t
WHERE premise = %s WHERE premise = %s
ORDER BY
premise
,description
""", """,
"params": (premiseId, ) "params": (premiseId, )
} }
@ -988,6 +1051,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 +1073,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 +1095,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 +1117,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, )
} }
@ -1240,6 +1315,7 @@ def get_account_entry_categorys(user, token_info):
SELECT SELECT
id id
,description ,description
,considerMinusArea
,overhead_relevant ,overhead_relevant
FROM account_entry_category_t FROM account_entry_category_t
ORDER BY ORDER BY
@ -1253,21 +1329,25 @@ def insert_account_entry_category(user, token_info, **args):
try: try:
body = args["body"] body = args["body"]
v_description = body["description"] v_description = body["description"]
v_considerMinusArea = body["considerMinusArea"]
v_overhead_relevant = body["overhead_relevant"] v_overhead_relevant = body["overhead_relevant"]
return dbInsert(user, token_info, { return dbInsert(user, token_info, {
"statement": """ "statement": """
INSERT INTO account_entry_category_t INSERT INTO account_entry_category_t
( (
description description
,considerMinusArea
,overhead_relevant ,overhead_relevant
) VALUES ( ) VALUES (
%s %s
,%s ,%s
,%s
) )
RETURNING * RETURNING *
""", """,
"params": [ "params": [
v_description v_description
,v_considerMinusArea
,v_overhead_relevant ,v_overhead_relevant
] ]
}) })
@ -1282,6 +1362,7 @@ def get_account_entry_category(user, token_info, account_entry_categoryId=None):
SELECT SELECT
id id
,description ,description
,considerMinusArea
,overhead_relevant ,overhead_relevant
FROM account_entry_category_t FROM account_entry_category_t
WHERE id = %s WHERE id = %s
@ -1300,11 +1381,13 @@ SELECT
,description ,description
,account ,account
,created_at ,created_at
,fiscal_year
,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 +1399,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_fiscal_year = body["fiscal_year"]
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 +1410,9 @@ INSERT INTO account_entry_t
description description
,account ,account
,created_at ,created_at
,fiscal_year
,amount ,amount
,document_no
,account_entry_category ,account_entry_category
) VALUES ( ) VALUES (
%s %s
@ -1333,6 +1420,8 @@ INSERT INTO account_entry_t
,%s ,%s
,%s ,%s
,%s ,%s
,%s
,%s
) )
RETURNING * RETURNING *
""", """,
@ -1340,7 +1429,9 @@ INSERT INTO account_entry_t
v_description v_description
,v_account ,v_account
,v_created_at ,v_created_at
,v_fiscal_year
,v_amount ,v_amount
,v_document_no
,v_account_entry_category ,v_account_entry_category
] ]
}) })
@ -1357,7 +1448,9 @@ SELECT
,description ,description
,account ,account
,created_at ,created_at
,fiscal_year
,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 +1469,14 @@ SELECT
,description ,description
,account ,account
,created_at ,created_at
,fiscal_year
,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 +1490,14 @@ SELECT
,description ,description
,account ,account
,created_at ,created_at
,fiscal_year
,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 +1512,8 @@ SELECT
,tenant ,tenant
,note ,note
FROM note_t FROM note_t
ORDER BY
created_at
""", """,
"params": () "params": ()
} }
@ -1474,6 +1577,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,10 @@ components:
type: string type: string
city: city:
type: string type: string
minus_area:
type: number
account:
type: integer
flat: flat:
description: flat description: flat
type: object type: object
@ -1651,6 +1716,9 @@ components:
premise: premise:
type: integer type: integer
nullable: true nullable: true
area:
type: number
nullable: true
tenancy: tenancy:
description: tenancy description: tenancy
type: object type: object
@ -1713,6 +1781,8 @@ components:
type: integer type: integer
description: description:
type: string type: string
considerMinusArea:
type: boolean
overhead_relevant: overhead_relevant:
type: boolean type: boolean
account_entry: account_entry:
@ -1727,8 +1797,13 @@ components:
type: integer type: integer
created_at: created_at:
type: string type: string
fiscal_year:
type: integer
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

@ -5,6 +5,6 @@ from flask_cors import CORS
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
app = connexion.App('hv2-api') app = connexion.App('hv2-api')
app.add_api('./openapi.yaml') app.add_api('./openapi.yaml', options = {"swagger_ui": True})
CORS(app.app) CORS(app.app)
app.run(port=8080) app.run(port=8080)

View File

@ -1,61 +0,0 @@
stages:
- check
- build
- deploy
variables:
IMAGE_NAME: $CI_REGISTRY/$CI_PROJECT_PATH
check:
image: registry.hottis.de/dockerized/base-build-env:latest
stage: check
tags:
- hottis
- linux
- docker
rules:
- if: $CI_COMMIT_TAG
script:
- checksemver.py -v
--versionToValidate "${CI_COMMIT_TAG}"
--validateMessage
--messageToValidate "${CI_COMMIT_MESSAGE}"
build:
image: registry.hottis.de/dockerized/docker-bash:latest
stage: build
tags:
- hottis
- linux
- docker
script:
- docker build --tag $IMAGE_NAME:latest .
- if [ "$CI_COMMIT_TAG" != "" ]; then
docker tag $IMAGE_NAME:latest $IMAGE_NAME:${CI_COMMIT_TAG};
docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY;
docker push $IMAGE_NAME:latest;
docker push $IMAGE_NAME:${CI_COMMIT_TAG};
fi
deploy:
stage: deploy
image: registry.hottis.de/dockerized/docker-bash:latest
only:
- tags
tags:
- hottis
- linux
- docker
variables:
GIT_STRATEGY: none
script:
- CONTAINER_NAME=$CI_PROJECT_NAME
- SERVICE_VOLUME=$CI_PROJECT_NAME"-conf"
- SERVICE_PORT=5000
- docker volume inspect $SERVICE_VOLUME || docker volume create $SERVICE_VOLUME
- docker stop $CONTAINER_NAME || echo "$CONTAINER_NAME not running, anyway okay"
- docker rm $CONTAINER_NAME || echo "$CONTAINER_NAME not exsting, anyway okay"
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY;
- docker pull $IMAGE_NAME:${CI_COMMIT_TAG}
- docker run -d --restart always --name $CONTAINER_NAME --network docker-server --ip 172.16.10.38 -v $SERVICE_VOLUME:/opt/app/config $IMAGE_NAME:${CI_COMMIT_TAG}

View File

@ -1,88 +0,0 @@
stages:
- check
- build
- dockerize
- deploy
variables:
IMAGE_NAME: $CI_REGISTRY/$CI_PROJECT_PATH
check:
image: registry.hottis.de/dockerized/base-build-env:latest
stage: check
tags:
- hottis
- linux
- docker
rules:
- if: $CI_COMMIT_TAG
script:
- checksemver.py -v
--versionToValidate "$CI_COMMIT_TAG"
--validateMessage
--messageToValidate "$CI_COMMIT_MESSAGE"
build:
image: registry.hottis.de/hv2/hv2-node-build-env:1.0.0
stage: build
variables:
GIT_SUBMODULE_STRATEGY: recursive
tags:
- hottis
- linux
- docker
artifacts:
paths:
- dist.tgz
expire_in: 1 day
script:
- cd hv2-ui
- if [ "$CI_COMMIT_TAG" != "" ]; then
sed -i -e 's/GITTAGVERSION/'"$CI_COMMIT_TAG"':'"$CI_COMMIT_SHORT_SHA"'/' ./src/app/navigation/navigation.component.html;
fi
- npm install
- for F in ./src/app/*.tmpl; do
python ../helpers/hv2-api/generate.py -s ../helpers/hv2-api/schema.json -t $F;
done
- ./node_modules/.bin/ng build --prod
- tar -czf ../dist.tgz dist
dockerize:
image: registry.hottis.de/dockerized/docker-bash:latest
stage: dockerize
tags:
- hottis
- linux
- docker
rules:
- if: $CI_COMMIT_TAG
script:
- tar -xzf dist.tgz
- docker build --tag $IMAGE_NAME:latest --tag $IMAGE_NAME:$CI_COMMIT_TAG .
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
- docker push $IMAGE_NAME:latest
- docker push $IMAGE_NAME:$CI_COMMIT_TAG
deploy:
stage: deploy
image: registry.hottis.de/dockerized/docker-bash:latest
only:
- tags
tags:
- hottis
- linux
- docker
variables:
GIT_STRATEGY: none
script:
- CONTAINER_NAME=$CI_PROJECT_NAME
- docker stop $CONTAINER_NAME || echo "$CONTAINER_NAME not running, anyway okay"
- docker rm $CONTAINER_NAME || echo "$CONTAINER_NAME not exsting, anyway okay"
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY;
- docker pull $IMAGE_NAME:${CI_COMMIT_TAG}
- docker run -d --restart always --name $CONTAINER_NAME --network docker-server --ip 172.16.10.39 $IMAGE_NAME:${CI_COMMIT_TAG}

View File

@ -1,10 +1,21 @@
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
import iso8601
def perform(dbh, params): def perform(dbh, params):
createdAt = params['created_at'] try:
createdAt = iso8601.parse_date(params['created_at'])
except iso8601.iso8601.ParseError:
msg = f"Can not parse given date {params['created_at']}"
logger.error(msg)
raise Exception(msg)
except KeyError:
createdAt = datetime.datetime.today()
year = createdAt.year
createdAt = createdAt.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:
@ -96,11 +107,11 @@ def perform(dbh, params):
accountEntry = dbGetOne(dbh, { accountEntry = dbGetOne(dbh, {
"statement": """ "statement": """
INSERT INTO account_entry_t INSERT INTO account_entry_t
(description, account, created_at, amount, account_entry_category) (description, account, created_at, fiscal_year, amount, account_entry_category)
VALUES (%s, %s, %s, %s, (SELECT id FROM account_entry_category_t WHERE description = %s)) VALUES (%s, %s, %s, %s, %s, (SELECT id FROM account_entry_category_t WHERE description = %s))
RETURNING id RETURNING id
""", """,
"params": (request['description'], request['account'], request['created_at'], request['amount'], request['category']) "params": (request['description'], request['account'], request['created_at'], year, request['amount'], request['category'])
} }
) )
logger.info(f" account entry entered with id {accountEntry['id']}") logger.info(f" account entry entered with id {accountEntry['id']}")

314
cli/OverheadAccounts.py Normal file
View File

@ -0,0 +1,314 @@
from db import dbGetMany, dbGetOne
import datetime
from loguru import logger
from decimal import *
from utils import getParam
from Cheetah.Template import Template
EPSILON = Decimal('0.000000001')
def perform(dbh, params):
year = getParam(params, '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)
houses = {}
for premise in premises:
# get overhead sums by object, category and timespan
overheadItems = dbGetMany(
dbh,
{
"statement":
"""
select sum(ae.amount) as sum,
aec.description as category,
aec.considerminusarea as considerminusarea
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
ae.fiscal_year = %(year)s and
p.id = %(premise)s
group by category, considerminusarea
union
select 0 as sum,
aec.description as category,
aec.considerminusarea as considerminusarea
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
ae.fiscal_year = %(year)s and
p.id = %(premise)s
group by category, considerminusarea
union
select 120 as sum,
'Waschmaschine' as category,
false as considerminusarea
from premise_t
where id = %(premise)s
order by category
""",
"params": {
"year": year,
"premise": premise
}
}
)
# get areas and factors
totalArea = {}
flatArea = dbGetOne(
dbh,
{
"statement":
"""
select
sum(f.area) as flat_area
from
premise_t p,
flat_t f
where
f.premise = p.id and
p.id = %(premise)s
""",
"params": {
"premise": premise
}
}
)
totalArea['flat_area'] = flatArea['flat_area']
commercialArea = dbGetOne(
dbh,
{
"statement":
"""
select
coalesce(sum(c.area), 0) as commercial_area
from
premise_t p full outer join commercial_premise_t c on c.premise = p.id
where
p.id = %(premise)s
""",
"params": {
"premise": premise
}
}
)
totalArea['commercial_area'] = commercialArea['commercial_area']
minusAreaAndDetails = dbGetOne(
dbh,
{
"statement":
"""
select
p.minus_area as minus_area,
p.id as house_id,
p.description as house
from
premise_t p
where
p.id = %(premise)s
""",
"params": {
"premise": premise
}
}
)
totalArea['minus_area'] = minusAreaAndDetails['minus_area']
details = { 'id': minusAreaAndDetails['house_id'], 'description': minusAreaAndDetails['house'] }
totalArea['other_area'] = totalArea['commercial_area'] + totalArea['minus_area']
totalArea['total_area'] = totalArea['flat_area'] + totalArea['other_area']
totalArea['flat_factor'] = totalArea['flat_area'] / totalArea['total_area']
totalArea['other_factor'] = totalArea['other_area'] / totalArea['total_area']
totalArea['factor_check'] = totalArea['flat_factor'] + totalArea['other_factor']
totalSum = Decimal('0')
flatSum = Decimal('0')
otherSum = Decimal('0')
for overheadItem in overheadItems:
totalSum += overheadItem['sum']
overheadItem['flat_part'] = overheadItem['sum'] * totalArea['flat_factor'] if overheadItem['considerminusarea'] else overheadItem['sum']
flatSum += overheadItem['flat_part']
overheadItem['other_part'] = overheadItem['sum'] * totalArea['other_factor'] if overheadItem['considerminusarea'] else Decimal('0')
otherSum += overheadItem['other_part']
verifyDifference = totalSum - flatSum - otherSum
logger.debug(f"{totalSum=}, {verifyDifference=}")
if abs(verifyDifference) > EPSILON:
raise Exception(f"Verify Difference is too large: {premise=}, {verifyDifference=}")
umlageAusfallWagnis = flatSum * Decimal('0.02')
flatSum2 = flatSum + umlageAusfallWagnis
partByMonthArea = flatSum2 / Decimal('12') / totalArea['flat_area']
houses[details['id']] = {
'details': details,
'areas': totalArea,
'overhead_items': overheadItems,
'flat_sum': flatSum,
'flat_sum_2': flatSum2,
'other_sum': otherSum,
'total_sum': totalSum,
'umlage_ausfall_wagnis': umlageAusfallWagnis,
'part_by_montharea': partByMonthArea,
'year': year }
logger.info(f"{houses=}")
printOverviews = getParam(params, 'printOverviews', True)
if printOverviews:
overviewTemplate = getParam(params, 'overviewTemplate', 'betriebskostenuebersicht.tmpl')
overviewPrefix = getParam(params, 'overviewPrefix', 'overview')
overviewSuffix = getParam(params, 'overviewSuffix', 'tex')
for house in houses.values():
logger.debug(f"Processing item: {house}")
outputFile = f"{overviewPrefix}-{house['details']['id']}.{overviewSuffix}"
tmpl = Template(file=overviewTemplate, searchList=[ house ])
logger.debug(tmpl)
with open(outputFile, 'w') as f:
f.write(str(tmpl))
# get flat tenants by object and timespan with paid overhead and due overhead
tenants = dbGetMany(
dbh,
{
"statement":
"""
select t.id as tenant_id,
t.salutation as tenant_salutation,
t.firstname as tenant_firstname,
t.lastname as tenant_lastname,
t.address1 as tenant_address1,
t.address2 as tenant_address2,
t.address3 as tenant_address3,
t.zip as tenant_zip,
t.city as tenant_city,
f.id as flat_id,
f.description as flat,
f.area as flat_area,
p.id as house_id,
p.description as house,
ty.startdate as startdate,
ty.enddate as enddate,
t.account as tenant_account
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
}
}
)
letters = []
for tenant in tenants:
letter = {}
paidTotal = dbGetOne(
dbh,
{
"statement":
"""
SELECT sum(amount) AS sum,
count(id) AS cnt
FROM account_entry_t
WHERE account = %(account)s AND
account_entry_category = 2 AND
fiscal_year = %(year)s
""",
"params": {
"account": tenant['tenant_account'],
"year": year
}
}
)
tenant['paid_total'] = paidTotal['sum']
receivableFee = dbGetOne(
dbh,
{
"statement":
"""
SELECT sum(amount) * -1 AS sum ,
count(id) AS cnt
FROM account_entry_t
WHERE account = %(account)s AND
account_entry_category = 3 AND
fiscal_year = %(year)s
""",
"params": {
"account": tenant['tenant_account'],
"year": year
}
}
)
tenant['receivable_fee'] = receivableFee['sum']
tenant['rent_time'] = receivableFee['cnt']
tenant['paid_overhead'] = paidTotal['sum'] - receivableFee['sum']
letter['tenant'] = tenant
letter['year'] = year
letter['flat_area'] = houses[tenant['house_id']]['areas']['flat_area']
letter['receivable_overhead'] = tenant['flat_area'] * houses[tenant['house_id']]['part_by_montharea'] * tenant['rent_time']
letter['unbalanced_overhead'] = tenant['paid_overhead'] - letter['receivable_overhead']
letter['unbalanced_overhead_unsigned'] = abs(letter['unbalanced_overhead'])
letter['total_overhead'] = houses[tenant['house_id']]['flat_sum_2']
letters.append(letter)
logger.info(f"{letter=}")
printLetters = getParam(params, 'printLetters', True)
if printLetters:
letterTemplate = getParam(params, 'letterTemplate', 'jahresabrechnung.tmpl')
letterPrefix = getParam(params, 'letterPrefix', 'letter')
letterSuffix = getParam(params, 'letterSuffix', 'tex')
for letter in letters:
logger.debug(f"Processing item: {letter}")
outputFile = f"{letterPrefix}-{letter['tenant']['tenant_id']}.{letterSuffix}"
tmpl = Template(file=letterTemplate, searchList=[ letter ])
logger.debug(tmpl)
with open(outputFile, 'w') as f:
f.write(str(tmpl))

View File

@ -0,0 +1,42 @@
\documentclass[12pt]{article}
\usepackage{german}
\usepackage{eurosym}
\usepackage[a4paper, left=2cm, right=2cm, top=2cm]{geometry}
\usepackage{icomma}
\setlength{\parindent}{0pt}
\begin{document}
\subsection*{Betriebskostenabrechnung}
\begin{tabular}{ll}
Objekt & $details['description'] \\
Jahr & $year \\
Eigent"umer & Nober Grundbesitz GmbH \\
\end{tabular}
\addvspace{1cm}
\begin{tabular}{|p{5cm}|r|r|r|}
\hline Art & Wohnungen & andere Fl"achen & Gesamtfl"ache \\
\hline
\hline Fl"ache & \tt{$areas['flat_area']\,m\textsuperscript{2}} & \tt{$areas['other_area']\,m\textsuperscript{2}} & \tt{$areas['total_area']\,m\textsuperscript{2}} \\
\hline Faktor & \tt{#echo '%.10f' % $areas['flat_factor'] #} & \tt{#echo '%.10f' % $areas['other_factor'] #} & \\
\hline
#for $item in $overhead_items
\hline $item['category'] & \tt{#echo '%.2f' % $item['flat_part'] #\,\euro{}} & \tt{#echo '%.2f' % $item['other_part'] #\,\euro{}} & \tt{#echo '%.2f' % $item['sum'] #\,\euro{}} \\
#end for
\hline \multicolumn{4}{c}{ } \\
\hline Zwischensumme & \tt{#echo '%.2f' % $flat_sum #\,\euro{}} & \tt{#echo '%.2f' % $other_sum #\,\euro{}} & \tt{#echo '%.2f' % $total_sum #\,\euro{}} \\
\hline Umlageausfallwagnis & \tt{#echo '%.2f' % $umlage_ausfall_wagnis #\,\euro{}} & & \\
\hline Summe & \tt{#echo '%.2f' % $flat_sum_2 #\,\euro{}} & & \\
\hline \multicolumn{4}{c}{ } \\
\hline Anteil pro Monat und m\textsuperscript{2} & \tt{#echo '%.10f' % $part_by_montharea #\,\euro{}} & & \\
\hline
\end{tabular}
\end{document}

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

74
cli/jahresabrechnung.tmpl Normal file
View File

@ -0,0 +1,74 @@
\documentclass[12pt]{dinbrief}
\usepackage{german}
\usepackage{eurosym}
\address{Nober Grundbesitz GbR\\
Eupenstr. 20\\
45259 Essen}
\backaddress{Nober Grundbesitz GbR, Eupenstr. 20, 45259 Essen}
\signature{Wolfgang Hottgenroth} %% Hier Unterschrift einfuegen
\nowindowrules
\writer{wh} %% Hier eigenen Namen oder Kuerzel einfuegen
\phone{0174}{3072474} %% Hier eigene Telefon einfuegen
\bottomtext{\sffamily\footnotesize Gesch"aftsf"uhrer: Wolfgang Hottgenroth, Robert Nober, Gregor Nober ---
EMail: hausverwaltung@nober.de\\
Bankverbindung: IBAN DE14 3605 0105 0001 5130 35, Sparkasse Essen}
\begin{document}
\pagenumbering{gobble}
\begin{letter}{$tenant['tenant_firstname'] $tenant['tenant_lastname']\\
#if $tenant['tenant_address1']
$tenant['tenant_address1']\\
#end if
#if $tenant['tenant_address2']
$tenant['tenant_address2']\\
#end if
#if $tenant['tenant_address3']
$tenant['tenant_address3']\\
#end if
$tenant['tenant_zip'] $tenant['tenant_city']}
\subject{Betriebskostenabrechnung $year}
\opening{$tenant['tenant_salutation'],}
f"ur die im vergangenen Jahr angefallenen Betriebskosten ergibt sich f"ur Sie folgende Abrechnung.
\begin{tabular}{|p{10cm}|r|}
\hline Jahr & \tt{$year} \\
\hline Haus & $tenant['house'] \\
\hline Betriebskosten gesamt & \tt{#echo '%.2f' % $total_overhead #\,\euro{}} \\
\hline Gesamt-Wohnfl"ache & \tt{$flat_area\,m\textsuperscript{2}} \\
\hline Ihre Wohnung & $tenant['flat'] \\
\hline Ihre Wohnfl"ache & \tt{$tenant['flat_area']\,m\textsuperscript{2}} \\
\hline Ihre Mietzeit & \tt{$tenant['rent_time']\,Monate} \\
\hline Ihr Betriebskostenanteil nach Fl"ache und Mietzeit & \tt{#echo '%.2f' % $receivable_overhead #\,\euro{}} \\
\hline Ihre Zahlungen & \tt{$tenant['paid_total']\,\euro{}} \\
\hline davon Anteil Miete & \tt{$tenant['receivable_fee']\,\euro{}} \\
\hline davon Anteil Betriebskostenvorauszahlung & \tt{$tenant['paid_overhead']\,\euro{}} \\
\hline
#if $unbalanced_overhead < 0
Zuwenig
#else
Zuviel
#end if
gezahlte Betriebskosten & \tt{#echo '%.2f' % $unbalanced_overhead_unsigned #\,\euro{}} \\
\hline
\end{tabular}
#if 1 < 0
Bitte "uberweisen Sie den Betrag von #echo '%.2f' % $unbalanced_overhead_unsigned #\,\euro{} kurzfristig auf mein Konto.
#else
Ich werde den Betrag von #echo '%.2f' % $unbalanced_overhead_unsigned #\,\euro{} in den n"achsten Tagen auf Ihr Konto "uberweisen.
#end if
Eine tabellarische "Ubersicht "uber die Zusammensetzung der Gesamt-Betriebskosten
finden Sie in der Anlage.
\closing{Mit freundlichen Gr"u"sen}
\end{letter}
\end{document}

2
cli/utils.py Normal file
View File

@ -0,0 +1,2 @@
def getParam(params, attr, default):
return params[attr] if attr in params else default

View File

@ -3,6 +3,10 @@ from Cheetah.Template import Template
import glob import glob
import argparse import argparse
parser = argparse.ArgumentParser(description="generate.py") parser = argparse.ArgumentParser(description="generate.py")
parser.add_argument('--schema', '-s', parser.add_argument('--schema', '-s',
help='Schema file. Default: schema.json in the current folder.', help='Schema file. Default: schema.json in the current folder.',

View File

@ -32,7 +32,9 @@
{ "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": "minus_area", "sqltype": "numeric(10,2)", "notnull": true, "default": 0},
{ "name": "account", "sqltype": "integer", "notnull": true, "foreignkey": true, "immutable": true, "unique": true }
] ]
}, },
{ {
@ -78,7 +80,8 @@
"name": "commercial_premise", "name": "commercial_premise",
"columns": [ "columns": [
{ "name": "description", "sqltype": "varchar(128)", "selector": 1 }, { "name": "description", "sqltype": "varchar(128)", "selector": 1 },
{ "name": "premise", "sqltype": "integer", "foreignkey": true, "selector": 0 } { "name": "premise", "sqltype": "integer", "foreignkey": true, "selector": 0 },
{ "name": "area", "sqltype": "numeric(10,2)", "notnull": false }
], ],
"tableConstraints": [ "tableConstraints": [
"unique(description, premise)" "unique(description, premise)"
@ -126,7 +129,8 @@
"immutable": true, "immutable": true,
"columns": [ "columns": [
{ "name": "description", "sqltype": "varchar(128)", "notnull": true, "selector": 0, "unique": true }, { "name": "description", "sqltype": "varchar(128)", "notnull": true, "selector": 0, "unique": true },
{ "name": "overhead_relevant", "sqltype": "boolean", "notnull": true, "default": "true" } { "name": "considerMinusArea", "sqltype": "boolean", "notnull": true, "default": true },
{ "name": "overhead_relevant", "sqltype": "boolean", "notnull": true, "default": true }
] ]
}, },
{ {
@ -135,8 +139,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": "fiscal_year", "sqltype": "integer", "notnull": true },
{ "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 +153,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,8 @@ 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
,minus_area numeric(10,2) not null default 0
,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;
@ -90,6 +92,7 @@ CREATE TABLE commercial_premise_t (
id serial not null primary key id serial not null primary key
,description varchar(128) ,description varchar(128)
,premise integer references premise_t (id) ,premise integer references premise_t (id)
,area numeric(10,2)
,unique(description, premise) ,unique(description, premise)
); );
@ -137,7 +140,8 @@ GRANT SELECT, UPDATE ON tenancy_fee_mapping_t_id_seq TO hv2;
CREATE TABLE account_entry_category_t ( CREATE TABLE account_entry_category_t (
id serial not null primary key id serial not null primary key
,description varchar(128) not null unique ,description varchar(128) not null unique
,overhead_relevant boolean not null default true ,considerMinusArea boolean not null default True
,overhead_relevant boolean not null default True
); );
GRANT SELECT, INSERT ON account_entry_category_t TO hv2; GRANT SELECT, INSERT ON account_entry_category_t TO hv2;
@ -145,10 +149,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()
,fiscal_year integer not null
,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,28 @@
<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">
<mat-label>Jahr</mat-label>
<input matInput type="number" name="fiscalYear" [formControl]="presetFiscalYear" ngModel/>
</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 +36,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="fiscalYear">
<th mat-header-cell *matHeaderCellDef>Jahr</th>
<td mat-cell *matCellDef="let element">{{element.rawAccountEntry.fiscal_year}}</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", "fiscalYear", "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 presetFiscalYear: 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)}`) fiscal_year: formData.value.fiscalYear,
} 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,13 @@ export class AccountComponent implements OnInit {
} }
private async init(): Promise<void> { private async init(): Promise<void> {
let currentDate = new Date()
let y = currentDate.getFullYear().toString()
this.presetFiscalYear = new FormControl(`${y}`)
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 +163,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

@ -22,6 +22,10 @@
<mat-option *ngFor="let p of premises" [value]="p.id">{{p.description}}</mat-option> <mat-option *ngFor="let p of premises" [value]="p.id">{{p.description}}</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Fläche</mat-label>
<input type="number" matInput name="area" [(ngModel)]="commercialPremise.area"/>
</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>

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,8 @@ export interface Premise {
street: string street: string
zip: string zip: string
city: string city: string
minus_area: number
account: number
} }
export const NULL_Premise: Premise = { export const NULL_Premise: Premise = {
id: 0 id: 0
@ -59,6 +61,8 @@ export const NULL_Premise: Premise = {
,street: '' ,street: ''
,zip: '' ,zip: ''
,city: '' ,city: ''
,minus_area: undefined
,account: undefined
} }
export interface Flat { export interface Flat {
@ -117,11 +121,13 @@ export interface CommercialPremise {
id: number id: number
description: string description: string
premise: number premise: number
area: number
} }
export const NULL_CommercialPremise: CommercialPremise = { export const NULL_CommercialPremise: CommercialPremise = {
id: 0 id: 0
,description: '' ,description: ''
,premise: undefined ,premise: undefined
,area: undefined
} }
export interface Tenancy { export interface Tenancy {
@ -176,11 +182,13 @@ export const NULL_TenancyFeeMapping: TenancyFeeMapping = {
export interface AccountEntryCategory { export interface AccountEntryCategory {
id: number id: number
description: string description: string
considerMinusArea: boolean
overhead_relevant: boolean overhead_relevant: boolean
} }
export const NULL_AccountEntryCategory: AccountEntryCategory = { export const NULL_AccountEntryCategory: AccountEntryCategory = {
id: 0 id: 0
,description: '' ,description: ''
,considerMinusArea: false
,overhead_relevant: false ,overhead_relevant: false
} }
@ -189,7 +197,9 @@ export interface AccountEntry {
description: string description: string
account: number account: number
created_at: string created_at: string
fiscal_year: number
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 +207,9 @@ export const NULL_AccountEntry: AccountEntry = {
,description: '' ,description: ''
,account: undefined ,account: undefined
,created_at: '' ,created_at: ''
,fiscal_year: undefined
,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

@ -19,8 +19,13 @@
<th mat-header-cell *matHeaderCellDef>Haus</th> <th mat-header-cell *matHeaderCellDef>Haus</th>
<td mat-cell *matCellDef="let element">{{element.premise.description}}</td> <td mat-cell *matCellDef="let element">{{element.premise.description}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="area">
<th mat-header-cell *matHeaderCellDef>Fläche</th>
<td mat-cell *matCellDef="let element">{{element.commercialPremise.area | number:'1.2-2'}}</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]="['/commercialunit/', row.commercialPremise.id]"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;" [routerLink]="['/commercialunit/', row.commercialPremise.id]"></tr>
</table> </table>
</div> </div>
</mat-card-content> </mat-card-content>

View File

@ -21,7 +21,7 @@ export class MyCommercialUnitsComponent implements OnInit {
dnCommercialPremises: DN_CommercialPremise[] = [] dnCommercialPremises: DN_CommercialPremise[] = []
dataSource: MatTableDataSource<DN_CommercialPremise> dataSource: MatTableDataSource<DN_CommercialPremise>
displayedColumns: string[] = ["description", "premise"] displayedColumns: string[] = ["description", "premise", "area"]
constructor( constructor(
private commercialPremiseService: CommercialPremiseService, private commercialPremiseService: CommercialPremiseService,

View File

@ -26,6 +26,14 @@
<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="minusArea">
<th mat-header-cell *matHeaderCellDef>Minus-Fläche</th>
<td mat-cell *matCellDef="let element">{{element.minus_area | number:'1.2-2'}}</td>
</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", "minusArea", "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,64 @@
</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">
<mat-label>Minus-Fläche</mat-label>
<input type="number" matInput name="minusArea" [(ngModel)]="premise.minus_area"/>
</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))