diff --git a/.gitignore b/.gitignore index b2f020e..065b201 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ cli/*.pdf cli/*.aux .DS_Store .Rproj.user +.venv diff --git a/api/Dockerfile b/api/Dockerfile index de86b6b..0db7997 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -11,19 +11,6 @@ ARG CONF_DIR="${APP_DIR}/config" RUN \ apt update && \ 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 ${CONF_DIR} && \ useradd -d ${APP_DIR} -u 1000 user @@ -31,11 +18,15 @@ RUN \ COPY *.py ${APP_DIR}/ COPY openapi.yaml ${APP_DIR}/ COPY methods.py ${APP_DIR}/ +COPY requirements.txt ${APP_DIR}/ COPY server.ini ${CONF_DIR}/ WORKDIR ${APP_DIR} VOLUME ${CONF_DIR} +RUN \ + pip3 install -r requirements.txt + USER 1000:1000 EXPOSE 5000 diff --git a/api/methods.py b/api/methods.py index ba49384..9a2145f 100644 --- a/api/methods.py +++ b/api/methods.py @@ -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)) + + diff --git a/api/openapi.yaml b/api/openapi.yaml index 1fb1bed..896b231 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -1440,6 +1440,92 @@ paths: $ref: '#/components/schemas/note' security: - 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 @@ -1818,6 +1904,21 @@ components: type: integer note: 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 diff --git a/api/requirements.txt b/api/requirements.txt new file mode 100644 index 0000000..cd73637 --- /dev/null +++ b/api/requirements.txt @@ -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 diff --git a/cli/accountStatement b/cli/accountStatement new file mode 100644 index 0000000..e69de29 diff --git a/cli/accountStatement.tmpl b/cli/accountStatement.tmplx similarity index 100% rename from cli/accountStatement.tmpl rename to cli/accountStatement.tmplx diff --git a/cli/betriebskostenuebersicht.tmpl b/cli/betriebskostenuebersicht.tmplx similarity index 100% rename from cli/betriebskostenuebersicht.tmpl rename to cli/betriebskostenuebersicht.tmplx diff --git a/cli/jahresabrechnung.tmpl b/cli/jahresabrechnung.tmplx similarity index 100% rename from cli/jahresabrechnung.tmpl rename to cli/jahresabrechnung.tmplx diff --git a/r-scripts/hv2-analysis/.Rhistory b/r-scripts/hv2-analysis/.Rhistory index 7d1ea50..8e9f8b3 100644 --- a/r-scripts/hv2-analysis/.Rhistory +++ b/r-scripts/hv2-analysis/.Rhistory @@ -103,3 +103,134 @@ 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) diff --git a/r-scripts/hv2-analysis/income-expense-analysis.Rmd b/r-scripts/hv2-analysis/income-expense-analysis.Rmd new file mode 100644 index 0000000..9b8cac4 --- /dev/null +++ b/r-scripts/hv2-analysis/income-expense-analysis.Rmd @@ -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) +``` diff --git a/r-scripts/hv2-analysis/income-expense-analysis.docx b/r-scripts/hv2-analysis/income-expense-analysis.docx new file mode 100644 index 0000000..36f8fb8 Binary files /dev/null and b/r-scripts/hv2-analysis/income-expense-analysis.docx differ diff --git a/r-scripts/hv2-analysis/income-expense-analysis.html b/r-scripts/hv2-analysis/income-expense-analysis.html new file mode 100644 index 0000000..2a0e2da --- /dev/null +++ b/r-scripts/hv2-analysis/income-expense-analysis.html @@ -0,0 +1,720 @@ + + + + + + + + + + + + + + + +HV2 Jahresabrechnung + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+

Übersicht über die Einnahmen

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
flatpremiseMieteBetriebskosten
1. OG linksHaus Heisinger Str. 3463736.081195.05
1. OG mitteHaus Heisinger Str. 3463050.64975.79
1. OG rechtsHaus Heisinger Str. 3463714.601188.19
2. OG linksHaus Heisinger Str. 3465478.121833.53
2. OG rechtsHaus Heisinger Str. 3465690.881724.84
EG linksHaus Heisinger Str. 3463736.081195.05
EG mitteHaus Heisinger Str. 3463050.64975.79
EG rechtsHaus Heisinger Str. 3463714.601188.67
1. OG linksHaus Hemsingskotten 383701.401195.57
1. OG mitteHaus Hemsingskotten 383110.881004.83
1. OG rechtsHaus Hemsingskotten 383701.401195.57
2. OG linksHaus Hemsingskotten 385826.001799.20
2. OG rechtsHaus Hemsingskotten 385466.001799.33
EG linksHaus Hemsingskotten 383701.401195.57
EG mitteHaus Hemsingskotten 383470.881004.83
EG rechtsHaus Hemsingskotten 383701.401195.57
EG linksWohnung Weststrasse 194964.280.00
+
+
+

Übersicht über die Ausgaben

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
overhead_relevantcategoryamount
FALSEBeerdigung Papa0.00
FALSEBetriebskostenausgleich-339.54
FALSEGründung der GbR-12379.00
FALSEInstandhaltung-2652.59
FALSEKontoführungsgebühr-465.29
FALSENebenkosten Eva-Maria Nober-4291.15
FALSEnicht abrechenbare Positionen-820.56
FALSERechtschutzversicherung-3749.83
FALSESchuldendienst-24234.22
FALSEsonstiges-1649.00
FALSESteuern-5395.00
FALSEVerwaltung-1110.01
TRUE1. Grundsteuer-5066.59
TRUE2. Müllabfuhr-2498.40
TRUE3. Straßenreinigung u. Winterdienst-317.40
TRUE4. Entwässerung-5871.54
TRUE5. Wasserversorgung-3274.34
TRUE9. Gartenpflege-1880.84
TRUE10. Beleuchtung-315.01
TRUE12. Sach- u. Haftpflichtversicherung-5282.91
TRUE17. Sonstiges (Wartung Rauchmelder)-835.73
+
+
+

Zusammenfassung

+
+

Einnahmen

+ + + + + + + + + + + + + + + + + + + + + +
KategorieBetrag
Mieteinnahmen69815.28
Betriebskostenvorauszahlungen20667.38
Einnahmen ingesamt90482.66
+
+
+

Ausgaben

+ + + + + + + + + + + + + + + + + +
KategorieBetrag
Ausgaben insgesamt82428.95
davon Betriebskostenausgaben25342.76
+
+
+

Überschuss

+ + + + + + + + + + + +
Überschuss
8053.71
+
+
+ + + + +
+ + + + + + + + + + + + + + + diff --git a/r-scripts/hv2-analysis/income-expense-analysis.pdf b/r-scripts/hv2-analysis/income-expense-analysis.pdf new file mode 100644 index 0000000..55cd33b Binary files /dev/null and b/r-scripts/hv2-analysis/income-expense-analysis.pdf differ diff --git a/readme.md b/readme.md index 62656e8..0824474 100644 --- a/readme.md +++ b/readme.md @@ -2,6 +2,7 @@ ## 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 * 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 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b191199 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +Cheetah3==3.2.6.post1 diff --git a/schema.json b/schema.json index 816d4ce..f22ccee 100644 --- a/schema.json +++ b/schema.json @@ -157,6 +157,19 @@ { "name": "tenant", "sqltype": "integer", "notnull": true, "foreignkey": 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)" + ] } ] } diff --git a/schema/account-statement.sql b/schema/account-statement.sql index 8072c99..662bf5d 100644 --- a/schema/account-statement.sql +++ b/schema/account-statement.sql @@ -88,4 +88,5 @@ 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; + aec.description +order by aec.overhead_relevant; diff --git a/schema/changes-contract.sql b/schema/changes-contract.sql new file mode 100644 index 0000000..73e9596 --- /dev/null +++ b/schema/changes-contract.sql @@ -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; diff --git a/schema/create.sql b/schema/create.sql index 30b50af..09ca34f 100644 --- a/schema/create.sql +++ b/schema/create.sql @@ -172,6 +172,18 @@ CREATE TABLE note_t ( GRANT SELECT, INSERT ON note_t 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; + diff --git a/ui/hv2-ui/package-lock.json b/ui/hv2-ui/package-lock.json index 5fc3a32..8cfc893 100644 --- a/ui/hv2-ui/package-lock.json +++ b/ui/hv2-ui/package-lock.json @@ -33,8 +33,6 @@ "@types/jasmine": "~3.6.0", "@types/node": "^12.11.1", "codelyzer": "^6.0.0", - "jasmine-core": "~3.6.0", - "jasmine-spec-reporter": "~5.0.0", "karma": "~5.1.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.0.3", @@ -3588,6 +3586,16 @@ "node": ">=8" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/blob": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", @@ -6612,6 +6620,13 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -8575,19 +8590,11 @@ } }, "node_modules/jasmine-core": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.6.0.tgz", - "integrity": "sha512-8uQYa7zJN8hq9z+g8z1bqCfdC8eoDAeVnM5sfqs7KHv9/ifoJ500m018fpFc7RDaO6SWCLCXwo/wPSNcdYTgcw==", - "dev": true - }, - "node_modules/jasmine-spec-reporter": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-5.0.2.tgz", - "integrity": "sha512-6gP1LbVgJ+d7PKksQBc2H0oDGNRQI3gKUsWlswKaQ2fif9X5gzhQcgM5+kiJGCQVurOG09jqNhk7payggyp5+g==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.5.0.tgz", + "integrity": "sha512-9PMzyvhtocxb3aXJVOPqBDswdgyAeSB81QnLop4npOpbqnheaTEwPc9ZloQeVswugPManznQBjD8kWDTjlnHuw==", "dev": true, - "dependencies": { - "colors": "1.4.0" - } + "peer": true }, "node_modules/jasmine/node_modules/jasmine-core": { "version": "2.8.0", @@ -8889,6 +8896,12 @@ "karma-jasmine": ">=1.1" } }, + "node_modules/karma-jasmine/node_modules/jasmine-core": { + "version": "3.99.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.99.1.tgz", + "integrity": "sha512-Hu1dmuoGcZ7AfyynN3LsfruwMbxMALMka+YtZeGoLuDEySVmVAPaonkNoBRIw/ectu8b9tVQCJNgp4a4knp+tg==", + "dev": true + }, "node_modules/karma-source-map-support": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", @@ -9980,6 +9993,13 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "node_modules/nan": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", + "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", + "dev": true, + "optional": true + }, "node_modules/nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -19949,6 +19969,16 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "blob": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", @@ -22456,6 +22486,13 @@ } } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -23973,19 +24010,11 @@ } }, "jasmine-core": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.6.0.tgz", - "integrity": "sha512-8uQYa7zJN8hq9z+g8z1bqCfdC8eoDAeVnM5sfqs7KHv9/ifoJ500m018fpFc7RDaO6SWCLCXwo/wPSNcdYTgcw==", - "dev": true - }, - "jasmine-spec-reporter": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-5.0.2.tgz", - "integrity": "sha512-6gP1LbVgJ+d7PKksQBc2H0oDGNRQI3gKUsWlswKaQ2fif9X5gzhQcgM5+kiJGCQVurOG09jqNhk7payggyp5+g==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.5.0.tgz", + "integrity": "sha512-9PMzyvhtocxb3aXJVOPqBDswdgyAeSB81QnLop4npOpbqnheaTEwPc9ZloQeVswugPManznQBjD8kWDTjlnHuw==", "dev": true, - "requires": { - "colors": "1.4.0" - } + "peer": true }, "jasminewd2": { "version": "2.2.0", @@ -24354,6 +24383,14 @@ "dev": true, "requires": { "jasmine-core": "^3.6.0" + }, + "dependencies": { + "jasmine-core": { + "version": "3.99.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.99.1.tgz", + "integrity": "sha512-Hu1dmuoGcZ7AfyynN3LsfruwMbxMALMka+YtZeGoLuDEySVmVAPaonkNoBRIw/ectu8b9tVQCJNgp4a4knp+tg==", + "dev": true + } } }, "karma-jasmine-html-reporter": { @@ -25101,6 +25138,13 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "nan": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", + "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", + "dev": true, + "optional": true + }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", diff --git a/ui/hv2-ui/package.json b/ui/hv2-ui/package.json index 23de218..9bd5017 100644 --- a/ui/hv2-ui/package.json +++ b/ui/hv2-ui/package.json @@ -37,8 +37,6 @@ "@types/jasmine": "~3.6.0", "@types/node": "^12.11.1", "codelyzer": "^6.0.0", - "jasmine-core": "~3.6.0", - "jasmine-spec-reporter": "~5.0.0", "karma": "~5.1.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.0.3", diff --git a/ui/hv2-ui/src/app/app-routing.module.ts b/ui/hv2-ui/src/app/app-routing.module.ts index e2865fe..e521dfd 100644 --- a/ui/hv2-ui/src/app/app-routing.module.ts +++ b/ui/hv2-ui/src/app/app-routing.module.ts @@ -20,11 +20,14 @@ import { FeeDetailsComponent } from './fee-details/fee-details.component'; import { EnterPaymentComponent } from './enter-payment/enter-payment.component'; import { HomeComponent } from './home/home.component'; import { LedgerComponent } from './ledger/ledger.component'; +import { ContractComponent } from './contract/contract.component'; +import { MyContractsComponent } from './my-contracts/my-contracts.component'; const routes: Routes = [ { path: 'tenants', component: MyTenantsComponent, canActivate: [ AuthGuardService ] }, { path: 'premises', component: MyPremisesComponent, canActivate: [ AuthGuardService ] }, + { path: 'contracts', component: MyContractsComponent, canActivate: [ AuthGuardService ] }, { path: 'flats', component: MyFlatsComponent, canActivate: [ AuthGuardService ] }, { path: 'parkings', component: MyParkingsComponent, canActivate: [ AuthGuardService ] }, { path: 'commercialunits', component: MyCommercialUnitsComponent, canActivate: [ AuthGuardService ] }, @@ -46,6 +49,8 @@ const routes: Routes = [ { path: 'fee', component: FeeDetailsComponent, canActivate: [ AuthGuardService ] }, { path: 'enterPayment', component: EnterPaymentComponent, 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: 'logout', component: LogoutComponent }, { path: 'login', component: LoginComponent }, diff --git a/ui/hv2-ui/src/app/app.module.ts b/ui/hv2-ui/src/app/app.module.ts index e5439aa..f38747e 100644 --- a/ui/hv2-ui/src/app/app.module.ts +++ b/ui/hv2-ui/src/app/app.module.ts @@ -51,6 +51,8 @@ import { HomeComponent } from './home/home.component'; import { LedgerComponent } from './ledger/ledger.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) @@ -81,7 +83,9 @@ registerLocaleData(localeDe) EnterPaymentComponent, HomeComponent, LedgerComponent, - ErrorDialogComponent + ErrorDialogComponent, + ContractComponent, + MyContractsComponent ], imports: [ BrowserModule, diff --git a/ui/hv2-ui/src/app/config.ts b/ui/hv2-ui/src/app/config.ts index b20965b..102eb6f 100644 --- a/ui/hv2-ui/src/app/config.ts +++ b/ui/hv2-ui/src/app/config.ts @@ -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.3.96:8080"; -// export const serviceBaseUrl = "http://localhost:8080" +export const serviceBaseUrl = "http://localhost:8080" export const authserviceBaseUrl = "https://authservice.hottis.de" export const applicationId = "hv2" diff --git a/ui/hv2-ui/src/app/contract/contract.component.css b/ui/hv2-ui/src/app/contract/contract.component.css new file mode 100644 index 0000000..b4f7195 --- /dev/null +++ b/ui/hv2-ui/src/app/contract/contract.component.css @@ -0,0 +1,4 @@ + +.notearea { + width: 75%; +} \ No newline at end of file diff --git a/ui/hv2-ui/src/app/contract/contract.component.html b/ui/hv2-ui/src/app/contract/contract.component.html new file mode 100644 index 0000000..2744eb0 --- /dev/null +++ b/ui/hv2-ui/src/app/contract/contract.component.html @@ -0,0 +1,46 @@ +
+ + + + {{contract?.supplier}} {{contract?.content}} + + + ID: {{contract?.id}} + + + +
> +
+
+ + Partner + + +
+ + Gegenstand + + +
+ + ID + + +
+ + Notizen + + +
+ +
+
+ +
+
+
diff --git a/ui/hv2-ui/src/app/contract/contract.component.spec.ts b/ui/hv2-ui/src/app/contract/contract.component.spec.ts new file mode 100644 index 0000000..425b908 --- /dev/null +++ b/ui/hv2-ui/src/app/contract/contract.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ContractComponent } from './contract.component'; + +describe('ContractComponent', () => { + let component: ContractComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ContractComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ContractComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ui/hv2-ui/src/app/contract/contract.component.ts b/ui/hv2-ui/src/app/contract/contract.component.ts new file mode 100644 index 0000000..76a1b5a --- /dev/null +++ b/ui/hv2-ui/src/app/contract/contract.component.ts @@ -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 { + 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() + } + +} diff --git a/ui/hv2-ui/src/app/data-object-service.ts b/ui/hv2-ui/src/app/data-object-service.ts index 13a5dba..38cd57c 100644 --- a/ui/hv2-ui/src/app/data-object-service.ts +++ b/ui/hv2-ui/src/app/data-object-service.ts @@ -27,6 +27,7 @@ import { TenancyFeeMapping } from './data-objects'; import { AccountEntryCategory } from './data-objects'; import { AccountEntry } 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 { + this.messageService.add(`ContractService: get data`); + return this.http.get(`${serviceBaseUrl}/v1/contracts`).toPromise() + } + + async getContract(id: number): Promise { + this.messageService.add(`ContractService: get data for ${id}`); + return this.http.get(`${serviceBaseUrl}/v1/contracts/${id}`).toPromise() + } + + async postContract(item: Contract): Promise { + let itemStr: string = JSON.stringify(item, undefined, 4) + this.messageService.add(`ContractService: post data for ${itemStr}`); + return this.http.post(`${serviceBaseUrl}/v1/contracts`, item).toPromise() + } + + async putContract(item: Contract): Promise { + 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(`${serviceBaseUrl}/v1/contracts/${id}`, item).toPromise() + } + + + +} + diff --git a/ui/hv2-ui/src/app/data-objects.ts b/ui/hv2-ui/src/app/data-objects.ts index 4730720..0582c9b 100644 --- a/ui/hv2-ui/src/app/data-objects.ts +++ b/ui/hv2-ui/src/app/data-objects.ts @@ -226,4 +226,19 @@ export const NULL_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: '' +} + diff --git a/ui/hv2-ui/src/app/my-contracts/my-contracts.component.css b/ui/hv2-ui/src/app/my-contracts/my-contracts.component.css new file mode 100644 index 0000000..e69de29 diff --git a/ui/hv2-ui/src/app/my-contracts/my-contracts.component.html b/ui/hv2-ui/src/app/my-contracts/my-contracts.component.html new file mode 100644 index 0000000..250ea23 --- /dev/null +++ b/ui/hv2-ui/src/app/my-contracts/my-contracts.component.html @@ -0,0 +1,31 @@ +
+ + + + Meine Verträge + + Neu anlegen + + + +
+ + + + + + + + + + + + + + + +
Partner{{element.supplier}}Gegenstand{{element.content}}ID{{element.identifier}}
+
+
+
+
\ No newline at end of file diff --git a/ui/hv2-ui/src/app/my-contracts/my-contracts.component.spec.ts b/ui/hv2-ui/src/app/my-contracts/my-contracts.component.spec.ts new file mode 100644 index 0000000..394901e --- /dev/null +++ b/ui/hv2-ui/src/app/my-contracts/my-contracts.component.spec.ts @@ -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; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ MyContractsComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MyContractsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ui/hv2-ui/src/app/my-contracts/my-contracts.component.ts b/ui/hv2-ui/src/app/my-contracts/my-contracts.component.ts new file mode 100644 index 0000000..b322429 --- /dev/null +++ b/ui/hv2-ui/src/app/my-contracts/my-contracts.component.ts @@ -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 + displayedColumns: string[] = [ "supplier", "content", "identifier" ] + + constructor(private contractService: ContractService, private messageService: MessageService) { } + + async getContracts(): Promise { + try { + this.messageService.add("Trying to load contracts") + this.contracts = await this.contractService.getContracts(); + this.messageService.add("Contracts loaded") + + this.dataSource = new MatTableDataSource(this.contracts) + } catch (err) { + this.messageService.add(JSON.stringify(err, undefined, 4)) + } + } + + ngOnInit(): void { + this.messageService.add("MyContractsComponent.ngOnInit") + this.getContracts() + } + + +} diff --git a/ui/hv2-ui/src/app/navigation/navigation.component.html b/ui/hv2-ui/src/app/navigation/navigation.component.html index 7cb2ba7..13a6c84 100644 --- a/ui/hv2-ui/src/app/navigation/navigation.component.html +++ b/ui/hv2-ui/src/app/navigation/navigation.component.html @@ -22,6 +22,8 @@ Meine Häuser Buchführung + + Verträge Abmelden