31 Commits

Author SHA1 Message Date
07dff73152 add contracts 2023-01-02 14:46:59 +01:00
10b7f89831 add analysis scripts 2022-09-18 20:15:16 +02:00
69005569aa fix tmpl 2022-05-30 15:23:12 +02:00
0e6ee3b2c5 nothing 2022-05-06 23:59:47 +02:00
3a56c4fa0f tenant overview added 2022-04-25 14:44:56 +02:00
b79ef11401 consider category which are 0 for one premise but not for the other 2022-04-25 12:22:39 +02:00
18cd0eeebc changes in account statement output 2022-04-04 14:44:44 +02:00
06e8a2f4bf Merge branch 'master' of ssh://repo.hottis.de:2922/hv2/hv2-all-in-one 2022-04-03 18:44:54 +02:00
b1b81eeb07 error analysis 2022-04-03 18:44:43 +02:00
fcd2633c72 select account to work on 2022-04-03 14:43:17 +02:00
f7eca3844d fix concerning FormControl 2022-04-01 16:31:43 +02:00
9a1e3231d6 remove broken sorting code 2022-04-01 15:42:37 +02:00
0075416fc3 forgotten change 2022-04-01 15:26:32 +02:00
47e96c10b1 forgotten config fix 2022-04-01 15:08:32 +02:00
3e32613a78 better handling of presets 2022-04-01 15:05:06 +02:00
74d0a6084b sorting not yet working 2022-04-01 14:25:58 +02:00
ac1636c162 sorting 2022-04-01 13:59:31 +02:00
d0771be1d1 Abrechnungsskript 2022-04-01 12:04:33 +02:00
452b4f69bd output dir 2022-04-01 10:59:52 +02:00
4553272c42 format of output 2022-03-31 22:30:09 +02:00
0d4e33505e account statement stuff 2022-03-31 21:40:49 +02:00
2be6a6e140 some forgotten details 2022-03-31 16:19:36 +02:00
4391cc1d8b account entry reference stuff 2022-03-31 16:07:00 +02:00
b346cc07d0 merged 2022-03-31 15:31:53 +02:00
169795c16e account entry reference stuff 2022-03-31 15:30:22 +02:00
25f6de1c11 Kontoauszug 2022-02-13 12:30:10 +01:00
1c6fe0dd16 account statement view 2022-02-11 20:33:25 +01:00
2a0bda012b year is number 2022-02-05 18:05:50 +01:00
5ca2f16757 Merge branch 'master' of ssh://repo.hottis.de:2922/hv2/hv2-all-in-one 2022-02-05 17:56:49 +01:00
292886f834 fiscalyear is number 2022-02-05 17:56:29 +01:00
3debcf6c2d fix formcontrol handling 2022-01-31 17:24:56 +01:00
47 changed files with 19861 additions and 147 deletions

4
.gitignore vendored
View File

@ -3,6 +3,7 @@ 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
cli/output/
*~ *~
.*~ .*~
.vscode/ .vscode/
@ -10,3 +11,6 @@ cli/*.tex
cli/*.log cli/*.log
cli/*.pdf cli/*.pdf
cli/*.aux cli/*.aux
.DS_Store
.Rproj.user
.venv

View File

@ -11,19 +11,6 @@ ARG CONF_DIR="${APP_DIR}/config"
RUN \ RUN \
apt update && \ apt update && \
apt install -y postgresql-client-common && \ apt install -y postgresql-client-common && \
pip3 install psycopg2 && \
pip3 install dateparser && \
pip3 install connexion && \
pip3 install connexion[swagger-ui] && \
pip3 install uwsgi && \
pip3 install flask-cors && \
pip3 install python-jose[cryptography] && \
pip3 install loguru && \
pip3 install Cheetah3
RUN \
mkdir -p ${APP_DIR} && \ mkdir -p ${APP_DIR} && \
mkdir -p ${CONF_DIR} && \ mkdir -p ${CONF_DIR} && \
useradd -d ${APP_DIR} -u 1000 user useradd -d ${APP_DIR} -u 1000 user
@ -31,11 +18,15 @@ RUN \
COPY *.py ${APP_DIR}/ COPY *.py ${APP_DIR}/
COPY openapi.yaml ${APP_DIR}/ COPY openapi.yaml ${APP_DIR}/
COPY methods.py ${APP_DIR}/ COPY methods.py ${APP_DIR}/
COPY requirements.txt ${APP_DIR}/
COPY server.ini ${CONF_DIR}/ COPY server.ini ${CONF_DIR}/
WORKDIR ${APP_DIR} WORKDIR ${APP_DIR}
VOLUME ${CONF_DIR} VOLUME ${CONF_DIR}
RUN \
pip3 install -r requirements.txt
USER 1000:1000 USER 1000:1000
EXPOSE 5000 EXPOSE 5000

View File

@ -1584,3 +1584,103 @@ SELECT
} }
) )
def get_contracts(user, token_info):
return dbGetMany(user, token_info, {
"statement": """
SELECT
id
,supplier
,content
,identifier
,notes
FROM contract_t
ORDER BY
supplier
,content
""",
"params": ()
}
)
def insert_contract(user, token_info, **args):
try:
body = args["body"]
v_supplier = body["supplier"]
v_content = body["content"]
v_identifier = body["identifier"]
v_notes = body["notes"]
return dbInsert(user, token_info, {
"statement": """
INSERT INTO contract_t
(
supplier
,content
,identifier
,notes
) VALUES (
%s
,%s
,%s
,%s
)
RETURNING *
""",
"params": [
v_supplier
,v_content
,v_identifier
,v_notes
]
})
except KeyError as e:
logger.warning("insert_contract: parameter missing: {}".format(e))
raise werkzeug.exceptions.UnprocessableEntity("parameter missing: {}".format(e))
def get_contract(user, token_info, contractId=None):
return dbGetOne(user, token_info, {
"statement": """
SELECT
id
,supplier
,content
,identifier
,notes
FROM contract_t
WHERE id = %s
""",
"params": (contractId, )
}
)
def update_contract(user, token_info, contractId=None, **args):
try:
body = args["body"]
v_supplier = body["supplier"]
v_content = body["content"]
v_identifier = body["identifier"]
v_notes = body["notes"]
return dbUpdate(user, token_info, {
"statement": """
UPDATE contract_t
SET
supplier = %s
,content = %s
,identifier = %s
,notes = %s
WHERE id = %s
RETURNING *
""",
"params": [
v_supplier,
v_content,
v_identifier,
v_notes,
contractId
]
})
except KeyError as e:
logger.warning("update_contract: parameter missing: {}".format(e))
raise werkzeug.exceptions.UnprocessableEntity("parameter missing: {}".format(e))

View File

@ -1440,6 +1440,92 @@ paths:
$ref: '#/components/schemas/note' $ref: '#/components/schemas/note'
security: security:
- jwt: ['secret'] - jwt: ['secret']
/v1/contracts:
get:
tags: [ "contract" ]
summary: Return all normalized contracts
operationId: methods.get_contracts
responses:
'200':
description: contracts response
content:
'application/json':
schema:
type: array
items:
$ref: '#/components/schemas/contract'
security:
- jwt: ['secret']
post:
tags: [ "contract" ]
summary: Insert a contract
operationId: methods.insert_contract
requestBody:
description: contract
content:
application/json:
schema:
$ref: '#/components/schemas/contract'
responses:
'200':
description: contract successfully inserted
content:
'application/json':
schema:
type: array
items:
$ref: '#/components/schemas/contract'
security:
- jwt: ['secret']
/v1/contracts/{contractId}:
get:
tags: [ "contract" ]
summary: Return the normalized contract with given id
operationId: methods.get_contract
parameters:
- name: contractId
in: path
required: true
schema:
type: integer
responses:
'200':
description: contract response
content:
'application/json':
schema:
type: array
items:
$ref: '#/components/schemas/contract'
security:
- jwt: ['secret']
put:
tags: [ "contract" ]
summary: Update a contract
operationId: methods.update_contract
parameters:
- name: contractId
in: path
required: true
schema:
type: integer
requestBody:
description: contract
content:
application/json:
schema:
$ref: '#/components/schemas/contract'
responses:
'200':
description: contract successfully inserted
content:
'application/json':
schema:
type: array
items:
$ref: '#/components/schemas/contract'
security:
- jwt: ['secret']
# ------------------------------------------------------------------- # -------------------------------------------------------------------
# ATTENTION: This file will not be parsed by Cheetah # ATTENTION: This file will not be parsed by Cheetah
@ -1818,6 +1904,21 @@ components:
type: integer type: integer
note: note:
type: string type: string
contract:
description: contract
type: object
properties:
id:
type: integer
supplier:
type: string
content:
type: string
identifier:
type: string
notes:
type: string
nullable: true
# ------------------------------------------------------------------- # -------------------------------------------------------------------
# ATTENTION: This file will not be parsed by Cheetah # ATTENTION: This file will not be parsed by Cheetah

40
api/requirements.txt Normal file
View File

@ -0,0 +1,40 @@
attrs==22.2.0
certifi==2022.12.7
cffi==1.15.1
charset-normalizer==2.1.1
Cheetah3==3.2.6.post1
click==8.1.3
clickclick==20.10.2
connexion==2.14.1
cryptography==39.0.0
dateparser==1.1.5
ecdsa==0.18.0
Flask==2.2.2
Flask-Cors==3.0.10
idna==3.4
inflection==0.5.1
itsdangerous==2.1.2
Jinja2==3.1.2
jsonschema==4.17.3
loguru==0.6.0
MarkupSafe==2.1.1
packaging==22.0
psycopg2==2.9.5
pyasn1==0.4.8
pycparser==2.21
pyrsistent==0.19.3
python-dateutil==2.8.2
python-jose==3.3.0
pytz==2022.7
pytz-deprecation-shim==0.1.0.post0
PyYAML==6.0
regex==2022.10.31
requests==2.28.1
rsa==4.9
six==1.16.0
swagger-ui-bundle==0.0.9
tzdata==2022.7
tzlocal==4.2
urllib3==1.26.13
uWSGI==2.0.21
Werkzeug==2.2.2

108
cli/AccountStatement.py Normal file
View File

@ -0,0 +1,108 @@
from db import dbGetMany, dbGetOne
from loguru import logger
import datetime
import iso8601
from utils import getParam
from Cheetah.Template import Template
def perform(dbh, params):
year = getParam(params, 'year', datetime.datetime.today().year)
accountEntries = dbGetMany(
dbh,
{
"statement": "SELECT * FROM account_statement_v WHERE fiscal_year = %(year)s",
"params": {
'year': year
}
}
)
overview = dbGetMany(
dbh,
{
"statement": "select sum(amount), category from account_statement_v where fiscal_year = %(year)s group by category",
"params": {
'year': year
}
}
)
sum_related = dbGetOne(
dbh,
{
"statement": "select coalesce(sum(amount), 0::numeric(10,2)) as sum from account_statement_v where fiscal_year = %(year)s and category != 'nicht abrechenbare Positionen'",
"params": {
'year': year
}
}
)
sum_unrelated = dbGetOne(
dbh,
{
"statement": "select coalesce(sum(amount), 0::numeric(10,2)) as sum from account_statement_v where fiscal_year = %(year)s and category = 'nicht abrechenbare Positionen'",
"params": {
'year': year
}
}
)
sum_total = dbGetOne(
dbh,
{
"statement": "select sum(amount) as sum from account_statement_v where fiscal_year = %(year)s",
"params": {
'year': year
}
}
)
sum_error = dbGetMany(
dbh,
{
"statement": """
select coalesce(sum(amount), 0::numeric(10,2)) as sum, 1 as error, 'booked in %(year1)s, accounted in %(year0)s' as remark from account_statement_v where fiscal_year = %(year0)s and extract(year from created_at) = %(year1)s
union
select coalesce(sum(amount), 0::numeric(10,2)) as sum, 2 as error, 'booked in %(year0)s, accounted in %(year1)s' as remark from account_statement_v where fiscal_year = %(year1)s and extract(year from created_at) = %(year0)s
union
select coalesce(sum(amount), 0::numeric(10,2)) as sum, 3 as error, 'booked in %(year2)s, accounted in %(year1)s' as remark from account_statement_v where fiscal_year = %(year1)s and extract(year from created_at) = %(year2)s
union
select coalesce(sum(amount), 0::numeric(10,2)) as sum, 4 as error, 'booked in %(year1)s, accounted in %(year2)s' as remark from account_statement_v where fiscal_year = %(year2)s and extract(year from created_at) = %(year1)s
order by error
""",
"params": {
'year0': year - 1,
'year1': year,
'year2': year + 1
}
}
)
count_entries = dbGetMany(
dbh,
{
"statement": "select extract(month from created_at) as month, count(*) as count from account_statement_v where extract(year from created_at) = %(year)s group by month",
"params": {
"year": year
}
}
)
raw_total = sum_total['sum']
logger.info(f"{raw_total=}")
err1 = sum_error[0]['sum']
logger.info(f"{err1=}")
err2 = sum_error[1]['sum']
logger.info(f"{err2=}")
err3 = sum_error[2]['sum']
logger.info(f"{err3=}")
err4 = sum_error[3]['sum']
logger.info(f"{err4=}")
adjusted_total = raw_total + err1 - err2 - err3 + err4
logger.info(f"{adjusted_total=}")
template = getParam(params, 'template', 'accountStatement.tmpl')
input = { 'year': year, 'entries': accountEntries, 'overview': overview, 'related': sum_related, 'unrelated': sum_unrelated, 'total': sum_total, 'errors': sum_error, 'adjusted_total' :adjusted_total, 'count_entries': count_entries }
tmpl = Template(file=template, searchList=[ input ])
print(tmpl)

51
cli/AccountStatement.sh Executable file
View File

@ -0,0 +1,51 @@
#!/bin/bash
SHOW="0"
PRINT="0"
while getopts "y:sphP:" flag
do
case "${flag}" in
h)
echo "y ... year for statement";
echo "s ... show output using evince";
echo "p ... print output";
echo "P ... printer name";
exit 1;
;;
y)
YEAR=${OPTARG}
;;
s)
SHOW="1"
;;
p)
PRINT="1"
;;
P)
PRINTER="-P ${OPTARG}"
;;
esac
done
if [ "$YEAR" = "" ]; then
echo "give year for statement as argument"
exit 1
fi
python3.10 hv2cli.py -o AccountStatement -p '{"year":'$YEAR'}' > ./output/$YEAR.tex
pushd ./output
pdflatex $YEAR.tex
pdflatex $YEAR.tex
pdflatex $YEAR.tex
if [ "$SHOW" = "1" ]; then
evince $YEAR.pdf
fi
if [ "$PRINT" = "1" ]; then
lpr $PRINTER $YEAR.pdf
fi
popd

View File

@ -4,8 +4,8 @@ from loguru import logger
from decimal import * from decimal import *
from utils import getParam from utils import getParam
from Cheetah.Template import Template from Cheetah.Template import Template
import xlsxwriter
from dateutil.relativedelta import relativedelta
EPSILON = Decimal('0.000000001') EPSILON = Decimal('0.000000001')
@ -23,7 +23,7 @@ def perform(dbh, params):
{ {
"statement": "statement":
""" """
select sum(ae.amount) as sum, select (coalesce(sum(ae.amount), 0::numeric(10,2)) * -1) as sum,
aec.description as category, aec.description as category,
aec.considerminusarea as considerminusarea aec.considerminusarea as considerminusarea
from account_t a, from account_t a,
@ -48,13 +48,13 @@ def perform(dbh, params):
where p.account = a.id and where p.account = a.id and
ae.account = a.id and ae.account = a.id and
aec.overhead_relevant = 't' and aec.overhead_relevant = 't' and
aec.id not in (select distinct account_entry_category from account_entry_t) and aec.id not in (select distinct account_entry_category from account_entry_t where fiscal_year = %(year)s and account = p.account) and
ae.fiscal_year = %(year)s and ae.fiscal_year = %(year)s and
p.id = %(premise)s p.id = %(premise)s
group by category, considerminusarea group by category, considerminusarea
union union
select 120 as sum, select 120 as sum,
'Waschmaschine' as category, '16. Waschmaschine' as category,
false as considerminusarea false as considerminusarea
from premise_t from premise_t
where id = %(premise)s where id = %(premise)s
@ -181,7 +181,7 @@ def perform(dbh, params):
for house in houses.values(): for house in houses.values():
logger.debug(f"Processing item: {house}") logger.debug(f"Processing item: {house}")
outputFile = f"{overviewPrefix}-{house['details']['id']}.{overviewSuffix}" outputFile = f"./output/{overviewPrefix}-{house['details']['id']}.{overviewSuffix}"
tmpl = Template(file=overviewTemplate, searchList=[ house ]) tmpl = Template(file=overviewTemplate, searchList=[ house ])
logger.debug(tmpl) logger.debug(tmpl)
with open(outputFile, 'w') as f: with open(outputFile, 'w') as f:
@ -190,6 +190,7 @@ def perform(dbh, params):
# get flat tenants by object and timespan with paid overhead and due overhead # get flat tenants by object and timespan with paid overhead and due overhead
tenants = dbGetMany( tenants = dbGetMany(
dbh, dbh,
@ -212,17 +213,23 @@ def perform(dbh, params):
p.description as house, p.description as house,
ty.startdate as startdate, ty.startdate as startdate,
ty.enddate as enddate, ty.enddate as enddate,
t.account as tenant_account t.account as tenant_account,
fe.amount as fee,
fe.fee_type as fee_type
from tenant_t t, from tenant_t t,
premise_t p, premise_t p,
flat_t f, flat_t f,
tenancy_t ty tenancy_t ty,
tenancy_fee_mapping_t tyfm,
fee_t fe
where ty.tenant = t.id and where ty.tenant = t.id and
ty.flat = f.id and ty.flat = f.id and
ty.startdate <= %(startDate)s and ty.startdate <= %(startDate)s and
(ty.enddate >= %(endDate)s or ty.enddate is null) and (ty.enddate >= %(endDate)s or ty.enddate is null) and
f.premise = p.id and f.premise = p.id and
p.id in %(premises)s p.id in %(premises)s and
tyfm.tenancy = ty.id and
tyfm.fee = fe.id
order by house_id, tenant_id order by house_id, tenant_id
""", """,
"params": { "params": {
@ -242,8 +249,7 @@ def perform(dbh, params):
{ {
"statement": "statement":
""" """
SELECT sum(amount) AS sum, SELECT sum(amount) AS sum
count(id) AS cnt
FROM account_entry_t FROM account_entry_t
WHERE account = %(account)s AND WHERE account = %(account)s AND
account_entry_category = 2 AND account_entry_category = 2 AND
@ -262,8 +268,7 @@ def perform(dbh, params):
{ {
"statement": "statement":
""" """
SELECT sum(amount) * -1 AS sum , SELECT sum(amount) * -1 AS sum
count(id) AS cnt
FROM account_entry_t FROM account_entry_t
WHERE account = %(account)s AND WHERE account = %(account)s AND
account_entry_category = 3 AND account_entry_category = 3 AND
@ -276,7 +281,9 @@ def perform(dbh, params):
} }
) )
tenant['receivable_fee'] = receivableFee['sum'] tenant['receivable_fee'] = receivableFee['sum']
tenant['rent_time'] = receivableFee['cnt']
delta = relativedelta((tenant['enddate'] or datetime.date(year, 12, 31)), (tenant['startdate'] if (tenant['startdate'].year == year) else datetime.date(year, 1, 1)))
tenant['rent_time'] = delta.months if delta.days == 0 else delta.months + 1
tenant['paid_overhead'] = paidTotal['sum'] - receivableFee['sum'] tenant['paid_overhead'] = paidTotal['sum'] - receivableFee['sum']
@ -301,14 +308,55 @@ def perform(dbh, params):
for letter in letters: for letter in letters:
logger.debug(f"Processing item: {letter}") logger.debug(f"Processing item: {letter}")
outputFile = f"{letterPrefix}-{letter['tenant']['tenant_id']}.{letterSuffix}" outputFile = f"./output/{letterPrefix}-{letter['tenant']['tenant_id']}.{letterSuffix}"
tmpl = Template(file=letterTemplate, searchList=[ letter ]) tmpl = Template(file=letterTemplate, searchList=[ letter ])
logger.debug(tmpl) logger.debug(tmpl)
with open(outputFile, 'w') as f: with open(outputFile, 'w') as f:
f.write(str(tmpl)) f.write(str(tmpl))
if printOverviews:
tenantsOverviewPrefix = getParam(params, 'tenantsOverviewPrefix', 'tenantsOverview')
tenantsOverviewSuffix = getParam(params, 'tenantsOverviewSuffix', 'xlsx')
logger.debug(f"Processing letters: {letters}")
outputFile = f"./output/{tenantsOverviewPrefix}-{year}.{tenantsOverviewSuffix}"
workbook = xlsxwriter.Workbook(outputFile)
worksheet = workbook.add_worksheet()
worksheet.write(0, 0, 'id')
worksheet.write(0, 1, 'lastname')
worksheet.write(0, 2, 'firstname')
worksheet.write(0, 3, 'house')
worksheet.write(0, 4, 'overhead_part_by_montharea')
worksheet.write(0, 5, 'flat')
worksheet.write(0, 6, 'flat_area')
worksheet.write(0, 7, 'fee')
worksheet.write(0, 8, 'rent_time')
worksheet.write(0, 9, 'paid_total')
worksheet.write(0, 10, 'receivable_fee')
worksheet.write(0, 11, 'paid_overhead')
worksheet.write(0, 12, 'receivable_overhead')
worksheet.write(0, 13, 'unbalanced_overhead')
row = 1
for entry in letters:
worksheet.write(row, 0, entry['tenant']['tenant_id'])
worksheet.write(row, 1, entry['tenant']['tenant_lastname'])
worksheet.write(row, 2, entry['tenant']['tenant_firstname'])
worksheet.write(row, 3, entry['tenant']['house'])
worksheet.write(row, 4, houses[entry['tenant']['house_id']]['part_by_montharea'])
worksheet.write(row, 5, entry['tenant']['flat'])
worksheet.write(row, 6, entry['tenant']['flat_area'])
worksheet.write(row, 7, entry['tenant']['fee'])
worksheet.write(row, 8, entry['tenant']['rent_time'])
worksheet.write(row, 9, entry['tenant']['paid_total'])
worksheet.write(row, 10, entry['tenant']['receivable_fee'])
worksheet.write(row, 11, entry['tenant']['paid_overhead'])
worksheet.write(row, 12, entry['receivable_overhead'])
worksheet.write(row, 13, entry['unbalanced_overhead'])
row += 1
workbook.close()

55
cli/OverheadAccounts.sh Executable file
View File

@ -0,0 +1,55 @@
#!/bin/bash
PRINT="0"
while getopts "y:phP:" flag
do
case "${flag}" in
h)
echo "y ... year for statement";
echo "p ... print output";
echo "P ... printer name";
exit 1;
;;
y)
YEAR=${OPTARG}
;;
p)
PRINT="1"
;;
P)
PRINTER="-P ${OPTARG}"
;;
esac
done
if [ "$YEAR" = "" ]; then
echo "give year for statement as argument"
exit 1
fi
python3.10 hv2cli.py -o OverheadAccounts -p '{"year":'$YEAR'}'
pushd ./output
for I in letter-*.tex; do
pdflatex $I
pdflatex $I
pdflatex $I
done
for I in overview-*.tex; do
pdflatex $I
pdflatex $I
pdflatex $I
done
if [ "$PRINT" = "1" ]; then
for I in letter-*.pdf; do
lpr $PRINTER $I
done
for I in overview-*.pdf; do
lpr $PRINTER $I
done
fi
popd

0
cli/accountStatement Normal file
View File

View File

@ -0,0 +1,83 @@
\documentclass[10pt]{article}
\usepackage{german}
\usepackage{eurosym}
\usepackage[a4paper, landscape=true, left=2cm, right=2cm, top=2cm]{geometry}
\usepackage{icomma}
\usepackage{longtable}
\usepackage{color}
\setlength{\parindent}{0pt}
\begin{document}
\subsection*{Account Statement $year}
\begin{longtable}{|r|r|p{6cm}|r|r|r|p{3cm}|r|p{5cm}|}
\hline \textcolor{blue}{\bf{ID}} & \textcolor{blue}{\bf{Date}} & \textcolor{blue}{\bf{Description}} & \textcolor{blue}{\bf{Amount}} & \textcolor{blue}{\bf{DocNo}} & \textcolor{blue}{\bf{Year}} & \textcolor{blue}{\bf{Category}} & \textcolor{blue}{\bf{IsRef}} & \textcolor{blue}{\bf{BaseAccount}} \\ \hline \hline
\endfirsthead
\hline \textcolor{blue}{\bf{ID}} & \textcolor{blue}{\bf{Date}} & \textcolor{blue}{\bf{Description}} & \textcolor{blue}{\bf{Amount}} & \textcolor{blue}{\bf{DocNo}} & \textcolor{blue}{\bf{Year}} & \textcolor{blue}{\bf{Category}} & \textcolor{blue}{\bf{IsRef}} & \textcolor{blue}{\bf{BaseAccount}} \\ \hline \hline
\endhead
\multicolumn{9}{r}{\em{to be continued}}
\endfoot
\endlastfoot
#for $entry in $entries
\tt{$entry['id']} & \tt{$entry['created_at']} & \tt{$entry['description']} & #slurp
#if $entry['amount'] <= 0
\textcolor{red}{#slurp
#end if
\tt{$entry['amount']} \,\euro{}#slurp
#if $entry['amount'] <= 0
}#slurp
#end if
& \tt{$entry['document_no']} & \tt{$entry['fiscal_year']} & \tt{$entry['category']} & \tt{$entry['is_reference']} & \tt{{$entry['base_account'].replace('_', ' ') }} \\ \hline
#end for
\end{longtable}
\pagebreak
\subsection*{Overviews}
\begin{tabular}{|l|r|} \hline
\hline \textcolor{blue}{\bf{category}} & \textcolor{blue}{\bf{sum}} \\
#for $entry in $overview
\hline \tt{$entry['category']} & #slurp
#if $entry['sum'] <= 0
\textcolor{red}{#slurp
#end if
\tt{$entry['sum']} \,\euro{}#slurp
#if $entry['sum'] <= 0
}#slurp
#end if
\\
#end for
\hline \hline related & \tt{$related['sum']} \,\euro{} \\
\hline unrelated & \tt{$unrelated['sum']} \,\euro{} \\
\hline total & \tt{$total['sum']} \,\euro{} \\
\hline \hline
\end{tabular}
\pagebreak
\subsection*{Errors}
\begin{tabular}{|r|r|l|} \hline
\hline \textcolor{blue}{\bf{type}} & \textcolor{blue}{\bf{sum}} & \textcolor{blue}{\bf{remark}} \\
#for $entry in $errors
\hline \tt{$entry['error']} & \tt{$entry['sum']} \,\euro{} & \tt{$entry['remark']} \\
#end for
\hline \tt{0} & \tt{$adjusted_total} \,\euro{} & \tt{adjusted total} \\
\hline \hline
\end{tabular}
\pagebreak
\subsection*{Counts}
\begin{tabular}{|r|r|} \hline
\hline \textcolor{blue}{\bf{Month}} & \textcolor{blue}{\bf{Count}} \\
#for $entry in $count_entries
\hline \tt{$entry['month']} & \tt{$entry['count']} \\
#end for
\hline \hline
\end{tabular}
\end{document}

View File

@ -1,4 +1,5 @@
\documentclass[12pt]{article} \documentclass[12pt]{article}
\renewcommand{\familydefault}{\sfdefault}
\usepackage{german} \usepackage{german}
\usepackage{eurosym} \usepackage{eurosym}
\usepackage[a4paper, left=2cm, right=2cm, top=2cm]{geometry} \usepackage[a4paper, left=2cm, right=2cm, top=2cm]{geometry}
@ -7,8 +8,10 @@
\begin{document} \begin{document}
\subsection*{Betriebskostenabrechnung} \subsection*{Aufstellung}
über die Ermittlung der Betriebskosten gemäß §27 II. BV
\vspace{15mm}
\begin{tabular}{ll} \begin{tabular}{ll}
Objekt & $details['description'] \\ Objekt & $details['description'] \\
@ -18,23 +21,25 @@ Eigent"umer & Nober Grundbesitz GmbH \\
\addvspace{1cm} \addvspace{1cm}
\begin{tabular}{|p{5cm}|r|r|r|} \begin{tabular}{|p{7cm}|r|r|r|}
\hline Art & Wohnungen & andere Fl"achen & Gesamtfl"ache \\ \hline Art & Wohnungen & andere Fl"achen & Gesamtfl"ache \\
\hline \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 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 Faktor & \tt{#echo '%.10f' % $areas['flat_factor'] #} & \tt{#echo '%.10f' % $areas['other_factor'] #} & \\
\hline \hline \hline
#for $item in $overhead_items #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{}} \\ \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 #end for
\hline \multicolumn{4}{c}{ } \\ \hline
% \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 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 Umlageausfallwagnis & \tt{#echo '%.2f' % $umlage_ausfall_wagnis #\,\euro{}} & & \\
\hline Summe & \tt{#echo '%.2f' % $flat_sum_2 #\,\euro{}} & & \\ \hline Summe & \tt{#echo '%.2f' % $flat_sum_2 #\,\euro{}} & & \\
\hline \multicolumn{4}{c}{ } \\ \hline
% \hline \multicolumn{4}{c}{ } \\
\hline Anteil pro Monat und m\textsuperscript{2} & \tt{#echo '%.10f' % $part_by_montharea #\,\euro{}} & & \\ \hline Anteil pro Monat und m\textsuperscript{2} & \tt{#echo '%.10f' % $part_by_montharea #\,\euro{}} & & \\
\hline \hline
\end{tabular} \end{tabular}

View File

@ -57,7 +57,7 @@
\hline \hline
\end{tabular} \end{tabular}
#if 1 < 0 #if $unbalanced_overhead < 0
Bitte "uberweisen Sie den Betrag von #echo '%.2f' % $unbalanced_overhead_unsigned #\,\euro{} kurzfristig auf mein Konto. Bitte "uberweisen Sie den Betrag von #echo '%.2f' % $unbalanced_overhead_unsigned #\,\euro{} kurzfristig auf mein Konto.
#else #else
Ich werde den Betrag von #echo '%.2f' % $unbalanced_overhead_unsigned #\,\euro{} in den n"achsten Tagen auf Ihr Konto "uberweisen. Ich werde den Betrag von #echo '%.2f' % $unbalanced_overhead_unsigned #\,\euro{} in den n"achsten Tagen auf Ihr Konto "uberweisen.

View File

@ -0,0 +1,236 @@
source("~/.active-rstudio-document")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
View(amounts)
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
View(amounts)
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
print(amounts)
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
print(amounts)
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
print(amounts)
print(amounts, digits=2)
print(amounts, width=100)
print(amounts, width=10)
print(amounts, width=100)
print(amounts, digits=10)
options(pillar.sigfig=7)
print(amounts)
options(pillar.sigfig=10)
print(amounts)
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
library(tidyverse)
flextable::flextable
library(table)
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
sum(amounts$fee)
sum(amounts$fee) + sum(amounts$overhead)
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
sum(amounts$Betriebskosten)
sum(amounts$Betriebskosten, na.rm = TRUE)
sum(amounts$Betriebskosten, na.rm = FALSE)
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
sum(amounts$Betriebskosten)
sum(amounts$Betriebskosten, na.rm = T)
sum(amounts$Betriebskosten, na.omit())
sum(amounts$Betriebskosten
)
sum(amounts$Betriebskosten)
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
amounts$Betriebskosten
amounts$Betriebskosten[is.na(.)]
amounts$Betriebskosten[is.na()]
amounts$Betriebskosten[NA]
amounts$Betriebskosten[1]
amounts$Betriebskosten[is.na()]
amounts$Betriebskosten[is.na]
d <- amounts$Betriebskosten
d
d[is.na(d)]
d[is.na(d)] <- 0
d
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
View(expense)
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
expense
expense[overhead_relevant==T]
expense[expense$overhead_relevant == T]
expense[expense$overhead_relevant == TRUE]
expense[expense$overhead_relevant]
expense %>% filter(overhead_relevant == TRUE)
source("~/Workspace/hv2-all-in-one/r-scripts/hv2-analysis/income.R")
View(expense)
knitr::opts_chunk$set(echo = TRUE)
total_fee <- sum(income$Miete)
knitr::opts_chunk$set(echo = TRUE)
library(tidyverse, warn.conflicts = FALSE)
library(DBI, warn.conflicts = FALSE)
library(tidyr, warn.conflicts = FALSE)
library(dplyr, warn.conflicts = FALSE)
YEAR <- 2021
HOME <- Sys.getenv("HOME")
Sys.setenv(PGHOST = "db.mainscnt.eu",
PGDATABASE = "hv2",
PGPORT = 5432,
PGUSER = "wn",
PGSSLMODE = "verify-ca",
PGSSLKEY = paste(HOME, "/keys/psql/wn-postgresql-client-2.key", sep=""),
PGSSLCERT = paste(HOME, "/keys/psql/wn-postgresql-client-2.crt", sep=""),
PGSSLROOTCERT = paste(HOME, "/keys/psql/postgres-ca.crt", sep=""))
con <- dbConnect(RPostgres::Postgres())
res <- dbSendQuery(con, "
select f.description as flat,
p.description as premise,
aec.description as category,
sum(ae.amount) as amount
from flat_t f,
premise_t p,
tenancy_t ty,
tenant_t t,
account_t a,
account_entry_t ae,
account_entry_category_t aec
where p.id = f.premise and
ty.flat = f.id and
ty.tenant = t.id and
a.id = t.account and
ae.account = a.id and
aec.id = ae.account_entry_category and
ae.fiscal_year = $1 and
aec.description in ('Mietzahlung', 'Mietforderung', 'Betriebskostenforderung', 'Betriebskostenausgleich')
group by p.description,
f.description,
aec.description;
")
dbBind(res, list(YEAR))
income <- dbFetch(res)
dbClearResult(res)
income <- income %>%
pivot_wider(names_from = category, values_from = amount) %>%
mutate(Miete := -1 * Mietforderung) %>%
mutate(Betriebskosten := Mietzahlung - Miete + Betriebskostenausgleich) %>%
select(flat, premise, Miete, Betriebskosten) %>%
arrange(premise, flat) %>%
mutate(across(where(is.numeric), ~num(., digits=2))) %>%
mutate(across(where(is.numeric), ~replace(., is.na(.), 0)))
knitr::kable(income)
total_fee <- sum(income$Miete)
total_overhead <- sum(income$Betriebskosten)
total_income <- total_fee + total_overhead
View(income)
knitr::opts_chunk$set(echo = TRUE)
library(tidyverse, warn.conflicts = FALSE)
library(DBI, warn.conflicts = FALSE)
library(tidyr, warn.conflicts = FALSE)
library(dplyr, warn.conflicts = FALSE)
YEAR <- 2021
HOME <- Sys.getenv("HOME")
Sys.setenv(PGHOST = "db.mainscnt.eu",
PGDATABASE = "hv2",
PGPORT = 5432,
PGUSER = "wn",
PGSSLMODE = "verify-ca",
PGSSLKEY = paste(HOME, "/keys/psql/wn-postgresql-client-2.key", sep=""),
PGSSLCERT = paste(HOME, "/keys/psql/wn-postgresql-client-2.crt", sep=""),
PGSSLROOTCERT = paste(HOME, "/keys/psql/postgres-ca.crt", sep=""))
con <- dbConnect(RPostgres::Postgres())
res <- dbSendQuery(con, "
select f.description as flat,
p.description as premise,
aec.description as category,
sum(ae.amount) as amount
from flat_t f,
premise_t p,
tenancy_t ty,
tenant_t t,
account_t a,
account_entry_t ae,
account_entry_category_t aec
where p.id = f.premise and
ty.flat = f.id and
ty.tenant = t.id and
a.id = t.account and
ae.account = a.id and
aec.id = ae.account_entry_category and
ae.fiscal_year = $1 and
aec.description in ('Mietzahlung', 'Mietforderung', 'Betriebskostenforderung', 'Betriebskostenausgleich')
group by p.description,
f.description,
aec.description;
")
dbBind(res, list(YEAR))
income <- dbFetch(res)
dbClearResult(res)
income <- income %>%
pivot_wider(names_from = category, values_from = amount) %>%
mutate(Miete := -1 * Mietforderung) %>%
mutate(Betriebskosten := Mietzahlung - Miete + Betriebskostenausgleich) %>%
select(flat, premise, Miete, Betriebskosten) %>%
arrange(premise, flat) %>%
mutate(across(where(is.numeric), ~num(., digits=2))) %>%
mutate(across(where(is.numeric), ~replace(., is.na(.), 0)))
knitr::kable(income)
total_fee <- sum(income$Miete)
total_overhead <- sum(income$Betriebskosten)
total_income <- total_fee + total_overhead
toString(total_fee)
total_fee$value
total_fee[1]
total_fee[2]
total_fee[1]
t=total_fee
t
unnest(t)
a <- 6.4234
a
num(a, digits=2)
a
format(a, digits=2)
format(a, digits=3)
a<-100.4234234
a
format(a, digits=3)
format(a, nsmall=2)

View File

@ -0,0 +1,13 @@
Version: 1.0
RestoreWorkspace: Default
SaveWorkspace: Default
AlwaysSaveHistory: Default
EnableCodeIndexing: Yes
UseSpacesForTab: Yes
NumSpacesForTab: 2
Encoding: UTF-8
RnwWeave: Sweave
LaTeX: pdfLaTeX

View File

@ -0,0 +1,173 @@
---
title: "HV2 Jahresabrechnung"
author: "Wolfgang Hottgenroth"
date: "2022-09-18"
output:
pdf_document: default
html_document: default
word_document: default
---
```{r setup, include=FALSE, echo=FALSE, message=FALSE}
knitr::opts_chunk$set(echo = FALSE, message=FALSE)
```
```{r}
library(tidyverse, warn.conflicts = FALSE)
library(DBI, warn.conflicts = FALSE)
library(tidyr, warn.conflicts = FALSE)
library(dplyr, warn.conflicts = FALSE)
```
```{r}
YEAR <- 2021
HOME <- Sys.getenv("HOME")
Sys.setenv(PGHOST = "db.mainscnt.eu",
PGDATABASE = "hv2",
PGPORT = 5432,
PGUSER = "wn",
PGSSLMODE = "verify-ca",
PGSSLKEY = paste(HOME, "/keys/psql/wn-postgresql-client-2.key", sep=""),
PGSSLCERT = paste(HOME, "/keys/psql/wn-postgresql-client-2.crt", sep=""),
PGSSLROOTCERT = paste(HOME, "/keys/psql/postgres-ca.crt", sep=""))
con <- dbConnect(RPostgres::Postgres())
```
# Übersicht über die Einnahmen
```{r}
res <- dbSendQuery(con, "
select f.description as flat,
p.description as premise,
aec.description as category,
sum(ae.amount) as amount
from flat_t f,
premise_t p,
tenancy_t ty,
tenant_t t,
account_t a,
account_entry_t ae,
account_entry_category_t aec
where p.id = f.premise and
ty.flat = f.id and
ty.tenant = t.id and
a.id = t.account and
ae.account = a.id and
aec.id = ae.account_entry_category and
ae.fiscal_year = $1 and
aec.description in ('Mietzahlung', 'Mietforderung', 'Betriebskostenforderung', 'Betriebskostenausgleich')
group by p.description,
f.description,
aec.description;
")
dbBind(res, list(YEAR))
income <- dbFetch(res)
dbClearResult(res)
income <- income %>%
pivot_wider(names_from = category, values_from = amount) %>%
mutate(Miete := -1 * Mietforderung) %>%
mutate(Betriebskosten := Mietzahlung - Miete + Betriebskostenausgleich) %>%
select(flat, premise, Miete, Betriebskosten) %>%
arrange(premise, flat) %>%
# mutate(across(where(is.numeric), ~num(., digits=2))) %>%
mutate(across(where(is.numeric), ~replace(., is.na(.), 0)))
```
```{r}
knitr::kable(income)
```
# Übersicht über die Ausgaben
```{r}
res <- dbSendQuery(con, "
select aec.overhead_relevant as overhead_relevant,
aec.description as category,
sum(ae.amount) as amount
from account_entry_category_t aec,
account_entry_t ae
where aec.id = ae.account_entry_category and
aec.id not in (2, 3, 4, 29) and
ae.fiscal_year = $1
group by aec.overhead_relevant,
aec.description
")
dbBind(res, list(YEAR))
expense <- dbFetch(res)
dbClearResult(res)
expense <- expense %>%
arrange(overhead_relevant, category) %>%
# mutate(across(where(is.numeric), ~num(., digits=2))) %>%
mutate(across(where(is.numeric), ~replace(., is.na(.), 0)))
```
```{r}
knitr::kable(expense)
```
# Zusammenfassung
```{r}
total_fee <- sum(income$Miete)
total_overhead <- sum(income$Betriebskosten)
total_income <- total_fee + total_overhead
total_expense <- sum(expense$amount)
overhead_relevant_expense <- expense %>%
filter(overhead_relevant == TRUE)
total_overhead_relevant_expense <- sum(overhead_relevant_expense$amount)
total <- total_income + total_expense
overview.income <- as_tibble(
data.frame(
"Kategorie" = c("Mieteinnahmen", "Betriebskostenvorauszahlungen", "Einnahmen ingesamt"),
"Betrag" = c(total_fee, total_overhead, total_income)
)
)
overview.expense <- as_tibble(
data.frame(
"Kategorie" = c("Ausgaben insgesamt", "davon Betriebskostenausgaben"),
"Betrag" = c(-1 * total_expense, -1 * total_overhead_relevant_expense)
)
)
overview.gain <- as_tibble(
data.frame(
"Überschuss" = c(total)
)
)
```
## Einnahmen
```{r}
knitr::kable(overview.income)
```
## Ausgaben
```{r}
knitr::kable(overview.expense)
```
## Überschuss
```{r}
knitr::kable(overview.gain)
```

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -0,0 +1,118 @@
library(tidyverse, warn.conflicts = FALSE)
library(DBI, warn.conflicts = FALSE)
library(tidyr, warn.conflicts = FALSE)
library(dplyr, warn.conflicts = FALSE)
# library(lubridate, warn.conflicts = FALSE)
# library(R.utils, warn.conflicts = FALSE)
YEAR <- 2021
HOME <- Sys.getenv("HOME")
Sys.setenv(PGHOST = "db.mainscnt.eu",
PGDATABASE = "hv2",
PGPORT = 5432,
PGUSER = "wn",
PGSSLMODE = "verify-ca",
PGSSLKEY = paste(HOME, "/keys/psql/wn-postgresql-client-2.key", sep=""),
PGSSLCERT = paste(HOME, "/keys/psql/wn-postgresql-client-2.crt", sep=""),
PGSSLROOTCERT = paste(HOME, "/keys/psql/postgres-ca.crt", sep=""))
con <- dbConnect(RPostgres::Postgres())
res <- dbSendQuery(con, "
select f.description as flat,
p.description as premise,
aec.description as category,
sum(ae.amount) as amount
from flat_t f,
premise_t p,
tenancy_t ty,
tenant_t t,
account_t a,
account_entry_t ae,
account_entry_category_t aec
where p.id = f.premise and
ty.flat = f.id and
ty.tenant = t.id and
a.id = t.account and
ae.account = a.id and
aec.id = ae.account_entry_category and
ae.fiscal_year = $1 and
aec.description in ('Mietzahlung', 'Mietforderung', 'Betriebskostenforderung', 'Betriebskostenausgleich')
group by p.description,
f.description,
aec.description;
")
dbBind(res, list(YEAR))
income <- dbFetch(res)
dbClearResult(res)
income <- income %>%
pivot_wider(names_from = category, values_from = amount) %>%
mutate(Miete := -1 * Mietforderung) %>%
mutate(Betriebskosten := Mietzahlung - Miete + Betriebskostenausgleich) %>%
select(flat, premise, Miete, Betriebskosten) %>%
arrange(premise, flat) %>%
mutate(across(where(is.numeric), ~num(., digits=2))) %>%
mutate(across(where(is.numeric), ~replace(., is.na(.), 0)))
total_fee <- sum(income$Miete)
total_overhead <- sum(income$Betriebskosten)
total_income <- total_fee + total_overhead
res <- dbSendQuery(con, "
select aec.overhead_relevant as overhead_relevant,
aec.description as category,
sum(ae.amount) as amount
from account_entry_category_t aec,
account_entry_t ae
where aec.id = ae.account_entry_category and
aec.id not in (2, 3, 4, 29) and
ae.fiscal_year = $1
group by aec.overhead_relevant,
aec.description
")
dbBind(res, list(YEAR))
expense <- dbFetch(res)
dbClearResult(res)
expense <- expense %>%
arrange(overhead_relevant, category) #%>%
#mutate(across(where(is.numeric), ~num(., digits=2))) #%>%
#mutate(across(where(is.numeric), ~replace(., is.na(.), 0)))
total_expense <- sum(expense$amount)
overhead_relevant_expense <- expense %>%
filter(overhead_relevant == TRUE)
total_overhead_relevant_expense <- sum(overhead_relevant_expense$amount)
print("Einkuenfte pro Wohnung")
print(income)
print("Einkuenfte Miete")
print(total_fee)
print("Einkuenfte Betriebskosten")
print(total_overhead)
print("Einkuenfte Total")
print(total_income)
print("Ausgaben")
print(expense)
print(total_expense)
print("davon betriebskosten-relevant")
print(total_overhead_relevant_expense)
profit <- total_income + total_expense
print("Überschuss")
print(profit)

View File

@ -2,6 +2,7 @@
## Tools and Templates ## Tools and Templates
* venv for Python tools is used, remember to activate, install dependencies via ``requirements.txt``, keep it up to date using ``pip freeze > requirements.txt``
* all tools provide should work on Linux and on Windows, shell scripts are avoided * all tools provide should work on Linux and on Windows, shell scripts are avoided
* templates files must be generated at development time, generated files must be put into the repository * templates files must be generated at development time, generated files must be put into the repository
* use ``generate.py`` in the umbrella project's root without any arguments to generate all template files in all subdirectories using the ``schema.json`` from the umbrella project's root * use ``generate.py`` in the umbrella project's root without any arguments to generate all template files in all subdirectories using the ``schema.json`` from the umbrella project's root
@ -14,3 +15,5 @@
* make sure these configuration files are not added to the repository, they contain secrets * make sure these configuration files are not added to the repository, they contain secrets
* in the development environment the API will answer at ``http://localhost:8080``, the Swagger-UI is available at ``http://localhost:8080/ui`` * in the development environment the API will answer at ``http://localhost:8080``, the Swagger-UI is available at ``http://localhost:8080/ui``

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
Cheetah3==3.2.6.post1

View File

@ -157,6 +157,19 @@
{ "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 }
] ]
},
{
"name": "contract",
"immutable": false,
"columns": [
{ "name": "supplier", "sqltype": "varchar(256)", "notnull": true, "selector": 0 },
{ "name": "content", "sqltype": "varchar(256)", "notnull": true, "selector": 1 },
{ "name": "identifier", "sqltype": "varchar(256)", "notnull": true },
{ "name": "notes", "sqltype": "varchar(4096)", "notnull": false }
],
"tableConstraints": [
"unique(supplier, content)"
]
} }
] ]
} }

View File

@ -0,0 +1,92 @@
create or replace view account_statement_v as
select ae.id as id,
ae.description as description,
ae.created_at::timestamp::date as created_at,
ae.amount as amount,
ae.document_no as document_no,
ae.fiscal_year as fiscal_year,
aec.description as category,
ac.description as account,
ae.is_reference as is_reference,
bac.description as base_account
from joined_account_entry_v ae,
account_entry_category_t aec,
account_t ac,
account_t bac
where ae.account_entry_category = aec.id and
ae.account = ac.id and
ae.base_account = bac.id and
ac.id = 1000
order by created_at;
grant select on account_statement_v to hv2;
create or replace view income_v as
select f.description as flat,
p.description as premise,
ae.amount as amount,
ty.id as tenancy_id,
t.lastname as tenant
from flat_t f,
premise_t p,
tenancy_t ty,
tenant_t t,
account_entry_t ae,
account_entry_category_t aec
where p.id = f.premise and
f.id = ty.flat and
t.id = ty.tenant and
ae.id = t.account and
aec.id = ae.account_entry_category and
aec.description = 'Mietzahlung' and
ae.fiscal_year = 2021
;
select f.description as flat,
p.description as premise,
--ty.id as tenancy_id,
--t.lastname || ', ' || t.firstname as tenant,
--a.description as account,
aec.description as category,
sum(ae.amount) as amount
from flat_t f,
premise_t p,
tenancy_t ty,
tenant_t t,
account_t a,
account_entry_t ae,
account_entry_category_t aec
where p.id = f.premise and
ty.flat = f.id and
ty.tenant = t.id and
a.id = t.account and
ae.account = a.id and
aec.id = ae.account_entry_category and
ae.fiscal_year = 2021 and
aec.description in ('Mietzahlung', 'Mietforderung', 'Betriebskostenforderung')
group by p.description,
f.description,
aec.description
;
select aec.overhead_relevant as overhead_relevant,
aec.description as category,
sum(ae.amount) as amount
from account_entry_category_t aec,
account_entry_t ae
where aec.id = ae.account_entry_category and
aec.id not in (2, 3, 4, 29) and
ae.fiscal_year = 2021
group by aec.overhead_relevant,
aec.description
order by aec.overhead_relevant;

View File

@ -0,0 +1,11 @@
CREATE TABLE contract_t (
id serial not null primary key
,supplier varchar(256) not null
,content varchar(256) not null
,identifier varchar(256) not null
,unique(supplier, content)
);
GRANT SELECT, INSERT, UPDATE ON contract_t TO hv2;
GRANT SELECT, UPDATE ON contract_t_id_seq TO hv2;

126
schema/changes02.sql Normal file
View File

@ -0,0 +1,126 @@
CREATE TABLE account_entry_reference_t (
id serial not null primary key,
account integer not null references account_t (id),
account_entry integer not null references account_entry_t (id)
);
CREATE OR REPLACE VIEW joined_account_entry_v AS
SELECT ae.id as id,
ae.description as description,
ae.account as account,
ae.created_at as created_at,
ae.amount as amount,
ae.account_entry_category as account_entry_category,
ae.document_no as document_no,
ae.fiscal_year as fiscal_year,
false as is_reference,
0 as base_account
FROM account_entry_t ae
UNION
SELECT ae.id as id,
ae.description as description,
aer.account as account,
ae.created_at as created_at,
ae.amount as amount,
ae.account_entry_category as account_entry_category,
ae.document_no as document_no,
ae.fiscal_year as fiscal_year,
true as is_reference,
ae.account as base_account
FROM account_entry_t ae,
account_entry_reference_t aer
WHERE ae.id = aer.account_entry;
update account_t set description = 'fallback_account' where id = 33;
delete from account_t where id = 32;
insert into account_t (id, description) values (1000, 'ledger');
update account_entry_t set amount = amount * -1.0 where account=33;
update account_entry_t set amount = amount * -1.0 where account in (34,35,36,37);
DO $$
DECLARE
entry_id integer;
BEGIN
RAISE NOTICE 'Start migration';
FOR entry_id IN
SELECT id
FROM account_entry_t
WHERE account IN (33,34,35,36,37)
LOOP
RAISE NOTICE 'About to migrate entry %', entry_id;
INSERT INTO account_entry_reference_t (account, account_entry) VALUES (1000, entry_id);
END LOOP;
END;
$$;
update account_entry_t
set description = 'Miete ' || extract(year from created_at)::text || ' ' || to_char(to_date(extract(month from created_at)::text, 'MM'), 'Month')
where account_entry_category = 2 and description = 'Miete';
DO $$
DECLARE
entry RECORD;
tenant RECORD;
BEGIN
RAISE NOTICE 'Start migration of tenant accounts';
FOR entry IN
SELECT id, description, account
FROM account_entry_t
WHERE account_entry_category = 2
LOOP
RAISE NOTICE 'About to migrate entry %', entry.id;
INSERT INTO account_entry_reference_t (account, account_entry) VALUES (1000, entry.id);
SELECT *
INTO tenant
FROM tenant_t
WHERE account = entry.account;
RAISE NOTICE 'Tenant: % %', tenant.firstname, tenant.lastname;
UPDATE account_entry_t
SET description = description || ' ' || tenant.firstname || ' ' || tenant.lastname
WHERE id = entry.id;
END LOOP;
END;
$$;
CREATE OR REPLACE FUNCTION maintain_ledger()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
tenant RECORD;
adjusted_description text;
BEGIN
IF ((NEW.description = 'Miete') AND (NEW.account_entry_category = 2)) THEN
SELECT firstname, lastname
INTO tenant
FROM tenant_t
WHERE account = NEW.account;
adjusted_description := 'Miete ' || extract(year from NEW.created_at)::text || ' ' || to_char(to_date(extract(month from NEW.created_at)::text, 'MM'), 'Month') || tenant.firstname || ' ' || tenant.lastname;
UPDATE account_entry_t
SET description = adjusted_description
WHERE id = NEW.id;
END IF;
INSERT INTO account_entry_reference_t (account, account_entry) VALUES (1000, NEW.id);
RETURN NEW;
END;
$$;
CREATE TRIGGER maintain_ledger_trigger
AFTER INSERT ON account_entry_t
FOR EACH ROW
WHEN (NEW.account != 1000 AND NEW.account_entry_category NOT IN (3, 4, 29))
EXECUTE FUNCTION maintain_ledger();
grant select, update on account_entry_reference_t_id_seq to hv2;
grant insert on account_entry_reference_t to hv2;
grant update on account_entry_t to hv2;

View File

@ -172,6 +172,18 @@ CREATE TABLE note_t (
GRANT SELECT, INSERT ON note_t TO hv2; GRANT SELECT, INSERT ON note_t TO hv2;
GRANT SELECT, UPDATE ON note_t_id_seq TO hv2; GRANT SELECT, UPDATE ON note_t_id_seq TO hv2;
CREATE TABLE contract_t (
id serial not null primary key
,supplier varchar(256) not null
,content varchar(256) not null
,identifier varchar(256) not null
,notes varchar(4096)
,unique(supplier, content)
);
GRANT SELECT, INSERT, UPDATE ON contract_t TO hv2;
GRANT SELECT, UPDATE ON contract_t_id_seq TO hv2;

17420
ui/hv2-ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -37,8 +37,6 @@
"@types/jasmine": "~3.6.0", "@types/jasmine": "~3.6.0",
"@types/node": "^12.11.1", "@types/node": "^12.11.1",
"codelyzer": "^6.0.0", "codelyzer": "^6.0.0",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~5.1.0", "karma": "~5.1.0",
"karma-chrome-launcher": "~3.1.0", "karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.0.3", "karma-coverage": "~2.0.3",

View File

@ -10,9 +10,9 @@
<mat-label>Jahr</mat-label> <mat-label>Jahr</mat-label>
<input matInput type="number" name="fiscalYear" [formControl]="presetFiscalYear" ngModel/> <input matInput type="number" name="fiscalYear" [formControl]="presetFiscalYear" ngModel/>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline" *ngIf="!shallBeRentPayment"> <mat-form-field appearance="outline">
<mat-label>Kategorie</mat-label> <mat-label>Kategorie</mat-label>
<mat-select ngModel name="category" [disabled]="shallBeRentPayment"> <mat-select ngModel name="category" [formControl]="presetCategory" >
<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>
@ -20,9 +20,9 @@
<mat-label>Betrag (€)</mat-label> <mat-label>Betrag (€)</mat-label>
<input matInput type="number" name="amount" ngModel/> <input matInput type="number" name="amount" ngModel/>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline" *ngIf="!shallBeRentPayment"> <mat-form-field appearance="outline">
<mat-label>Beschreibung</mat-label> <mat-label>Beschreibung</mat-label>
<input matInput name="description" [disabled]="shallBeRentPayment" ngModel/> <input matInput name="description" [formControl]="presetDescription" 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>
@ -33,11 +33,11 @@ Saldo: {{saldo?.saldo | number:'1.2-2'}} €
<div id="secondBlock"> <div id="secondBlock">
<table mat-table [dataSource]="accountEntriesDataSource" #zftable> <table mat-table [dataSource]="accountEntriesDataSource" #zftable>
<ng-container matColumnDef="createdAt"> <ng-container matColumnDef="createdAt">
<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"> <ng-container matColumnDef="fiscalYear">
<th mat-header-cell *matHeaderCellDef>Jahr</th> <th mat-header-cell *matHeaderCellDef >Jahr</th>
<td mat-cell *matCellDef="let element">{{element.rawAccountEntry.fiscal_year}}</td> <td mat-cell *matCellDef="let element">{{element.rawAccountEntry.fiscal_year}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="description"> <ng-container matColumnDef="description">
@ -45,7 +45,7 @@ Saldo: {{saldo?.saldo | number:'1.2-2'}} €
<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"> <ng-container matColumnDef="document_no">
<th mat-header-cell *matHeaderCellDef>Belegnummer</th> <th mat-header-cell *matHeaderCellDef >Belegnummer</th>
<td mat-cell *matCellDef="let element">{{element.rawAccountEntry.document_no}}</td> <td mat-cell *matCellDef="let element">{{element.rawAccountEntry.document_no}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="amount"> <ng-container matColumnDef="amount">
@ -53,11 +53,11 @@ Saldo: {{saldo?.saldo | number:'1.2-2'}} €
<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>
</ng-container> </ng-container>
<ng-container matColumnDef="category"> <ng-container matColumnDef="category">
<th mat-header-cell *matHeaderCellDef>Kategorie</th> <th mat-header-cell *matHeaderCellDef >Kategorie</th>
<td mat-cell *matCellDef="let element">{{element.accountEntryCategory}}</td> <td mat-cell *matCellDef="let element">{{element.accountEntryCategory}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="overhead_relevant"> <ng-container matColumnDef="overhead_relevant">
<th mat-header-cell *matHeaderCellDef>BK relevant</th> <th mat-header-cell *matHeaderCellDef >BK relevant</th>
<td mat-cell *matCellDef="let element">{{element.overheadRelevant}}</td> <td mat-cell *matCellDef="let element">{{element.overheadRelevant}}</td>
</ng-container> </ng-container>
<tr mat-header-row *matHeaderRowDef="accountEntriesDisplayedColumns"></tr> <tr mat-header-row *matHeaderRowDef="accountEntriesDisplayedColumns"></tr>

View File

@ -31,6 +31,7 @@ export class AccountComponent implements OnInit {
@Input() shallBeRentPayment: boolean @Input() shallBeRentPayment: boolean
@ViewChild('addAccountEntryButton') addAccountEntryButton: MatButton @ViewChild('addAccountEntryButton') addAccountEntryButton: MatButton
account: Account account: Account
accountEntries: DN_AccountEntry[] accountEntries: DN_AccountEntry[]
accountEntriesDataSource: MatTableDataSource<DN_AccountEntry> accountEntriesDataSource: MatTableDataSource<DN_AccountEntry>
@ -42,6 +43,8 @@ export class AccountComponent implements OnInit {
accountEntryCategoriesInverseMap: Map<string, AccountEntryCategory> accountEntryCategoriesInverseMap: Map<string, AccountEntryCategory>
presetFiscalYear: FormControl presetFiscalYear: FormControl
presetCategory: FormControl
presetDescription: FormControl
constructor( constructor(
private accountService: AccountService, private accountService: AccountService,
@ -96,23 +99,14 @@ export class AccountComponent implements OnInit {
let uniquenumber: UniqueNumber = await this.extApiService.getUniqueNumber(); let uniquenumber: UniqueNumber = await this.extApiService.getUniqueNumber();
this.messageService.add(`Got unique number as document_no: ${uniquenumber.number}`) this.messageService.add(`Got unique number as document_no: ${uniquenumber.number}`)
let newAccountEntry: AccountEntry = { let newAccountEntry: AccountEntry = {
description: formData.value.description, description: this.presetDescription.value,
account: this.account.id, account: this.account.id,
created_at: formData.value.createdAt, created_at: formData.value.createdAt,
fiscal_year: formData.value.fiscalYear, fiscal_year: this.presetFiscalYear.value,
amount: formData.value.amount, amount: formData.value.amount,
id: 0, id: 0,
document_no: uniquenumber.number, document_no: uniquenumber.number,
account_entry_category: 0 account_entry_category: this.presetCategory.value
}
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) }`) this.messageService.add(`addAccountEntry: ${ JSON.stringify(newAccountEntry, undefined, 4) }`)
@ -146,12 +140,28 @@ export class AccountComponent implements OnInit {
} }
private async init(): Promise<void> { private async init(): Promise<void> {
this.messageService.add(`AccountComponent.init start, account: ${this.selectedAccountId}`)
let currentDate = new Date() let currentDate = new Date()
let y = currentDate.getFullYear().toString() let y = currentDate.getFullYear()
this.presetFiscalYear = new FormControl(`${y}`) this.presetFiscalYear = new FormControl(y)
this.messageService.add(`AccountComponent.init a, account: ${this.selectedAccountId}`)
await this.getAccountEntryCategories()
if (this.shallBeRentPayment) {
this.messageService.add(`AccountComponent.init b, account: ${this.selectedAccountId}`)
this.presetCategory = new FormControl(this.accountEntryCategoriesInverseMap.get('Mietzahlung').id)
this.messageService.add(`AccountComponent.init c, account: ${this.selectedAccountId}`)
this.presetDescription = new FormControl("Miete")
this.messageService.add(`shall be rentpayment`)
this.messageService.add(`AccountComponent.init d, account: ${this.selectedAccountId}`)
} else {
this.presetCategory = new FormControl()
this.presetDescription = new FormControl()
}
this.messageService.add(`AccountComponent.init, account: ${this.selectedAccountId}`) this.messageService.add(`AccountComponent.init, account: ${this.selectedAccountId}`)
this.getAccount() this.getAccount()
await this.getAccountEntryCategories()
} }
@ -163,5 +173,4 @@ export class AccountComponent implements OnInit {
this.init() this.init()
} }
} }

View File

@ -20,11 +20,14 @@ 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'; import { LedgerComponent } from './ledger/ledger.component';
import { ContractComponent } from './contract/contract.component';
import { MyContractsComponent } from './my-contracts/my-contracts.component';
const routes: Routes = [ const routes: Routes = [
{ path: 'tenants', component: MyTenantsComponent, canActivate: [ AuthGuardService ] }, { path: 'tenants', component: MyTenantsComponent, canActivate: [ AuthGuardService ] },
{ path: 'premises', component: MyPremisesComponent, canActivate: [ AuthGuardService ] }, { path: 'premises', component: MyPremisesComponent, canActivate: [ AuthGuardService ] },
{ path: 'contracts', component: MyContractsComponent, canActivate: [ AuthGuardService ] },
{ path: 'flats', component: MyFlatsComponent, canActivate: [ AuthGuardService ] }, { path: 'flats', component: MyFlatsComponent, canActivate: [ AuthGuardService ] },
{ path: 'parkings', component: MyParkingsComponent, canActivate: [ AuthGuardService ] }, { path: 'parkings', component: MyParkingsComponent, canActivate: [ AuthGuardService ] },
{ path: 'commercialunits', component: MyCommercialUnitsComponent, canActivate: [ AuthGuardService ] }, { path: 'commercialunits', component: MyCommercialUnitsComponent, canActivate: [ AuthGuardService ] },
@ -46,6 +49,8 @@ const routes: Routes = [
{ 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: 'ledger', component: LedgerComponent, canActivate: [ AuthGuardService ] },
{ path: 'contract', component: ContractComponent, canActivate: [ AuthGuardService ] },
{ path: 'contract/:id', component: ContractComponent, 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

@ -50,6 +50,9 @@ 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 { LedgerComponent } from './ledger/ledger.component';
import { ErrorDialogComponent } from './error-dialog/error-dialog.component' import { ErrorDialogComponent } from './error-dialog/error-dialog.component'
import { MatSortModule } from '@angular/material/sort';
import { ContractComponent } from './contract/contract.component';
import { MyContractsComponent } from './my-contracts/my-contracts.component';
registerLocaleData(localeDe) registerLocaleData(localeDe)
@ -80,7 +83,9 @@ registerLocaleData(localeDe)
EnterPaymentComponent, EnterPaymentComponent,
HomeComponent, HomeComponent,
LedgerComponent, LedgerComponent,
ErrorDialogComponent ErrorDialogComponent,
ContractComponent,
MyContractsComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
@ -102,7 +107,8 @@ registerLocaleData(localeDe)
MatSelectModule, MatSelectModule,
MatDatepickerModule, MatDatepickerModule,
MatNativeDateModule, MatNativeDateModule,
MatExpansionModule MatExpansionModule,
MatSortModule
], ],
exports: [ exports: [
MatMomentDateModule MatMomentDateModule

View File

@ -1,5 +1,5 @@
// export const serviceBaseUrl = "https://api.hv.nober.de"; // export const serviceBaseUrl = "https://api.hv.nober.de";
// export const serviceBaseUrl = "http://172.16.10.38:5000"; // export const serviceBaseUrl = "http://172.16.3.96:8080";
export const serviceBaseUrl = "http://localhost:8080" export const serviceBaseUrl = "http://localhost:8080"
export const authserviceBaseUrl = "https://authservice.hottis.de" export const authserviceBaseUrl = "https://authservice.hottis.de"
export const applicationId = "hv2" export const applicationId = "hv2"

View File

@ -0,0 +1,4 @@
.notearea {
width: 75%;
}

View File

@ -0,0 +1,46 @@
<section class="mat-typography">
<mat-card class="defaultCard">
<mat-card-header>
<mat-card-title>
{{contract?.supplier}} {{contract?.content}}
</mat-card-title>
<mat-card-subtitle>
ID: {{contract?.id}}
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div>>
<form (ngSubmit)="saveContract()">
<div>
<mat-form-field appearance="outline">
<mat-label>Partner</mat-label>
<input matInput name="supplier" [(ngModel)]="contract.supplier"/>
</mat-form-field>
</div><div>
<mat-form-field appearance="outline">
<mat-label>Gegenstand</mat-label>
<input matInput name="content" [(ngModel)]="contract.content"/>
</mat-form-field>
</div><div>
<mat-form-field appearance="outline">
<mat-label>ID</mat-label>
<input matInput name="identifier" [(ngModel)]="contract.identifier"/>
</mat-form-field>
</div><div>
<mat-form-field appearance="outline" class="notearea">
<mat-label>Notizen</mat-label>
<textarea matInput
cdkTextareaAutosize
#autosize="cdkTextareaAutosize"
cdkAutosizeMinRows="5"
cdkAutosizeMaxRows="10"
name="notes" [(ngModel)]="contract.notes"></textarea>
</mat-form-field>
</div>
<button #submitButton type="submit" mat-raised-button color="primary">Speichern</button>
</form>
</div>
</mat-card-content>
</mat-card>
</section>

View File

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

View File

@ -0,0 +1,61 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatButton } from '@angular/material/button';
import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router';
import { ContractService } from '../data-object-service';
import { Contract, NULL_CommercialPremise, NULL_Contract } from '../data-objects';
import { MessageService } from '../message.service';
@Component({
selector: 'app-contract',
templateUrl: './contract.component.html',
styleUrls: ['./contract.component.css']
})
export class ContractComponent implements OnInit {
@ViewChild('submitButton') submitButton: MatButton
contract: Contract = NULL_Contract
constructor(
private contractService: ContractService,
private messageService: MessageService,
private route: ActivatedRoute,
private router: Router
) { }
async getContract(): Promise<void> {
try {
const id = +this.route.snapshot.paramMap.get('id')
if (id != 0) {
this.contract = await this.contractService.getContract(id)
}
} catch (err) {
this.messageService.add(JSON.stringify(err, undefined, 4))
}
}
async saveContract() {
try {
this.submitButton.disabled = true
this.messageService.add(`saveContract: ${ JSON.stringify(this.contract, undefined, 4) }`)
if (this.contract.id == 0) {
this.messageService.add("about to insert new contract")
this.contract = await this.contractService.postContract(this.contract)
this.messageService.add(`Successfully added contract with id ${this.contract.id}`)
} else {
this.messageService.add("about to update existing contract")
this.contract = await this.contractService.putContract(this.contract)
this.messageService.add(`Successfully changed contract with id ${this.contract.id}`)
}
this.router.navigate(['/contracts'])
} finally {
this.submitButton.disabled = false
}
}
ngOnInit(): void {
this.getContract()
}
}

View File

@ -27,6 +27,7 @@ import { TenancyFeeMapping } from './data-objects';
import { AccountEntryCategory } from './data-objects'; import { AccountEntryCategory } from './data-objects';
import { AccountEntry } from './data-objects'; import { AccountEntry } from './data-objects';
import { Note } from './data-objects'; import { Note } from './data-objects';
import { Contract } from './data-objects';
@ -519,4 +520,35 @@ export class NoteService {
} }
@Injectable({ providedIn: 'root' })
export class ContractService {
constructor(private messageService: MessageService, private http: HttpClient) { }
async getContracts(): Promise<Contract[]> {
this.messageService.add(`ContractService: get data`);
return this.http.get<Contract[]>(`${serviceBaseUrl}/v1/contracts`).toPromise()
}
async getContract(id: number): Promise<Contract> {
this.messageService.add(`ContractService: get data for ${id}`);
return this.http.get<Contract>(`${serviceBaseUrl}/v1/contracts/${id}`).toPromise()
}
async postContract(item: Contract): Promise<Contract> {
let itemStr: string = JSON.stringify(item, undefined, 4)
this.messageService.add(`ContractService: post data for ${itemStr}`);
return this.http.post<Contract>(`${serviceBaseUrl}/v1/contracts`, item).toPromise()
}
async putContract(item: Contract): Promise<Contract> {
let itemStr: string = JSON.stringify(item, undefined, 4)
this.messageService.add(`ContractService: put data for ${itemStr}`)
let id: number = item["id"]
return this.http.put<Contract>(`${serviceBaseUrl}/v1/contracts/${id}`, item).toPromise()
}
}

View File

@ -226,4 +226,19 @@ export const NULL_Note: Note = {
,note: '' ,note: ''
} }
export interface Contract {
id: number
supplier: string
content: string
identifier: string
notes: string
}
export const NULL_Contract: Contract = {
id: 0
,supplier: ''
,content: ''
,identifier: ''
,notes: ''
}

View File

@ -5,30 +5,16 @@
</mat-card-title> </mat-card-title>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<mat-accordion> <div>
<mat-expansion-panel (opened)="collapseExpenseDetails = true" <span>Konto auswählen: </span>
(closed)="collapseExpenseDetails = false"> <mat-form-field appearance="outline">
<mat-expansion-panel-header> <mat-select #mapSelect [(ngModel)]="accountId" name="account">
<mat-panel-title> <mat-label>Mieter</mat-label>
Ausgaben <mat-option *ngFor="let p of accounts" [value]="p.id">{{p.description}}</mat-option>
</mat-panel-title> </mat-select>
<mat-panel-description> </mat-form-field>
<div>Betriebskosten-relevante Ausgaben nicht hier sondern im Betriebskostenkonto unter "Meine Häuser" erfassen.</div> </div>
</mat-panel-description>
</mat-expansion-panel-header> <app-account [selectedAccountId]="accountId" [shallBeRentPayment]="false"></app-account>
<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-content>
</mat-card> </mat-card>

View File

@ -1,5 +1,6 @@
import { Component, OnInit, ViewChild } from '@angular/core'; import { Component, OnInit, ViewChild } from '@angular/core';
import { AccountComponent } from '../account/account.component'; import { AccountComponent } from '../account/account.component';
import { AccountService } from '../data-object-service';
import { Account } from '../data-objects'; import { Account } from '../data-objects';
import { ExtApiService } from '../ext-data-object-service'; import { ExtApiService } from '../ext-data-object-service';
import { MessageService } from '../message.service'; import { MessageService } from '../message.service';
@ -11,29 +12,23 @@ import { MessageService } from '../message.service';
}) })
export class LedgerComponent implements OnInit { export class LedgerComponent implements OnInit {
incomeAccount: Account accounts: Account[]
incomeAccountId: number accountId: number
expenseAccount: Account
expenseAccountId: number
collapseIncomeDetails: boolean = false
collapseExpenseDetails: boolean = false
@ViewChild('incomeAccountComponent') incomeAccountComponent: AccountComponent @ViewChild('accountComponent') accountComponent: AccountComponent
@ViewChild('expenseAccountComponent') expenseAccountComponent: AccountComponent
constructor( constructor(
private extApiService: ExtApiService, private accountService: AccountService,
private messageService: MessageService private messageService: MessageService
) { } ) { }
async getAccount(): Promise<void> { async getAccount(): Promise<void> {
try { try {
this.messageService.add("Trying to load ledger account") this.messageService.add("Trying to load accounts")
this.incomeAccount = await this.extApiService.getAccountByDescription('LedgerIncome') this.accounts = await this.accountService.getAccounts()
this.expenseAccount = await this.extApiService.getAccountByDescription('LedgerExpense') this.messageService.add("Accounts loaded")
this.messageService.add("Account loaded")
} catch (err) { } catch (err) {
this.messageService.add(JSON.stringify(err, undefined, 4)) this.messageService.add(JSON.stringify(err, undefined, 4))
} }
@ -41,8 +36,6 @@ export class LedgerComponent implements OnInit {
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
await this.getAccount() await this.getAccount()
this.incomeAccountId = this.incomeAccount.id
this.expenseAccountId = this.expenseAccount.id
} }
} }

View File

@ -0,0 +1,31 @@
<section class="mat-typography">
<mat-card class="defaultCard">
<mat-card-header>
<mat-card-title>
<span>Meine Verträge</span>
<span class="spacer"></span>
<a mat-button routerLink="/contract">Neu anlegen</a>
</mat-card-title>
</mat-card-header>
<mat-card-content>
<div>
<table mat-table [dataSource]="dataSource" #zftable>
<ng-container matColumnDef="supplier">
<th mat-header-cell *matHeaderCellDef>Partner</th>
<td mat-cell *matCellDef="let element">{{element.supplier}}</td>
</ng-container>
<ng-container matColumnDef="content">
<th mat-header-cell *matHeaderCellDef>Gegenstand</th>
<td mat-cell *matCellDef="let element">{{element.content}}</td>
</ng-container>
<ng-container matColumnDef="identifier">
<th mat-header-cell *matHeaderCellDef>ID</th>
<td mat-cell *matCellDef="let element">{{element.identifier}}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" [routerLink]="['/contract/', row.id]"></tr>
</table>
</div>
</mat-card-content>
</mat-card>
</section>

View File

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

View File

@ -0,0 +1,38 @@
import { Component, OnInit } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { ContractService } from '../data-object-service';
import { Contract } from '../data-objects';
import { MessageService } from '../message.service';
@Component({
selector: 'app-my-contracts',
templateUrl: './my-contracts.component.html',
styleUrls: ['./my-contracts.component.css']
})
export class MyContractsComponent implements OnInit {
contracts: Contract[]
dataSource: MatTableDataSource<Contract>
displayedColumns: string[] = [ "supplier", "content", "identifier" ]
constructor(private contractService: ContractService, private messageService: MessageService) { }
async getContracts(): Promise<void> {
try {
this.messageService.add("Trying to load contracts")
this.contracts = await this.contractService.getContracts();
this.messageService.add("Contracts loaded")
this.dataSource = new MatTableDataSource<Contract>(this.contracts)
} catch (err) {
this.messageService.add(JSON.stringify(err, undefined, 4))
}
}
ngOnInit(): void {
this.messageService.add("MyContractsComponent.ngOnInit")
this.getContracts()
}
}

View File

@ -22,6 +22,8 @@
<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"> </mat-nav-list><mat-divider *ngIf="authenticated"></mat-divider><mat-nav-list *ngIf="authenticated">
<a mat-list-item href="/ledger">Buchführung</a> <a mat-list-item href="/ledger">Buchführung</a>
</mat-nav-list><mat-divider *ngIf="authenticated"></mat-divider><mat-nav-list *ngIf="authenticated">
<a mat-list-item href="/contracts">Verträge</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>