From 75baf9b923e15d74149dafc0d8564a5a3b5a8d8c Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Tue, 10 Dec 2024 10:01:34 +0000 Subject: [PATCH 01/30] chore: update dependencies --- Gemfile.lock | 8 +- package.json | 8 +- yarn.lock | 671 ++++++++++++++++++++++++++------------------------- 3 files changed, 351 insertions(+), 336 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 313a63f73..4840bfbfb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -84,7 +84,7 @@ GEM rake (>= 0.8.7) ast (2.4.2) aws-eventstream (1.3.0) - aws-partitions (1.1018.0) + aws-partitions (1.1019.0) aws-sdk-core (3.214.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -225,7 +225,7 @@ GEM railties (>= 5.0.0) faker (3.5.1) i18n (>= 1.8.11, < 2) - faraday (2.12.1) + faraday (2.12.2) faraday-net_http (>= 2.0, < 3.5) json logger @@ -318,7 +318,7 @@ GEM net-smtp (0.5.0) net-protocol nio4r (2.7.4) - nokogiri (1.16.8-x86_64-linux) + nokogiri (1.17.0-x86_64-linux) racc (~> 1.4) orm_adapter (0.5.0) ostruct (0.6.1) @@ -514,7 +514,7 @@ GEM actionpack (>= 3.1) railties (>= 3.1) slim (>= 3.0, < 6.0, != 5.0.0) - sorbet-runtime (0.5.11681) + sorbet-runtime (0.5.11690) squasher (0.8.0) statesman (12.1.0) statsd-ruby (1.5.0) diff --git a/package.json b/package.json index caf6e5d46..55c70dbc0 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,8 @@ "@modyfi/vite-plugin-yaml": "^1.1.0", "@popperjs/core": "^2.11.4", "@rails/ujs": "^7.0.2-2", - "@types/react": "^18.3.1", - "@types/react-dom": "^18.3.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.2.1", "axios": "^1.2.0", "bootstrap": "^5.3.2", @@ -25,9 +25,9 @@ "postcss-import": "^16.1.0", "postcss-preset-env": "^10.0.0", "prop-types": "^15.8.1", - "react": "^18.3.1", + "react": "^19.0.0", "react-bootstrap": "^2.3.1", - "react-dom": "^18.3.1", + "react-dom": "^19.0.0", "react-hook-form": "^7.51.3", "react-i18next": "^15.0.0", "react-sortablejs": "^6.1.4", diff --git a/yarn.lock b/yarn.lock index cf97d0ec7..c50a27d39 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,7 +10,7 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0", "@babel/code-frame@^7.26.2": version "7.26.2" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== @@ -20,9 +20,9 @@ picocolors "^1.0.0" "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.25.9", "@babel/compat-data@^7.26.0": - version "7.26.2" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.2.tgz#278b6b13664557de95b8f35b90d96785850bb56e" - integrity sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg== + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.3.tgz#99488264a56b2aded63983abd6a417f03b92ed02" + integrity sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g== "@babel/core@7.26.0", "@babel/core@^7.26.0": version "7.26.0" @@ -45,13 +45,13 @@ json5 "^2.2.3" semver "^6.3.1" -"@babel/generator@^7.25.9", "@babel/generator@^7.26.0": - version "7.26.2" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.2.tgz#87b75813bec87916210e5e01939a4c823d6bb74f" - integrity sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw== +"@babel/generator@^7.26.0", "@babel/generator@^7.26.3": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.3.tgz#ab8d4360544a425c90c248df7059881f4b2ce019" + integrity sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ== dependencies: - "@babel/parser" "^7.26.2" - "@babel/types" "^7.26.0" + "@babel/parser" "^7.26.3" + "@babel/types" "^7.26.3" "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.25" jsesc "^3.0.2" @@ -63,14 +63,6 @@ dependencies: "@babel/types" "^7.25.9" -"@babel/helper-builder-binary-assignment-operator-visitor@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.25.9.tgz#f41752fe772a578e67286e6779a68a5a92de1ee9" - integrity sha512-C47lC7LIDCnz0h4vai/tpNOI95tCd5ZT3iBt/DBH5lXKHZsyNQv18yf1wIIg2ntiQNgmAvA+DgZ82iW8Qdym8g== - dependencies: - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" - "@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz#55af025ce365be3cdc0c1c1e56c6af617ce88875" @@ -96,12 +88,12 @@ semver "^6.3.1" "@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.9.tgz#3e8999db94728ad2b2458d7a470e7770b7764e26" - integrity sha512-ORPNZ3h6ZRkOyAa/SaHU+XsLZr0UQzRwuDQ0cczIA17nAzZ+85G5cVkOJIj7QavLZGSe8QXUmNFxSZzjcZF9bw== + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz#5169756ecbe1d95f7866b90bb555b022595302a0" + integrity sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong== dependencies: "@babel/helper-annotate-as-pure" "^7.25.9" - regexpu-core "^6.1.1" + regexpu-core "^6.2.0" semver "^6.3.1" "@babel/helper-define-polyfill-provider@^0.6.2", "@babel/helper-define-polyfill-provider@^0.6.3": @@ -170,14 +162,6 @@ "@babel/helper-optimise-call-expression" "^7.25.9" "@babel/traverse" "^7.25.9" -"@babel/helper-simple-access@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.25.9.tgz#6d51783299884a2c74618d6ef0f86820ec2e7739" - integrity sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q== - dependencies: - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" - "@babel/helper-skip-transparent-expression-wrappers@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz#0b2e1b62d560d6b1954893fd2b705dc17c91f0c9" @@ -218,12 +202,12 @@ "@babel/template" "^7.25.9" "@babel/types" "^7.26.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.2": - version "7.26.2" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.2.tgz#fd7b6f487cfea09889557ef5d4eeb9ff9a5abd11" - integrity sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ== +"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.3": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.3.tgz#8c51c5db6ddf08134af1ddbacf16aaab48bac234" + integrity sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA== dependencies: - "@babel/types" "^7.26.0" + "@babel/types" "^7.26.3" "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.9": version "7.25.9" @@ -418,11 +402,10 @@ "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-exponentiation-operator@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.25.9.tgz#ece47b70d236c1d99c263a1e22b62dc20a4c8b0f" - integrity sha512-KRhdhlVk2nObA5AYa7QMgTMTVJdfHprfpAk4DjZVtllqRg9qarilstTKEhpVjyt+Npi8ThRyiV8176Am3CodPA== + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz#e29f01b6de302c7c2c794277a48f04a9ca7f03bc" + integrity sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ== dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.25.9" "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-export-namespace-from@^7.25.9": @@ -486,13 +469,12 @@ "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-modules-commonjs@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.9.tgz#d165c8c569a080baf5467bda88df6425fc060686" - integrity sha512-dwh2Ol1jWwL2MgkCzUSOvfmKElqQcuswAZypBSUsScMXvgdT8Ekq5YA6TtqpTVWH+4903NmboMuH1o9i8Rxlyg== + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz#8f011d44b20d02c3de44d8850d971d8497f981fb" + integrity sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ== dependencies: - "@babel/helper-module-transforms" "^7.25.9" + "@babel/helper-module-transforms" "^7.26.0" "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-simple-access" "^7.25.9" "@babel/plugin-transform-modules-systemjs@^7.25.9": version "7.25.9" @@ -723,9 +705,9 @@ "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-typescript@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.9.tgz#69267905c2b33c2ac6d8fe765e9dc2ddc9df3849" - integrity sha512-7PbZQZP50tzv2KGGnhh82GSyMB01yKY9scIjf1a+GfZCtInOWqUH5+1EBU4t9fyR5Oykkkc9vFTs4OHrhHXljQ== + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.26.3.tgz#3d6add9c78735623317387ee26d5ada540eee3fd" + integrity sha512-6+5hpdr6mETwSKjmJUdYw0EIkATiQhnELWlE3kJFBwSg/BGIVwVaVbX+gOXBCdc7Ln1RXZxyWGecIXhUfnl7oA== dependencies: "@babel/helper-annotate-as-pure" "^7.25.9" "@babel/helper-create-class-features-plugin" "^7.25.9" @@ -849,9 +831,9 @@ esutils "^2.0.2" "@babel/preset-react@^7.18.6", "@babel/preset-react@^7.22.5": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.25.9.tgz#5f473035dc2094bcfdbc7392d0766bd42dce173e" - integrity sha512-D3to0uSPiWE7rBrdIICCd0tJSIGpLaaGptna2+w7Pft5xMqLpA1sz99DK5TZ1TjGbdQ/VI1eCSZ06dv3lT4JOw== + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.26.3.tgz#7c5e028d623b4683c1f83a0bd4713b9100560caa" + integrity sha512-Nl03d6T9ky516DGK2YMxrTqvnpUW63TnJMOMonj+Zae0JiPC5BC9xPMSL6L8fiSpA5vP88qfygavVQvnLp+6Cw== dependencies: "@babel/helper-plugin-utils" "^7.25.9" "@babel/helper-validator-option" "^7.25.9" @@ -888,22 +870,22 @@ "@babel/types" "^7.25.9" "@babel/traverse@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84" - integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw== + version "7.26.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.4.tgz#ac3a2a84b908dde6d463c3bfa2c5fdc1653574bd" + integrity sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w== dependencies: - "@babel/code-frame" "^7.25.9" - "@babel/generator" "^7.25.9" - "@babel/parser" "^7.25.9" + "@babel/code-frame" "^7.26.2" + "@babel/generator" "^7.26.3" + "@babel/parser" "^7.26.3" "@babel/template" "^7.25.9" - "@babel/types" "^7.25.9" + "@babel/types" "^7.26.3" debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.4.4": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.0.tgz#deabd08d6b753bc8e0f198f8709fb575e31774ff" - integrity sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA== +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.3", "@babel/types@^7.4.4": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.3.tgz#37e79830f04c2b5687acc77db97fbc75fb81f3c0" + integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA== dependencies: "@babel/helper-string-parser" "^7.25.9" "@babel/helper-validator-identifier" "^7.25.9" @@ -1301,10 +1283,10 @@ source-map "^0.5.7" stylis "4.2.0" -"@emotion/cache@^11.13.5": - version "11.13.5" - resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.13.5.tgz#e78dad0489e1ed7572507ba8ed9d2130529e4266" - integrity sha512-Z3xbtJ+UcK76eWkagZ1onvn/wAVb1GOMuR15s30Fm2wrMgC7jzpnO2JZXr4eujTTqoQFUrZIw/rT0c6Zzjca1g== +"@emotion/cache@^11.13.5", "@emotion/cache@^11.14.0": + version "11.14.0" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.14.0.tgz#ee44b26986eeb93c8be82bb92f1f7a9b21b2ed76" + integrity sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA== dependencies: "@emotion/memoize" "^0.9.0" "@emotion/sheet" "^1.4.0" @@ -1334,15 +1316,15 @@ integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ== "@emotion/react@^11.8.2": - version "11.13.5" - resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.13.5.tgz#fc818ff5b13424f86501ba4d0740f343ae20b8d9" - integrity sha512-6zeCUxUH+EPF1s+YF/2hPVODeV/7V07YU5x+2tfuRL8MdW6rv5vb2+CBEGTGwBdux0OIERcOS+RzxeK80k2DsQ== + version "11.14.0" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.14.0.tgz#cfaae35ebc67dd9ef4ea2e9acc6cd29e157dd05d" + integrity sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA== dependencies: "@babel/runtime" "^7.18.3" "@emotion/babel-plugin" "^11.13.5" - "@emotion/cache" "^11.13.5" + "@emotion/cache" "^11.14.0" "@emotion/serialize" "^1.3.3" - "@emotion/use-insertion-effect-with-fallbacks" "^1.1.0" + "@emotion/use-insertion-effect-with-fallbacks" "^1.2.0" "@emotion/utils" "^1.4.2" "@emotion/weak-memoize" "^0.4.0" hoist-non-react-statics "^3.3.1" @@ -1368,10 +1350,10 @@ resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.10.0.tgz#2af2f7c7e5150f497bdabd848ce7b218a27cf745" integrity sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg== -"@emotion/use-insertion-effect-with-fallbacks@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz#1a818a0b2c481efba0cf34e5ab1e0cb2dcb9dfaf" - integrity sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw== +"@emotion/use-insertion-effect-with-fallbacks@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz#8a8cb77b590e09affb960f4ff1e9a89e532738bf" + integrity sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg== "@emotion/utils@^1.4.2": version "1.4.2" @@ -1516,18 +1498,20 @@ integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== "@eslint/config-array@^0.19.0": - version "0.19.0" - resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.19.0.tgz#3251a528998de914d59bb21ba4c11767cf1b3519" - integrity sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ== + version "0.19.1" + resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.19.1.tgz#734aaea2c40be22bbb1f2a9dac687c57a6a4c984" + integrity sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA== dependencies: - "@eslint/object-schema" "^2.1.4" + "@eslint/object-schema" "^2.1.5" debug "^4.3.1" minimatch "^3.1.2" "@eslint/core@^0.9.0": - version "0.9.0" - resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.9.0.tgz#168ee076f94b152c01ca416c3e5cf82290ab4fcd" - integrity sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg== + version "0.9.1" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.9.1.tgz#31763847308ef6b7084a4505573ac9402c51f9d1" + integrity sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q== + dependencies: + "@types/json-schema" "^7.0.15" "@eslint/eslintrc@^3.2.0": version "3.2.0" @@ -1549,15 +1533,15 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.16.0.tgz#3df2b2dd3b9163056616886c86e4082f45dbf3f4" integrity sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg== -"@eslint/object-schema@^2.1.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.4.tgz#9e69f8bb4031e11df79e03db09f9dbbae1740843" - integrity sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ== +"@eslint/object-schema@^2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.5.tgz#8670a8f6258a2be5b2c620ff314a1d984c23eb2e" + integrity sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ== "@eslint/plugin-kit@^0.2.3": - version "0.2.3" - resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz#812980a6a41ecf3a8341719f92a6d1e784a2e0e8" - integrity sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA== + version "0.2.4" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz#2b78e7bb3755784bb13faa8932a1d994d6537792" + integrity sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg== dependencies: levn "^0.4.1" @@ -1819,95 +1803,100 @@ estree-walker "^2.0.2" picomatch "^2.3.1" -"@rollup/rollup-android-arm-eabi@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.0.tgz#462e7ecdd60968bc9eb95a20d185e74f8243ec1b" - integrity sha512-wLJuPLT6grGZsy34g4N1yRfYeouklTgPhH1gWXCYspenKYD0s3cR99ZevOGw5BexMNywkbV3UkjADisozBmpPQ== - -"@rollup/rollup-android-arm64@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.0.tgz#78a2b8a8a55f71a295eb860a654ae90a2b168f40" - integrity sha512-eiNkznlo0dLmVG/6wf+Ifi/v78G4d4QxRhuUl+s8EWZpDewgk7PX3ZyECUXU0Zq/Ca+8nU8cQpNC4Xgn2gFNDA== - -"@rollup/rollup-darwin-arm64@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.0.tgz#5b783af714f434f1e66e3cdfa3817e0b99216d84" - integrity sha512-lmKx9yHsppblnLQZOGxdO66gT77bvdBtr/0P+TPOseowE7D9AJoBw8ZDULRasXRWf1Z86/gcOdpBrV6VDUY36Q== - -"@rollup/rollup-darwin-x64@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.0.tgz#f72484e842521a5261978034e18e20f778a2850d" - integrity sha512-8hxgfReVs7k9Js1uAIhS6zq3I+wKQETInnWQtgzt8JfGx51R1N6DRVy3F4o0lQwumbErRz52YqwjfvuwRxGv1w== - -"@rollup/rollup-freebsd-arm64@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.0.tgz#3c919dff72b2fe344811a609c674a8347b033f62" - integrity sha512-lA1zZB3bFx5oxu9fYud4+g1mt+lYXCoch0M0V/xhqLoGatbzVse0wlSQ1UYOWKpuSu3gyN4qEc0Dxf/DII1bhQ== - -"@rollup/rollup-freebsd-x64@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.0.tgz#b62a3a8365b363b3fdfa6da11a9188b6ab4dca7c" - integrity sha512-aI2plavbUDjCQB/sRbeUZWX9qp12GfYkYSJOrdYTL/C5D53bsE2/nBPuoiJKoWp5SN78v2Vr8ZPnB+/VbQ2pFA== - -"@rollup/rollup-linux-arm-gnueabihf@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.0.tgz#0d02cc55bd229bd8ca5c54f65f916ba5e0591c94" - integrity sha512-WXveUPKtfqtaNvpf0iOb0M6xC64GzUX/OowbqfiCSXTdi/jLlOmH0Ba94/OkiY2yTGTwteo4/dsHRfh5bDCZ+w== - -"@rollup/rollup-linux-arm-musleabihf@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.0.tgz#c51d379263201e88a60e92bd8e90878f0c044425" - integrity sha512-yLc3O2NtOQR67lI79zsSc7lk31xjwcaocvdD1twL64PK1yNaIqCeWI9L5B4MFPAVGEVjH5k1oWSGuYX1Wutxpg== - -"@rollup/rollup-linux-arm64-gnu@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.0.tgz#93ce2addc337b5cfa52b84f8e730d2e36eb4339b" - integrity sha512-+P9G9hjEpHucHRXqesY+3X9hD2wh0iNnJXX/QhS/J5vTdG6VhNYMxJ2rJkQOxRUd17u5mbMLHM7yWGZdAASfcg== - -"@rollup/rollup-linux-arm64-musl@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.0.tgz#730af6ddc091a5ba5baac28a3510691725dc808b" - integrity sha512-1xsm2rCKSTpKzi5/ypT5wfc+4bOGa/9yI/eaOLW0oMs7qpC542APWhl4A37AENGZ6St6GBMWhCCMM6tXgTIplw== - -"@rollup/rollup-linux-powerpc64le-gnu@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.0.tgz#b5565aac20b4de60ca1e557f525e76478b5436af" - integrity sha512-zgWxMq8neVQeXL+ouSf6S7DoNeo6EPgi1eeqHXVKQxqPy1B2NvTbaOUWPn/7CfMKL7xvhV0/+fq/Z/J69g1WAQ== - -"@rollup/rollup-linux-riscv64-gnu@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.0.tgz#d488290bf9338bad4ae9409c4aa8a1728835a20b" - integrity sha512-VEdVYacLniRxbRJLNtzwGt5vwS0ycYshofI7cWAfj7Vg5asqj+pt+Q6x4n+AONSZW/kVm+5nklde0qs2EUwU2g== - -"@rollup/rollup-linux-s390x-gnu@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.0.tgz#eb2e3f3a06acf448115045c11a5a96868c95a556" - integrity sha512-LQlP5t2hcDJh8HV8RELD9/xlYtEzJkm/aWGsauvdO2ulfl3QYRjqrKW+mGAIWP5kdNCBheqqqYIGElSRCaXfpw== - -"@rollup/rollup-linux-x64-gnu@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.0.tgz#065952ef2aea7e837dc7e02aa500feeaff4fc507" - integrity sha512-Nl4KIzteVEKE9BdAvYoTkW19pa7LR/RBrT6F1dJCV/3pbjwDcaOq+edkP0LXuJ9kflW/xOK414X78r+K84+msw== - -"@rollup/rollup-linux-x64-musl@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.0.tgz#3435d484d05f5c4d1ffd54541b4facce2887103a" - integrity sha512-eKpJr4vBDOi4goT75MvW+0dXcNUqisK4jvibY9vDdlgLx+yekxSm55StsHbxUsRxSTt3JEQvlr3cGDkzcSP8bw== - -"@rollup/rollup-win32-arm64-msvc@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.0.tgz#69682a2a10d9fedc334f87583cfca83c39c08077" - integrity sha512-Vi+WR62xWGsE/Oj+mD0FNAPY2MEox3cfyG0zLpotZdehPFXwz6lypkGs5y38Jd/NVSbOD02aVad6q6QYF7i8Bg== - -"@rollup/rollup-win32-ia32-msvc@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.0.tgz#b64470f9ac79abb386829c56750b9a4711be3332" - integrity sha512-kN/Vpip8emMLn/eOza+4JwqDZBL6MPNpkdaEsgUtW1NYN3DZvZqSQrbKzJcTL6hd8YNmFTn7XGWMwccOcJBL0A== - -"@rollup/rollup-win32-x64-msvc@4.28.0": - version "4.28.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.0.tgz#cb313feef9ac6e3737067fdf34f42804ac65a6f2" - integrity sha512-Bvno2/aZT6usSa7lRDL2+hMjVAGjuqaymF1ApZm31JXzniR/hvr14jpU+/z4X6Gt5BPlzosscyJZGUvguXIqeQ== +"@rollup/rollup-android-arm-eabi@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz#7f4c4d8cd5ccab6e95d6750dbe00321c1f30791e" + integrity sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ== + +"@rollup/rollup-android-arm64@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz#17ea71695fb1518c2c324badbe431a0bd1879f2d" + integrity sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA== + +"@rollup/rollup-darwin-arm64@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz#dac0f0d0cfa73e7d5225ae6d303c13c8979e7999" + integrity sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ== + +"@rollup/rollup-darwin-x64@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz#8f63baa1d31784904a380d2e293fa1ddf53dd4a2" + integrity sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ== + +"@rollup/rollup-freebsd-arm64@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz#30ed247e0df6e8858cdc6ae4090e12dbeb8ce946" + integrity sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA== + +"@rollup/rollup-freebsd-x64@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz#57846f382fddbb508412ae07855b8a04c8f56282" + integrity sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ== + +"@rollup/rollup-linux-arm-gnueabihf@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz#378ca666c9dae5e6f94d1d351e7497c176e9b6df" + integrity sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA== + +"@rollup/rollup-linux-arm-musleabihf@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz#a692eff3bab330d5c33a5d5813a090c15374cddb" + integrity sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg== + +"@rollup/rollup-linux-arm64-gnu@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz#6b1719b76088da5ac1ae1feccf48c5926b9e3db9" + integrity sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA== + +"@rollup/rollup-linux-arm64-musl@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz#865baf5b6f5ff67acb32e5a359508828e8dc5788" + integrity sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A== + +"@rollup/rollup-linux-loongarch64-gnu@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz#23c6609ba0f7fa7a7f2038b6b6a08555a5055a87" + integrity sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA== + +"@rollup/rollup-linux-powerpc64le-gnu@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz#652ef0d9334a9f25b9daf85731242801cb0fc41c" + integrity sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A== + +"@rollup/rollup-linux-riscv64-gnu@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz#1eb6651839ee6ebca64d6cc64febbd299e95e6bd" + integrity sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA== + +"@rollup/rollup-linux-s390x-gnu@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz#015c52293afb3ff2a293cf0936b1d43975c1e9cd" + integrity sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg== + +"@rollup/rollup-linux-x64-gnu@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz#b83001b5abed2bcb5e2dbeec6a7e69b194235c1e" + integrity sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw== + +"@rollup/rollup-linux-x64-musl@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz#6cc7c84cd4563737f8593e66f33b57d8e228805b" + integrity sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g== + +"@rollup/rollup-win32-arm64-msvc@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz#631ffeee094d71279fcd1fe8072bdcf25311bc11" + integrity sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A== + +"@rollup/rollup-win32-ia32-msvc@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz#06d1d60d5b9f718e8a6c4a43f82e3f9e3254587f" + integrity sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA== + +"@rollup/rollup-win32-x64-msvc@4.28.1": + version "4.28.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz#4dff5c4259ebe6c5b4a8f2c5bc3829b7a8447ff0" + integrity sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA== "@sinclair/typebox@^0.27.8": version "0.27.8" @@ -2007,17 +1996,10 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== -"@types/prop-types@*": - version "15.7.13" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.13.tgz#2af91918ee12d9d32914feb13f5326658461b451" - integrity sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA== - -"@types/react-dom@^18.3.0": - version "18.3.1" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.1.tgz#1e4654c08a9cdcfb6594c780ac59b55aad42fe07" - integrity sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ== - dependencies: - "@types/react" "*" +"@types/react-dom@^19.0.0": + version "19.0.2" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.0.2.tgz#ad21f9a1ee881817995fd3f7fd33659c87e7b1b7" + integrity sha512-c1s+7TKFaDRRxr1TxccIX2u7sfCnc3RxkVyBIUA2lCpyqCF+QoAwQ/CBg7bsMdVwP120HEH143VQezKtef5nCg== "@types/react-transition-group@^4.4.6": version "4.4.11" @@ -2026,12 +2008,11 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@>=16.9.11", "@types/react@^18.3.1": - version "18.3.12" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.12.tgz#99419f182ccd69151813b7ee24b792fe08774f60" - integrity sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw== +"@types/react@*", "@types/react@>=16.9.11", "@types/react@^19.0.0": + version "19.0.1" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.1.tgz#a000d5b78f473732a08cecbead0f3751e550b3df" + integrity sha512-YW6614BDhqbpR5KtUYzTA+zlA7nayzJRA9ljz9CQoxthR0sDisYZLuvSMsil36t4EH/uAt8T52Xb4sVw17G+SQ== dependencies: - "@types/prop-types" "*" csstype "^3.0.2" "@types/sizzle@*": @@ -2069,61 +2050,61 @@ "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^8.0.1": - version "8.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.17.0.tgz#2ee073c421f4e81e02d10e731241664b6253b23c" - integrity sha512-HU1KAdW3Tt8zQkdvNoIijfWDMvdSweFYm4hWh+KwhPstv+sCmWb89hCIP8msFm9N1R/ooh9honpSuvqKWlYy3w== + version "8.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.0.tgz#0901933326aea4443b81df3f740ca7dfc45c7bea" + integrity sha512-NR2yS7qUqCL7AIxdJUQf2MKKNDVNaig/dEB0GBLU7D+ZdHgK1NoH/3wsgO3OnPVipn51tG3MAwaODEGil70WEw== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.17.0" - "@typescript-eslint/type-utils" "8.17.0" - "@typescript-eslint/utils" "8.17.0" - "@typescript-eslint/visitor-keys" "8.17.0" + "@typescript-eslint/scope-manager" "8.18.0" + "@typescript-eslint/type-utils" "8.18.0" + "@typescript-eslint/utils" "8.18.0" + "@typescript-eslint/visitor-keys" "8.18.0" graphemer "^1.4.0" ignore "^5.3.1" natural-compare "^1.4.0" ts-api-utils "^1.3.0" "@typescript-eslint/parser@^8.0.1": - version "8.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.17.0.tgz#2ee972bb12fa69ac625b85813dc8d9a5a053ff52" - integrity sha512-Drp39TXuUlD49F7ilHHCG7TTg8IkA+hxCuULdmzWYICxGXvDXmDmWEjJYZQYgf6l/TFfYNE167m7isnc3xlIEg== - dependencies: - "@typescript-eslint/scope-manager" "8.17.0" - "@typescript-eslint/types" "8.17.0" - "@typescript-eslint/typescript-estree" "8.17.0" - "@typescript-eslint/visitor-keys" "8.17.0" + version "8.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.18.0.tgz#a1c9456cbb6a089730bf1d3fc47946c5fb5fe67b" + integrity sha512-hgUZ3kTEpVzKaK3uNibExUYm6SKKOmTU2BOxBSvOYwtJEPdVQ70kZJpPjstlnhCHcuc2WGfSbpKlb/69ttyN5Q== + dependencies: + "@typescript-eslint/scope-manager" "8.18.0" + "@typescript-eslint/types" "8.18.0" + "@typescript-eslint/typescript-estree" "8.18.0" + "@typescript-eslint/visitor-keys" "8.18.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@8.17.0": - version "8.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.17.0.tgz#a3f49bf3d4d27ff8d6b2ea099ba465ef4dbcaa3a" - integrity sha512-/ewp4XjvnxaREtqsZjF4Mfn078RD/9GmiEAtTeLQ7yFdKnqwTOgRMSvFz4et9U5RiJQ15WTGXPLj89zGusvxBg== +"@typescript-eslint/scope-manager@8.18.0": + version "8.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.18.0.tgz#30b040cb4557804a7e2bcc65cf8fdb630c96546f" + integrity sha512-PNGcHop0jkK2WVYGotk/hxj+UFLhXtGPiGtiaWgVBVP1jhMoMCHlTyJA+hEj4rszoSdLTK3fN4oOatrL0Cp+Xw== dependencies: - "@typescript-eslint/types" "8.17.0" - "@typescript-eslint/visitor-keys" "8.17.0" + "@typescript-eslint/types" "8.18.0" + "@typescript-eslint/visitor-keys" "8.18.0" -"@typescript-eslint/type-utils@8.17.0": - version "8.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.17.0.tgz#d326569f498cdd0edf58d5bb6030b4ad914e63d3" - integrity sha512-q38llWJYPd63rRnJ6wY/ZQqIzPrBCkPdpIsaCfkR3Q4t3p6sb422zougfad4TFW9+ElIFLVDzWGiGAfbb/v2qw== +"@typescript-eslint/type-utils@8.18.0": + version "8.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.18.0.tgz#6f0d12cf923b6fd95ae4d877708c0adaad93c471" + integrity sha512-er224jRepVAVLnMF2Q7MZJCq5CsdH2oqjP4dT7K6ij09Kyd+R21r7UVJrF0buMVdZS5QRhDzpvzAxHxabQadow== dependencies: - "@typescript-eslint/typescript-estree" "8.17.0" - "@typescript-eslint/utils" "8.17.0" + "@typescript-eslint/typescript-estree" "8.18.0" + "@typescript-eslint/utils" "8.18.0" debug "^4.3.4" ts-api-utils "^1.3.0" -"@typescript-eslint/types@8.17.0": - version "8.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.17.0.tgz#ef84c709ef8324e766878834970bea9a7e3b72cf" - integrity sha512-gY2TVzeve3z6crqh2Ic7Cr+CAv6pfb0Egee7J5UAVWCpVvDI/F71wNfolIim4FE6hT15EbpZFVUj9j5i38jYXA== +"@typescript-eslint/types@8.18.0": + version "8.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.18.0.tgz#3afcd30def8756bc78541268ea819a043221d5f3" + integrity sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA== -"@typescript-eslint/typescript-estree@8.17.0": - version "8.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.17.0.tgz#40b5903bc929b1e8dd9c77db3cb52cfb199a2a34" - integrity sha512-JqkOopc1nRKZpX+opvKqnM3XUlM7LpFMD0lYxTqOTKQfCWAmxw45e3qlOCsEqEB2yuacujivudOFpCnqkBDNMw== +"@typescript-eslint/typescript-estree@8.18.0": + version "8.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.0.tgz#d8ca785799fbb9c700cdff1a79c046c3e633c7f9" + integrity sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg== dependencies: - "@typescript-eslint/types" "8.17.0" - "@typescript-eslint/visitor-keys" "8.17.0" + "@typescript-eslint/types" "8.18.0" + "@typescript-eslint/visitor-keys" "8.18.0" debug "^4.3.4" fast-glob "^3.3.2" is-glob "^4.0.3" @@ -2131,22 +2112,22 @@ semver "^7.6.0" ts-api-utils "^1.3.0" -"@typescript-eslint/utils@8.17.0": - version "8.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.17.0.tgz#41c05105a2b6ab7592f513d2eeb2c2c0236d8908" - integrity sha512-bQC8BnEkxqG8HBGKwG9wXlZqg37RKSMY7v/X8VEWD8JG2JuTHuNK0VFvMPMUKQcbk6B+tf05k+4AShAEtCtJ/w== +"@typescript-eslint/utils@8.18.0": + version "8.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.18.0.tgz#48f67205d42b65d895797bb7349d1be5c39a62f7" + integrity sha512-p6GLdY383i7h5b0Qrfbix3Vc3+J2k6QWw6UMUeY5JGfm3C5LbZ4QIZzJNoNOfgyRe0uuYKjvVOsO/jD4SJO+xg== dependencies: "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.17.0" - "@typescript-eslint/types" "8.17.0" - "@typescript-eslint/typescript-estree" "8.17.0" + "@typescript-eslint/scope-manager" "8.18.0" + "@typescript-eslint/types" "8.18.0" + "@typescript-eslint/typescript-estree" "8.18.0" -"@typescript-eslint/visitor-keys@8.17.0": - version "8.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.17.0.tgz#4dbcd0e28b9bf951f4293805bf34f98df45e1aa8" - integrity sha512-1Hm7THLpO6ww5QU6H/Qp+AusUUl+z/CAm3cNZZ0jQvon9yicgO7Rwd+/WWRpMKLYV6p2UvdbR27c86rzCPpreg== +"@typescript-eslint/visitor-keys@8.18.0": + version "8.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.0.tgz#7b6d33534fa808e33a19951907231ad2ea5c36dd" + integrity sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw== dependencies: - "@typescript-eslint/types" "8.17.0" + "@typescript-eslint/types" "8.18.0" eslint-visitor-keys "^4.2.0" "@vitejs/plugin-react@^4.2.1": @@ -2411,16 +2392,23 @@ browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.23.1, browserslist@^4 node-releases "^2.0.18" update-browserslist-db "^1.1.1" -call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" - integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== +call-bind-apply-helpers@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz#32e5892e6361b29b0b545ba6f7763378daca2840" + integrity sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g== dependencies: - es-define-property "^1.0.0" es-errors "^1.3.0" function-bind "^1.1.2" + +call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7, call-bind@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-define-property "^1.0.0" get-intrinsic "^1.2.4" - set-function-length "^1.2.1" + set-function-length "^1.2.2" callsites@^3.0.0: version "3.1.0" @@ -2438,9 +2426,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001669: - version "1.0.30001686" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001686.tgz#0e04b8d90de8753188e93c9989d56cb19d902670" - integrity sha512-Y7deg0Aergpa24M3qLC5xjNklnKnhsmSyR/V89dLZ1n0ucJIFNs7PgR2Yfa/Zf6W79SbBicgtGxZr2juHkEUIA== + version "1.0.30001687" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001687.tgz#d0ac634d043648498eedf7a3932836beba90ebae" + integrity sha512-0S/FDhf4ZiqrTUiQ39dKeUjYRjkv7lOZU1Dgif2rIqrTzX/1wV2hfKu9TOm1IHkdSijfLswxTFzl/cvir+SLSQ== chalk@^4.0.0: version "4.1.2" @@ -2628,9 +2616,9 @@ css-what@^6.1.0: integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== cssdb@^8.2.1: - version "8.2.2" - resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-8.2.2.tgz#0a5bcbc47a297e6b0296e6082f60363e17b337d4" - integrity sha512-Z3kpWyvN68aKyeMxOUGmffQeHjvrzDxbre2B2ikr/WqQ4ZMkhHu2nOD6uwSeq3TpuOYU7ckvmJRAUIt6orkYUg== + version "8.2.3" + resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-8.2.3.tgz#7e6980bb5a785a9b4eb2a21bd38d50624b56cb46" + integrity sha512-9BDG5XmJrJQQnJ51VFxXCAtpZ5ebDlAREmO8sxMOVU0aSxN/gocbctjIG5LMh3WBUq+xTlb/jw2LoljBEqraTA== cssesc@^3.0.0: version "3.0.0" @@ -2731,9 +2719,9 @@ date-fns@^4.1.0: integrity sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg== debug@^4.1.0, debug@^4.1.1, debug@^4.3, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: - version "4.3.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" - integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== dependencies: ms "^2.1.3" @@ -2820,10 +2808,19 @@ domutils@^3.0.1: domelementtype "^2.3.0" domhandler "^5.0.3" +dunder-proto@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.0.tgz#c2fce098b3c8f8899554905f4377b6d85dabaa80" + integrity sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-errors "^1.3.0" + gopd "^1.2.0" + electron-to-chromium@^1.5.41: - version "1.5.68" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.68.tgz#4f46be4d465ef00e2100d5557b66f4af70e3ce6c" - integrity sha512-FgMdJlma0OzUYlbrtZ4AeXjKxKPk6KT8WOP8BjcqxWtlg8qyJQjRzPJzUtUn5GBg1oQ26hFs7HOOHJMYiJRnvQ== + version "1.5.72" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.72.tgz#a732805986d3a5b5fedd438ddf4616c7d78ac2df" + integrity sha512-ZpSAUOZ2Izby7qnZluSrAlGgGQzucmFbN0n64dYzocYxnxV5ufurpj3VgEe4cUp7ir9LmeLxNYo8bVnlM8bQHw== emojis-list@^3.0.0: version "3.0.0" @@ -2894,12 +2891,10 @@ es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23 unbox-primitive "^1.0.2" which-typed-array "^1.1.15" -es-define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" - integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== - dependencies: - get-intrinsic "^1.2.4" +es-define-property@^1.0.0, es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== es-errors@^1.2.1, es-errors@^1.3.0: version "1.3.0" @@ -3277,15 +3272,18 @@ gensync@^1.0.0-beta.2: integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" - integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + version "1.2.5" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.5.tgz#dfe7dd1b30761b464fe51bf4bb00ac7c37b681e7" + integrity sha512-Y4+pKa7XeRUPWFNvOOYHkRYrfzW07oraURSvjDmRVOJ748OrVmeXtpE4+GCEHncjCjkTxPNRt8kEbxDhsn6VTg== dependencies: + call-bind-apply-helpers "^1.0.0" + dunder-proto "^1.0.0" + es-define-property "^1.0.1" es-errors "^1.3.0" function-bind "^1.1.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.0" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" get-symbol-description@^1.0.2: version "1.0.2" @@ -3328,12 +3326,10 @@ globalthis@^1.0.4: define-properties "^1.2.1" gopd "^1.0.1" -gopd@^1.0.1, gopd@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.1.0.tgz#df8f0839c2d48caefc32a025a49294d39606c912" - integrity sha512-FQoVQnqcdk4hVM4JN1eromaun4iuS34oStkdlLENLdpULsuQcTyXj8w7ayhuUfPwEYZ1ZOooOTT6fdA9Vmx/RA== - dependencies: - get-intrinsic "^1.2.4" +gopd@^1.0.1, gopd@^1.1.0, gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== graceful-fs@^4.2.9: version "4.2.11" @@ -3362,14 +3358,14 @@ has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: dependencies: es-define-property "^1.0.0" -has-proto@^1.0.1, has-proto@^1.0.3: - version "1.1.0" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.1.0.tgz#deb10494cbbe8809bce168a3b961f42969f5ed43" - integrity sha512-QLdzI9IIO1Jg7f9GT1gXpPpXArAn6cS31R1eEZqz08Gc+uQ8/XiqHWt17Fiw+2p6oTTIq5GXEpQkAlA88YRl/Q== +has-proto@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.2.0.tgz#5de5a6eabd95fdffd9818b43055e8065e39fe9d5" + integrity sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ== dependencies: - call-bind "^1.0.7" + dunder-proto "^1.0.0" -has-symbols@^1.0.3: +has-symbols@^1.0.3, has-symbols@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== @@ -3892,9 +3888,9 @@ node-addon-api@^7.0.0: integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== node-releases@^2.0.18: - version "2.0.18" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" - integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== + version "2.0.19" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" + integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== normalize-range@^0.1.2: version "0.1.2" @@ -4645,7 +4641,7 @@ react-bootstrap@^2.3.1: uncontrollable "^7.2.1" warning "^4.0.3" -react-dom@^18.2.0, react-dom@^18.3.1: +react-dom@^18.2.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== @@ -4653,15 +4649,22 @@ react-dom@^18.2.0, react-dom@^18.3.1: loose-envify "^1.1.0" scheduler "^0.23.2" +react-dom@^19.0.0: + version "19.0.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.0.0.tgz#43446f1f01c65a4cd7f7588083e686a6726cfb57" + integrity sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ== + dependencies: + scheduler "^0.25.0" + react-hook-form@^7.51.3: - version "7.53.2" - resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.53.2.tgz#6fa37ae27330af81089baadd7f322cc987b8e2ac" - integrity sha512-YVel6fW5sOeedd1524pltpHX+jgU2u3DSDtXEaBORNdqiNrsX/nUI/iGXONegttg0mJVnfrIkiV0cmTU6Oo2xw== + version "7.54.0" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.54.0.tgz#46bd9142d65fd16ac064a2bbf4dc0333e2d6840d" + integrity sha512-PS05+UQy/IdSbJNojBypxAo9wllhHgGmyr8/dyGQcPoiMf3e7Dfb9PWYVRco55bLbxH9S+1yDDJeTdlYCSxO3A== react-i18next@^15.0.0: - version "15.1.3" - resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.1.3.tgz#172c3905038ea4f90699a19949a0084b5641c94f" - integrity sha512-J11oA30FbM3NZegUZjn8ySK903z6PLBz/ZuBYyT1JMR0QPrW6PFXvl1WoUhortdGi9dM0m48/zJQlPskVZXgVw== + version "15.1.4" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.1.4.tgz#65c03c31a5e42202000652e163f22f23a9306a60" + integrity sha512-2tai71gmehbvl9ZIqPMqlCCkm/cbeV1G4STpmM3C8Uzo6T2l8jDvZxEVSsQKt8blP9X34iRFP/k1ROqG2296MQ== dependencies: "@babel/runtime" "^7.25.0" html-parse-stringify "^3.0.1" @@ -4704,13 +4707,18 @@ react-transition-group@^4.4.5: loose-envify "^1.4.0" prop-types "^15.6.2" -react@^18.2.0, react@^18.3.1: +react@^18.2.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== dependencies: loose-envify "^1.1.0" +react@^19.0.0: + version "19.0.0" + resolved "https://registry.yarnpkg.com/react/-/react-19.0.0.tgz#6e1969251b9f108870aa4bff37a0ce9ddfaaabdd" + integrity sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ== + react_ujs@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/react_ujs/-/react_ujs-2.7.1.tgz#d87cbcb82593fe59d30fc5dbc51800d1571001a8" @@ -4744,17 +4752,18 @@ readdirp@^4.0.1: integrity sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA== reflect.getprototypeof@^1.0.4, reflect.getprototypeof@^1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.7.tgz#04311b33a1b713ca5eb7b5aed9950a86481858e5" - integrity sha512-bMvFGIUKlc/eSfXNX+aZ+EL95/EgZzuwA0OBPTbZZDEJw/0AkentjMuM1oiRfwHrshqk4RzdgiTg5CcDalXN5g== + version "1.0.8" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.8.tgz#c58afb17a4007b4d1118c07b92c23fca422c5d82" + integrity sha512-B5dj6usc5dkk8uFliwjwDHM8To5/QwdKz9JcBZ8Ic4G1f0YmeeJTtE/ZTdgRFPAfxZFiUaPhZ1Jcs4qeagItGQ== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" define-properties "^1.2.1" + dunder-proto "^1.0.0" es-abstract "^1.23.5" es-errors "^1.3.0" get-intrinsic "^1.2.4" - gopd "^1.0.1" - which-builtin-type "^1.1.4" + gopd "^1.2.0" + which-builtin-type "^1.2.0" regenerate-unicode-properties@^10.2.0: version "10.2.0" @@ -4790,7 +4799,7 @@ regexp.prototype.flags@^1.5.2, regexp.prototype.flags@^1.5.3: es-errors "^1.3.0" set-function-name "^2.0.2" -regexpu-core@^6.1.1: +regexpu-core@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.2.0.tgz#0e5190d79e542bf294955dccabae04d3c7d53826" integrity sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA== @@ -4853,30 +4862,31 @@ rollup-plugin-gzip@^3.1.0: integrity sha512-9xemMyvCjkklgNpu6jCYqQAbvCLJzA2nilkiOGzFuXTUX3cXEFMwIhsIBRF7kTKD/SnZ1tNPcxFm4m4zJ3VfNQ== rollup@^4.23.0: - version "4.28.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.28.0.tgz#eb8d28ed43ef60a18f21d0734d230ee79dd0de77" - integrity sha512-G9GOrmgWHBma4YfCcX8PjH0qhXSdH8B4HDE2o4/jaxj93S4DPCIDoLcXz99eWMji4hB29UFCEd7B2gwGJDR9cQ== + version "4.28.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.28.1.tgz#7718ba34d62b449dfc49adbfd2f312b4fe0df4de" + integrity sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg== dependencies: "@types/estree" "1.0.6" optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.28.0" - "@rollup/rollup-android-arm64" "4.28.0" - "@rollup/rollup-darwin-arm64" "4.28.0" - "@rollup/rollup-darwin-x64" "4.28.0" - "@rollup/rollup-freebsd-arm64" "4.28.0" - "@rollup/rollup-freebsd-x64" "4.28.0" - "@rollup/rollup-linux-arm-gnueabihf" "4.28.0" - "@rollup/rollup-linux-arm-musleabihf" "4.28.0" - "@rollup/rollup-linux-arm64-gnu" "4.28.0" - "@rollup/rollup-linux-arm64-musl" "4.28.0" - "@rollup/rollup-linux-powerpc64le-gnu" "4.28.0" - "@rollup/rollup-linux-riscv64-gnu" "4.28.0" - "@rollup/rollup-linux-s390x-gnu" "4.28.0" - "@rollup/rollup-linux-x64-gnu" "4.28.0" - "@rollup/rollup-linux-x64-musl" "4.28.0" - "@rollup/rollup-win32-arm64-msvc" "4.28.0" - "@rollup/rollup-win32-ia32-msvc" "4.28.0" - "@rollup/rollup-win32-x64-msvc" "4.28.0" + "@rollup/rollup-android-arm-eabi" "4.28.1" + "@rollup/rollup-android-arm64" "4.28.1" + "@rollup/rollup-darwin-arm64" "4.28.1" + "@rollup/rollup-darwin-x64" "4.28.1" + "@rollup/rollup-freebsd-arm64" "4.28.1" + "@rollup/rollup-freebsd-x64" "4.28.1" + "@rollup/rollup-linux-arm-gnueabihf" "4.28.1" + "@rollup/rollup-linux-arm-musleabihf" "4.28.1" + "@rollup/rollup-linux-arm64-gnu" "4.28.1" + "@rollup/rollup-linux-arm64-musl" "4.28.1" + "@rollup/rollup-linux-loongarch64-gnu" "4.28.1" + "@rollup/rollup-linux-powerpc64le-gnu" "4.28.1" + "@rollup/rollup-linux-riscv64-gnu" "4.28.1" + "@rollup/rollup-linux-s390x-gnu" "4.28.1" + "@rollup/rollup-linux-x64-gnu" "4.28.1" + "@rollup/rollup-linux-x64-musl" "4.28.1" + "@rollup/rollup-win32-arm64-msvc" "4.28.1" + "@rollup/rollup-win32-ia32-msvc" "4.28.1" + "@rollup/rollup-win32-x64-msvc" "4.28.1" fsevents "~2.3.2" run-parallel@^1.1.9: @@ -4928,6 +4938,11 @@ scheduler@^0.23.2: dependencies: loose-envify "^1.1.0" +scheduler@^0.25.0: + version "0.25.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.25.0.tgz#336cd9768e8cceebf52d3c80e3dcf5de23e7e015" + integrity sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA== + schema-utils@^4.0.0, schema-utils@^4.0.1: version "4.2.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.2.0.tgz#70d7c93e153a273a805801882ebd3bff20d89c8b" @@ -4955,7 +4970,7 @@ serialize-javascript@^6.0.1: dependencies: randombytes "^2.1.0" -set-function-length@^1.2.1: +set-function-length@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== @@ -5350,9 +5365,9 @@ vite-plugin-stimulus-hmr@^3.0.0: stimulus-vite-helpers "^3.0.0" vite@^6.0.1: - version "6.0.2" - resolved "https://registry.yarnpkg.com/vite/-/vite-6.0.2.tgz#7a22630c73c7b663335ddcdb2390971ffbc14993" - integrity sha512-XdQ+VsY2tJpBsKGs0wf3U/+azx8BBpYRHFAyKm5VeEZNOJZRB63q7Sc8Iup3k0TrN3KO6QgyzFf+opSbfY1y0g== + version "6.0.3" + resolved "https://registry.yarnpkg.com/vite/-/vite-6.0.3.tgz#cc01f403e326a9fc1e064235df8a6de084c8a491" + integrity sha512-Cmuo5P0ENTN6HxLSo6IHsjCLn/81Vgrp81oaiFFMRa8gGDj5xEjIcEpf2ZymZtZR8oU0P2JX5WuUp/rlXcHkAw== dependencies: esbuild "^0.24.0" postcss "^8.4.49" @@ -5383,7 +5398,7 @@ which-boxed-primitive@^1.0.2: is-string "^1.1.0" is-symbol "^1.1.0" -which-builtin-type@^1.1.4: +which-builtin-type@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.2.0.tgz#58042ac9602d78a6d117c7e811349df1268ba63c" integrity sha512-I+qLGQ/vucCby4tf5HsLmGueEla4ZhwTBSqaooS+Y0BuxN4Cp+okmGuV+8mXZ84KDI9BA+oklo+RzKg0ONdSUA== From 3a80d93be57ac65af09fea6b74255326dac37b65 Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Sat, 7 Dec 2024 22:01:34 +0000 Subject: [PATCH 02/30] feat: add key sequences --- app/models/key_sequence.rb | 13 ++++++++ app/models/organisation.rb | 1 + .../20241207212400_create_key_sequences.rb | 13 ++++++++ db/schema.rb | 14 +++++++- spec/factories/key_sequence.rb | 8 +++++ spec/models/key_sequence_spec.rb | 33 +++++++++++++++++++ 6 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 app/models/key_sequence.rb create mode 100644 db/migrate/20241207212400_create_key_sequences.rb create mode 100644 spec/factories/key_sequence.rb create mode 100644 spec/models/key_sequence_spec.rb diff --git a/app/models/key_sequence.rb b/app/models/key_sequence.rb new file mode 100644 index 000000000..d70538f05 --- /dev/null +++ b/app/models/key_sequence.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class KeySequence < ApplicationRecord + belongs_to :organisation, inverse_of: :key_sequences + + validates :key, presence: true, uniqueness: { scope: %i[organisation_id year] } + + scope :key, ->(key, year: nil) { find_or_create_by(key:, year: year.is_a?(TrueClass) ? Time.zone.today.year : year) } + + def lease! + increment!(:value).value # rubocop:disable Rails/SkipsModelValidations + end +end diff --git a/app/models/organisation.rb b/app/models/organisation.rb index bc01d5a18..7a1c69a0d 100644 --- a/app/models/organisation.rb +++ b/app/models/organisation.rb @@ -64,6 +64,7 @@ class Organisation < ApplicationRecord has_many :tarifs, dependent: :destroy, inverse_of: :organisation has_many :plan_b_backups, dependent: :destroy, inverse_of: :organisation has_many :vat_categories, dependent: :destroy, inverse_of: :organisation + has_many :key_sequences, dependent: :destroy, inverse_of: :organisation has_one_attached :logo has_one_attached :contract_signature diff --git a/db/migrate/20241207212400_create_key_sequences.rb b/db/migrate/20241207212400_create_key_sequences.rb new file mode 100644 index 000000000..016d1f14b --- /dev/null +++ b/db/migrate/20241207212400_create_key_sequences.rb @@ -0,0 +1,13 @@ +class CreateKeySequences < ActiveRecord::Migration[8.0] + def change + create_table :key_sequences do |t| + t.string :key, null: false + t.references :organisation, null: false, foreign_key: true + t.integer :year, null: true + t.integer :value, null: false, default: 0 + + t.timestamps + end + add_index :key_sequences, [:key, :year, :organisation_id], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 27972344e..8ca3696b8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2024_12_05_133043) do +ActiveRecord::Schema[8.0].define(version: 2024_12_07_212400) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "pgcrypto" @@ -337,6 +337,17 @@ t.index ["type"], name: "index_invoices_on_type" end + create_table "key_sequences", force: :cascade do |t| + t.string "key", null: false + t.bigint "organisation_id", null: false + t.integer "year" + t.integer "value", default: 0, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["key", "year", "organisation_id"], name: "index_key_sequences_on_key_and_year_and_organisation_id", unique: true + t.index ["organisation_id"], name: "index_key_sequences_on_organisation_id" + end + create_table "mail_template_designated_documents", id: false, force: :cascade do |t| t.bigint "mail_template_id" t.bigint "designated_document_id" @@ -668,6 +679,7 @@ add_foreign_key "invoice_parts", "vat_categories" add_foreign_key "invoices", "bookings" add_foreign_key "invoices", "invoices", column: "supersede_invoice_id" + add_foreign_key "key_sequences", "organisations" add_foreign_key "mail_template_designated_documents", "designated_documents" add_foreign_key "mail_template_designated_documents", "rich_text_templates", column: "mail_template_id" add_foreign_key "meter_reading_periods", "tarifs" diff --git a/spec/factories/key_sequence.rb b/spec/factories/key_sequence.rb new file mode 100644 index 000000000..2e1f4a39e --- /dev/null +++ b/spec/factories/key_sequence.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :key_sequence do + key { 'test' } + organisation + end +end diff --git a/spec/models/key_sequence_spec.rb b/spec/models/key_sequence_spec.rb new file mode 100644 index 000000000..9ff260512 --- /dev/null +++ b/spec/models/key_sequence_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe KeySequence, type: :model do + let(:organisation) { key_sequence.organisation } + let(:key) { 'test' } + subject(:key_sequence) { create(:key_sequence, value: 10, key:) } + + describe '#lease!' do + subject(:lease!) { key_sequence.lease! } + + it { expect { lease! }.to(change { key_sequence.reload.value }.by(1)) } + it { is_expected.to eq(11) } + end + + describe '::key' do + subject { organisation.key_sequences.key(key, year:) } + let(:year) { nil } + + it { is_expected.to eq(key_sequence) } + + context 'with not existing key_sequence' do + let(:year) { 2023 } + it { is_expected.to have_attributes(year:, key:) } + end + + context 'with not existing key_sequence' do + let(:year) { true } + it { is_expected.to have_attributes(year: Time.zone.today.year, key:) } + end + end +end From 61cdb41e0a4b807d4bd7b334301961a1498db1fe Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Tue, 10 Dec 2024 07:09:41 +0000 Subject: [PATCH 03/30] stashcommit --- app/models/invoice.rb | 19 ++++++++++++------- app/models/key_sequence.rb | 16 +++++++++++++++- ...641_add_key_sequence_number_to_invoices.rb | 15 +++++++++++++++ db/schema.rb | 5 ++++- 4 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 db/migrate/20241209160641_add_key_sequence_number_to_invoices.rb diff --git a/app/models/invoice.rb b/app/models/invoice.rb index b517ecb1d..6f75cb361 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -68,10 +68,11 @@ class Invoice < ApplicationRecord accepts_nested_attributes_for :invoice_parts, reject_if: :all_blank, allow_destroy: true before_save :recalculate + # after_create { generate_ref? && generate_ref && save } + before_create :sequence_number, :build_refs after_create :supersede! before_update :generate_pdf, if: :generate_pdf? after_save :recalculate! - after_create { generate_ref? && generate_ref && save } delegate :currency, to: :organisation @@ -84,10 +85,6 @@ def generate_pdf? kept? && ref.present? && !skip_generate_pdf && (pdf.blank? || changed?) end - def generate_ref? - ref.blank? - end - def supersede! return if supersede_invoice.blank? || supersede_invoice.discarded? @@ -95,6 +92,10 @@ def supersede! supersede_invoice.discard! end + def sequence_number + @sequence_number ||= organisation.key_sequences.key(Invoice.sti_name, year: :current).lease! + end + def generate_pdf I18n.with_locale(locale || I18n.locale) do self.pdf = { io: StringIO.new(Export::Pdf::InvoicePdf.new(self).render_document), @@ -102,8 +103,12 @@ def generate_pdf end end - def generate_ref - self.ref = invoice_ref_service.generate(self) + # def generate_ref + # self.ref = invoice_ref_service.generate(self) + # end + + def build_refs + # self.sequence_number ||= organisation.key_sequences.key end def paid? diff --git a/app/models/key_sequence.rb b/app/models/key_sequence.rb index d70538f05..2ee807561 100644 --- a/app/models/key_sequence.rb +++ b/app/models/key_sequence.rb @@ -5,9 +5,23 @@ class KeySequence < ApplicationRecord validates :key, presence: true, uniqueness: { scope: %i[organisation_id year] } - scope :key, ->(key, year: nil) { find_or_create_by(key:, year: year.is_a?(TrueClass) ? Time.zone.today.year : year) } + scope :key, ->(key, year: nil) { find_or_create_by(key:, year: (year == :current ? Time.zone.today.year : year)) } def lease! increment!(:value).value # rubocop:disable Rails/SkipsModelValidations end + + # module ActiveRecord + # extend ActiveSupport::Concern + + # class_methods do + # def sequence_number(key, column = :sequence_number, year: nil) + # before_save do + # # year = year.call if year.respond_to?(:call) + # leased_sequence_number = organisation&.key_sequences&.key(key, year: year)&.lease! + # try("#{column}||=", leased_sequence_number) if leased_sequence_number.present? + # end + # end + # end + # end end diff --git a/db/migrate/20241209160641_add_key_sequence_number_to_invoices.rb b/db/migrate/20241209160641_add_key_sequence_number_to_invoices.rb new file mode 100644 index 000000000..c96c272dc --- /dev/null +++ b/db/migrate/20241209160641_add_key_sequence_number_to_invoices.rb @@ -0,0 +1,15 @@ +class AddKeySequenceNumberToInvoices < ActiveRecord::Migration[8.0] + def change + add_column :invoices, :sequence_number, :integer, null: true + add_column :tenants, :sequence_number, :integer, null: true + add_column :bookings, :sequence_number, :integer, null: true + end + + # protected + + # def backfill_sequence_numbers + # Organisation.find_each do |organisation| + # organisation. + # end + # end +end diff --git a/db/schema.rb b/db/schema.rb index 8ca3696b8..2405e8f7c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2024_12_07_212400) do +ActiveRecord::Schema[8.0].define(version: 2024_12_09_160641) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "pgcrypto" @@ -223,6 +223,7 @@ t.integer "home_id", null: false t.boolean "ignore_conflicting", default: false, null: false t.jsonb "booking_questions" + t.integer "sequence_number" t.index ["booking_state_cache"], name: "index_bookings_on_booking_state_cache" t.index ["locale"], name: "index_bookings_on_locale" t.index ["organisation_id"], name: "index_bookings_on_organisation_id" @@ -330,6 +331,7 @@ t.bigint "supersede_invoice_id" t.string "locale" t.boolean "payment_required", default: true + t.integer "sequence_number" t.index ["booking_id"], name: "index_invoices_on_booking_id" t.index ["discarded_at"], name: "index_invoices_on_discarded_at" t.index ["ref"], name: "index_invoices_on_ref" @@ -588,6 +590,7 @@ t.boolean "bookings_without_invoice", default: false t.integer "salutation_form" t.string "accounting_account_nr" + t.integer "sequence_number" t.index ["email", "organisation_id"], name: "index_tenants_on_email_and_organisation_id", unique: true t.index ["email"], name: "index_tenants_on_email" t.index ["organisation_id"], name: "index_tenants_on_organisation_id" From 55950aaccacc90b324b91304b3424a4c1723c4d7 Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Tue, 10 Dec 2024 12:00:11 +0000 Subject: [PATCH 04/30] fix: column config react component --- .../ColumnsConfigForm.tsx | 92 ++++++++++--------- 1 file changed, 48 insertions(+), 44 deletions(-) diff --git a/app/javascript/components/data_digest_templates/ColumnsConfigForm.tsx b/app/javascript/components/data_digest_templates/ColumnsConfigForm.tsx index e47be8eb5..c62b91764 100644 --- a/app/javascript/components/data_digest_templates/ColumnsConfigForm.tsx +++ b/app/javascript/components/data_digest_templates/ColumnsConfigForm.tsx @@ -17,6 +17,7 @@ type ColumnsConfigFormProps = { }; type DefaultColumnConfig = { + id: string; index: number; type: ColumnConfigType; header: string; @@ -25,7 +26,7 @@ type DefaultColumnConfig = { type UsageColumnConfig = DefaultColumnConfig & { type: ColumnConfigType.Usage; - tarif_index: number; + tarif_id: number; }; type CostColumnConfig = DefaultColumnConfig & { @@ -36,10 +37,8 @@ function isColumnConfigType(type: string): type is ColumnConfigType { return Object.values(ColumnConfigType).includes(type); } -function toJson(columnsConfig: ColumnConfig[]): string { - return JSON.stringify( - columnsConfig.map(({ header, body, type, tarif_id, id }) => ({ header, body, type, tarif_id, id })), - ); +function toJson(columnsConfigs: ColumnConfig[]): string { + return JSON.stringify(columnsConfigs) } type ColumnConfig = DefaultColumnConfig | UsageColumnConfig | CostColumnConfig; @@ -47,7 +46,7 @@ type ColumnConfig = DefaultColumnConfig | UsageColumnConfig | CostColumnConfig; export default function ColumnsConfigFrom({ json, name }: ColumnsConfigFormProps) { const { t } = useTranslation(); const [columnsConfig, setColumnsConfig] = useState(() => - (JSON.parse(json) as unknown as ColumnConfig[]).map((data, index) => ({ ...data, index })), + (JSON.parse(json) as unknown as ColumnConfig[]).map((data, index) => ({ ...data, index, id: crypto.randomUUID() })), ); const handleUpdate = (updatedConfig: ColumnConfig) => @@ -58,20 +57,18 @@ export default function ColumnsConfigFrom({ json, name }: ColumnsConfigFormProps setColumnsConfig((prev) => prev.filter((prevConfig) => prevConfig.index != removedConfig.index)); const handleAdd = (type: string) => isColumnConfigType(type) && - setColumnsConfig((prev) => [...prev, { index: prev.length, type, body: "", header: "" }]); + setColumnsConfig((prev) => [...prev, { index: prev.length, type, body: "", header: "", id: crypto.randomUUID() }]); return ( {t("activerecord.attributes.data_digest_template.columns_config")} -
    - - {columnsConfig.map((config) => ( -
  1. - -
  2. - ))} -
    -
+ + {columnsConfig.map((config) => ( +
  • + +
  • + ))} +
    handleAdd(event.target.value)}> @@ -121,33 +118,40 @@ function ColumnConfigForm({ config, onUpdate, onRemove }: ColumnConfigFormProps) }; return ( - - - - onUpdate({ ...config, header: event.target.value })} - > - - - - - onUpdate({ ...config, body: event.target.value })} - > - - - {typeSpecificComponents[config.type]?.()} - -
    - -
    - -
    + + + + + + + + + onUpdate({ ...config, header: event.target.value })} + > + + + + + onUpdate({ ...config, body: event.target.value })} + > + + + {typeSpecificComponents[config.type]?.()} + + + +
    + +
    + +
    ); } From a1318e43b99e6369491eb9dc2d1324825427b467 Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Tue, 10 Dec 2024 14:54:56 +0000 Subject: [PATCH 05/30] fix: colums config form --- .../ColumnsConfigForm.tsx | 120 ++++++++++-------- app/models/data_digest_templates/booking.rb | 3 +- .../data_digest_templates/_form.html.slim | 4 +- 3 files changed, 70 insertions(+), 57 deletions(-) diff --git a/app/javascript/components/data_digest_templates/ColumnsConfigForm.tsx b/app/javascript/components/data_digest_templates/ColumnsConfigForm.tsx index c62b91764..72f1cca85 100644 --- a/app/javascript/components/data_digest_templates/ColumnsConfigForm.tsx +++ b/app/javascript/components/data_digest_templates/ColumnsConfigForm.tsx @@ -16,21 +16,22 @@ type ColumnsConfigFormProps = { name: string; }; -type DefaultColumnConfig = { +type BaseColumnConfig = { id: string; index: number; - type: ColumnConfigType; header: string; body: string; + type: ColumnConfigType; }; -type UsageColumnConfig = DefaultColumnConfig & { +type UsageColumnConfig = BaseColumnConfig & { type: ColumnConfigType.Usage; - tarif_id: number; + tarif_id: string | number; }; -type CostColumnConfig = DefaultColumnConfig & { - type: "costs"; +type BookingQuestionResponseColumnConfig = BaseColumnConfig & { + type: ColumnConfigType.BookingQuestionResponse; + booking_question_id: string | number; }; function isColumnConfigType(type: string): type is ColumnConfigType { @@ -38,15 +39,18 @@ function isColumnConfigType(type: string): type is ColumnConfigType { } function toJson(columnsConfigs: ColumnConfig[]): string { - return JSON.stringify(columnsConfigs) + return JSON.stringify(columnsConfigs); } -type ColumnConfig = DefaultColumnConfig | UsageColumnConfig | CostColumnConfig; +type ColumnConfig = BaseColumnConfig | UsageColumnConfig | BookingQuestionResponseColumnConfig; export default function ColumnsConfigFrom({ json, name }: ColumnsConfigFormProps) { const { t } = useTranslation(); const [columnsConfig, setColumnsConfig] = useState(() => - (JSON.parse(json) as unknown as ColumnConfig[]).map((data, index) => ({ ...data, index, id: crypto.randomUUID() })), + (JSON.parse(json) as unknown as ColumnConfig[]).map((data) => ({ + ...data, + id: data.id ? data.id : crypto.randomUUID(), + })), ); const handleUpdate = (updatedConfig: ColumnConfig) => @@ -62,7 +66,13 @@ export default function ColumnsConfigFrom({ json, name }: ColumnsConfigFormProps return ( {t("activerecord.attributes.data_digest_template.columns_config")} - + {columnsConfig.map((config) => (
  • @@ -94,64 +104,66 @@ type ColumnConfigFormProps = { }; function ColumnConfigForm({ config, onUpdate, onRemove }: ColumnConfigFormProps) { - const typeSpecificComponents = { - [ColumnConfigType.Default]: () => <>, - [ColumnConfigType.Costs]: () => <>, - [ColumnConfigType.Usage]: () => ( + const typeSpecificComponents: Record React.ReactElement> = { + [ColumnConfigType.Default]: (_config) => <>, + [ColumnConfigType.Costs]: (_config) => <>, + [ColumnConfigType.Usage]: (config) => ( onUpdate({ ...config, tarif_id: event.target.value })} + defaultValue={(config as UsageColumnConfig).tarif_id} + onChange={(event) => onUpdate({ ...(config as UsageColumnConfig), tarif_id: event.target.value })} > ), - [ColumnConfigType.BookingQuestionResponse]: () => ( + [ColumnConfigType.BookingQuestionResponse]: (config) => ( onUpdate({ ...config, id: event.target.value })} + defaultValue={(config as BookingQuestionResponseColumnConfig).booking_question_id} + onChange={(event) => + onUpdate({ ...(config as BookingQuestionResponseColumnConfig), booking_question_id: event.target.value }) + } > ), }; return ( - - - - - - - - - onUpdate({ ...config, header: event.target.value })} - > - - - - - onUpdate({ ...config, body: event.target.value })} - > - - - {typeSpecificComponents[config.type]?.()} - - - -
    - -
    - -
    + + + + + + + + + onUpdate({ ...config, header: event.target.value })} + > + + + + + onUpdate({ ...config, body: event.target.value })} + > + + + {typeSpecificComponents[config.type]?.(config)} + + + +
    + +
    + +
    ); } diff --git a/app/models/data_digest_templates/booking.rb b/app/models/data_digest_templates/booking.rb index 235997858..8031b83d2 100644 --- a/app/models/data_digest_templates/booking.rb +++ b/app/models/data_digest_templates/booking.rb @@ -98,7 +98,8 @@ class Booking < Tabular column_type :booking_question_response do body do |booking, template_context_cache| - response = booking.booking_question_responses.find_by(booking_question_id: @config[:id]) + booking_question_id = @config[:booking_question_id] || @config[:id] # TODO: remove legacy id + response = booking.booking_question_responses.find_by(booking_question_id:) context = template_context_cache[cache_key(booking, :booking_question_response, response&.id)] ||= TemplateContext.new(booking_question_response: response).to_h @templates[:body]&.render!(context) diff --git a/app/views/manage/data_digest_templates/_form.html.slim b/app/views/manage/data_digest_templates/_form.html.slim index 4e11d9ed5..58e2b3a0d 100644 --- a/app/views/manage/data_digest_templates/_form.html.slim +++ b/app/views/manage/data_digest_templates/_form.html.slim @@ -4,12 +4,12 @@ = f.text_field :label = f.text_field :group - = render partial: @data_digest_template.to_partial_path('form_fields'), locals: { data_digest_template: @data_digest_template } - - if @data_digest_template.filter_class h5= @data_digest_template.filter_class.model_name.human = f.fields_for :prefilter_params, @data_digest_template.prefilter do |ff| = render partial: @data_digest_template.filter_class.to_partial_path('filter_fields'), locals: { f: ff } + + = render partial: @data_digest_template.to_partial_path('form_fields'), locals: { data_digest_template: @data_digest_template } = f.submit From 23e68755a9ceac7e5f908f3bdf7464b62fee5a2a Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Wed, 11 Dec 2024 10:42:44 +0000 Subject: [PATCH 06/30] feat: implement accounting_ref, payment_ref and tenant_ref --- app/models/booking.rb | 10 +-- app/models/booking_state_checklist_item.rb | 2 +- app/models/data_digest_templates/invoice.rb | 2 +- .../data_digest_templates/invoice_part.rb | 2 +- app/models/data_digest_templates/payment.rb | 2 +- app/models/invoice.rb | 36 ++++------ app/models/invoice/factory.rb | 2 +- app/models/invoices/offer.rb | 4 ++ app/models/key_sequence.rb | 14 ---- app/models/payment_info.rb | 2 +- app/models/payment_infos/qr_bill.rb | 6 +- app/models/tenant.rb | 9 +++ app/params/manage/organisation_params.rb | 3 +- app/serializers/manage/invoice_serializer.rb | 5 +- .../manage/organisation_serializer.rb | 2 +- app/services/booking_ref_service.rb | 54 --------------- app/services/camt_service.rb | 12 +++- app/services/export/pdf/invoice_pdf.rb | 2 +- app/services/import/csv/booking_importer.rb | 3 +- .../import/hash/organisation_importer.rb | 2 +- app/services/invoice_ref_service.rb | 66 ------------------- app/services/ref_builder.rb | 24 +++++++ app/services/ref_builders/booking.rb | 46 +++++++++++++ .../ref_builders/invoice_accounting.rb | 22 +++++++ app/services/ref_builders/invoice_payment.rb | 36 ++++++++++ app/services/ref_builders/tenant.rb | 18 +++++ app/views/manage/invoices/_form.html.slim | 21 +++--- app/views/manage/invoices/index.html.slim | 4 +- .../manage/organisations/_form.html.slim | 2 +- config/locales/de.yml | 1 - config/locales/en.yml | 1 - config/locales/fr.yml | 1 - config/locales/it.yml | 1 - ...invoice_ref_strategy_from_organisations.rb | 2 - ...641_add_key_sequence_number_to_invoices.rb | 11 +--- ...1210160506_rename_and_add_ref_templates.rb | 7 ++ .../20241211091234_rename_and_add_refs.rb | 31 +++++++++ db/schema.rb | 15 +++-- spec/models/invoice_spec.rb | 20 ++++-- spec/models/key_sequence_spec.rb | 4 +- spec/models/payment_infos/qr_bill_spec.rb | 4 +- spec/services/camt_service_spec.rb | 24 +++++++ spec/services/invoice_ref_service_spec.rb | 31 --------- .../booking_spec.rb} | 25 ++++--- .../ref_builders/payment_info_spec.rb | 15 +++++ 45 files changed, 338 insertions(+), 268 deletions(-) delete mode 100644 app/services/booking_ref_service.rb delete mode 100644 app/services/invoice_ref_service.rb create mode 100644 app/services/ref_builder.rb create mode 100644 app/services/ref_builders/booking.rb create mode 100644 app/services/ref_builders/invoice_accounting.rb create mode 100644 app/services/ref_builders/invoice_payment.rb create mode 100644 app/services/ref_builders/tenant.rb create mode 100644 db/migrate/20241210160506_rename_and_add_ref_templates.rb create mode 100644 db/migrate/20241211091234_rename_and_add_refs.rb create mode 100644 spec/services/camt_service_spec.rb delete mode 100644 spec/services/invoice_ref_service_spec.rb rename spec/services/{booking_ref_service_spec.rb => ref_builders/booking_spec.rb} (55%) create mode 100644 spec/services/ref_builders/payment_info_spec.rb diff --git a/app/models/booking.rb b/app/models/booking.rb index 80b9c5878..5dbba3e36 100644 --- a/app/models/booking.rb +++ b/app/models/booking.rb @@ -119,7 +119,7 @@ class Booking < ApplicationRecord scope :with_default_includes, -> { includes(DEFAULT_INCLUDES) } before_validation :update_occupancies, :assert_tenant! - before_create :set_ref + before_create :generate_ref accepts_nested_attributes_for :tenant, update_only: true, reject_if: :reject_tenant_attributes? accepts_nested_attributes_for :usages, reject_if: :all_blank, allow_destroy: true @@ -195,14 +195,14 @@ def editable? booking_state&.editable || false end + def generate_ref(force: false) + self.ref = RefBuilders::Booking.new(self).generate if ref.blank? || force + end + private def reject_tenant_attributes?(tenant_attributes) (tenant_id_changed? && tenant_id_was.present?) || tenant_attributes.slice(:email, :first_name, :last_name, :street_address, :zipcode, :city).values.all?(&:blank?) end - - def set_ref - self.ref ||= BookingRefService.new(organisation).generate(self) - end end diff --git a/app/models/booking_state_checklist_item.rb b/app/models/booking_state_checklist_item.rb index e26837f43..79c12c035 100644 --- a/app/models/booking_state_checklist_item.rb +++ b/app/models/booking_state_checklist_item.rb @@ -23,7 +23,7 @@ class BookingStateChecklistItem invoices_settled: lambda do |booking| booking.invoices.kept.ordered.map do |invoice| label_key = invoice.refund? ? :invoice_refunded : :invoice_paid - label_invoice = "#{invoice.class.model_name.human} #{invoice.formatted_ref}" + label_invoice = "#{invoice.class.model_name.human} #{invoice.accounting_ref}" BookingStateChecklistItem.new(key: :invoice_settled, context: { booking: }, label: BookingStateChecklistItem.translate(label_key, invoice: label_invoice), checked: invoice.settled?, diff --git a/app/models/data_digest_templates/invoice.rb b/app/models/data_digest_templates/invoice.rb index 849a8608f..feacfce6c 100644 --- a/app/models/data_digest_templates/invoice.rb +++ b/app/models/data_digest_templates/invoice.rb @@ -30,7 +30,7 @@ class Invoice < Tabular DEFAULT_COLUMN_CONFIG = [ { header: ::Invoice.human_attribute_name(:ref), - body: '{{ invoice.ref }}' + body: '{{ invoice.payment_ref }}' }, { header: ::Booking.human_attribute_name(:ref), diff --git a/app/models/data_digest_templates/invoice_part.rb b/app/models/data_digest_templates/invoice_part.rb index 4493d7f22..e65a383ed 100644 --- a/app/models/data_digest_templates/invoice_part.rb +++ b/app/models/data_digest_templates/invoice_part.rb @@ -30,7 +30,7 @@ class InvoicePart < Tabular DEFAULT_COLUMN_CONFIG = [ { header: ::Invoice.human_attribute_name(:ref), - body: '{{ invoice.ref }}' + body: '{{ invoice.payment_ref }}' }, { header: ::Booking.human_attribute_name(:ref), diff --git a/app/models/data_digest_templates/payment.rb b/app/models/data_digest_templates/payment.rb index 89800ddd3..347dce76e 100644 --- a/app/models/data_digest_templates/payment.rb +++ b/app/models/data_digest_templates/payment.rb @@ -30,7 +30,7 @@ class Payment < Tabular DEFAULT_COLUMN_CONFIG = [ { header: ::Payment.human_attribute_name(:ref), - body: '{{ payment.invoice.ref }}' + body: '{{ payment.invoice.payment_ref }}' }, { header: ::Booking.human_attribute_name(:ref), diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 6f75cb361..5698c4511 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -68,8 +68,7 @@ class Invoice < ApplicationRecord accepts_nested_attributes_for :invoice_parts, reject_if: :all_blank, allow_destroy: true before_save :recalculate - # after_create { generate_ref? && generate_ref && save } - before_create :sequence_number, :build_refs + before_create :sequence_number, :generate_accounting_ref, :generate_payment_ref after_create :supersede! before_update :generate_pdf, if: :generate_pdf? after_save :recalculate! @@ -82,7 +81,7 @@ class Invoice < ApplicationRecord end def generate_pdf? - kept? && ref.present? && !skip_generate_pdf && (pdf.blank? || changed?) + kept? && payment_ref.present? && !skip_generate_pdf && (pdf.blank? || changed?) end def supersede! @@ -93,7 +92,11 @@ def supersede! end def sequence_number - @sequence_number ||= organisation.key_sequences.key(Invoice.sti_name, year: :current).lease! + @sequence_number ||= organisation.key_sequences.key(::Invoice.sti_name, year: sequence_year).lease! + end + + def sequence_year + @sequence_year ||= created_at.year end def generate_pdf @@ -103,12 +106,13 @@ def generate_pdf end end - # def generate_ref - # self.ref = invoice_ref_service.generate(self) - # end + def generate_accounting_ref(force: false) + self.accounting_ref = RefBuilders::InvoiceAccounting.new(self).generate if accounting_ref.blank? || force + end - def build_refs - # self.sequence_number ||= organisation.key_sequences.key + # this should never be forced + def generate_payment_ref + self.payment_ref = RefBuilders::InvoicePayment.new(self).generate if payment_ref.blank? end def paid? @@ -160,16 +164,8 @@ def sent! update(sent_at: Time.zone.now) end - def formatted_ref - invoice_ref_service.format_ref(ref) - end - def to_s - "#{booking.ref} - #{formatted_ref}" - end - - def invoice_ref_service - @invoice_ref_service ||= InvoiceRefService.new(organisation) + accounting_ref end def payment_info @@ -196,10 +192,6 @@ def journal_entries [debitor_journal_entry] + invoice_parts.map(&:journal_entries) end - def accounting_ref - format('HV%05d', id + 1) - end - def debitor_journal_entry Accounting::JournalEntry.new( account: organisation.accounting_settings.debitor_account_nr, diff --git a/app/models/invoice/factory.rb b/app/models/invoice/factory.rb index 909fc0db0..06bca0ca9 100644 --- a/app/models/invoice/factory.rb +++ b/app/models/invoice/factory.rb @@ -21,7 +21,7 @@ def prepare_to_supersede(invoice) supersede_invoice = invoice.supersede_invoice invoice.booking ||= supersede_invoice.booking - invoice.ref ||= supersede_invoice.ref + invoice.payment_ref ||= supersede_invoice.payment_ref end def defaults(booking) diff --git a/app/models/invoices/offer.rb b/app/models/invoices/offer.rb index 40da21ddc..fa8359f08 100644 --- a/app/models/invoices/offer.rb +++ b/app/models/invoices/offer.rb @@ -51,5 +51,9 @@ def payment_info def payment_required false end + + def sequence_number + @sequence_number ||= organisation.key_sequences.key(self.class.sti_name, year: sequence_year).lease! + end end end diff --git a/app/models/key_sequence.rb b/app/models/key_sequence.rb index 2ee807561..cd2a617d1 100644 --- a/app/models/key_sequence.rb +++ b/app/models/key_sequence.rb @@ -10,18 +10,4 @@ class KeySequence < ApplicationRecord def lease! increment!(:value).value # rubocop:disable Rails/SkipsModelValidations end - - # module ActiveRecord - # extend ActiveSupport::Concern - - # class_methods do - # def sequence_number(key, column = :sequence_number, year: nil) - # before_save do - # # year = year.call if year.respond_to?(:call) - # leased_sequence_number = organisation&.key_sequences&.key(key, year: year)&.lease! - # try("#{column}||=", leased_sequence_number) if leased_sequence_number.present? - # end - # end - # end - # end end diff --git a/app/models/payment_info.rb b/app/models/payment_info.rb index 2a4a1f324..14b706c0a 100644 --- a/app/models/payment_info.rb +++ b/app/models/payment_info.rb @@ -23,7 +23,7 @@ def invoice_address end def formatted_ref - invoice_ref_service.format_ref(ref) + invoice.payment_ref end def formatted_amount diff --git a/app/models/payment_infos/qr_bill.rb b/app/models/payment_infos/qr_bill.rb index 8bd1fa22e..4ffe19ac1 100644 --- a/app/models/payment_infos/qr_bill.rb +++ b/app/models/payment_infos/qr_bill.rb @@ -102,12 +102,12 @@ def ref_type end def scor_ref - @scor_ref ||= format('RF%02d%s', checksum: checksum(invoice.ref), - unchecked_ref: invoice.ref) + @scor_ref ||= format('RF%02d%s', checksum: checksum(invoice.payment_ref), + unchecked_ref: invoice.payment_ref) end def qrr_ref - invoice.ref.rjust(27, '0') + invoice.payment_ref.rjust(27, '0') end def ref diff --git a/app/models/tenant.rb b/app/models/tenant.rb index 0d880c174..35a869b50 100644 --- a/app/models/tenant.rb +++ b/app/models/tenant.rb @@ -66,6 +66,7 @@ class Tenant < ApplicationRecord scope :ordered, -> { order(last_name: :ASC, first_name: :ASC, id: :ASC) } after_validation { errors.delete(:bookings) } + before_create :sequence_number, :generate_accounting_ref before_save do self.search_cache = contact_lines.flatten.join('\n') end @@ -98,6 +99,14 @@ def salutations end.symbolize_keys end + def sequence_number + @sequence_number ||= organisation.key_sequences.key(Tenant.sti_name).lease! + end + + def generate_accounting_ref(force: false) + self.accounting_ref = RefBuilders::Tenant.new(self).generate if accounting_ref.blank? || force + end + def address_lines [ address_addon&.strip, diff --git a/app/params/manage/organisation_params.rb b/app/params/manage/organisation_params.rb index a7b9daa79..be05c8f0a 100644 --- a/app/params/manage/organisation_params.rb +++ b/app/params/manage/organisation_params.rb @@ -22,7 +22,8 @@ def self.settings_permitted_keys end def self.admin_permitted_keys - permitted_keys + %i[smtp_settings slug booking_ref_template invoice_ref_template booking_flow_type currency] + permitted_keys + %i[smtp_settings slug booking_ref_template invoice_payment_ref_template booking_flow_type + currency] end end end diff --git a/app/serializers/manage/invoice_serializer.rb b/app/serializers/manage/invoice_serializer.rb index 099490a57..c7a097919 100644 --- a/app/serializers/manage/invoice_serializer.rb +++ b/app/serializers/manage/invoice_serializer.rb @@ -3,7 +3,8 @@ module Manage class InvoiceSerializer < ApplicationSerializer identifier :id - fields :type, :text, :issued_at, :payable_until, :ref, :sent_at, :booking_id, - :amount_paid, :percentage_paid, :amount, :locale, :payment_required + fields :type, :text, :issued_at, :payable_until, :sent_at, :booking_id, + :amount_paid, :percentage_paid, :amount, :locale, :payment_required, + :accounting_ref, :payment_ref end end diff --git a/app/serializers/manage/organisation_serializer.rb b/app/serializers/manage/organisation_serializer.rb index 04c672cf9..e1bae3df9 100644 --- a/app/serializers/manage/organisation_serializer.rb +++ b/app/serializers/manage/organisation_serializer.rb @@ -11,7 +11,7 @@ class OrganisationSerializer < Public::OrganisationSerializer association :booking_questions, blueprint: Public::BookingQuestionSerializer fields :esr_beneficiary_account, :iban, :mail_from, :booking_ref_template, - :booking_flow_type, :invoice_ref_template, :notifications_enabled, :location, :nickname_label_i18n + :booking_flow_type, :invoice_payment_ref_template, :notifications_enabled, :location, :nickname_label_i18n field :designated_documents do |organisation| organisation.designated_documents.pluck(:designation).map do |designation| diff --git a/app/services/booking_ref_service.rb b/app/services/booking_ref_service.rb deleted file mode 100644 index c4e8b5f06..000000000 --- a/app/services/booking_ref_service.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -class BookingRefService - DEFAULT_TEMPLATE = '%s%04d%02d%02d%s' - - def initialize(organisation) - @organisation = organisation - end - - def self.ref_parts - @ref_parts ||= {} - end - - def self.ref_part(hash) - ref_parts.merge!(hash) - end - - ref_part home_ref: ->(booking) { booking.home.ref }, - year: ->(booking) { booking.begins_at.year }, - month: ->(booking) { booking.begins_at.month }, - day: ->(booking) { booking.begins_at.day } - - ref_part occupiable_refs: (lambda do |booking| - booking.occupancies.map(&:occupiable).sort_by(&:ordinal).map(&:ref).join - end) - - ref_part same_month_count: (lambda do |booking| - day = booking.begins_at - booking.organisation.bookings.begins_at(after: day.beginning_of_month, before: day.end_of_month).count - end) - - ref_part same_year_count: (lambda do |booking| - day = booking.begins_at - booking.organisation.bookings.begins_at(after: day.beginning_of_year, before: day.end_of_year).count - end) - - ref_part same_day_alpha: (lambda do |booking| - day = booking.begins_at - count = booking.organisation.bookings.begins_at(after: day.beginning_of_day, before: day.end_of_day).count - - next '' if count < 2 - next count if count > 25 - - # CHAR_START = 96 - (count + 95).chr - end) - - def generate(booking, template_string = @organisation.booking_ref_template) - template_string = template_string.presence || DEFAULT_TEMPLATE - ref_parts = self.class.ref_parts.select { |key| template_string.include?(key.to_s) } - .transform_values { |callable| callable.call(booking) } - format(template_string, ref_parts) - end -end diff --git a/app/services/camt_service.rb b/app/services/camt_service.rb index 24296fcf6..22d078649 100644 --- a/app/services/camt_service.rb +++ b/app/services/camt_service.rb @@ -39,8 +39,16 @@ def payment_from_transaction(transaction, entry) end def find_invoice_by_ref(ref) - invoice_ref_service = InvoiceRefService.new(@organisation) - invoice_ref_service.find_invoice_by_ref(ref, scope: @organisation.invoices.kept) + @organisation.invoices.kept.where(normalized_ref_condition(ref)).first + end + + def self.normalized_ref_condition(ref) + Arel::Nodes::NamedFunction.new('LPAD', [Invoice.arel_table[:ref], 27, Arel::Nodes.build_quoted('0')]) + .eq(normalize_ref(ref)) + end + + def self.normalize_ref(ref) + ref.delete(' ').gsub(/\ARF\d\d/, '').rjust(27, '0') end def transaction_to_h(transaction) diff --git a/app/services/export/pdf/invoice_pdf.rb b/app/services/export/pdf/invoice_pdf.rb index fac926bd8..1d7b94354 100644 --- a/app/services/export/pdf/invoice_pdf.rb +++ b/app/services/export/pdf/invoice_pdf.rb @@ -21,7 +21,7 @@ def initialize(invoice) end to_render do - header_text = "#{Booking.human_model_name} #{booking.ref}" + header_text = invoice.accounting_ref || booking.ref render Renderables::PageHeader.new(text: header_text, logo: organisation.logo) end diff --git a/app/services/import/csv/booking_importer.rb b/app/services/import/csv/booking_importer.rb index 93073d987..f1183fed6 100644 --- a/app/services/import/csv/booking_importer.rb +++ b/app/services/import/csv/booking_importer.rb @@ -23,7 +23,6 @@ def initialize(home, **) super(**) @home = home.is_a?(Home) ? home : Home.find(home) @tenant_importer = TenantImporter.new(organisation) - @booking_ref_service = BookingRefService.new(organisation) end def initialize_record(_row) @@ -40,7 +39,7 @@ def initial_state def persist_record(booking) booking.transition_to ||= initial_state booking.assert_tenant! - booking.ref ||= @booking_ref_service.generate(booking) if booking.valid? + booking.generate_ref if booking.valid? return false unless booking.save booking.deadline&.clear diff --git a/app/services/import/hash/organisation_importer.rb b/app/services/import/hash/organisation_importer.rb index e3ab6911d..af17711ef 100644 --- a/app/services/import/hash/organisation_importer.rb +++ b/app/services/import/hash/organisation_importer.rb @@ -4,7 +4,7 @@ module Import module Hash class OrganisationImporter < Base use_attributes(*%w[name email address booking_flow_type currency location mail_from - iban invoice_ref_template booking_ref_template notifications_enabled slug + iban invoice_payment_ref_template booking_ref_template notifications_enabled slug settings currency country_code]) def initialize_record(_hash) diff --git a/app/services/invoice_ref_service.rb b/app/services/invoice_ref_service.rb deleted file mode 100644 index 2f31b3eb6..000000000 --- a/app/services/invoice_ref_service.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -class InvoiceRefService - # ESR Modes - # 01 = ESR in CHF - # 04 = ESR+ in CHF - # 11 = ESR in CHF zur Gutschrift auf das eigene Konto - # 14 = ESR+ in CHF zur Gutschrift auf das eigene Konto - # 21 = ESR in EUR - # 23 = ESR in EUR zur Gutschrift auf das eigene Konto - # 31 = ESR+ in EUR - # 33 = ESR+ in EUR zur Gutschrift auf das eigene Konto - DEFAULT_TEMPLATE = '%s%03d%06d%07d' - - def initialize(organisation) - @organisation = organisation - end - - def generate(invoice, template_string = @organisation.invoice_ref_template) - with_checksum format(template_string.presence || DEFAULT_TEMPLATE, - prefix: digits(invoice.organisation.esr_ref_prefix).join, - home_id: invoice.booking.home_id, - tenant_id: invoice.booking.tenant_id, - invoice_id: invoice.id) - end - - def digits(string) - string.to_s.scan(/\d/) || [] - end - - def checksum(ref) - check_table = [0, 9, 4, 6, 8, 2, 7, 1, 3, 5] - (10 - digits(ref).inject(0) { |carry, digit| check_table[(digit.to_i + carry) % check_table.size] }) % 10 - end - - def with_checksum(ref) - "#{ref}#{checksum(ref)}" - end - - def format_ref(ref) - return '' if ref.blank? - - ref.reverse.chars.in_groups_of(5).reverse.map { |group| group.reverse.join }.join(' ') - end - - def find_invoice_by_ref(ref, scope:) - ref_column = Invoice.arel_table[:ref] - padded_ref_column = Arel::Nodes::NamedFunction.new('LPAD', [ref_column, 27, Arel::Nodes.build_quoted('0')]) - scope.where(padded_ref_column.eq(normalize_ref(ref))).first - end - - def normalize_ref(ref) - ref = ref.delete(' ') - qr_ref = ref.match(/\ARF\d{2}(?\d+)\z/) - ref = qr_ref[:ref] if qr_ref - ref.rjust(27, '0') - end - - def account_nr_to_code(value) - parts = value&.match(/(?\d{2})-(?\d{3,6})-(?\d)/)&.named_captures || {} - parts.transform_keys!(&:to_sym).transform_values!(&:to_i) - return '' if parts.none? - - format('%02d%06d%1d', parts) - end -end diff --git a/app/services/ref_builder.rb b/app/services/ref_builder.rb new file mode 100644 index 000000000..62e8ebe13 --- /dev/null +++ b/app/services/ref_builder.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class RefBuilder + DEFAULT_TEMPLATE = '' + + def initialize(organisation) + @organisation = organisation + end + + def self.ref_parts + @ref_parts ||= {} + end + + def self.ref_part(hash) + ref_parts.merge!(hash) + end + + def generate_lazy(template_string) + template_string = template_string.presence || self.class::DEFAULT_TEMPLATE + ref_parts = self.class.ref_parts.select { |key| template_string.include?(key.to_s) } + .transform_values { |callable| instance_eval(&callable) } + format(template_string, ref_parts) + end +end diff --git a/app/services/ref_builders/booking.rb b/app/services/ref_builders/booking.rb new file mode 100644 index 000000000..3ebc9b9ae --- /dev/null +++ b/app/services/ref_builders/booking.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module RefBuilders + class Booking < RefBuilder + DEFAULT_TEMPLATE = '%s%04d%02d%02d%s' + + def initialize(booking) + super(booking.organisation) + @booking = booking + end + + ref_part home_ref: proc { @booking.home.ref }, + year: proc { @booking.begins_at.year }, + month: proc { @booking.begins_at.month }, + day: proc { @booking.begins_at.day } + + ref_part occupiable_refs: (proc do + @booking.occupancies.map(&:occupiable).sort_by(&:ordinal).map(&:ref).join + end) + + # ref_part same_month_count: (proc do |@booking| + # day = @booking.begins_at + # @booking.organisation.@bookings.begins_at(after: day.beginning_of_month, before: day.end_of_month).count + # end) + + # ref_part same_year_count: (proc do |@booking| + # day = @booking.begins_at + # @booking.organisation.@bookings.begins_at(after: day.beginning_of_year, before: day.end_of_year).count + # end) + + ref_part same_day_alpha: (proc do + day = @booking.begins_at + count = @booking.organisation.bookings.begins_at(after: day.beginning_of_day, before: day.end_of_day).count + + next '' if count < 2 + next count if count > 25 + + # CHAR_START = 96 + (count + 95).chr + end) + + def generate(template_string = @organisation.booking_ref_template) + generate_lazy(template_string) + end + end +end diff --git a/app/services/ref_builders/invoice_accounting.rb b/app/services/ref_builders/invoice_accounting.rb new file mode 100644 index 000000000..c1da36316 --- /dev/null +++ b/app/services/ref_builders/invoice_accounting.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module RefBuilders + class InvoiceAccounting < RefBuilder + DEFAULT_TEMPLATE = '%2d%04d' + + def initialize(invoice) + super(invoice.organisation) + @invoice = invoice + end + + def generate(template_string = @organisation.invoice_accounting_ref_template) + generate_lazy(template_string) + end + + ref_part home_id: proc { @invoice.booking.home_id }, + tenant_id: proc { @invoice.booking.tenant_id }, + sequence_number: proc { @invoice.sequence_number }, + sequence_year: proc { @invoice.sequence_year }, + short_year: proc { @invoice.sequence_year - 2000 } + end +end diff --git a/app/services/ref_builders/invoice_payment.rb b/app/services/ref_builders/invoice_payment.rb new file mode 100644 index 000000000..2e2b217bf --- /dev/null +++ b/app/services/ref_builders/invoice_payment.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module RefBuilders + class InvoicePayment < RefBuilder + DEFAULT_TEMPLATE = '%s%06d%04d%05d' + + def initialize(invoice) + super(invoice.organisation) + @invoice = invoice + end + + def generate(template_string = @organisation.invoice_payment_ref_template) + generate_lazy(template_string) + end + + ref_part home_id: proc { @invoice.booking.home_id }, + tenant_id: proc { @invoice.booking.tenant_id }, + tenant_sequence_number: proc { @invoice.booking.tenant.sequence_number }, + sequence_number: proc { @invoice.sequence_number }, + sequence_year: proc { @invoice.sequence_year }, + prefix: proc { self.class.digits(@invoice.organisation.esr_ref_prefix.to_s).join } + + def self.digits(ref) + ref.to_s.scan(/\d/) || [] + end + + def self.with_checksum(ref) + "#{ref}#{checksum(ref)}" + end + + def self.checksum(ref) + check_table = [0, 9, 4, 6, 8, 2, 7, 1, 3, 5] + (10 - digits(ref).inject(0) { |carry, digit| check_table[(digit.to_i + carry) % check_table.size] }) % 10 + end + end +end diff --git a/app/services/ref_builders/tenant.rb b/app/services/ref_builders/tenant.rb new file mode 100644 index 000000000..828add313 --- /dev/null +++ b/app/services/ref_builders/tenant.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module RefBuilders + class Tenant < RefBuilder + DEFAULT_TEMPLATE = '%d' + + def initialize(tenant) + super(tenant.organisation) + @tenant = tenant + end + + ref_part sequence_number: proc { @tenant.sequence_number } + + def generate(template_string = @organisation.tenant_accounting_ref_template) + generate_lazy(template_string) + end + end +end diff --git a/app/views/manage/invoices/_form.html.slim b/app/views/manage/invoices/_form.html.slim index 26465370f..196a6ee29 100644 --- a/app/views/manage/invoices/_form.html.slim +++ b/app/views/manage/invoices/_form.html.slim @@ -13,20 +13,23 @@ label.btn.btn-default.mb-3 for="toggle_invoice_booking_id" i.fa.fa-unlock - = f.text_field :ref, help: t('generated') = f.text_area :text, class: 'rich-text-area' = f.date_field :issued_at, lang: I18n.locale, help: t('optional') - = f.date_field :payable_until, lang: I18n.locale - = f.check_box :payment_required + / = f.date_field :payable_until, lang: I18n.locale + / = f.text_field :respite_days, inputmode: "numeric" + = f.select :payment_info_type, subtype_options_for_select(PaymentInfo.subtypes), include_blank: true + = f.check_box :payment_required - h5.mt-4= InvoicePart.model_name.human(count: 2) - = f.fields_for :invoice_parts, @invoice.invoice_parts do |ipf| - = render partial: ipf.object.to_partial_path('form_fields'), locals: { f: ipf, invoice_part: ipf.object } + - if @invoice.invoice_parts.any? + h5.mt-4= InvoicePart.model_name.human(count: 2) + = f.fields_for :invoice_parts, @invoice.invoice_parts do |ipf| + = render partial: ipf.object.to_partial_path('form_fields'), locals: { f: ipf, invoice_part: ipf.object } - h5.mt-4= t('.suggested_invoice_parts') - = f.fields_for :invoice_parts, @invoice.suggested_invoice_parts do |ipf| - = render partial: ipf.object.to_partial_path('form_fields'), locals: { f: ipf, invoice_part: ipf.object } + - if @invoice.suggested_invoice_parts.any? + h5.mt-4= t('.suggested_invoice_parts') + = f.fields_for :invoice_parts, @invoice.suggested_invoice_parts do |ipf| + = render partial: ipf.object.to_partial_path('form_fields'), locals: { f: ipf, invoice_part: ipf.object } div - unless @invoice.new_record? diff --git a/app/views/manage/invoices/index.html.slim b/app/views/manage/invoices/index.html.slim index bf0d8d7c8..8dc0ffdac 100644 --- a/app/views/manage/invoices/index.html.slim +++ b/app/views/manage/invoices/index.html.slim @@ -49,10 +49,10 @@ div= link_to invoice.booking, manage_booking_path(invoice.booking) - if invoice.pdf.attached? = link_to manage_invoice_path(invoice, format: :pdf), target: :_blank do - small= invoice.ref + invoice.accounting_ref span.ms-2.fa.fa-print - else - = link_to invoice.ref, manage_invoice_path(invoice) + = link_to invoice.accounting_ref, manage_invoice_path(invoice) td.align-middle = invoice.model_name.human diff --git a/app/views/manage/organisations/_form.html.slim b/app/views/manage/organisations/_form.html.slim index e0a547bbc..f6bd98da0 100644 --- a/app/views/manage/organisations/_form.html.slim +++ b/app/views/manage/organisations/_form.html.slim @@ -44,7 +44,7 @@ div[v-pre]= f.text_area :account_address, rows: 4, help: t('optional') - if current_user.role_admin? - = f.text_field :invoice_ref_template + = f.text_field :payment_ref_template = f.text_field :currency fieldset diff --git a/config/locales/de.yml b/config/locales/de.yml index 9154483b4..8f3b3b704 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1302,7 +1302,6 @@ de: update: notice: Deine Reservationsanfrage wurde aktualisiert. Du kannst das Browserfenster schliessen. from: von - generated: wird automatisch generiert helpers: select: prompt: Bitte wählen diff --git a/config/locales/en.yml b/config/locales/en.yml index 845b10a3b..68e220040 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1064,7 +1064,6 @@ en: update: notice: Deine Reservationsanfrage wurde aktualisiert. Du kannst das Browserfenster schliessen. from: von - generated: wird automatisch generiert helpers: select: prompt: Bitte wählen diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 4b08c5c38..f8ccc2d1b 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1155,7 +1155,6 @@ fr: update: notice: Votre demande de réservation a été mise à jour. Vous pouvez fermer la fenêtre de votre navigateur. from: de - generated: est généré automatiquement helpers: select: prompt: Veuillez sélectionner diff --git a/config/locales/it.yml b/config/locales/it.yml index 0816a7049..30979da65 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -1155,7 +1155,6 @@ it: update: notice: La richiesta di prenotazione è stata aggiornata. Potete chiudere la finestra del browser. from: da - generated: viene generato automaticamente helpers: select: prompt: Selezionare diff --git a/db/migrate/20240314145711_remove_invoice_ref_strategy_from_organisations.rb b/db/migrate/20240314145711_remove_invoice_ref_strategy_from_organisations.rb index 1480c0a98..7075d4aa8 100644 --- a/db/migrate/20240314145711_remove_invoice_ref_strategy_from_organisations.rb +++ b/db/migrate/20240314145711_remove_invoice_ref_strategy_from_organisations.rb @@ -10,8 +10,6 @@ def change direction.up do Organisation.find_each do |organisation| organisation.instance_exec do - self.booking_ref_template = nil if booking_ref_template == BookingRefService::DEFAULT_TEMPLATE - self.invoice_ref_template = nil if invoice_ref_template == InvoiceRefService::DEFAULT_TEMPLATE self.update!(currency: 'CHF', country_code: 'CH') end end diff --git a/db/migrate/20241209160641_add_key_sequence_number_to_invoices.rb b/db/migrate/20241209160641_add_key_sequence_number_to_invoices.rb index c96c272dc..644414321 100644 --- a/db/migrate/20241209160641_add_key_sequence_number_to_invoices.rb +++ b/db/migrate/20241209160641_add_key_sequence_number_to_invoices.rb @@ -1,15 +1,6 @@ class AddKeySequenceNumberToInvoices < ActiveRecord::Migration[8.0] def change add_column :invoices, :sequence_number, :integer, null: true - add_column :tenants, :sequence_number, :integer, null: true - add_column :bookings, :sequence_number, :integer, null: true + add_column :invoices, :sequence_year, :integer, null: true end - - # protected - - # def backfill_sequence_numbers - # Organisation.find_each do |organisation| - # organisation. - # end - # end end diff --git a/db/migrate/20241210160506_rename_and_add_ref_templates.rb b/db/migrate/20241210160506_rename_and_add_ref_templates.rb new file mode 100644 index 000000000..123c83e03 --- /dev/null +++ b/db/migrate/20241210160506_rename_and_add_ref_templates.rb @@ -0,0 +1,7 @@ +class RenameAndAddRefTemplates < ActiveRecord::Migration[8.0] + def change + rename_column :organisations, :invoice_ref_template, :invoice_payment_ref_template + add_column :organisations, :invoice_accounting_ref_template, :string, null: true + add_column :organisations, :tenant_accounting_ref_template, :string, null: true + end +end diff --git a/db/migrate/20241211091234_rename_and_add_refs.rb b/db/migrate/20241211091234_rename_and_add_refs.rb new file mode 100644 index 000000000..4eadf11fc --- /dev/null +++ b/db/migrate/20241211091234_rename_and_add_refs.rb @@ -0,0 +1,31 @@ +class RenameAndAddRefs < ActiveRecord::Migration[8.0] + def change + rename_column :invoices, :ref, :payment_ref + add_column :invoices, :accounting_ref, :string, null: true + add_column :tenants, :accounting_ref, :string, null: true + + reversible do |direction| + direction.up do + backfill + end + end + end + + def backfill + Organisation.find_each(batch_size: 1) do |organisation| + organisation.invoices.order(created_at: :ASC).each do |invoice| + invoice.sequence_number + invoice.generate_accounting_ref(force: true); + # this will overwrite the payment ref and mess up all payments! + # invoice.generate_payment_ref(force: true); + invoice.skip_generate_pdf = true + invoice.save! + end + organisation.tenants.order(created_at: :ASC).each do |tenant| + tenant.sequence_number + tenant.generate_accounting_ref(force: true) + tenant.save! + end + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 2405e8f7c..622bb22c4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2024_12_09_160641) do +ActiveRecord::Schema[8.0].define(version: 2024_12_11_091234) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "pgcrypto" @@ -223,7 +223,6 @@ t.integer "home_id", null: false t.boolean "ignore_conflicting", default: false, null: false t.jsonb "booking_questions" - t.integer "sequence_number" t.index ["booking_state_cache"], name: "index_bookings_on_booking_state_cache" t.index ["locale"], name: "index_bookings_on_locale" t.index ["organisation_id"], name: "index_bookings_on_organisation_id" @@ -321,7 +320,7 @@ t.datetime "payable_until", precision: nil t.datetime "sent_at", precision: nil t.text "text" - t.string "ref" + t.string "payment_ref" t.decimal "amount", default: "0.0" t.datetime "discarded_at", precision: nil t.datetime "created_at", precision: nil, null: false @@ -332,9 +331,11 @@ t.string "locale" t.boolean "payment_required", default: true t.integer "sequence_number" + t.integer "sequence_year" + t.string "accounting_ref" t.index ["booking_id"], name: "index_invoices_on_booking_id" t.index ["discarded_at"], name: "index_invoices_on_discarded_at" - t.index ["ref"], name: "index_invoices_on_ref" + t.index ["payment_ref"], name: "index_invoices_on_payment_ref" t.index ["supersede_invoice_id"], name: "index_invoices_on_supersede_invoice_id" t.index ["type"], name: "index_invoices_on_type" end @@ -484,7 +485,7 @@ t.jsonb "smtp_settings" t.string "esr_ref_prefix" t.string "default_payment_info_type" - t.string "invoice_ref_template", default: "" + t.string "invoice_payment_ref_template", default: "" t.string "booking_ref_template", default: "" t.jsonb "settings", default: {} t.text "creditor_address" @@ -493,6 +494,8 @@ t.text "cors_origins" t.jsonb "nickname_label_i18n", default: {} t.jsonb "accounting_settings", default: {} + t.string "invoice_accounting_ref_template" + t.string "tenant_accounting_ref_template" t.index ["slug"], name: "index_organisations_on_slug", unique: true end @@ -590,7 +593,7 @@ t.boolean "bookings_without_invoice", default: false t.integer "salutation_form" t.string "accounting_account_nr" - t.integer "sequence_number" + t.string "accounting_ref" t.index ["email", "organisation_id"], name: "index_tenants_on_email_and_organisation_id", unique: true t.index ["email"], name: "index_tenants_on_email" t.index ["organisation_id"], name: "index_tenants_on_organisation_id" diff --git a/spec/models/invoice_spec.rb b/spec/models/invoice_spec.rb index 8a1b85f72..331f0f2e0 100644 --- a/spec/models/invoice_spec.rb +++ b/spec/models/invoice_spec.rb @@ -40,11 +40,7 @@ RSpec.describe Invoice, type: :model do let(:organisation) { create(:organisation, :with_templates) } - let(:invoice) { create(:invoice) } - - describe '#ref' do - it { is_expected.not_to be_blank } - end + let(:invoice) { create(:invoice, organisation:) } describe '::unsettled' do let!(:offer) { create(:invoice, type: Invoices::Offer) } @@ -82,7 +78,7 @@ successor.save expect(predecessor).to be_discarded expect(successor.type).to eq(Invoices::LateNotice.to_s) - expect(successor.ref).to eq(predecessor.ref) + expect(successor.payment_ref).to eq(predecessor.payment_ref) end it 'migrates payments and invoice_parts' do @@ -90,5 +86,17 @@ expect(predecessor.payments.reload).to be_blank expect(successor.payments.count).to eq(1) end + + describe '#accounting_ref' do + let(:current_year) { Time.zone.today.year } + let(:year) { current_year - 2000 } + it 'tracks sequence' do + expect(create(:invoice, organisation:)).to have_attributes(sequence_year: current_year, + sequence_number: 1, + accounting_ref: "#{year}0001") + expect(create(:invoice, organisation:)).to have_attributes(sequence_number: 2, + accounting_ref: "#{year}0002") + end + end end end diff --git a/spec/models/key_sequence_spec.rb b/spec/models/key_sequence_spec.rb index 9ff260512..738231ff9 100644 --- a/spec/models/key_sequence_spec.rb +++ b/spec/models/key_sequence_spec.rb @@ -25,8 +25,8 @@ it { is_expected.to have_attributes(year:, key:) } end - context 'with not existing key_sequence' do - let(:year) { true } + context 'with :current as year' do + let(:year) { :current } it { is_expected.to have_attributes(year: Time.zone.today.year, key:) } end end diff --git a/spec/models/payment_infos/qr_bill_spec.rb b/spec/models/payment_infos/qr_bill_spec.rb index 2e5317d6c..9e26ee7aa 100644 --- a/spec/models/payment_infos/qr_bill_spec.rb +++ b/spec/models/payment_infos/qr_bill_spec.rb @@ -22,7 +22,7 @@ before do allow(invoice).to receive(:amount).and_return(1255.35) - allow(invoice).to receive(:ref).and_return('00000123456789') + allow(invoice).to receive(:payment_ref).and_return('00000123456789') end let(:expected_payload) do @@ -42,7 +42,7 @@ subject { qr_bill.formatted_ref } before do - allow(invoice).to receive(:ref).and_return('12345678910111213') + allow(invoice).to receive(:payment_ref).and_return('12345678910111213') end context 'with QRR Ref' do diff --git a/spec/services/camt_service_spec.rb b/spec/services/camt_service_spec.rb new file mode 100644 index 000000000..0fafbb4f9 --- /dev/null +++ b/spec/services/camt_service_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe CamtService, type: :model do + let(:invoice) { create(:invoice) } + subject(:ref_service) { described_class.new(invoice.organisation) } + + describe '#normalize_ref' do + subject { described_class.normalize_ref(ref) } + + context 'with RF Reference' do + let(:ref) { 'RF42 0100 0250 9000 0789 0' } + + it { is_expected.to eq('000000000001000250900007890') } + end + + context 'with normal Reference' do + let(:ref) { '0100 0250 9000 0789 0' } + + it { is_expected.to eq('000000000001000250900007890') } + end + end +end diff --git a/spec/services/invoice_ref_service_spec.rb b/spec/services/invoice_ref_service_spec.rb deleted file mode 100644 index 62ccffa9a..000000000 --- a/spec/services/invoice_ref_service_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe InvoiceRefService, type: :model do - let(:organisation) { create(:organisation) } - subject(:ref_service) { described_class.new(organisation) } - - describe '#checksum' do - it 'calculates the checksum' do - expect(ref_service.checksum('00000001000014000000000001')).to eq(8) - expect(ref_service.checksum('00100000007000000000000133')).to eq(0) - end - end - - describe '#normalize_ref' do - subject { ref_service.normalize_ref(ref) } - - context 'with RF Reference' do - let(:ref) { 'RF42 0100 0250 9000 0789 0' } - - it { is_expected.to eq('000000000001000250900007890') } - end - - context 'with normal Reference' do - let(:ref) { '0100 0250 9000 0789 0' } - - it { is_expected.to eq('000000000001000250900007890') } - end - end -end diff --git a/spec/services/booking_ref_service_spec.rb b/spec/services/ref_builders/booking_spec.rb similarity index 55% rename from spec/services/booking_ref_service_spec.rb rename to spec/services/ref_builders/booking_spec.rb index 5f3c4bf7c..ee95fae69 100644 --- a/spec/services/booking_ref_service_spec.rb +++ b/spec/services/ref_builders/booking_spec.rb @@ -2,35 +2,34 @@ require 'rails_helper' -RSpec.describe BookingRefService, type: :model do +RSpec.describe RefBuilders::Booking, type: :model do let(:organisation) { create(:organisation) } - subject(:ref_strategy) { described_class.new(organisation) } + let(:begins_at) { DateTime.new(2030, 10, 15, 14) } + let(:ends_at) { begins_at + 2.hours } + let(:home) { create(:home, ref: 'P', organisation:) } + let(:booking) { create(:booking, organisation:, begins_at:, ends_at:, home:) } + subject(:ref_builder) { described_class.new(booking) } describe '#generate' do - subject(:ref) { ref_strategy.generate(booking, template) } + subject(:generate) { ref_builder.generate(template) } let(:template) { nil } - let(:begins_at) { DateTime.new(2030, 10, 15, 14) } - let(:organisation) { create(:organisation) } - let(:home) { create(:home, ref: 'P', organisation:) } - let(:booking) do - create(:booking, organisation:, begins_at:, ends_at: begins_at + 2.hours, home:) - end context 'with default template' do it { is_expected.to eq('P20301015') } end context 'with special template' do - let(:template) { 'X%04d%02d-%d' } + before { create(:booking, organisation:, begins_at:, ends_at:) } + let(:template) { 'X%04d%02d-%s' } - it { is_expected.to eq('X203010-1') } + it { is_expected.to eq('X203010-a') } end context 'with default template and multiple bookings' do before do - create(:booking, home: booking.home, begins_at: begins_at - 4.hours, ends_at: begins_at - 2.hours, - organisation:) + create(:booking, home: booking.home, organisation:, + begins_at: begins_at - 4.hours, ends_at: begins_at - 2.hours) end it { is_expected.to eq('P20301015a') } diff --git a/spec/services/ref_builders/payment_info_spec.rb b/spec/services/ref_builders/payment_info_spec.rb new file mode 100644 index 000000000..b867999e4 --- /dev/null +++ b/spec/services/ref_builders/payment_info_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe RefBuilders::InvoicePayment, type: :model do + let(:invoice) { create(:invoice) } + subject(:ref_service) { described_class.new(invoice) } + + describe '::checksum' do + it 'calculates the checksum' do + expect(described_class.checksum('00000001000014000000000001')).to eq(8) + expect(described_class.checksum('00100000007000000000000133')).to eq(0) + end + end +end From 34dfc7214570cb6783a49a9acf375ed3e0dcb64f Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Wed, 11 Dec 2024 11:26:18 +0000 Subject: [PATCH 07/30] fix: rollback to react 18 --- package.json | 8 ++++---- yarn.lock | 44 ++++++++++++++++++++------------------------ 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index 55c70dbc0..f3d65d995 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,8 @@ "@modyfi/vite-plugin-yaml": "^1.1.0", "@popperjs/core": "^2.11.4", "@rails/ujs": "^7.0.2-2", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", "@vitejs/plugin-react": "^4.2.1", "axios": "^1.2.0", "bootstrap": "^5.3.2", @@ -25,9 +25,9 @@ "postcss-import": "^16.1.0", "postcss-preset-env": "^10.0.0", "prop-types": "^15.8.1", - "react": "^19.0.0", + "react": "^18.0.0", "react-bootstrap": "^2.3.1", - "react-dom": "^19.0.0", + "react-dom": "^18.0.0", "react-hook-form": "^7.51.3", "react-i18next": "^15.0.0", "react-sortablejs": "^6.1.4", diff --git a/yarn.lock b/yarn.lock index c50a27d39..0606db9a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1996,10 +1996,15 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== -"@types/react-dom@^19.0.0": - version "19.0.2" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.0.2.tgz#ad21f9a1ee881817995fd3f7fd33659c87e7b1b7" - integrity sha512-c1s+7TKFaDRRxr1TxccIX2u7sfCnc3RxkVyBIUA2lCpyqCF+QoAwQ/CBg7bsMdVwP120HEH143VQezKtef5nCg== +"@types/prop-types@*": + version "15.7.14" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.14.tgz#1433419d73b2a7ebfc6918dcefd2ec0d5cd698f2" + integrity sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ== + +"@types/react-dom@^18.0.0": + version "18.3.5" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.5.tgz#45f9f87398c5dcea085b715c58ddcf1faf65f716" + integrity sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q== "@types/react-transition-group@^4.4.6": version "4.4.11" @@ -2008,13 +2013,21 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@>=16.9.11", "@types/react@^19.0.0": +"@types/react@*", "@types/react@>=16.9.11": version "19.0.1" resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.1.tgz#a000d5b78f473732a08cecbead0f3751e550b3df" integrity sha512-YW6614BDhqbpR5KtUYzTA+zlA7nayzJRA9ljz9CQoxthR0sDisYZLuvSMsil36t4EH/uAt8T52Xb4sVw17G+SQ== dependencies: csstype "^3.0.2" +"@types/react@^18.0.0": + version "18.3.16" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.16.tgz#5326789125fac98b718d586ad157442ceb44ff28" + integrity sha512-oh8AMIC4Y2ciKufU8hnKgs+ufgbA/dhPTACaZPM86AbwX9QwnFtSoPWEeRUj8fge+v6kFt78BXcDhAU1SrrAsw== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + "@types/sizzle@*": version "2.3.9" resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.9.tgz#d4597dbd4618264c414d7429363e3f50acb66ea2" @@ -4641,7 +4654,7 @@ react-bootstrap@^2.3.1: uncontrollable "^7.2.1" warning "^4.0.3" -react-dom@^18.2.0: +react-dom@^18.0.0, react-dom@^18.2.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== @@ -4649,13 +4662,6 @@ react-dom@^18.2.0: loose-envify "^1.1.0" scheduler "^0.23.2" -react-dom@^19.0.0: - version "19.0.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.0.0.tgz#43446f1f01c65a4cd7f7588083e686a6726cfb57" - integrity sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ== - dependencies: - scheduler "^0.25.0" - react-hook-form@^7.51.3: version "7.54.0" resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.54.0.tgz#46bd9142d65fd16ac064a2bbf4dc0333e2d6840d" @@ -4707,18 +4713,13 @@ react-transition-group@^4.4.5: loose-envify "^1.4.0" prop-types "^15.6.2" -react@^18.2.0: +react@^18.0.0, react@^18.2.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== dependencies: loose-envify "^1.1.0" -react@^19.0.0: - version "19.0.0" - resolved "https://registry.yarnpkg.com/react/-/react-19.0.0.tgz#6e1969251b9f108870aa4bff37a0ce9ddfaaabdd" - integrity sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ== - react_ujs@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/react_ujs/-/react_ujs-2.7.1.tgz#d87cbcb82593fe59d30fc5dbc51800d1571001a8" @@ -4938,11 +4939,6 @@ scheduler@^0.23.2: dependencies: loose-envify "^1.1.0" -scheduler@^0.25.0: - version "0.25.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.25.0.tgz#336cd9768e8cceebf52d3c80e3dcf5de23e7e015" - integrity sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA== - schema-utils@^4.0.0, schema-utils@^4.0.1: version "4.2.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.2.0.tgz#70d7c93e153a273a805801882ebd3bff20d89c8b" From e0678062249a7da01df6ec18f28c414b8415844d Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Thu, 12 Dec 2024 14:08:06 +0000 Subject: [PATCH 08/30] refactor: changes from pr --- Gemfile.lock | 118 +++++++++--------- app/models/booking_state_checklist_item.rb | 2 +- app/models/invoice.rb | 12 +- app/models/invoice_parts/add.rb | 4 +- app/models/tenant.rb | 6 +- app/serializers/manage/invoice_serializer.rb | 2 +- app/services/export/pdf/invoice_pdf.rb | 2 +- app/services/onboarding_service.rb | 6 +- app/services/ref_builder.rb | 5 +- .../{invoice_accounting.rb => invoice.rb} | 4 +- app/services/ref_builders/tenant.rb | 2 +- app/services/taf_block.rb | 2 +- app/views/manage/invoices/index.html.slim | 4 +- ...1210160506_rename_and_add_ref_templates.rb | 4 +- .../20241211091234_rename_and_add_refs.rb | 31 ----- ..._default_ref_templates_to_organisations.rb | 10 ++ .../20241212150434_rename_and_add_refs.rb | 35 ++++++ db/schema.rb | 10 +- spec/models/invoice_spec.rb | 6 +- 19 files changed, 142 insertions(+), 123 deletions(-) rename app/services/ref_builders/{invoice_accounting.rb => invoice.rb} (82%) delete mode 100644 db/migrate/20241211091234_rename_and_add_refs.rb create mode 100644 db/migrate/20241212140043_add_default_ref_templates_to_organisations.rb create mode 100644 db/migrate/20241212150434_rename_and_add_refs.rb diff --git a/Gemfile.lock b/Gemfile.lock index 4840bfbfb..295e18299 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,29 +1,29 @@ GEM remote: https://rubygems.org/ specs: - actioncable (8.0.0) - actionpack (= 8.0.0) - activesupport (= 8.0.0) + actioncable (8.0.0.1) + actionpack (= 8.0.0.1) + activesupport (= 8.0.0.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.0.0) - actionpack (= 8.0.0) - activejob (= 8.0.0) - activerecord (= 8.0.0) - activestorage (= 8.0.0) - activesupport (= 8.0.0) + actionmailbox (8.0.0.1) + actionpack (= 8.0.0.1) + activejob (= 8.0.0.1) + activerecord (= 8.0.0.1) + activestorage (= 8.0.0.1) + activesupport (= 8.0.0.1) mail (>= 2.8.0) - actionmailer (8.0.0) - actionpack (= 8.0.0) - actionview (= 8.0.0) - activejob (= 8.0.0) - activesupport (= 8.0.0) + actionmailer (8.0.0.1) + actionpack (= 8.0.0.1) + actionview (= 8.0.0.1) + activejob (= 8.0.0.1) + activesupport (= 8.0.0.1) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.0.0) - actionview (= 8.0.0) - activesupport (= 8.0.0) + actionpack (8.0.0.1) + actionview (= 8.0.0.1) + activesupport (= 8.0.0.1) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -31,15 +31,15 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.0.0) - actionpack (= 8.0.0) - activerecord (= 8.0.0) - activestorage (= 8.0.0) - activesupport (= 8.0.0) + actiontext (8.0.0.1) + actionpack (= 8.0.0.1) + activerecord (= 8.0.0.1) + activestorage (= 8.0.0.1) + activesupport (= 8.0.0.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.0.0) - activesupport (= 8.0.0) + actionview (8.0.0.1) + activesupport (= 8.0.0.1) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) @@ -49,22 +49,22 @@ GEM bigdecimal logger mutex_m - activejob (8.0.0) - activesupport (= 8.0.0) + activejob (8.0.0.1) + activesupport (= 8.0.0.1) globalid (>= 0.3.6) - activemodel (8.0.0) - activesupport (= 8.0.0) - activerecord (8.0.0) - activemodel (= 8.0.0) - activesupport (= 8.0.0) + activemodel (8.0.0.1) + activesupport (= 8.0.0.1) + activerecord (8.0.0.1) + activemodel (= 8.0.0.1) + activesupport (= 8.0.0.1) timeout (>= 0.4.0) - activestorage (8.0.0) - actionpack (= 8.0.0) - activejob (= 8.0.0) - activerecord (= 8.0.0) - activesupport (= 8.0.0) + activestorage (8.0.0.1) + actionpack (= 8.0.0.1) + activejob (= 8.0.0.1) + activerecord (= 8.0.0.1) + activesupport (= 8.0.0.1) marcel (~> 1.0) - activesupport (8.0.0) + activesupport (8.0.0.1) base64 benchmark (>= 0.3) bigdecimal @@ -84,7 +84,7 @@ GEM rake (>= 0.8.7) ast (2.4.2) aws-eventstream (1.3.0) - aws-partitions (1.1019.0) + aws-partitions (1.1021.0) aws-sdk-core (3.214.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -267,7 +267,7 @@ GEM ice_cube (0.17.0) interception (0.5) io-console (0.8.0) - irb (1.14.1) + irb (1.14.2) rdoc (>= 4.0.0) reline (>= 0.4.2) jmespath (1.6.2) @@ -318,7 +318,7 @@ GEM net-smtp (0.5.0) net-protocol nio4r (2.7.4) - nokogiri (1.17.0-x86_64-linux) + nokogiri (1.17.1-x86_64-linux) racc (~> 1.4) orm_adapter (0.5.0) ostruct (0.6.1) @@ -368,20 +368,20 @@ GEM rack (>= 1.3) rackup (2.2.1) rack (>= 3) - rails (8.0.0) - actioncable (= 8.0.0) - actionmailbox (= 8.0.0) - actionmailer (= 8.0.0) - actionpack (= 8.0.0) - actiontext (= 8.0.0) - actionview (= 8.0.0) - activejob (= 8.0.0) - activemodel (= 8.0.0) - activerecord (= 8.0.0) - activestorage (= 8.0.0) - activesupport (= 8.0.0) + rails (8.0.0.1) + actioncable (= 8.0.0.1) + actionmailbox (= 8.0.0.1) + actionmailer (= 8.0.0.1) + actionpack (= 8.0.0.1) + actiontext (= 8.0.0.1) + actionview (= 8.0.0.1) + activejob (= 8.0.0.1) + activemodel (= 8.0.0.1) + activerecord (= 8.0.0.1) + activestorage (= 8.0.0.1) + activesupport (= 8.0.0.1) bundler (>= 1.15.0) - railties (= 8.0.0) + railties (= 8.0.0.1) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -392,9 +392,9 @@ GEM rails-i18n (8.0.1) i18n (>= 0.7, < 2) railties (>= 8.0.0, < 9) - railties (8.0.0) - actionpack (= 8.0.0) - activesupport (= 8.0.0) + railties (8.0.0.1) + actionpack (= 8.0.0.1) + activesupport (= 8.0.0.1) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -419,7 +419,7 @@ GEM tilt redis (5.3.0) redis-client (>= 0.22.0) - redis-client (0.22.2) + redis-client (0.23.0) connection_pool regexp_parser (2.9.3) reline (0.5.12) @@ -471,7 +471,7 @@ GEM rack (>= 1.1) rubocop (>= 1.52.0, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rspec (3.2.0) + rubocop-rspec (3.3.0) rubocop (~> 1.61) ruby-lsp (0.22.1) language_server-protocol (~> 3.17.0) @@ -514,7 +514,7 @@ GEM actionpack (>= 3.1) railties (>= 3.1) slim (>= 3.0, < 6.0, != 5.0.0) - sorbet-runtime (0.5.11690) + sorbet-runtime (0.5.11691) squasher (0.8.0) statesman (12.1.0) statsd-ruby (1.5.0) diff --git a/app/models/booking_state_checklist_item.rb b/app/models/booking_state_checklist_item.rb index 79c12c035..db7e844d4 100644 --- a/app/models/booking_state_checklist_item.rb +++ b/app/models/booking_state_checklist_item.rb @@ -23,7 +23,7 @@ class BookingStateChecklistItem invoices_settled: lambda do |booking| booking.invoices.kept.ordered.map do |invoice| label_key = invoice.refund? ? :invoice_refunded : :invoice_paid - label_invoice = "#{invoice.class.model_name.human} #{invoice.accounting_ref}" + label_invoice = "#{invoice.class.model_name.human} #{invoice.ref}" BookingStateChecklistItem.new(key: :invoice_settled, context: { booking: }, label: BookingStateChecklistItem.translate(label_key, invoice: label_invoice), checked: invoice.settled?, diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 5698c4511..4484da332 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -68,7 +68,7 @@ class Invoice < ApplicationRecord accepts_nested_attributes_for :invoice_parts, reject_if: :all_blank, allow_destroy: true before_save :recalculate - before_create :sequence_number, :generate_accounting_ref, :generate_payment_ref + before_create :sequence_number, :generate_ref, :generate_payment_ref after_create :supersede! before_update :generate_pdf, if: :generate_pdf? after_save :recalculate! @@ -106,8 +106,8 @@ def generate_pdf end end - def generate_accounting_ref(force: false) - self.accounting_ref = RefBuilders::InvoiceAccounting.new(self).generate if accounting_ref.blank? || force + def generate_ref(force: false) + self.ref = RefBuilders::Invoice.new(self).generate if ref.blank? || force end # this should never be forced @@ -165,7 +165,7 @@ def sent! end def to_s - accounting_ref + ref end def payment_info @@ -196,8 +196,8 @@ def debitor_journal_entry Accounting::JournalEntry.new( account: organisation.accounting_settings.debitor_account_nr, date: issued_at, amount:, amount_type: :brutto, side: :soll, - reference: accounting_ref, source: self, currency:, booking:, - text: "#{self.class.model_name.human} #{accounting_ref} - #{booking.tenant.last_name}" + reference: ref, source: self, currency:, booking:, + text: "#{self.class.model_name.human} #{ref} - #{booking.tenant.last_name}" ) end end diff --git a/app/models/invoice_parts/add.rb b/app/models/invoice_parts/add.rb index 5a7f15dc6..ac9ab78db 100644 --- a/app/models/invoice_parts/add.rb +++ b/app/models/invoice_parts/add.rb @@ -35,9 +35,9 @@ def journal_entries # rubocop:disable Metrics/AbcSize [ Accounting::JournalEntry.new( account: tarif&.accounting_account_nr, date: invoice.issued_at, amount: amount.abs, amount_type: :brutto, - side: :haben, tax_code: vat_category&.accounting_vat_code, reference: invoice.accounting_ref, source: self, + side: :haben, tax_code: vat_category&.accounting_vat_code, reference: invoice.ref, source: self, currency: organisation.currency, booking:, cost_center: tarif&.accounting_profit_center_nr, - text: "#{invoice.class.model_name.human} #{invoice.accounting_ref} #{label}" + text: "#{invoice.class.model_name.human} #{invoice.ref} #{label}" ) ] end diff --git a/app/models/tenant.rb b/app/models/tenant.rb index 35a869b50..b7dc0478f 100644 --- a/app/models/tenant.rb +++ b/app/models/tenant.rb @@ -66,7 +66,7 @@ class Tenant < ApplicationRecord scope :ordered, -> { order(last_name: :ASC, first_name: :ASC, id: :ASC) } after_validation { errors.delete(:bookings) } - before_create :sequence_number, :generate_accounting_ref + before_create :sequence_number, :generate_ref before_save do self.search_cache = contact_lines.flatten.join('\n') end @@ -103,8 +103,8 @@ def sequence_number @sequence_number ||= organisation.key_sequences.key(Tenant.sti_name).lease! end - def generate_accounting_ref(force: false) - self.accounting_ref = RefBuilders::Tenant.new(self).generate if accounting_ref.blank? || force + def generate_ref(force: false) + self.ref = RefBuilders::Tenant.new(self).generate if ref.blank? || force end def address_lines diff --git a/app/serializers/manage/invoice_serializer.rb b/app/serializers/manage/invoice_serializer.rb index c7a097919..93f72295e 100644 --- a/app/serializers/manage/invoice_serializer.rb +++ b/app/serializers/manage/invoice_serializer.rb @@ -5,6 +5,6 @@ class InvoiceSerializer < ApplicationSerializer identifier :id fields :type, :text, :issued_at, :payable_until, :sent_at, :booking_id, :amount_paid, :percentage_paid, :amount, :locale, :payment_required, - :accounting_ref, :payment_ref + :ref, :payment_ref end end diff --git a/app/services/export/pdf/invoice_pdf.rb b/app/services/export/pdf/invoice_pdf.rb index 1d7b94354..eea416e20 100644 --- a/app/services/export/pdf/invoice_pdf.rb +++ b/app/services/export/pdf/invoice_pdf.rb @@ -21,7 +21,7 @@ def initialize(invoice) end to_render do - header_text = invoice.accounting_ref || booking.ref + header_text = invoice.ref || booking.ref render Renderables::PageHeader.new(text: header_text, logo: organisation.logo) end diff --git a/app/services/onboarding_service.rb b/app/services/onboarding_service.rb index 876272002..a39bff130 100644 --- a/app/services/onboarding_service.rb +++ b/app/services/onboarding_service.rb @@ -5,7 +5,11 @@ class OnboardingService def self.create(**attributes) defaults = { - booking_flow_type: BookingFlows::Default + booking_flow_type: BookingFlows::Default, + booking_ref_template: RefBuilders::Booking::DEFAULT_TEMPLATE, + tenant_ref_template: RefBuilders::Tenant::DEFAULT_TEMPLATE, + invoice_ref_template: RefBuilders::Invoice::DEFAULT_TEMPLATE, + invoice_payment_ref_template: RefBuilders::InvoicePayment::DEFAULT_TEMPLATE } organisation = Organisation.create!(defaults.merge(attributes)) new(organisation) diff --git a/app/services/ref_builder.rb b/app/services/ref_builder.rb index 62e8ebe13..24db11953 100644 --- a/app/services/ref_builder.rb +++ b/app/services/ref_builder.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class RefBuilder - DEFAULT_TEMPLATE = '' + DEFAULT_TEMPLATE = nil def initialize(organisation) @organisation = organisation @@ -16,7 +16,8 @@ def self.ref_part(hash) end def generate_lazy(template_string) - template_string = template_string.presence || self.class::DEFAULT_TEMPLATE + return if template_string.nil? + ref_parts = self.class.ref_parts.select { |key| template_string.include?(key.to_s) } .transform_values { |callable| instance_eval(&callable) } format(template_string, ref_parts) diff --git a/app/services/ref_builders/invoice_accounting.rb b/app/services/ref_builders/invoice.rb similarity index 82% rename from app/services/ref_builders/invoice_accounting.rb rename to app/services/ref_builders/invoice.rb index c1da36316..88e63769a 100644 --- a/app/services/ref_builders/invoice_accounting.rb +++ b/app/services/ref_builders/invoice.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module RefBuilders - class InvoiceAccounting < RefBuilder + class Invoice < RefBuilder DEFAULT_TEMPLATE = '%2d%04d' def initialize(invoice) @@ -9,7 +9,7 @@ def initialize(invoice) @invoice = invoice end - def generate(template_string = @organisation.invoice_accounting_ref_template) + def generate(template_string = @organisation.invoice_ref_template) generate_lazy(template_string) end diff --git a/app/services/ref_builders/tenant.rb b/app/services/ref_builders/tenant.rb index 828add313..1e10599d2 100644 --- a/app/services/ref_builders/tenant.rb +++ b/app/services/ref_builders/tenant.rb @@ -11,7 +11,7 @@ def initialize(tenant) ref_part sequence_number: proc { @tenant.sequence_number } - def generate(template_string = @organisation.tenant_accounting_ref_template) + def generate(template_string = @organisation.tenant_ref_template) generate_lazy(template_string) end end diff --git a/app/services/taf_block.rb b/app/services/taf_block.rb index 673509a56..142dd7167 100644 --- a/app/services/taf_block.rb +++ b/app/services/taf_block.rb @@ -187,7 +187,7 @@ def self.derive(value, **override) derive_from Invoice do |invoice, **_override| next unless invoice.is_a?(Invoices::Invoice) || invoice.is_a?(Invoices::Deposit) - op_id = Value.cast(invoice.accounting_ref, as: :symbol) + op_id = Value.cast(invoice.ref, as: :symbol) pk_key = [invoice.booking.tenant.accounting_debitor_account_nr, invoice.organisation.accounting_settings.currency_account_nr].then { "[#{_1.join(',')}]" } diff --git a/app/views/manage/invoices/index.html.slim b/app/views/manage/invoices/index.html.slim index 8dc0ffdac..1b2f1f3e6 100644 --- a/app/views/manage/invoices/index.html.slim +++ b/app/views/manage/invoices/index.html.slim @@ -49,10 +49,10 @@ div= link_to invoice.booking, manage_booking_path(invoice.booking) - if invoice.pdf.attached? = link_to manage_invoice_path(invoice, format: :pdf), target: :_blank do - invoice.accounting_ref + invoice.ref span.ms-2.fa.fa-print - else - = link_to invoice.accounting_ref, manage_invoice_path(invoice) + = link_to invoice.ref, manage_invoice_path(invoice) td.align-middle = invoice.model_name.human diff --git a/db/migrate/20241210160506_rename_and_add_ref_templates.rb b/db/migrate/20241210160506_rename_and_add_ref_templates.rb index 123c83e03..9e816bfa3 100644 --- a/db/migrate/20241210160506_rename_and_add_ref_templates.rb +++ b/db/migrate/20241210160506_rename_and_add_ref_templates.rb @@ -1,7 +1,7 @@ class RenameAndAddRefTemplates < ActiveRecord::Migration[8.0] def change rename_column :organisations, :invoice_ref_template, :invoice_payment_ref_template - add_column :organisations, :invoice_accounting_ref_template, :string, null: true - add_column :organisations, :tenant_accounting_ref_template, :string, null: true + add_column :organisations, :invoice_ref_template, :string, null: true + add_column :organisations, :tenant_ref_template, :string, null: true end end diff --git a/db/migrate/20241211091234_rename_and_add_refs.rb b/db/migrate/20241211091234_rename_and_add_refs.rb deleted file mode 100644 index 4eadf11fc..000000000 --- a/db/migrate/20241211091234_rename_and_add_refs.rb +++ /dev/null @@ -1,31 +0,0 @@ -class RenameAndAddRefs < ActiveRecord::Migration[8.0] - def change - rename_column :invoices, :ref, :payment_ref - add_column :invoices, :accounting_ref, :string, null: true - add_column :tenants, :accounting_ref, :string, null: true - - reversible do |direction| - direction.up do - backfill - end - end - end - - def backfill - Organisation.find_each(batch_size: 1) do |organisation| - organisation.invoices.order(created_at: :ASC).each do |invoice| - invoice.sequence_number - invoice.generate_accounting_ref(force: true); - # this will overwrite the payment ref and mess up all payments! - # invoice.generate_payment_ref(force: true); - invoice.skip_generate_pdf = true - invoice.save! - end - organisation.tenants.order(created_at: :ASC).each do |tenant| - tenant.sequence_number - tenant.generate_accounting_ref(force: true) - tenant.save! - end - end - end -end diff --git a/db/migrate/20241212140043_add_default_ref_templates_to_organisations.rb b/db/migrate/20241212140043_add_default_ref_templates_to_organisations.rb new file mode 100644 index 000000000..633ec4a3d --- /dev/null +++ b/db/migrate/20241212140043_add_default_ref_templates_to_organisations.rb @@ -0,0 +1,10 @@ +class AddDefaultRefTemplatesToOrganisations < ActiveRecord::Migration[8.0] + def up + Organisation.find_each do |organisation| + organisation.booking_ref_template ||= RefBuilders::Booking::DEFAULT_TEMPLATE + organisation.tenant_ref_template ||= RefBuilders::Tenant::DEFAULT_TEMPLATE + organisation.invoice_ref_template ||= RefBuilders::Invoice::DEFAULT_TEMPLATE + organisation.invoice_payment_ref_template ||= RefBuilders::InvoicePayment::DEFAULT_TEMPLATE + end + end +end diff --git a/db/migrate/20241212150434_rename_and_add_refs.rb b/db/migrate/20241212150434_rename_and_add_refs.rb new file mode 100644 index 000000000..f82595626 --- /dev/null +++ b/db/migrate/20241212150434_rename_and_add_refs.rb @@ -0,0 +1,35 @@ +class RenameAndAddRefs < ActiveRecord::Migration[8.0] + def change + rename_column :invoices, :ref, :payment_ref + add_column :invoices, :ref, :string, null: true + add_column :tenants, :ref, :string, null: true + + reversible do |direction| + direction.up do + Organisation.find_each do |organisation| + backfill_invoices(organisation) + backfill_tenants(organisation) + end + end + end + end + + def backfill_invoices(organisation) + organisation.invoices.order(created_at: :ASC).each do |invoice| + invoice.sequence_number + invoice.generate_ref(force: true); + # this will overwrite the payment ref and mess up all payments! + # invoice.generate_payment_ref(force: true); + invoice.skip_generate_pdf = true + invoice.save! + end + end + + def backfill_tenants(organisation) + organisation.tenants.order(created_at: :ASC).each do |tenant| + tenant.sequence_number + tenant.generate_ref(force: true) + tenant.save! + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 622bb22c4..6bedb48e6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2024_12_11_091234) do +ActiveRecord::Schema[8.0].define(version: 2024_12_12_150434) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "pgcrypto" @@ -332,7 +332,7 @@ t.boolean "payment_required", default: true t.integer "sequence_number" t.integer "sequence_year" - t.string "accounting_ref" + t.string "ref" t.index ["booking_id"], name: "index_invoices_on_booking_id" t.index ["discarded_at"], name: "index_invoices_on_discarded_at" t.index ["payment_ref"], name: "index_invoices_on_payment_ref" @@ -494,8 +494,8 @@ t.text "cors_origins" t.jsonb "nickname_label_i18n", default: {} t.jsonb "accounting_settings", default: {} - t.string "invoice_accounting_ref_template" - t.string "tenant_accounting_ref_template" + t.string "invoice_ref_template" + t.string "tenant_ref_template" t.index ["slug"], name: "index_organisations_on_slug", unique: true end @@ -593,7 +593,7 @@ t.boolean "bookings_without_invoice", default: false t.integer "salutation_form" t.string "accounting_account_nr" - t.string "accounting_ref" + t.string "ref" t.index ["email", "organisation_id"], name: "index_tenants_on_email_and_organisation_id", unique: true t.index ["email"], name: "index_tenants_on_email" t.index ["organisation_id"], name: "index_tenants_on_organisation_id" diff --git a/spec/models/invoice_spec.rb b/spec/models/invoice_spec.rb index 331f0f2e0..8eaae02fe 100644 --- a/spec/models/invoice_spec.rb +++ b/spec/models/invoice_spec.rb @@ -87,15 +87,15 @@ expect(successor.payments.count).to eq(1) end - describe '#accounting_ref' do + describe '#ref' do let(:current_year) { Time.zone.today.year } let(:year) { current_year - 2000 } it 'tracks sequence' do expect(create(:invoice, organisation:)).to have_attributes(sequence_year: current_year, sequence_number: 1, - accounting_ref: "#{year}0001") + ref: "#{year}0001") expect(create(:invoice, organisation:)).to have_attributes(sequence_number: 2, - accounting_ref: "#{year}0002") + ref: "#{year}0002") end end end From 4db9e2eabd3c66eb6d30e07b8cdfd97d82f2d81c Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Thu, 12 Dec 2024 16:06:36 +0000 Subject: [PATCH 09/30] fix: specs for key_sequences --- app/javascript/images/esr_orange.jpg | Bin 53130 -> 0 bytes app/javascript/images/esr_orange.png | Bin 123494 -> 0 bytes app/models/booking.rb | 8 ++++ app/models/invoice.rb | 4 +- app/models/key_sequence.rb | 22 +++++++++ app/models/tenant.rb | 2 +- app/params/manage/organisation_params.rb | 4 +- app/services/export/pdf/invoice_pdf.rb | 4 +- app/services/ref_builders/booking.rb | 6 ++- app/services/ref_builders/invoice.rb | 9 ++-- app/services/ref_builders/invoice_payment.rb | 5 +- app/views/manage/invoices/index.html.slim | 4 +- .../manage/organisations/_form.html.slim | 36 ++++++--------- config/locales/de.yml | 7 ++- config/locales/fr.yml | 4 +- config/locales/it.yml | 4 +- ...641_add_key_sequence_number_to_invoices.rb | 6 --- ...20241209160641_add_key_sequence_numbers.rb | 9 ++++ ..._default_ref_templates_to_organisations.rb | 11 +++-- .../20241212150434_rename_and_add_refs.rb | 28 ------------ db/schema.rb | 3 ++ spec/factories/organisations.rb | 4 ++ spec/services/ref_builders/booking_spec.rb | 27 ++++++++--- .../ref_builders/invoice_payment_spec.rb | 43 ++++++++++++++++++ .../ref_builders/payment_info_spec.rb | 15 ------ 25 files changed, 163 insertions(+), 102 deletions(-) delete mode 100644 app/javascript/images/esr_orange.jpg delete mode 100644 app/javascript/images/esr_orange.png delete mode 100644 db/migrate/20241209160641_add_key_sequence_number_to_invoices.rb create mode 100644 db/migrate/20241209160641_add_key_sequence_numbers.rb create mode 100644 spec/services/ref_builders/invoice_payment_spec.rb delete mode 100644 spec/services/ref_builders/payment_info_spec.rb diff --git a/app/javascript/images/esr_orange.jpg b/app/javascript/images/esr_orange.jpg deleted file mode 100644 index e9e228642abfa358990c5b2d38b9c781b353b5cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53130 zcmdqJ1ymf}wl3PZLvV)x9Uy3M_uv77J0!s&K(OF65+FD<76|T62=49#f(LiEZnW{o zzxRzi|R_5F|gWxyi^8F?8192^`V;o$?gUjaBv zyIGk707^;#CIA3{3P6M-10X#d!96rlIP(8EmVsjgApGMzJOB`E1wj0_Ym^`Me{By> z`^P>1+{0(U|N9k)MHvYHehhE)*QNJ!04V@6A|etZ0x}X35;8I}3Mw`l>cfVMiG_|$ zfJaD3fJcB&MEaPVh=hs+pMZj%f{KO~2m}(5GcqyIF+HXO(*5-ixQAO&QBWVDp*^A_ zCLpH!FK_p409;gfBg7&EI9dQaE*t_b+-hO^oz+vrIV~^CsZ4S0J+{c z`lF!}5tERTJ)vh{e9FYl!^_7nASm@*T1Hk*UO`<$Q%hS%SI@-M%-q7#%G$}<<-Mz$ zyGOu>z@XrdA)(;dxcG#`Pf5vH**Up+`2~eVUn{GsYijH28#+3>x_f&2`Ul3wCnl$+ zf6UCTtgfwZY;JAu>_U%EPS4ISV3${a$pr^M_@`L^A=z(o;XcR(kBEqXi1L?QaPY1V z4FMMsiG~9iPeK*twF5parynYTWK3p7I~pCA8ieqT<0v{2kbC6`^e@r=A=$qsnE(GP z$^JvI|CDPMfQbP2U_1m|fEeJ4XrKfBLKmlN9aL&BaismKR?&i$=nVMkW*?N4Q|sF? zjwf*fWD}`FxKb~ZCjv*y(O{!qX(yrP)QFINtF7ltz6UJX4@dFb16r0+y+$EY`rmm9 zoK+6WN!(iO@tRj?Cl(EPssat-u-YZj{i5|6Q?=K=O@^n@c^V@d8O^}#ZcAOr<69yK zABlFmAex0dzR7rpEWcUn0Dk{WEuzfh;+GEGdxP)TMJv|6F+XpFOl~IGlT#*haJwLuJy8LlSC#+n@_~i_Ja2yH)V=qN{d#@!4&Dm7-@k#y!Alr{+qh@k?Me zfrjO*N!aK(1>AMziC0#mF}|1agfLBMOo^NP!KtbTtIr~joStEBO-ZW-HE-v1xq{bM zjO=l*>APzmXZP~k^P9av9#HsIU~Nc~V)hOdX2WDIWLwuRSATm|g~5Dv=5^{~C>#Sy z9@A7lG;LB?XvH%XU#4#IObox!21~?3=i@B$?J-5*tTNZW_ftKoCKdS;I7YGV*qDGw2kkTVaAF@ynB5}d z^bOa!biyh>%j=p++Is!e<>($wxR;m|TOy2qX(sRTuOV;$ql@X(-Bv<7FP>%8vyn$` z4yEOGc!i zg07eo>xSNapy?qn`xNQ%DlP5_J^gBw*Uu90kBM^3wZyL`dC6w0WG=dy*cj`1XR={1 z`^woH5tptrjBL5`yTL_#V+<4pwX|X`alS8)L{tkSM{DL4UTv&ev@|jJ*Dwl0o4hlL4e? z3Vu7~JDACc%i!ZC6}_KT&o^=R)OwK`;x^$WyMmF*c6WFz#LC$rBKdT2S00Y}1c2z) zKD^Q+G%b>OqxuCR=I140BNlX6a;Aht*H~YbsIHjTgfCaTd7CML={OJVMfgGV;6hbD zyHJu9H7P?qR4X{&hKithw4_NguqtfEE<1LF9}^+#gi6A|H1-5ra4iY4)ZL8V0mA04 z%!0jNmoK)yAgZZJB>ObC$&590()TPQ!2r`WJ3+!NZR;ls7}>0fg}E-0g>|0u`4A%* zPYtyg23W@cIS}~=4REUt5pn&NC>=gExvv$c??;GrjKO>k&Tik2F4~GNX@ufbO7c5S z18qsy;gMBDuoLY`Bvn#GjI6HyF0AT$(F?p$rYRARX}62 z1q|%9FJ5^`0xQ!F=#s=C zcgqqZWB-^NATyrPl$4n)#sOda6p3Rn98VA%2V@%7O=c7Z^*A(q-=V`3#*Tt<(J0yjw;j8d5%n`SDGf^NP!Osm!b+iVU2gIA9{ z#;QV_RMdZdK5y_EPwJ7sFl)&Vt0*U@RiU#A|1M?1EUz!omA2b~?)=*l#R=&Is|{nq z_8#2%sw)Czw9Y5(*V=IW)a=D_b!%i-XlqvPyp?`bCys0 zkt21{p(5n}2vFI7c2Q)N{DG_{#;Rbp!p5@>&n<-H38fUy_s~l9_nOha+Qyc^FtV?> z#6XG2&l~927nv}|`LzP^Q9hz32*YsR0LCtt=Dv}j`}VOr<$QuUM!xgQcZm@Z3Be0mouy5;5ry8EhrXT``KIm3k_&fd>6EzuV z3|M(eNSq&`ocn;yze&a5XTx$sExNx+b>0!zKzX7*%&9?6&vA{so6mGu&o2XL#aO>~ z*HdzV-wA5Yw9vTu-QZ-~ycfj)TUC?jS=pAk@Rt&e%J3e7CHh}E0;uY<#wb@^7k?7m zDYk*tv)RB_Ok%aGY^#;V*kjm>`c>LGfv_4l5;`iYslhbv6BCttb$J6LrH^ zVu`mc*pif8^$lSmw++l76@(c;VQT6$U9RW#m6%(_;^*N%gL$sSRY$cgV(_wzxUP`v zLtQqP8Fe}i<|2KL3?{wR*S^&xX6!bkBx7}jVcv-lsz~Pvayhm;v#)wH?}PQ7MnCgB9Lx8EqJWHztvKxPMFJn zRX`ZaCzs-*_H5+B5tBMhU|q-WX4*#0_y_+oBcH4GO5+tuzU!6N2!TXm4;2R zTRdxLxF20Z7FcMx#$kzYQ*Fl0rlY|p(FKaLsvSxL#ky@UN(j1hj^OIo8RKChpnB%9 zma!wLRN7wk>!eCOb@$kE5eF8q=B-DGx4C)`P#S-`S(kXw^<+RGLQ@l!26bLwi@y}D zg}dK&WzJU89&PujXt75^15#4b^JH)xqI@5f+OgpJv54DWqkj4GsOj35a5tbRp+`*| zrQ7P;WM?TD&QnqE646+Ie$7Aw1b=Zl%DcJ)E1}F{j-T!CJp1$^xLkixFK;MY$N%|L zO|`4%`2}uLJw8}aQ@&|cD~Da`nX4QOE>t}M8NwN^fZ8nqy0Tg8gW1*h3R^`#hh!+M zxOeRZY^Hd3T01dY9P%FQzdE(3UbA_=yh2bWSus0rk!disnGQ{`uUY(Ivq+jK+Zv_t zS)=Nsaj#=7M^A1NU&bXhLMBRUjbV<`s>iSDal5$TCQe*dUoB2YU24xAT<VD6p!N4*PUeBmO_F-N8X?l%w;4E(#Pu# zem^$Di_YA7T6IPYr9T@q zexz`hiukrLqq4rGAyyVD`f15LNcU4W)&&1Nu6)Oac8UwTLH6EWsT3I2clK`*P(kAN53jOVTou%g{XH0i@3wJA+Wo-G^JiK zgGk7oPHlKdJHju{l|{w6+zEP4V%ji~4Otj!A+BnXdKj#1jWWI8YVExoe< zeGH@Pto0XOaU5a4=DiS9xoX48*M1?G)e%!)7YsKhxPxA-1lw##K8^P(gSO!lN-BGy-a>+u>T#)G%){4)6UL^0~uL5igc zVp#KoE1rr+6-vcnKLYILA2rO~o-1{E&D;q3i55fi?r`wjV&zAh$X$wCgD1@zlGxWm z#ejLMm#rQ+q&KvArZGnphziJz5^!Hy30_L3a7$ zQ|>wJED>~Xi2DV^xF|fIs5SmbH1%Bi1w`G-BA^WF5c5EspvR;3O^St<9?sfiXOlg7 zG2Ci$d6J=~g9@Dtem_iRYtGUm&J)5`va`2clLy_vWAgiNLdC)M25&f@i3_LU-0|Rp8WAisd_58ac9!}D^p4ia&)89Pzz;%shGAgXP z8Ak?^I$X&XV?lWX&fS%aK#&2N3k6BQ`J(d?ZRIgxv9EoZI1=U)a?`q-lY(Lm%;;IG z&G)8P;a8Q%alQ{8I0RZC`uznNdsM&R99eI#=9kvVyswaN!D!P*POMkx)Q4vShfmYF{Gv)-uF|_j z)TkfL!R+NBE4mWNnVK4A zRxQNOPz5GkoI0F($2T|hX+D^ODr4*Wxd7otd6DAU5h1Ko2g(rYVu;^CMbgBJJ7-mk zgOfb)z6#8VHH6lVa9j360MEph3i?x~V?(h@mHeIQS^poMZai{k@wy?XBk(r^`J}^cR4^2-wmV^H zSp{^dE15Spt1sERd}cC(?@LVH)9^2E3x--=x39$y3#nS!5r^#mV&6@zdjRnnhB2v? zp4dk(;FYjoY41#_1z8Rs9!v0HNesi&G@-Osz0(GjB!ZIOUoau}y%5dfPrl+j%h-tV zUpW)$Ow356whz~L*>|V0`;^bS-KvNgWHEOoe-8lZ-`LQP4Ir4Hb>0JtjV~=1)Ag2$ zUg~gbjrJJIOJk&Xp!YbBTldWpk}2;uzDUAMI1C^)du*{|aCAdcD|X7~mUwp$c(}Kx z;2!XSck3?b2=Af(1_Qw+?M+`jlwB}|Nug3JcM*9o?;MEf6*uvdLm-|ZPk+Rc)4`?W zVLN|c?~4iW&6BC64r5&IQJ1{SMTQB}hOhP<=RM(Vel#=DK7PejzdxHA5;T7g_`a}e ze0>kVxd)IHZBDHpr@&xNMNyPDVpF1{Cg&h5tFc)lwu>b?p4J3R6FbzALJI9TFF}ZF zM3V)CcTarm=v0TBQbjHCfXe{QXWWiRnx%kvnyfL6#o z09q6@-bVc@puY;r+iFjw_%`HV)X^gawXkpoA!2AyCD_#i)Z@+G^?qC*WV&-lZbGy(|HdJ7}uJZ2RKhZ`bGpUE7?oJnJD%I@2_0d%HRsrD%Ut;eG32eTGb>)X@4 zgksmbV>@lkle-g()>+DB%2B8=!klc&VC7vP+%tV@nC#hRYr-&&;5jLBsPzy0=<@txn5%91SVhWpd4vL%-JOOvbtCB|V0T57$vxI=0KJ(&$eg` zeb;bnB3uQwJSS33I}Iia0lQ`hbK@UJTgV|Pc?*pvp# z`HK5ES#oe=16d9wbq++f*Tl12DkTGJUp>N~SF)bco7H2ja;PN5ABeKCM-nF$d8V?W z_qSllzI!y6ensA-_z?dq_kcAG0po&+w^GZ-n8uc_WOup6+&G0cf;oxGZ{D=i0Dv6p zeyIi7Q2Hs8sh=OVO0N<=x|P+6uY2iX`-`!Z+y4j+_r{c*l_VH$VCtEHGaN zMm>vNwhyuMB3$`ozXdtZEq1fr3?i{d1kW1jiSm}DGn$!gKSN!1(sKnN9r3gVXs2V+XfHaA5t@uW3*0i&UXX)6RQ?Su6d{OL_JT88dw1_z!uKJ)-f59!i3%}zY;2~h@7vFe{sl)wLhr;gN z74ZW~lEA_YAm*8?hEg(uV!<@J`*%ISlf(I}eNJR@jff&;v@Ovl+58&@sAw!Cj~$aY zefie1`RrSF$1Xd^b8Oj!(XSaa!2-*kWJlBO>RE^?M6ei5834XGfcj!L^>0cL2#7nG zX?X-oURuG9UtXaOJX=9jFPSDQPbsxNoRLCc1R^1xwewS=5Mt*;sMX0-0xg2P$qgr} z6p1PtYNt$`Y+Xpr%>V@~xfqY~DSWviB~Jeq9Dnb0>D`7jH8RXZ9a8J(f=V;yC6Ko} zp7bOLi`9^|b{}4(J4n8dlz->DgPs^p)3Ze^m}l-bxS`*{9^1}TJq9?e*f%4b(#Xe9 z&GoyRf_@Fu+{U>0C=q+VlS;FC0-XFtlA)qu<)7w) z;G6$s)<9m0!o(YfTmwrZl+misH6e#A&M?+I%MeCGI?X5YfS729XlA|o^50?lZ{qkz zCWLOb{t5(tQ|(vEZ{ALK(#=wL&1MgbLP-k(y7{Qba2xQjTz9HB<-CZJyt=TG*qsKn zTvsd0b8|<06?LXDXAIXIAIDpB#|#C&>m=(XLtE)6AVIj*&dARVEFCUt+FCXC7gjHK zTkBY)gEY-D&0Cr;e>&rfR~-s1;*XFQMwowJ7W@Za3(T$)QP$Uo7)J`fMhV=S&V|hL zHSmSdOym-AD${yeLf$n_#L~OId&d!J`2}WDqPH!E$FjJk!s%ADx~qss2_=aq)gHQZ zYqxuUcw)4Y~#BE0e59Hv9Oa6j&vUK-GZ`in@`-NO66yR$5mfunN z{ju0=xzZm6%uWqv;^1iwCid9s5sG@GBJyR7&5MgZjOF#*$oyefH1EM&b;Douki3*q z;s#}930~K&wX@}MZIn)k^PV?{;n5SreJ4?6I?Z{v9uFs9C2#V-xq7A*q-CJv?C^l6 z)M#K`#&)?`s&jgO$KI2uV`q}&(Lm>R8-=HK+}=F^ z%ZJG(9b3iE?o14Q(aKkv!Xqxt%hkx>ZAoL?Q^O@j%G+W0@&~f;2TSIp22Bp*Jgi-! zxEz4Apt;?po1a3;trFfRWa$EtsEEQVylC2lusPBem|c57SZA#E;*gRnk%Qc_#+4-6 zgo$C2xdF+d243Xle9LvrW#=tA(7#~K7t|))Q({5A z%@#VCgqJI1O%rmrJ*Hm(8eM#GAg>W5DTnA+_h8F!e~bIS4T+VfE-fD}_Xcxi+`f_nkOym$Jy{BZC8g;5IzM+4v`W0q&;i4PTBHAQ%_Yq zXLn6>{Q~%z!qqe`_5M*YyZ^z8=Qkolg%O=ZwVLrwdEhu$06wU8YissBwnLhT^6r`a zAEzaSzf!kh(fC|hW0k7hW7#gNn zR_e7ArrhXt+CyJgJ!2RooyF#8-=b3heemeXgfwr|YF%>>`+N?K- zZSNtD>S<8HwA70%w=EDE32T!6?5(^x$vcR8Nf%wTbz%69+2Z%$nNN3M%*NM-feM#k zYi(pz>NBjZL940kgsA~?MnH*;h&tiIU0H%9_=ht4!|KM-$C1yuBP$CgFF&ixEHTVx zC%iD@h-d^+bxubt{ZY5tW1Y3?#A_&>x9YumRG%4wk8JmOnwc)`N)9c|HC6pd9gBL? z<4*6$a@`?$2V&;sFzqYnb1N@fI+Ya-0~zsKO;TsTu<)_`@Q z6Jp=FpwhWS;G%+SIAZpsdNJ0@jI3+lR6CSg3rL7y#oQ~Jg1&knim>ZJmFsF{kp-jE z8E0s;%4nL*NIcMjR`g2}*m}Jepn_50JS@`w0i?2%E{K@~G+_Eg-RxaoV|RAbn5uQS zmnrW7E+0*!F4EZ%*hCKxxs;3pX9v0=qdw#LTO9$!_kbqQqR#>G1&Beo@OsXsE zZcU;0UxLVz(Enc`GxMM4y{taA|Gg91bd+1`a$4>xP6UU|dF_;go6oMNVyV%$nGdmp z_H}FoPt{O+wJ3G2wEvj6D+#^rx4upx4)ej@{tJXHHsB(!7TE|2;#C+Z_%GTQ(fKCpfIH;7BZq!0R@Ft z%a=Ky5LIUB@0{;~1x&&Gh;Q|oJJ#RHC<|$MQKMG(HK9N*WcfVoXN9>+xmmAWATJx%FeIDAKklMbL$U;9+nH zmKriUO-)RK?5msutkGM+sGjP>CY-fWvEFpZXG?WA*K1?daqzfyMWcJmw`PankO@0} zbYxUYoN`LnKlMq`KA;BEUzUI1*`Mp|kJPR;CdzXr*f%2-GUK7q^;?#`>eCGQ3R{Pw zRItttAK|G7ng_SEeXO_Vc$;+(crp3G4{;t0Z7^L2D`TUGzWS>62qsjuTpN5`zdQ$B|)gpchubHVDtlw%ovA_9M@E4X-(o z35$`QU=2hA;Mf+9Kc1qM{wMjcwUNlFWbGj+-MBlvM63^ki%IU(RNS2Os#iLI;Stfw zql23`mhr(TbfhAsBfi4wR#2NR!Kn!EyzX2Wc$l9Z7@tGT3zo%=FnWwU{^Jka?zpdBA=d>`}%P?DwI(fk*9LUv}ho1>d2}i$3{!>z3 zD1TzdU0U(4@M5JHZ9%>N6A<^mnX~I&?KOEQo18dW1j>b+S0XUM!}aw@&=u_I5n@f2 zXeVrv1`x269@nd(M@t|+l0ikTEJ(>3V@ruM|GaH(V3f)`H>7XX`K06oG0pMd*p{Ec z2_fYQsxDt(&|!yXU)clc0ViB0DO9`;cNx}(_7!90&Nk8W=>pZsd%XHnBd2>-z)r(R ztW!xg?(9jN0-)GUU0s~Px^hWDBC&P!L2%a0JwWS}cPsD0c=A>O--eHgf06Bqruxtqf|-6txgYT4?VGx&^JF++*P6AXCF{Tjg&6>^g;OI6 zo$#@2>nS1sS6`-rsKB@(QIm^)VERxp0n_3^K%rtnKdF{p_S`mGx724_UZIpq?^P)X z=qPHc1WS3@K)}-E(Ub2r!)tY00KQG}ZY8!b6K;6okfsV^u%_87Qh|;XDp`i|!oqLy z^=d7~Sd5=;dXM~3#Xb?p5UN#osOKykw4Tjp+ZP!OI+%YWpv9G_V;o3B0j0*IGMH|HlXjW0WxJeG&LbP+VJ&KmJ^PreFL5f}DRDj9pq z0o`@iFwMiI=~XbZ-y@4H%bw(9s;N$%-HHv_Ij^x2t4!a7?;M#|G}!1!q(YI$S%P7M z40rwMj!L@8@@A8)wgE8}FHYis@Eq0J+wu%|aLQ5gRiWySWv$R8bvIlYte&_t)T~`j ze(>k61DhEy$^lH$Ix}unwzNr9ifPq@pPQ1`n^a%pE7GJ1`A05qH055N!15+X3iEo) z7eor8b=UhhNRA^zF&fIox||}~)?Kzjr>-gBZx-E6MmR2|k9`iE!kzb2yL0tAQA4y; zSP0Ezk_O7gNZ+BYA!4U1$5RcL%icP(YUECW3>BPg@O3|@SIK2>^?(XRCm%i>Gj6cm?be`Gqrnu$y>=lFdX02HNTK|NiL>YXZC zFkHZTp{Fm4CMuEW9PZHG!+&;mpe~wjcWlqt-gG47*oe?-HxKdV?^OueQj9!OBgRWW zr{TB(bMi4T#M6a(RpWV_-5`Bas?JyGD!#2}nc_5R$Mkvr2snnq-~{j2NA*cf;LvTg zq$aO}!8Yu8p|ozRfF)ZDb|z0BYrzaZ`t0G?QzhX=1A_1w%Tc8)$%Z+#qMkk7H{FR; zCP<5Qowg6FT$wWE?;NwPK*pd#O&*~H2(-m*P1j6yT_z``@)aY)e2l_zD*YzDNLQ4d zZ75eG{z(sp#jdu4*kNcLi|rJ-oBwOpK8~`rc7Wg6-eRhY1=2pP(8sQi6#S%gG2{7{ z+?NctXLn_6Zbnn%SGDsdbGy^;>H?P&WM!nD9WV58Qg*?^0Up1eE)#Molqq*T*Es}6 zlDu|(W%_EUB9Q;GVqCNt#beZy#OUNetVx%@UtU+rBu|`QEP~UJZOTT~Zt(F3WF#W3 zm+cJ!0ex2!o`S`NcDeLtA!=)S&#XliKEz;f!apn@F(aC9RMd(+B^kD6KkycRZZM}G zuIF76vE_F=oa%k$skxRtLweImj^CWZo;)Y@>~VB`8xa38Iqru(0h7sc=Q{PL+Ytq? zc*Dc)5ox`?jN}RMt_=UdLMA?H2(Eyzud5w`B_J*7qM)_<$aGxGn2jWPb#mjQzj8+g zmA50dcg14WxJqs0_OSnR6aRLzS0&HPVM_#R(*Y(3?CTYzhvfC zP>naje6)yjKtqO3AnDGTw4+w++4|Xty7B}I(zQM^znu9AIi|#Q2{P)oqr}bAf(iLu z1}0@i$?)X)J*GK#=JSX#6f@bFcb`3w5Ko$YQqGPnlqcBOw_;uD{DB&MUPtqNrT#)D z{Dq*erCxOtETJYW1AAP-gWmgJ1OzTb)8d7Bp>4yXg~*Z^;4ZK@z~vnNV@Ez;(#A>X{!!V~DY1uvbk)9{`I)qYo z*x_`wmQ*TJixlzIq#XBWeYep^FI3Uvv{UEV7C+4^Ge7N616$6ftdUd=OnIa(4Cr&m zNKwybT0~(gk8w;-JaRe+eX~1FKRslEGD7o+Fk*fn!?9hPf%Isc;)2lSv{9Z_Z6Qlk zPv*@_NXe|oTdm92#x&94hO3J+Vx(ijUeO6@b1YnGWlxM zY!#r@7lQ^NYF&BNBfse%nuFvuWH~kKc6dr$rfHwwJKO`JG~>ti$7Ysw9Pry}_8X(F zDx!(YuVt5#b-CxPuoVPrPUzQT;?~2dBt}HzvX!%HGMVC1_d^kM8s0 zIKSO$*<@JJOTi3OBf@LZ$yDP^cNJqKkK)f|oKjsf-s^LSCuXg>YwG9GOjeRoj(Fjt zuB=#$FpL{@4=~_(E&7-4v`O((|yXmXADDhb(*h9B6#s!o7j%l zm?b4cxB_K@(Ij2(lwVCqexzExTU?RTcM6M3 zkf8kja(yzXSvXhQ+?s$eg->f##ewq6gq^)xkny{0{!+5>t9tCL3URF*3kmoCM|j%y(QYMR-+ zTW@J3oMZv8@hGtg>dmQwdlx5}8h4^wKx zECWBA;spyh)vb`=d@gg87^!AI*DR0wNF@D3H-5?jCt)uEg+a`CLt{;~);2^fFRQGY zxxIw@`_SeSb;8=@&i6jAhI1^&pHAgeOwc!^ZRE&h`(qV1G5|k~o@@gaLcNlu2pFUl=mC;PeLD_(Sbft#C$Qdm&&Q-U12T3`i#4E6 zwJ&G_Be~zqJc{+hzsWGE`K}rD{c6I#SUK@%qnJM_MLNz^Q|_aBhrgq4l&G(#*2I@y zk;YEipoocV_BW{Qgoagv>X^76oFB3M2$dTc;0QORNDC-^s-8Xrn!b6qrbTT9>I8Mh z#cDSvljcT_Sa;D4JbSbJwV0xS^iqlEVZQW?8g+nTZ|H1V52buv2DZ}PgnodfZduB&<)vJ7& ztE~D-_GB~Mx%L{x{F)c_BZGBEqCcd1_f-4KhehQM6bH$K1>~J3PN!%Z`>`lNXP+$>1<$Zg!!@h^cd)JG@5R6Y$?iMM`k0gs8Ci~V` zA}#SbQ!n|FKAmhTREiC`yUYdL10vZsRC(jAXD&_EOga541>;RpVgh;`9Z?Lvuc%$L zo`H_>QknIZCR>L>&a_o<43<764%O{s0u0yFYOktx%J-!n^CZMm^ zgTlHwU8VPBcphaLgHwq!$z_E$JlzLe4gFX8^1Hx#aA6&Q3%Y+u0p z9*~{fV7=*ub`PKu?7al;?~udcVpm*HPwJV^k1R|9LfeQ&w+7x&vXYf^H=I(ha zN`2II{#@qyp5kKURe6`l%t7`I78w(>&8M=1DNB7vz8X}KcFK>!Z*q&3ud>LjW*+F@ z3yJ@RzW1;ATZO@rK0Hb?r`fSMV7+daLtW^DL3D4lsA_C~_ZwaP5woIs!+uO~FZW@< z`jgDTcjAYoJ#t&V?v%{BbBh=n@4?1m7m}+S_OuBg9t@5*a&NH~gs3G9T4oJZr}ybb z&jqcSQ?H+_3p$E?9cIf3b$Qd>uo7w^@=Uz<9$-{%8U3lN1ZYBz)MdUXR}+(xo&uI% z6I1c1n{)(g(K^SySr|;XIG@&%*(Ht7Apd#`=rw9pplPYC^P`V=So(Ra7TH!{yTFm$ zRZ&?VI=eX3{y69WH}8qu@jz4xwNe{n@AV`Bud?%}z{#raH@x{)5g~wAynwC9GjO>O zE0!Uu;oig!_<1y5I5}cr`!MP*6JJB5(8rZmD>>d_$0_(#wZ>V=ZJt_zdix2T1>$FlyPSy`E!tu?|S6SW*&GUtI={Z$n{I)Mom_6D>65oU%z4ZtzU1h zcI|%1Ipt#B0b&0SX&3$vRh2T0+I{c+<>hE)Mutj#JuF0IYiyLqrgSW7Kfk>*SsZ@C zqJL{-#vHTLVX}9mKsbMJ80k#Fenieg*|lS113WebTI+t{YAdmoFQe9#B48_}_+DTZ z_@jZyaO<6r20-VL)A)|2c}0Cx=Pq63PF|u?yqWQ*N>O4vW!%FxT&*1*dWOI>-a(vya63){fj% zE%-@4=TK$rV;4ntxOc|>)cpJ;Jt)c|R>D%vaWY>9rX2-CnIgiGXRmgF+B%{#;*K(M zLKW#5H!96WrK^Y40_!FF4+yGC^FIq)gOM(kOMKbe52yElTNeKZJpN^#BA8l!a9_r( z=zpeu8pP9*!#(onM@-K?ibaHUl&{KBN6?RHo5;Gr(y93#(FYayIX!PGbDS|1@lYL7m@UX(oSSfQrmG*{tY4 z&I)B3>Ih}bfd99KxmPXd(rMg<173n&kUWes&DSIHE_)vRJ0hn)>f>Y7BSj7Fqf_~f z@sWiv9F$upk=A7(?&+NF4d*c^sl1n(=aP6kEA)FGV;M*t>e8H0<7_G2)R0}JXm88f z7e3g;!TLQqgU-~~n^|3c0=Y1+bR=0`LQTOr@(r>Y25T(un3z4c z%V=fh3k;Jqb%T`0BziYgl8ibwYz-=;Id!xB_!Mbn>U(s33DB}Qx!*iz%Vuc|5s*qZ@;iut#KgoDTydxr3fW@@PDV)6{qc5K=F~bs~Ar z$@zSYf~hh*c3WWcGjFC1A}dQG?gtsv-go@KHG=xAXvhAq-Gn%^S-43+=LiH_%;9Gs zit(Y*U{q7_Z=)Y2U05$l#y&n=9bGX3awpPsgroj+4_W^iU%_J?z-rOTX4 zG4)t2QI!UQe~{b1d*Biuf8}n!bGc9ea}q4%;Wcc_d%%~p^m{;Vw6@Z3L6|&G)vF{; zp!N@ZKm7Xop~~U?RxHrCNbfs?L=)W+kY(@gI6P_s zJv1GGI~ST`m=*nK0Cn4)pxWGma&_?2+v-vJOt*9kFaZ%H;_VO4XRP%fF&o*mYL$mK%3~RpJw#a2h zzA9``3N!%TMxfBdqh}700=*tPjAgFDVS-xp zikXskq-3jOpi1(Q_C@{bgbV#}IQ7Fi3j6%3)P+-Bm4$h5R`bi8PG?>Mog zoljK-apUSgWqypK8;D8u#0=03+S)I5A?t$ftJO=&33NUZ07MNg7E%gs#Ly50nnvp8 z*o2n>F)1*eQc?$_WspSz?Nv3IIAUARZY>$7;xtQdK`+{3rD(@oA}0~<0p_T*>!6ZO zhqK{Dklv^5>%-?!Z$G4p4|lD_&x(ts@s}nu`IN)mzA-1$U6@7Tm`&mcSJrF{{ksMZ zW=O&;gtdzu&y~16H*WhF6%J>D+WEIj1TU^8o>^A*=FcPhz1U>@U=UDM{utqI1VME2 z`>4t;^sZ(3s;jlU>D%~9GM3CTDwU8q%kp5Q&>-IXcwoH1h;Y<*D^+P5lH+2!r-Mlg z54>4J{9kd|-}Aw;Bsa@*JwNUfGb@jo;s1p7Rvc`_N0X~6D3{Bo387r*ZovW<>F&b!V&^X(C^+2Uazh0j#$}YW` zWr4b;prYJhez*tB6fp@&Bq|x%OjO&U&vxOw^rari$qKE`WLGCvhf3!q7J0{%vNObe zoOb3YG(6OirIIHxsXAMB0CRHLKW|W6pkav6L}*Hqz==~0_`)<8djgMYk|xK{tjGI> zbk+pZTn3%=JUlfge(OhUR4LK<_t`EJf1U`8st?%vL%LC`@?sGjc%%QrWccrZ`X|8s z|LXhSGg*TXKe4r5od=xXxVVe7-YIPEs^OeAl<_|sVkx!UZ&dU?!_a*&V*NFY}-_d$Aw|!FG&vOI= zi@pJEIg4d-KR^6G*u*-t7o-q0+r3=xcl_C;`cbrq1a&@grMdxCe`B+wsMP3#6uQ&hqLh_ zR^w|ZFy_z%jW$6ITLwkFS6nDmy??v3?AH~lAY zZPMEjLFFty6EK;K+3Yh{3Q=@unm|#!ZqA$@F4;?4;2BS3zBy3iWZH@Gb$A+T-;Lb1 z(1faumfDQ0!k~1*)0ab^zkkXR+fi;L#YN`DiLV#8600bC z@ebv=1Az*hgbVO>T(p##Y%Sf+2tjkM?ZIXI{`LW=Z@R7_Xp%55n$aYXQjuC5g+A#v z+v+?pc*O7^9LJFp?iBcb@8!JiK&76NbxfsxF?~OUuZG`43H6znp5N(ptvg*8{L?a% zI*SuDGbW9A1bg9f6uN-%NawiCw2#Bj0`$JNO`Wbb2F{zBJl-leqsn--HQubl_Y%oz zvkce2Vnoux4|YhMs#-l}g!mO}!+i1E@>^H)nZC9B0c)f&`#06s!sheERoI8aj=Av2 z0F#st%j+(MTSW=QSGAY%{qoJ!4`o31mSU?KRmv_IxygRaAEZVZ>2`PVbjEYQO`JZ011_iWOk>04o*>kg1c*tg@x&ke^d&DH)PjT&Vq+XWpN(gO|_RHZU zIu8H9s805D%l@T$q#)HqbFOmx{*LR7>f7BKnrQNZ78mR~0df_<);L|M3TBIa(g0(SHkDD%~>Eesk_c)>VALf3?82(?xzk2olHTI(o6%M z0qulb1_OO{iV4~#+eLbBM=JIYNvgYB2tS?0fF4Tjj|47|%SK6^m_t=J-ak|~3!=0B zvos1QA{N^b{8nAJ@d|lWW%4C;Aq!}k&~9~ANZ%q&YxW~&Pw~zdxfqeIy_(gPre5Pi z?9f)<-nH1p%7oJAV`L1p5809)=_gUzX&zRNR2J@0r?2b+xgajT5EGd~Eo2}*XCT$rGZaWVrY3i;P1eSvakJah!d zXG%$TX44k3mLo*yY47{@iV){8ULxe!0qXKckMFOZ-+$H5e{qj=j;k?AP%Lwc!Y1za z%ohu%bZ30-_b@v$%Ckbw4{h!%OH2@(VKeW--GJCOS#vfsM*JhRtW8P7ZPIZLqAjmS z@!B>y@N0bHUD^kjo_7(=pou;bdDCTXm-4Qf^S(}W7@#RX+QZ5D5hYduNX?uH&||vmZ+l{Rnq`$=W_}F ztJV3h`rF@PaR5s@?CTHyl>fHx;wN(ya46h%u9FuuZQBe>3lRQzRD#7wr^YuRjxB;? z(YXo7<1GvAjh};Z7LoWn3(+1)x#`kgK4`1ZNXuyAQnpM?Pb0gSOw@4eoz>bxU49Ax zFtt1`2Ch?qAnjKPb4}kPmZO~l)Iu*(7NGoV8l8;UN$5#F`(%^}okJ{dQ$Q4AhXc zS|8+D?9YS}^gj|x05MRRX*DV5{M5I?^Z%+1Kn#?)fHlv)4?uhO14RT7aCEy%KnzrT zz$*B5i-7>q+pQ=?NMb_k1|YDv`VRy_-EIXC1Z6}S_dTj&`3BTI_aDR*|H)G){*5{d zyq_D;uZ$l+fXwY7{~lQI&zL2*&-Z?QrMU=wzBiyr^jU$q02d)fzSoh)l1MDXu(Hs+ zGBO@-Cf(*eoq}H{n)zRNApUBiDe%|*`K8~Qr~fj;@M|A_;IjCuKKyeR%RhSD{tx*Q zZfkXgnVl__`X_B>M;#1k@(Kt+cST69DWBeQ?i+4qTILw8&Ja7YG@`IcpCMa364x87 zJ%n_nMJ1)AEQ_2@?OopQ07yTEAO=^aXV-TDQPsEsWps?Mf?NR}ig|v3<_7gv2`w5j zH**65jn0YzggBlArT|Fdqgu+@HBW13wc+<x-FJ@*yu=j9xNN8xj zGJqvUjE5`Wi^Up3+oR#Nn6-@_)dcxEUaLjc5gCX1V3*RK7&t4(o$4Ta*43ovNz(&u zxbbE&8;_ecJDUkxX}Kj>vqT<2N;dN+uRgmFw>f16k!Z6*Zt5Rl#*aC z+i$tGZm8`m^`0hHCo{8O-jW0Ht`*p;=59JVix@>aV-Fd-H~d$Hc9^VvyWF$7#^x0J zQV86hes%Tm_DYq=qC;9dF9PJV8KTRRM5~7|{YN4?+2WkJRBxF>7FYQ{zmgaM;Xe=> znk-1!;iqUfWjx%3Q#hI;#MO1})4GsFL0IqW$l3@zVz6XDQr#n{89vLA z8P#w)dS0!=U4%*@tm@ExsChz40idV&dEE1j$zNO{MQSk~u4)$HCWNdCdLJ@O=Ml6Q z7`5Q1gNP{c*zSO|^tHZV!tC0iCX3&IcESCOebeCF>m#?lAj?C`HdIm0UH~I=cmpB~ zs!^m)aXPvI(e;*Y^i%L@9Z^%G@gBAdAM(^9UvW2xOZz}KF=8;kG7ip&$0`LDb)ve? ze2Wa93&O8((j6`&qU(8=CG4A9zAXgxkjBAD2l;I{DaI7k)5MQI+T@lf7vZ?7g77_3 zovoTzs&1$W6bPB;%duE}PS+@)_fCfOxJUjz-aEc@!s(uNpXvQcVay3WB3+s>4^}!4u!JUXI4@!)ReZ# zjEK=wgBUi(9l8NktDHDL*kPAMe91M5L-THPtl}YN_8r@!1Vp-^CuNxwsJfNryRMDh zRb&k+8cusql_zbk+4TdjbOKl$@mxBH2+`o-p4mG^u%7~P2vbtE4KbLDhg#giF$y-+ zn7fqTSa?lq(=sjQt{`djluSYy(VaLShOu+QSC@7%UpcqCdk)tMxkWR2W}qaW z-*)XIh^s6rCo88tG`RP^F-Ea#+_IyO4RnghUGb)1+CD~+uxrinsahA3+ddV(EPkp4 zY&HIg`L?3-iuv{ojm<}Yte9ROcR!=WdMIaBGEAY4e|0srdy+#*6` zu7-QpR^3WzLtwY}i;#P9>W>eqi;4Gaah$G>bDj@Vr#sG_z2GoR_a4)ld8YY3nr&%s zpVrBCHSu7R-WLF(ReeYxU?`cc-*=M`B0b7<4cY4v;VH9eSO+N_sS@L{O-v>L4_aD% z-Zet=t$j2t`F!@f6M1q+BbnpdMD>IRHz6 z&Do<0bz^#HWWy3G}zXFFUcJ2mrk9!IL?7gbt*}DV>-!cXm0D{95V*&^q z7)liSW8u#Rgx#SA81nc>tK)BL0L?0N@BEJW^E2RF8~F!|W6Hd79fS1x1|)fq-aXQ| zHVL4p?h@R9{BD=hpysYh-6t^+5HaSuSYWfezn-5C5w=k$-L1PtIhOv&KWrp~ksL zU==fk-vZ%Q*s{=SDD~>ZEeTo7%85V2;txT6Dy?cB59vzJyWnXbNXrJBOx=L`aP}Z~ z5^g~L=~sGlP=`72JmMTg9DfwA|2kT7<=rKP?`cmXeUmZL zI@cdV_s7uvG3$O<=D#jjbd5udVer-`@FghkBq@_E`SQ{dw>^}|^hn#F<15HP^O3<| zR=ykHbfws!iGX+pkQE?){d=5r{DbQFn-H1-6dw2Ez@aCGQuImy^45({Ycgw#8n`K zoLLd@>GJ?&Hsw(vk{uj*%*X79xR#UYQ-ZC@t%^+9`OKr#XiIgPfhI3?t0SIunW@)+ zu8poMM%t!6ciDu)8q|-_MF`!yi{whrwzp3A+E`GcT_&9@uXG1ZbWo^f*er8)_Khwt z_L`?+jCuDCAhd_s<#A$h+s!8~`|!%m4U zZnj8?T5q6D+gpk$M>ox!&c1u;4FK<-j}*-xb;s;$&yK5$`1087UcffCPaki{?#ye9 zteOlcYfQ#+n}lfl@#2kF^3YA&qe;yrAW_t-G%QQC;ii(rE3e7F^B<>34{w(e*@mss zupIN$%oZ=&8lFkZ7Rwf9ykqywV9iDQ%4Hi3 ztM)Rf!L4C4Nnq&mb91hF(TQmf1;zWu18QN-%GpD;9)LE0(?epgc{##~d};KfxXhvP zNLdKxy>C57SRZ!o!|M=86JA=6p|a-~G|5nyGo z3@YeVvI!ehW(rhW*zE=Q7Q7@+yf#AH7}FK#?RY8Z1L~-y1<-QB{uTt=t!u=?!k<1F zdr-_+AfZ3-=(?*WVs_$HC6%X$w2#i5y-q)}%cyns$3eZ~PiyF7C)TNHrDjZGb$t6B zi1d;l>`b*$eZ!vhWiq5VnVg+^`~s`-A=Gmjki%?-;6!%!W473+ha{tuUpc&O?5<|{dk|N@t(N$?tE?2y=UoF zM=8#U=+m2WmP5;1mvUDQ$m4G&(u$WyskLGjmk>xEP+B8uAZY|_kd+h+Px-=C;&5_am6MjU zTW4I}PmSTR^>N#hxbxLmZG}G~U2(kuDJBW)6NVrsORtmksninOjnM2CR+h zi@A!f3Y(K02Ax}5z1F3+NabI4(l=EVmsnWFSqS1gLz&7!w0Eg1o(q}QImk!TJ!5~) zTM(>5nNmJV{qk5>I z5Vm_UrR0KK*i!H*nLN1tTdh0P9w&~nLeBePqD~Pt?7UG!zDbIeYf#~sj!Zxcbh_H%T^!ZYlP04o6d&uz zE+6@&%5QycJP5V#LxxmUnzxG>Q$OT1!8J0txzIr1y%ng`VbV#qyUG>hN0vufSi*f? z4yJV&VJ@4qiZ_Z+Aw%FsbVoC*s={8YbncfycNhW&_-=XC%vUfWS&<jh+RhEkW9o8y$`8|lXajxSreD-;nxpv#K| z#-(1p3yX$qkZ6=uTFL8f3SE6-ySyV$;r|p{JONb!QRTWh#+Y7(;iX?F`f~WI>CIFz z+9p=b??K%KV8b5Nu_rT3^;D>iQcXV46k4uU30is~{0fSCEQ$A{s~KqiNICYl&Sw54 zXlB^tUavdV;Jz(CXu>7vo6MgulI=Y$cxjoh7#>G7De`U}073F@jR(mF8+4g(T~=rC zNzYwE^9`kyCcSr>z26%J-c#)7lUT%ESt7Y+Yvmy@N|789UpabZSNZ|czWmxE!#V^3 zkwvh6S2q9`M$7TLB|ZNm9P#%<+vQCK#aL}FUJO4Ez)`Y%86ozfS^N|2 zbAJ%9?bEAwUW$KFv{d9fi#7a7xBrAoLa;%6O2JM%S%vP5S_pzLEGsQD;1wB?z4PoK z%l+haWBgomEn^zw`CqAkkeqWf(oo0Hj(i!ct> zPJJOvu8pVWJ|++m9tbhuR1vF>#9;T=$f)5+dkylS99;R4;8gjm88 zLQnR`LWEZVF#(1075=Nv(>d1(W~Pu#hHB(`28orZOCM6={j-y-CM|Q5^@CJIk1yIQ zq20VD5soC;%a`&2?kf$K^9WQ*k@}Q&h;h0gWNrrN*uIfAS zB3R=diHIenFr-IJ+5xCUNrSL#OnQKQ{`;X;CP9Hu8H3P3&;40J_Jul<2Z|mQNfKO? zl4HllUDXQ)GP4NF_6A>*y4>ejK`273t;S*3pY~q(E@Y-?Q@ELSJJLQ(w#!Z|L3msbLP8EhtOkAGbpA8tr_Uy=E&J8ame#lZ4rATTf ziM57*T%}=jh1?`rVsLsn(l$DgvZuN%^*>)j$SUwoC4PQ5b}&@p+55az1E2!*u(r#ScIkpzmU|0 zD@o+(SgTX!A(HIGeS1RCvgUC?ZC?bXt_ElFb{=<{9VO%ly~AwD@=g0{f|X|V{JkHgEgVN_9YQuz*t%Z`exoDBMw5PO2&oFw>} zJ>76GxcDcSa^X?eYkD29VfIK|c@hB6{uIXP9F3>Wn%^GwA|6{T5(g58C6RfM%iWLr&^NY+%l z2w785nrY{DmfK8CJ9nadJTR$^k&_i0I!g>=iq#&5<5;|u!@|ss9QG!KY2=+A$tBB7 zOD|4LtkM!dl_mK-o3Gn0(eI&Ne;pJ+jz`oVS6XJ8VE-Z6dE4|C(hutmS=WGRilW%B zor9qt_InajAP!JlE;RTjs>DKUToZy+=&hKD3u`aA$acbxDW~xl} ztr5{has-wKH7DwjlJ?(GSpSs6$6QJwuTMF%D;Opii}EJ@ECPhNR=mz5doiS2=Be6V z_?A}Bk5;f_iOT^vjk-26qNq`5KH}Py3l*yf;h|EHq@id+ER?rs^0~u`NoaFv^C>~3 zygK?*w62|&M_kIvGC^!gY#6&vhFV(I#MjYcKyo=p4T+WqdFBynu_V>?Xt$j1bg*^I z>)?@(UYb_qe-#e*fS2bD|3FAeLPiR*V0V(w1^Ty+1iY#&=4eB%z9bS8rP_9(4^3j! z%q40|#4uGxssQm;r+|a>Hws516+zRywoZBnT=`nEQ_F^(Y-f?lv3_7um)`w*c|M9= z^yh=0w$&n^zO6ACY%Y8rAUypkteu^mcHQyP$xC%)O#%Pqfm`8G5_{*MjrVB@`~xob z;zFnK?0!o?p^hq!Z=`xh+rEewF}F8uqiM=hwow-n8+OrJyq4*`?4QIevb`Vmx^Cc% zj64`P9>)oBAh9n17&Ms&Ii>dxhrTLSVGN9`#KC^D{d4q%`j z%#G&&qytBCtjL037NDH@}75UXzZdE`qnev!t{i!iPo&tbrY(6&N#tLpiaQ}<38xS7g&R&NOp95>+Re}b@{st77ak72`B5B-5Cye$RHFj4P={wdri10Bsg^#y0b^5{;M@Oaq3vczxAjA~WM)i`2D7jc!A4`kKz;#q< zwUi);NUp(T62RDiOV2eb_cAa-l@P>+UxAh<7{sRT@I_uVIN%>*5 zSi3s$bkIQVC-K~xt>7P50N4=n+q3m|-r+c!v)G7bFuK8X9sKD@orwYkk_1Bt=A~ll z>y`yUE>TZo70m2WGjhf;TOEpX<^F(3!>oxptz&5co5W0gKL}gKdD*uyZ00h9SUW|~ zLt+W!gRg#(?ek1X?~n)#e(b7n8a(ipZ>b6 zc0X}o#^`xHC2F|RgK#=RTX>hg$ViXYnF@^mO6=T#$}tXsj2(Bht`h-u%yW*y5l`uJ zThydq$u7cP-!%mv%t5KMwg#Xg;Q5!}TQBmKDEmX~{2_4v6Xg_|=Wy{PsXR|AmtF!R z>-tQU8*Z!+!k^Y-?LdU`yeY`h*dsXC)wM^RQLMXbJcT3P^Q z{t`STaw~gJuh+tmqX99!zdYEX29vGO?U8X?Eb~AgdaThvd+I=Oy(c~LT%l-nyKKty z1UJ#OcQ0`_zfD?`RE~`Akrm=xN&aOvhwkwFunI$qgf5-~9;8xe>R*&_IrNqi9{N!U zr~i2|aR0xq-^6KfZc|{kQkyocEcu4LYxjkfz^ngYUvwAc>cgl8^L7?sI^bQSMJ}kz zxaxmBtr~lFZ*Xs>Skqz~cAZkCcB4nmGk#*zQqC)j1-7>Tw{{14LN)P!KMJz{4vpEu zXN((A_6T^Z_6DS~YwUBb0Zo^`0Tpgt8~Zd~jnG^htFnR0bFeVXf691_cL(2uj5H50Hjn|Z3Wl$4M2q?kgr!$av@!xp8|wl9O5^i zB3-C_fim~4qKXt0)%6Kx=#u0JR7;H|glE})f9={{;~KMKdhF3s=mj2x4S;q42ACj~ zwI0Eji=rMpHuD1jdZKSFfU0Qu<=0RjOCB`vvU)l2l%nhCyizYH7XS&d0aGlwQQUyq z>kI*|O^qu`#doj}XCdf1RQ{XL^={E8usH{+ zZd@DN^8qy(O90&$ZaluDqydC^EWa_o+)Wv+vZ1*^FW%5l_)`JfeLA4fZ89FG=m?wg&wke17qXo9Z@>K zKwncLUjZYUj$MnrR(}VeMnUtv28MnhSpAX2)9cxkG)O=zxL)oA9QPY^8j51mE2;od zkub1J|K||psj&bl^G>jffH}9N-s7n6Rj;hI&;-U?RHKX6>{=n@Vi+S)ObX0_cLcm4;mzBo2-xriXP$%AW(`L_XdF-haC9pKtBMui9Wl&)PPci7A-U;!JfKNUSN$& ztAQa@c*syk4e(qBu=U6WURru80O+CIyE<-!$P;LM1G6J#uOEJq)qeJx;O4tPpHR^0Bbt+M)i&C<~44&-Hr!-yP&z(lqDxq$qV!Txy_H-S24$F1P1XhDW1zN5XvC zpCbAVE%;mK`z5UJ373gos9~RC??-*yj)(Mq`wBiJHEcZDPU)=3BmGOE}= zAnxsn&1ub*%5~9>LtAvn#7=Vc8Xjd8QjsWk;2|p=^@#rF4Tzzr5T}jckmKAMT2}-O zQrUfA*{3q^Mx>Sz8(_nc1B2=fX`N*IVoOR8ZLLQ|@#yo`kPWoZU27=|5a|51D~Wtsuu{(jMa)Erfc1=9x#WGr>GpS^DIsBoCzJ~TU2N% zK9!NOR|wrS4a{IyQjv}?t8e5JB};2~+RbXrb3u(Uck%_iUS7Cb5BMWgD*L%c#?fkE$YgY9nH*8?4>X^-TK17@#kl zA{R6u0cSiX=W~~L6?v+%R52tcI!`5!*}01pLXfh`gA2z1J41mdFr=OnFPi`n!8-p! zV_ka*2VvQ^504?}AW=VnlgM^yeuwisQ8ie<6G*s&em+h>n#sBmv=p?4tC(X-?Wj1~ zs^8pr@e~JImvvGwg8&sM;D9y|eFy4lxd56foUU=J7Oaw<=^{Lkf9xz~NYp@s_z=f5 z*fQ)RuFJ?uX9GKH2YbjQ&F~2^Uxo>;JnP&nI0D!o|AzJ`{6&{y{&y79UyLB&8Mjn> zY7vBQkckIbeWD3(JjNh^++i5;3NjLHVuvsHDQdKP)CcA%c8M9Xjro6VBH3v1%xqlJ zv;&6n{|y^SE~|8KXVFlRQB zm5hz4B!jXhJF9x#wA|S}6Vv4Rqoj-{n7FnZ8{sk{TA3|Xu1RUTnt2~$Bb;sWd-$ai zr=C?c`tWH+T~yASPGixvs%A<$>17eC_MJLXXFL(&s`JP7l%ms|zI3VS*oahO4_EA8 zms{Q0b93`)J5~3EoC5-Q4&bJQ=PBLhS!*4GLW<56URDkHC@s}0lwI+sNu1%ZxCiiE zxDzZ=bDb`6m-5;@AEwfbl;Pzx2)2-!M+*x-%q_y2Y2viuhVU3moUUM$W=CHHi@anU zmvxD;3#fs4@qAMP)?Ai<+_0n+wya!cr@|WL&_aUU2}bQ`lYX|aeYM#-->Og#rQCE) zC@4rvvZr(T7;>yT%S5)LqKt=fV8_6$%+Gg$b-4^>j8+oaz;9uf4H#ALq1kTWya9DZ z7hPXy?5AH*e}lT1g7<*s|zX)gj*e>dnez``9Ah&WZ}* z5nY^|%0xt0&8uKXTq~{`f+Vt`@>M|%_G9keS z0{aMXZsqI8T(V7_?N5nt60LAHMbX07ufQ0fQ`_E9l_C( zF<_f2LddfuhU7<72KyOY`?b|lvd|JZBQNY_x%Pvo27+Zo-iu0s?0mMJ60dXuJZDcA z_vA!S4ANE!o>7yIucFIQ9h!3ZbNE<~2xXCvc-pOCXGrHA;edarpA6asm_sQz8dryn z3vv}=(V@zfKF*^RRnCseeirV<8=9`3(W51~^_J%Klz%=|3&)>zFq6b= zI!?1SY$@;O1Bj@Gre}Vv>v5!q2n65bOq2=tZZm^4F`@*N!J{Oj_0El$9gS z*4mo2X_;Q@y)3GXlRG2loa4c@ok{_HwuU=@n3-G10!f z_sHU{28WL1X-^5=L>{zK1+gF2@s+N?;STtw&-ql)L)OtKJfnyb*s#@9kBAa}1mK!yn?)(H(ymL%%?Eks= z&3v=DMn7(>`ufi+%Q->~WkfbDXe|U^k-$&qWp&7Kc3=&kMy+&CDE_!g2!5Cu6eDO^ zB+gIzsF$SCS-%qHQUu+Xb|)W}mJ^m^P*I%@)AuA7-e`c)Im*fYH0bPUY824;!`-*$ zl|pt#2Y!onSIvL%FjN4d@%yh(hJm>o6JSJc@_u=C8#oQrjb14(kLM?;EEZnNW7Ld1 z9|HeHAj;=bI1-P41|+J+b{+D?0pRv-q=cgW@ZR&}q&x7N`lwg_xZq!N9MBk<_oq^o zv_qbgk`Q@gK`2DYqyr`%c1qd-UMFed7e17Jr{dVcHN9g}LR(U|-mF}8RxP=xn4bKP zvP0i0~=W-*N>Os`NL^5E7++8+lBa{j322X^zC7 zU=(>j=W<}Fq0|J}c>jsu`9Bd=zCqN%l^GBKk&3F2eZ1F7#u;L0@bPXW?rKZ`h@387 zh3pPop9)jUFlkSi5Ph)`ln^U?v zrU_t${=Y_dp+1aSMRtqQ+E)^0%h&+2K7D+KY{K93rF+vUAvMpunnTp&)g zxp?$OnR?+cOPM72iwq2^o%$DiyULkw<00kS7(g`9f0p6se}U9Fo5tL))>JG8Iok&7mi;GpOGeZDR%u8ZOdqiD&e ztS>?^cfxC=de?CLmXY}=b-qBEOAlHDnPw^~dZK#@zNySks8dgvW@^;u^WY=jfYhWr zcPwffLIVB=+@?Q;hW{^TzWN=<&TKvA;=|NTF(3Ht?G3a90ys=%#Q2|jP6ghlz{D*g ziJ<#(GO&NBF7)whYy?Ml0a4P5zZ70&;p&|f7#K$aW)}N=0u*C9!^xAWp2c@V@squ5 zsZ}812Hlssp;($zukO)u;%Y7@rNFDJ%{dt1Q3EpUCKxAx%FOt){N)T?DyU3fEU^EIf4A(jDuS!srlZ-K!I@{wdLf44LTzOE z$unz37pm;@$gdjci~E8V1%(Yd&Y4U*!QWIqp-iEgW->o)l3^$KSXkDYd~eXjR?8GA z?aXcr-P-ZgzEajv5q%KsNRa3XN1SCrk@cP%dm|ZT)XdfkoV&%|(9MT2%p{h=uJxGh zaCrW{+L1fOMJ$zHSwr{pPUu+QDj35HHag`J(D)0}RXak9O9a#E_#N-s&U%HfDL)>B ze#=9~G*VDWIgQCn-;5DHXmZU|gL^kVBV7vjZYh&Om2I%dE`Q42q*ti0s13g|0O(Pni?tVbGkm&Q1&$e zZKT-h6G2@E$vjfD@jR1H0gHD95875ZRl}Zt(UjC4mZ>)fEHlA7(jB zI^Q3qE=q3g)#5PhAS!4W3awiWuKR#2{>8~l9Xned{v+rC2rt`e@Jo6@3lIqi|F77bbN0B6k)B*JL;$f6S&DZ)zySpOLvok>>|rW!JNew-+eWsI z*9g)Xh7xZjW2iC*ANx_Q8hgEX{n;o2q-Gi9G@w~CF!1-?H~vfR2hoJohR0s-&!*-w z3d)yO#4NWWA-tCv?=xBaxL;WRnQY%Ku8`!xj4guF?&~*G0S`b+ocaamn;1+0n(5D( z7~L(bf&D7LYYjM*fvi;m_|`X|vKx>~fD?jrl;4Yo0*JiN z0`$ISUW)K!d0?H;kw*6HrY`MwOn9BqtKXqvk+BDB*A5$VmsEKpZfyj4Znj^~1Lwis z_2P?p1x~xB+6Y@20&xZAvuK(DXbu`$DuPiJHArdew#SMvHz35l1)58^bAVP17G>b4 z;x>V$v(D$V>g%WT_CDp0?8f9OJ|DpJnUa18gcDK>zq7-xEODE>Np(Smbb4mEEygS-*X$99IJPMHYD_pmNi-B!j z{68-HTL#2(_TKGetsC69S}?!5H+2JAzgtFfod_X#DUKTpeQskR zA6?-Hr|INL+0b9lq_r@ItZZy zxMpzMy(sM}+(jv%g)ylvfz@T|kDO|@HV(5F5GNvJm5xCbIP$1vrQ%~xOEYb#GA_+g z?%c$Xnzggz%ar3@6fNEhytV@qXSvZ!+oYUs^kTu838}konbE7K3ed*F3>5@>@YdzJ zz;#W==`fY>35qBAfzhPPUMc04GR{>VedL8+!LU^m>PD&3pun;1J-2cd*RdIZa*+n9 zZx2APiwH1^SEj26Yzcy2j!oAp=Hy<^Q@ z;XK~)sB#u+0=9R%?gf&V4n!@>MH|k^@hc2`_rrVso*=*SJm&JeG+$Zl835H?==CeN z+`r2w408Zk5$Dc->_hlo-HQO>iDcw&d0gi>jRLrDE#e(ms1u7c!4M|tK3>n83|%>$ z?&b&L`eA)&zS>!jEccRAd1SZQ*M+e8QQEL6h=M4xzrggvzx9mqYn^OpBi9fB(pjUu z{&f+dn!QwiVVs9jGL5f}{E(&0iQv?ic-m4{dG`6TK(sO*j!AgcY;Xhe#j+Jei}Ytt z;S7w^Y!!o6k|1!GE_83$79V2Il%N)$E_ZP~`Zg^NabQ~Y(*Ui*3_m5({Lq%zbWh&* zM|o!BISXSq4ff_}(=>r56QYg;UlA|&I^M%i+Prq`P0wC0APp%GoFJ@jNWKIxbM7l68H_6+JsK4r zNzg>Du>ahj>wsm_-~Yl)z}B2|NC5Rfgai>qSLj}A^MgC?>lVCC+X7;5Z5P6G47f>4 z5@ZL>OjMQq7lvOrYTG#X^wFb@FMIgJ-66Cf!gJ3R%QLp)K`tl~EVE^rIrB2BASa16 zzE=~%3eS${^3k!Njo%~LN^Av#Zc`p%rU`Fbp@7042=7r8u6c7qlmf&#n25q_`>I zbk`Kc)Qy8mOs8WJPT_o7;{%YxunFr?oiXM&I=Q*3)cGJ?+_Y6Llr#B~p(@wHRaU#H z4y&nNI#g%-#sPb3x3q$>Xk)8c@r63y`ug$)^K(I_ISOavqH;>1&vOR!rt014E2Mae z$<`{9W_kHD5WW~JTS~ZODhZ|JR&gPdV529M_p1&sNK7N$wokCyKr2W(jev7N^gkBc zVE*J_VF2D6V;kVBTEyQSHu8Jq*gxS~#7VNW`lE6HN%6}jvLIV6K@1}rl`-+95!iox zL1nQ3g5+6XcvsElwxb^Ra)c2=%G{P+*oji~F3#2&0J`MU0nko)BM8bYQ~V-^ zsC8S{V0FF#vOI?W7{xz(SIJb6OEx2wk3*!)o}I3(X?zI-&%2GdWp6gtsv4J!J{;rO zEg7G4f0XjINRD4{(U409;PgV%(^8_$-&7P?6wWu*jM)>fxnhc-n=GZP^pUQQ9J2X@ ztK+eyI^z8<&BzY_#c+|A@-jB(&bAW3^2 zD}5iv`*5@HjP>@uDzacGTC`=i;Kw>UsJ3#OukqRqZ>t~57{D>CD@*+rCFNS+}t0N zZ$)iB5mbM-*(;7c5NbX97PqnMdhIs$68I2gaU#s*KW(pS9>xpe0Wi?f?} z{9?m9qq}THI4+f~!n2fduk+ti=Cr?i!|aUDwh;3f&XWL5GMCYbsx8T?N(Rg3%TODc z0IY5uR5}`o{^7l33BgCOIVHJUm6Phj=%xJD)?8mX1JUYV2tLW|-xc~en~o>=GHXhGrt2O_3+RJO{#r397lS*!1zUlss$hN1sObq8^C^PZhmR2x zkS!hhS~kITJc}ujAg!tIH2z=~fkaZBaL$evNZ7UL&q=(S$!}#DY0*`{dfh++b zu&M0@S{&0`Os3k8h&aH^Y^yP=<}cth#rueN^uBD<<~B#X`ckMj))vg|^CB4EIJUm- zdc4BUpg^+?{4Ps-HMWB(H@6SMqX?aI26#IC1Z52G>Z>FG;-WIb^> zpLoK)3-dLX-HZ z>4LxE_;U!_a6f6R9=6?LN=Bd}+T5JMzN6n9mQfG?`ZX&Y(x?Q#jyYNe!TUmm1*+ck zhl4u93DT4#f$bU^$Mik- zHCpG!>60ytw@gEYB+**c!!a0im6*gzDxPt9>b9=e*22_>Va|RiYvSSd{%nbNrC*zORGNXoch=&Uo!kW z=ZSK2F6^1FTl{!ztJj|O^fye zU4MDPm9)8Srm`elbmo(eyQTh^5gPYg7ezWYty~TD`^)nZM5r={UVicvQpRWY5G|wd zRv^aX%XN3Y4^shTVAtVYLD%k zQThv-szkp{Fy2J@40UVA2U8t$v+xKmRyj>t64Zl7b1C zC4rx(g+e$V$0-w373JPJE8*5Nb z5F9cy)2~jEERQkvdsM!=pT}5&3*GU@Eh6=5V^oK4qbxiJfUqK?gaMPrfz>-^`T0d3 zrkYDA`zXj?wv1Gvg$u)dLJT@tdp?EgFjc(2DNE#uJy0{D%*!$>Lav+Vos}~LvX0bKJF{V6kH6Zc&$`v-MDrHYx?1yeXgZLo_gsZ zm9256&xsjyJkJo71CI7n5&meeh!#4Mb8#zO-`h$2sLAT#De*FvrEn`v4Mxk*hM?o6 zjg~j@u8GF*85y;HnDvB$urbY#MGu;ueZX}}QK9zL({$jG9^0Epk*|#1E}d*)WI-w> zqi43y^;-r<;=o7tsYR-D$qbdN(+C{_NfNd=Xjt&|C+3yTbXa>~Svw#->YrMHo$jaq zze!!8-Fm@^4ND`sli(2Wu_h$ZbD!{*QBftMgwJk~zipM|0bO(QF*Vu}0kOQKEL`Ub}Tg7zh zM_w<&e+00Kee)F8uWcj2#DJ0d83@1q>uP?j?yo)#6-*5k1&}hCYJs2BAKdw8#qRzu zJ@1i7-a_I!Do!Jmjm)9MQ;5nSdpEzq1U=V_;*{>nd~?e7M7?$ zUQkqYT_!NF4A>jQJ^QsgQZqBplk&u02*15s^xS`*?2I(PWk|>zMvNg>(CcNl1t?eH zK*ZIistRtPHSUV>C9RO@Hc!0}?>K>sB}%M7jb-ShGs?zj-~BqybC#zpadf!uZN;Y` z5O=g$07Y7qd{_o4>3761J+LkL_&4q0{{4MQMzuAj`A9=vA+1So9jAf8x%=yOjdEr} z$PO)#(WARW%>jNBE#SIEMcuC4rljlMtmmB^FCA9g>@ zo&2qNI~CksjshjQev%o@g!=Gq2r63$@fLK*IXTIF&nbW+9S!9jDb-hi{PA~@=TVB5 z8H?7E+0qq!rzcyd{dE-oJwCHX7fD}r5up(idJRikl!WS>?KW=B-+-Pe?jge9;#KPFvekis}yjwFq^`UeqB_<^*zbl(!P#YUlum<;_jBLBTXUd%)Pkcb1luO1fXCS$>Fhht7$v VWv_GtbQgH2!;Wto8f9-L{yz!R(f0rV diff --git a/app/javascript/images/esr_orange.png b/app/javascript/images/esr_orange.png deleted file mode 100644 index 0ab909ad0e3a86b707f63d4e0d2c9ce85a7cec87..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 123494 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 diff --git a/app/models/booking.rb b/app/models/booking.rb index 5dbba3e36..12df804c4 100644 --- a/app/models/booking.rb +++ b/app/models/booking.rb @@ -195,6 +195,14 @@ def editable? booking_state&.editable || false end + def sequence_number + self[:sequence_number] ||= organisation.key_sequences.key(self.class.sti_name, year: sequence_year).lease! + end + + def sequence_year + self[:sequence_year] ||= begins_at.year + end + def generate_ref(force: false) self.ref = RefBuilders::Booking.new(self).generate if ref.blank? || force end diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 4484da332..f919e3c05 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -92,11 +92,11 @@ def supersede! end def sequence_number - @sequence_number ||= organisation.key_sequences.key(::Invoice.sti_name, year: sequence_year).lease! + self[:sequence_number] ||= organisation.key_sequences.key(::Invoice.sti_name, year: sequence_year).lease! end def sequence_year - @sequence_year ||= created_at.year + self[:sequence_year] ||= created_at&.year || Time.zone.today.year end def generate_pdf diff --git a/app/models/key_sequence.rb b/app/models/key_sequence.rb index cd2a617d1..0be78a86e 100644 --- a/app/models/key_sequence.rb +++ b/app/models/key_sequence.rb @@ -10,4 +10,26 @@ class KeySequence < ApplicationRecord def lease! increment!(:value).value # rubocop:disable Rails/SkipsModelValidations end + + def self.backfill_invoices(organisation) + organisation.invoices.order(created_at: :ASC).each do |invoice| + invoice.sequence_number + invoice.skip_generate_pdf = true + invoice.save + end + end + + def self.backfill_tenants(organisation) + organisation.tenants.order(created_at: :ASC).each do |tenant| + tenant.sequence_number + tenant.save + end + end + + def self.backfill_bookings(organisation) + organisation.bookings.order(created_at: :ASC).each do |tenant| + tenant.sequence_number + tenant.save + end + end end diff --git a/app/models/tenant.rb b/app/models/tenant.rb index b7dc0478f..d79abf547 100644 --- a/app/models/tenant.rb +++ b/app/models/tenant.rb @@ -100,7 +100,7 @@ def salutations end def sequence_number - @sequence_number ||= organisation.key_sequences.key(Tenant.sti_name).lease! + self[:sequence_number] ||= organisation.key_sequences.key(Tenant.sti_name).lease! end def generate_ref(force: false) diff --git a/app/params/manage/organisation_params.rb b/app/params/manage/organisation_params.rb index be05c8f0a..6cd22edaf 100644 --- a/app/params/manage/organisation_params.rb +++ b/app/params/manage/organisation_params.rb @@ -22,8 +22,8 @@ def self.settings_permitted_keys end def self.admin_permitted_keys - permitted_keys + %i[smtp_settings slug booking_ref_template invoice_payment_ref_template booking_flow_type - currency] + permitted_keys + %i[smtp_settings slug booking_flow_type currency + booking_ref_template tenant_ref_template invoice_ref_template invoice_payment_ref_template] end end end diff --git a/app/services/export/pdf/invoice_pdf.rb b/app/services/export/pdf/invoice_pdf.rb index eea416e20..96c4239d5 100644 --- a/app/services/export/pdf/invoice_pdf.rb +++ b/app/services/export/pdf/invoice_pdf.rb @@ -21,8 +21,7 @@ def initialize(invoice) end to_render do - header_text = invoice.ref || booking.ref - render Renderables::PageHeader.new(text: header_text, logo: organisation.logo) + render Renderables::PageHeader.new(text: invoice.ref || booking.ref, logo: organisation.logo) end to_render do @@ -40,6 +39,7 @@ def initialize(invoice) next if invoice.is_a?(Invoices::Offer) font_size(9) do + text "#{::Invoice.human_attribute_name(:ref)}: #{invoice.ref}" if invoice.ref.present? text "#{::Invoice.human_attribute_name(:sent_at)}: #{I18n.l(invoice.sent_at&.to_date || Time.zone.today)}" if invoice.payable_until text "#{::Invoice.human_attribute_name(:payable_until)}: #{I18n.l(invoice.payable_until.to_date)}" diff --git a/app/services/ref_builders/booking.rb b/app/services/ref_builders/booking.rb index 3ebc9b9ae..37c3f7b9a 100644 --- a/app/services/ref_builders/booking.rb +++ b/app/services/ref_builders/booking.rb @@ -11,8 +11,12 @@ def initialize(booking) ref_part home_ref: proc { @booking.home.ref }, year: proc { @booking.begins_at.year }, + short_year: proc { @booking.begins_at.year - 2000 }, month: proc { @booking.begins_at.month }, - day: proc { @booking.begins_at.day } + day: proc { @booking.begins_at.day }, + sequence_number: proc { @booking.sequence_number }, + sequence_year: proc { @booking.sequence_year }, + short_sequence_year: proc { @booking.sequence_year - 2000 } ref_part occupiable_refs: (proc do @booking.occupancies.map(&:occupiable).sort_by(&:ordinal).map(&:ref).join diff --git a/app/services/ref_builders/invoice.rb b/app/services/ref_builders/invoice.rb index 88e63769a..ba53399d9 100644 --- a/app/services/ref_builders/invoice.rb +++ b/app/services/ref_builders/invoice.rb @@ -2,7 +2,7 @@ module RefBuilders class Invoice < RefBuilder - DEFAULT_TEMPLATE = '%2d%04d' + DEFAULT_TEMPLATE = '%2d%04d' def initialize(invoice) super(invoice.organisation) @@ -14,9 +14,12 @@ def generate(template_string = @organisation.invoice_ref_template) end ref_part home_id: proc { @invoice.booking.home_id }, - tenant_id: proc { @invoice.booking.tenant_id }, + tenant_id: proc { @invoice.booking.tenant.id }, + tenant_sequence_number: proc { @invoice.booking.tenant.sequence_number }, + booking_sequence_number: proc { @invoice.booking.sequence_number }, + booking_sequence_year: proc { @invoice.booking.sequence_year }, sequence_number: proc { @invoice.sequence_number }, sequence_year: proc { @invoice.sequence_year }, - short_year: proc { @invoice.sequence_year - 2000 } + short_sequence_year: proc { @invoice.sequence_year - 2000 } end end diff --git a/app/services/ref_builders/invoice_payment.rb b/app/services/ref_builders/invoice_payment.rb index 2e2b217bf..3beb559ea 100644 --- a/app/services/ref_builders/invoice_payment.rb +++ b/app/services/ref_builders/invoice_payment.rb @@ -2,7 +2,7 @@ module RefBuilders class InvoicePayment < RefBuilder - DEFAULT_TEMPLATE = '%s%06d%04d%05d' + DEFAULT_TEMPLATE = '%s%06d%04d%05d' def initialize(invoice) super(invoice.organisation) @@ -14,10 +14,11 @@ def generate(template_string = @organisation.invoice_payment_ref_template) end ref_part home_id: proc { @invoice.booking.home_id }, - tenant_id: proc { @invoice.booking.tenant_id }, + tenant_id: proc { @invoice.booking.tenant.id }, tenant_sequence_number: proc { @invoice.booking.tenant.sequence_number }, sequence_number: proc { @invoice.sequence_number }, sequence_year: proc { @invoice.sequence_year }, + short_sequence_year: proc { @invoice.sequence_year - 2000 }, prefix: proc { self.class.digits(@invoice.organisation.esr_ref_prefix.to_s).join } def self.digits(ref) diff --git a/app/views/manage/invoices/index.html.slim b/app/views/manage/invoices/index.html.slim index 1b2f1f3e6..4d443e192 100644 --- a/app/views/manage/invoices/index.html.slim +++ b/app/views/manage/invoices/index.html.slim @@ -49,10 +49,10 @@ div= link_to invoice.booking, manage_booking_path(invoice.booking) - if invoice.pdf.attached? = link_to manage_invoice_path(invoice, format: :pdf), target: :_blank do - invoice.ref + = invoice.ref || invoice.payment_ref span.ms-2.fa.fa-print - else - = link_to invoice.ref, manage_invoice_path(invoice) + = link_to (invoice.ref || invoice.payment_ref), manage_invoice_path(invoice) td.align-middle = invoice.model_name.human diff --git a/app/views/manage/organisations/_form.html.slim b/app/views/manage/organisations/_form.html.slim index f6bd98da0..b55356246 100644 --- a/app/views/manage/organisations/_form.html.slim +++ b/app/views/manage/organisations/_form.html.slim @@ -6,7 +6,6 @@ h3.mt-5= t('.general') .card.shadow-sm .card-body - = f.text_field :slug if current_user.role_admin? = f.text_field :name div[v-pre]= f.text_area :address, rows: 4 = f.file_field :logo, help: organisation.logo.present? && organisation.logo.persisted? && image_tag(organisation.logo, width: 120) @@ -21,9 +20,6 @@ = f.check_box :notifications_enabled = f.email_field :email = f.email_field :bcc - - if current_user.role_admin? - = f.text_field :mail_from - div[v-pre]= f.text_area :smtp_settings, rows: 4, value: @organisation.smtp_settings.to_json fieldset h3.mt-5= Contract.model_name.human(count: 2) @@ -42,10 +38,6 @@ = f.text_field :esr_ref_prefix div[v-pre]= f.text_area :creditor_address, rows: 4, help: t('optional') div[v-pre]= f.text_area :account_address, rows: 4, help: t('optional') - - - if current_user.role_admin? - = f.text_field :payment_ref_template - = f.text_field :currency fieldset h3.mt-5= Tenant.model_name.human(count: 2) @@ -53,9 +45,6 @@ .card-body = sf.check_box :tenant_birth_date_required - - if current_user.role_admin? - = sf.select :predefined_salutation_form, salutation_form_options_for_select(settings.predefined_salutation_form), include_blank: true - ul.nav.nav-tabs.mt-4 role="tablist" - I18n.available_locales.each do |locale| - current_locale = locale == I18n.locale @@ -70,14 +59,6 @@ .tab-pane.pt-3[id="nickname_label-#{locale}-tab" class="#{'show active' if current_locale}" aria-labelledby="nickname_label-#{locale}-tab" role='tabpanel'] = f.text_field "nickname_label_#{locale}", label: Organisation.human_attribute_name(:nickname_label) - - if current_user.role_admin? - fieldset - h3.mt-5= Booking.model_name.human(count: 2) - .card.shadow-sm - .card-body - = f.text_field :booking_ref_template - = f.select :booking_flow_type, [BookingFlows::Default].map { [_1.to_s, _1.to_s] } - fieldset h3.mt-5= Occupancy.model_name.human(count: 2) .card.shadow-sm @@ -112,11 +93,24 @@ = sf.text_field :payment_overdue_deadline, value: settings.payment_overdue_deadline&.iso8601 = sf.text_field :upcoming_soon_window, value: settings.upcoming_soon_window&.iso8601 - - if current_user.role_admin? || true + - if current_user.role_admin? fieldset - .card.shadow-sm.mt-5 + h3.mt-5.text-danger + | Admin + .card.shadow-sm.border-danger .card-body + = f.text_field :slug + = f.text_field :mail_from + div[v-pre]= f.text_area :smtp_settings, rows: 4, value: @organisation.smtp_settings.to_json + = f.text_field :currency + = sf.select :predefined_salutation_form, salutation_form_options_for_select(settings.predefined_salutation_form), include_blank: true + = f.select :booking_flow_type, [BookingFlows::Default].map { [_1.to_s, _1.to_s] } + = f.text_field :booking_ref_template + = f.text_field :tenant_ref_template + = f.text_field :invoice_ref_template + = f.text_field :invoice_payment_ref_template = f.text_area :cors_origins, rows: 3 + .form-actions.pt-4.mt-3 = f.submit diff --git a/config/locales/de.yml b/config/locales/de.yml index 8f3b3b704..3ff976d4f 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -285,15 +285,17 @@ de: account_address: Adresse des Kontos für QR-Rechnungen (falls abweichend) address: Anschrift bcc: BCC - booking_ref_tempate: Buchung Referenznummer Format + booking_ref_template: Buchungreferenz Format contract_signature: Vertragsunterschrift Bild + cors_origins: CORS Origins creditor_address: Anschrift auf Rechnungen currency: Währung default_payment_info_type: Standartzahlungsteil esr_beneficiary_account: ESR Teilnehmernummer / PC-Konto esr_ref_prefix: Referenznummer Präfix iban: IBAN - invoice_ref_tempate: Rechnung Referenznummer Format + invoice_payment_ref_template: Zahlungsreferenz Format + invoice_ref_template: Rechnungsnummer Format locale: Hauptsprache locales: Sprachen location: Vertragsunterschrift Ort @@ -308,6 +310,7 @@ de: slug: Namespace smtp_settings: Mailserver Einstellungen smtp_settings_json: Mailserver Einstellungen + tenant_ref_template: Mieterreferenz Format terms_pdf: AGB organisation_user: email: Email diff --git a/config/locales/fr.yml b/config/locales/fr.yml index f8ccc2d1b..a7f18e2d7 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -283,7 +283,7 @@ fr: account_address: address: Adresse bcc: BCC - booking_ref_tempate: + booking_ref_template: contract_signature: Image de signature de contrat creditor_address: Adresse sur les factures currency: Währung @@ -291,7 +291,7 @@ fr: esr_beneficiary_account: Numéro de participant BVR / compte postal esr_ref_prefix: Numéro d'identification BVR / Préfixe iban: IBAN - invoice_ref_tempate: + invoice_ref_template: locale: Langue locales: Langues location: Lieu de signature du contrat diff --git a/config/locales/it.yml b/config/locales/it.yml index 30979da65..ec356336b 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -283,7 +283,7 @@ it: account_address: address: Indirizzo bcc: BCC - booking_ref_tempate: + booking_ref_template: contract_signature: Immagine della firma del contratto creditor_address: Indirizzo sulle fatture currency: Valuta @@ -291,7 +291,7 @@ it: esr_beneficiary_account: esr_ref_prefix: Numero di identificazione del PVR / prefisso iban: IBAN - invoice_ref_tempate: + invoice_ref_template: locale: Lingua locales: location: Luogo della firma del contratto diff --git a/db/migrate/20241209160641_add_key_sequence_number_to_invoices.rb b/db/migrate/20241209160641_add_key_sequence_number_to_invoices.rb deleted file mode 100644 index 644414321..000000000 --- a/db/migrate/20241209160641_add_key_sequence_number_to_invoices.rb +++ /dev/null @@ -1,6 +0,0 @@ -class AddKeySequenceNumberToInvoices < ActiveRecord::Migration[8.0] - def change - add_column :invoices, :sequence_number, :integer, null: true - add_column :invoices, :sequence_year, :integer, null: true - end -end diff --git a/db/migrate/20241209160641_add_key_sequence_numbers.rb b/db/migrate/20241209160641_add_key_sequence_numbers.rb new file mode 100644 index 000000000..3f1a05133 --- /dev/null +++ b/db/migrate/20241209160641_add_key_sequence_numbers.rb @@ -0,0 +1,9 @@ +class AddKeySequenceNumbers < ActiveRecord::Migration[8.0] + def change + add_column :invoices, :sequence_number, :integer, null: true + add_column :invoices, :sequence_year, :integer, null: true + add_column :bookings, :sequence_number, :integer, null: true + add_column :bookings, :sequence_year, :integer, null: true + add_column :tenants, :sequence_number, :integer, null: true + end +end diff --git a/db/migrate/20241212140043_add_default_ref_templates_to_organisations.rb b/db/migrate/20241212140043_add_default_ref_templates_to_organisations.rb index 633ec4a3d..573b0c946 100644 --- a/db/migrate/20241212140043_add_default_ref_templates_to_organisations.rb +++ b/db/migrate/20241212140043_add_default_ref_templates_to_organisations.rb @@ -1,10 +1,13 @@ class AddDefaultRefTemplatesToOrganisations < ActiveRecord::Migration[8.0] def up Organisation.find_each do |organisation| - organisation.booking_ref_template ||= RefBuilders::Booking::DEFAULT_TEMPLATE - organisation.tenant_ref_template ||= RefBuilders::Tenant::DEFAULT_TEMPLATE - organisation.invoice_ref_template ||= RefBuilders::Invoice::DEFAULT_TEMPLATE - organisation.invoice_payment_ref_template ||= RefBuilders::InvoicePayment::DEFAULT_TEMPLATE + organisation.instance_eval do + self.booking_ref_template = RefBuilders::Booking::DEFAULT_TEMPLATE if booking_ref_template.blank? + self.tenant_ref_template = RefBuilders::Tenant::DEFAULT_TEMPLATE if tenant_ref_template.blank? + self.invoice_ref_template = RefBuilders::Invoice::DEFAULT_TEMPLATE if invoice_ref_template.blank? + self.invoice_payment_ref_template = RefBuilders::InvoicePayment::DEFAULT_TEMPLATE if invoice_payment_ref_template.blank? + save! + end end end end diff --git a/db/migrate/20241212150434_rename_and_add_refs.rb b/db/migrate/20241212150434_rename_and_add_refs.rb index f82595626..a44a5c7f7 100644 --- a/db/migrate/20241212150434_rename_and_add_refs.rb +++ b/db/migrate/20241212150434_rename_and_add_refs.rb @@ -3,33 +3,5 @@ def change rename_column :invoices, :ref, :payment_ref add_column :invoices, :ref, :string, null: true add_column :tenants, :ref, :string, null: true - - reversible do |direction| - direction.up do - Organisation.find_each do |organisation| - backfill_invoices(organisation) - backfill_tenants(organisation) - end - end - end - end - - def backfill_invoices(organisation) - organisation.invoices.order(created_at: :ASC).each do |invoice| - invoice.sequence_number - invoice.generate_ref(force: true); - # this will overwrite the payment ref and mess up all payments! - # invoice.generate_payment_ref(force: true); - invoice.skip_generate_pdf = true - invoice.save! - end - end - - def backfill_tenants(organisation) - organisation.tenants.order(created_at: :ASC).each do |tenant| - tenant.sequence_number - tenant.generate_ref(force: true) - tenant.save! - end end end diff --git a/db/schema.rb b/db/schema.rb index 6bedb48e6..74052b383 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -223,6 +223,8 @@ t.integer "home_id", null: false t.boolean "ignore_conflicting", default: false, null: false t.jsonb "booking_questions" + t.integer "sequence_number" + t.integer "sequence_year" t.index ["booking_state_cache"], name: "index_bookings_on_booking_state_cache" t.index ["locale"], name: "index_bookings_on_locale" t.index ["organisation_id"], name: "index_bookings_on_organisation_id" @@ -593,6 +595,7 @@ t.boolean "bookings_without_invoice", default: false t.integer "salutation_form" t.string "accounting_account_nr" + t.integer "sequence_number" t.string "ref" t.index ["email", "organisation_id"], name: "index_tenants_on_email_and_organisation_id", unique: true t.index ["email"], name: "index_tenants_on_email" diff --git a/spec/factories/organisations.rb b/spec/factories/organisations.rb index 7c308f5c0..a3adced5d 100644 --- a/spec/factories/organisations.rb +++ b/spec/factories/organisations.rb @@ -52,6 +52,10 @@ location { nil } locale { I18n.locale } currency { 'CHF' } + booking_ref_template { RefBuilders::Booking::DEFAULT_TEMPLATE } + tenant_ref_template { RefBuilders::Tenant::DEFAULT_TEMPLATE } + invoice_ref_template { RefBuilders::Invoice::DEFAULT_TEMPLATE } + invoice_payment_ref_template { RefBuilders::InvoicePayment::DEFAULT_TEMPLATE } after(:build) do |organisation, _evaluator| build(:booking_category, key: :camp, title: 'Lager', organisation:) diff --git a/spec/services/ref_builders/booking_spec.rb b/spec/services/ref_builders/booking_spec.rb index ee95fae69..3c382ad70 100644 --- a/spec/services/ref_builders/booking_spec.rb +++ b/spec/services/ref_builders/booking_spec.rb @@ -7,23 +7,36 @@ let(:begins_at) { DateTime.new(2030, 10, 15, 14) } let(:ends_at) { begins_at + 2.hours } let(:home) { create(:home, ref: 'P', organisation:) } - let(:booking) { create(:booking, organisation:, begins_at:, ends_at:, home:) } + let(:booking) do + create(:booking, organisation:, begins_at:, ends_at:, home:, sequence_number: 420, sequence_year: 2024) + end subject(:ref_builder) { described_class.new(booking) } describe '#generate' do subject(:generate) { ref_builder.generate(template) } - - let(:template) { nil } + let(:template) { described_class::DEFAULT_TEMPLATE } context 'with default template' do it { is_expected.to eq('P20301015') } end - context 'with special template' do + context 'with no template' do + let(:template) { nil } + it { is_expected.to be_nil } + end + + context 'with date ref_parts' do + before { create(:booking, organisation:, begins_at:, ends_at:) } + let(:template) { 'X%04d%02d-%02d%s' } + + it { is_expected.to eq('X203010-15a') } + end + + context 'with other ref_parts' do before { create(:booking, organisation:, begins_at:, ends_at:) } - let(:template) { 'X%04d%02d-%s' } + let(:template) { '%d-%04d' } - it { is_expected.to eq('X203010-a') } + it { is_expected.to eq('2024-0420') } end context 'with default template and multiple bookings' do @@ -35,7 +48,7 @@ it { is_expected.to eq('P20301015a') } end - describe '#occupiable_refs' do + describe 'with occupiable ref_parts' do let(:template) { '%s-%04d%02d' } let(:occupiables) do %w[A B C].map do |ref| diff --git a/spec/services/ref_builders/invoice_payment_spec.rb b/spec/services/ref_builders/invoice_payment_spec.rb new file mode 100644 index 000000000..1e4d192a5 --- /dev/null +++ b/spec/services/ref_builders/invoice_payment_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe RefBuilders::InvoicePayment, type: :model do + let(:organisation) { create(:organisation, esr_ref_prefix: 9999) } + let(:invoice) { create(:invoice, organisation:, sequence_number: 386, sequence_year: 2023) } + + subject(:ref_builder) { described_class.new(invoice) } + + describe '::checksum' do + it 'calculates the checksum' do + expect(described_class.checksum('00000001000014000000000001')).to eq(8) + expect(described_class.checksum('00100000007000000000000133')).to eq(0) + end + end + + describe '::DEFAULT_TEPMPLATE' do + it do + expect(described_class::DEFAULT_TEMPLATE) + .to eq('%s%06d%04d%05d') + end + end + + describe '#generate' do + subject(:generate) { ref_builder.generate(template) } + + context 'with default template' do + let(:template) { described_class::DEFAULT_TEMPLATE } + it { is_expected.to eq('9999000001202300386') } + end + + context 'with no template' do + let(:template) { nil } + it { is_expected.to be_nil } + end + + context 'with sequence_numnber ref_parts' do + let(:template) { '%d-%d-%05d' } + it { is_expected.to eq('23-2023-00386') } + end + end +end diff --git a/spec/services/ref_builders/payment_info_spec.rb b/spec/services/ref_builders/payment_info_spec.rb deleted file mode 100644 index b867999e4..000000000 --- a/spec/services/ref_builders/payment_info_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe RefBuilders::InvoicePayment, type: :model do - let(:invoice) { create(:invoice) } - subject(:ref_service) { described_class.new(invoice) } - - describe '::checksum' do - it 'calculates the checksum' do - expect(described_class.checksum('00000001000014000000000001')).to eq(8) - expect(described_class.checksum('00100000007000000000000133')).to eq(0) - end - end -end From c76062ff50a6972a048e8a9f2ab3e6a7630818bd Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Thu, 12 Dec 2024 18:49:10 +0000 Subject: [PATCH 10/30] fix: more specs --- app/services/ref_builders/invoice.rb | 2 +- app/services/ref_builders/invoice_payment.rb | 1 + app/views/manage/invoices/_form.html.slim | 2 +- spec/services/ref_builders/invoice_spec.rb | 31 ++++++++++++++++++++ spec/services/ref_builders/tenant_spec.rb | 31 ++++++++++++++++++++ 5 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 spec/services/ref_builders/invoice_spec.rb create mode 100644 spec/services/ref_builders/tenant_spec.rb diff --git a/app/services/ref_builders/invoice.rb b/app/services/ref_builders/invoice.rb index ba53399d9..d5e1bc807 100644 --- a/app/services/ref_builders/invoice.rb +++ b/app/services/ref_builders/invoice.rb @@ -2,7 +2,7 @@ module RefBuilders class Invoice < RefBuilder - DEFAULT_TEMPLATE = '%2d%04d' + DEFAULT_TEMPLATE = '%02d%04d' def initialize(invoice) super(invoice.organisation) diff --git a/app/services/ref_builders/invoice_payment.rb b/app/services/ref_builders/invoice_payment.rb index 3beb559ea..3f754f555 100644 --- a/app/services/ref_builders/invoice_payment.rb +++ b/app/services/ref_builders/invoice_payment.rb @@ -13,6 +13,7 @@ def generate(template_string = @organisation.invoice_payment_ref_template) generate_lazy(template_string) end + # TODO: remove ids ref_part home_id: proc { @invoice.booking.home_id }, tenant_id: proc { @invoice.booking.tenant.id }, tenant_sequence_number: proc { @invoice.booking.tenant.sequence_number }, diff --git a/app/views/manage/invoices/_form.html.slim b/app/views/manage/invoices/_form.html.slim index 196a6ee29..0c5112071 100644 --- a/app/views/manage/invoices/_form.html.slim +++ b/app/views/manage/invoices/_form.html.slim @@ -15,7 +15,7 @@ = f.text_area :text, class: 'rich-text-area' = f.date_field :issued_at, lang: I18n.locale, help: t('optional') - / = f.date_field :payable_until, lang: I18n.locale + = f.date_field :payable_until, lang: I18n.locale / = f.text_field :respite_days, inputmode: "numeric" = f.select :payment_info_type, subtype_options_for_select(PaymentInfo.subtypes), include_blank: true diff --git a/spec/services/ref_builders/invoice_spec.rb b/spec/services/ref_builders/invoice_spec.rb new file mode 100644 index 000000000..5098bf93e --- /dev/null +++ b/spec/services/ref_builders/invoice_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe RefBuilders::Invoice, type: :model do + let(:organisation) { create(:organisation, esr_ref_prefix: 9999) } + let(:invoice) { create(:invoice, organisation:, sequence_number: 386, sequence_year: 2023) } + + subject(:ref_builder) { described_class.new(invoice) } + + describe '::DEFAULT_TEPMPLATE' do + it do + expect(described_class::DEFAULT_TEMPLATE) + .to eq('%02d%04d') + end + end + + describe '#generate' do + subject(:generate) { ref_builder.generate(template) } + + context 'with default template' do + let(:template) { described_class::DEFAULT_TEMPLATE } + it { is_expected.to eq('230386') } + end + + context 'with no template' do + let(:template) { nil } + it { is_expected.to be_nil } + end + end +end diff --git a/spec/services/ref_builders/tenant_spec.rb b/spec/services/ref_builders/tenant_spec.rb new file mode 100644 index 000000000..732ff2eb2 --- /dev/null +++ b/spec/services/ref_builders/tenant_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe RefBuilders::Tenant, type: :model do + let(:organisation) { create(:organisation) } + let(:invoice) { create(:tenant, organisation:, sequence_number: 386) } + + subject(:ref_builder) { described_class.new(invoice) } + + describe '::DEFAULT_TEPMPLATE' do + it do + expect(described_class::DEFAULT_TEMPLATE) + .to eq('%d') + end + end + + describe '#generate' do + subject(:generate) { ref_builder.generate(template) } + + context 'with default template' do + let(:template) { described_class::DEFAULT_TEMPLATE } + it { is_expected.to eq('386') } + end + + context 'with no template' do + let(:template) { nil } + it { is_expected.to be_nil } + end + end +end From deb7d9fd3a4b87ff8c5130e31a6d4b2d27391b57 Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Thu, 12 Dec 2024 22:56:16 +0000 Subject: [PATCH 11/30] feat: journal entries as records --- .../ColumnsConfigForm.tsx | 9 +- app/models/accounting.rb | 49 ----- app/models/accounting_settings.rb | 1 + app/models/data_digest_templates/invoice.rb | 3 +- .../data_digest_templates/invoice_part.rb | 2 +- ...ting_journal_entry.rb => journal_entry.rb} | 54 ++---- app/models/data_digest_templates/payment.rb | 2 +- app/models/data_digest_templates/tenant.rb | 2 +- app/models/invoice.rb | 23 +-- app/models/invoice_part.rb | 4 - app/models/invoice_parts/add.rb | 11 -- app/models/journal_entry.rb | 182 ++++++++++++++++++ .../manage/journal_entry_serializer.rb | 12 +- app/services/taf_block.rb | 26 +-- app/services/template_context.rb | 2 +- .../_form_fields.html.slim | 0 .../_show_data_digest.html.slim | 0 .../filter/_filter_fields.html.slim | 4 + config/locales/de.yml | 19 ++ .../20241212191319_create_journal_entries.rb | 20 ++ db/schema.rb | 23 ++- spec/factories/journal_entries.rb | 45 +++++ spec/models/journal_entry_spec.rb | 34 ++++ spec/services/taf_block_spec.rb | 27 +-- 24 files changed, 400 insertions(+), 154 deletions(-) delete mode 100644 app/models/accounting.rb rename app/models/data_digest_templates/{accounting_journal_entry.rb => journal_entry.rb} (54%) create mode 100644 app/models/journal_entry.rb rename app/views/renderables/data_digest_templates/{accounting_journal_entry => journal_entry}/_form_fields.html.slim (100%) rename app/views/renderables/data_digest_templates/{accounting_journal_entry => journal_entry}/_show_data_digest.html.slim (100%) create mode 100644 app/views/renderables/journal_entry/filter/_filter_fields.html.slim create mode 100644 db/migrate/20241212191319_create_journal_entries.rb create mode 100644 spec/factories/journal_entries.rb create mode 100644 spec/models/journal_entry_spec.rb diff --git a/app/javascript/components/data_digest_templates/ColumnsConfigForm.tsx b/app/javascript/components/data_digest_templates/ColumnsConfigForm.tsx index 72f1cca85..8a6ae9525 100644 --- a/app/javascript/components/data_digest_templates/ColumnsConfigForm.tsx +++ b/app/javascript/components/data_digest_templates/ColumnsConfigForm.tsx @@ -18,7 +18,6 @@ type ColumnsConfigFormProps = { type BaseColumnConfig = { id: string; - index: number; header: string; body: string; type: ColumnConfigType; @@ -44,7 +43,7 @@ function toJson(columnsConfigs: ColumnConfig[]): string { type ColumnConfig = BaseColumnConfig | UsageColumnConfig | BookingQuestionResponseColumnConfig; -export default function ColumnsConfigFrom({ json, name }: ColumnsConfigFormProps) { +export default function ColumnsConfigForm({ json, name }: ColumnsConfigFormProps) { const { t } = useTranslation(); const [columnsConfig, setColumnsConfig] = useState(() => (JSON.parse(json) as unknown as ColumnConfig[]).map((data) => ({ @@ -55,13 +54,13 @@ export default function ColumnsConfigFrom({ json, name }: ColumnsConfigFormProps const handleUpdate = (updatedConfig: ColumnConfig) => setColumnsConfig((prev) => - prev.map((prevConfig: ColumnConfig) => (prevConfig.index == updatedConfig.index ? updatedConfig : prevConfig)), + prev.map((prevConfig: ColumnConfig) => (prevConfig.id == updatedConfig.id ? updatedConfig : prevConfig)), ); const handleRemove = (removedConfig: ColumnConfig) => - setColumnsConfig((prev) => prev.filter((prevConfig) => prevConfig.index != removedConfig.index)); + setColumnsConfig((prev) => prev.filter((prevConfig) => prevConfig.id != removedConfig.id)); const handleAdd = (type: string) => isColumnConfigType(type) && - setColumnsConfig((prev) => [...prev, { index: prev.length, type, body: "", header: "", id: crypto.randomUUID() }]); + setColumnsConfig((prev) => [...prev, { type, body: "", header: "", id: crypto.randomUUID() }]); return ( diff --git a/app/models/accounting.rb b/app/models/accounting.rb deleted file mode 100644 index 37770efca..000000000 --- a/app/models/accounting.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true - -module Accounting - JournalEntry = Data.define(:id, :account, :date, :tax_code, :text, :amount, :side, :cost_center, - :index, :amount_type, :source, :reference, :currency, :booking) do - extend ActiveModel::Translation - extend ActiveModel::Naming - - def initialize(**args) - args.symbolize_keys! - defaults = { id: nil, index: nil, tax_code: nil, text: nil, cost_center: nil, source: nil } - side = args.delete(:side) if %i[soll haben].include?(args[:side]) - date = args.delete(:date)&.then { _1.try(:to_date) || Date.parse(_1).to_date } - super(**defaults, **args, side:, date:) - end - - def soll? - side == :soll - end - - def haben? - side == :haben - end - - def soll_account - account if soll? - end - - def haben_account - account if haben? - end - - def valid? - (soll_account.present? || haben_account.present?) && amount.present? - end - - def to_s - [ - (id || index).presence&.then { "[#{_1}]" }, - soll_account, - '->', - haben_account, - ActiveSupport::NumberHelper.number_to_currency(amount, unit: currency), - ':', - text - ].compact.join(' ') - end - end -end diff --git a/app/models/accounting_settings.rb b/app/models/accounting_settings.rb index 65f261d32..fafc724b4 100644 --- a/app/models/accounting_settings.rb +++ b/app/models/accounting_settings.rb @@ -4,4 +4,5 @@ class AccountingSettings < Settings attribute :tenant_debitor_account_nr_base, :integer, default: -> { 0 } attribute :debitor_account_nr, :string attribute :currency_account_nr, :string + attribute :default_payment_account_nr, :string end diff --git a/app/models/data_digest_templates/invoice.rb b/app/models/data_digest_templates/invoice.rb index feacfce6c..5eeea5cdf 100644 --- a/app/models/data_digest_templates/invoice.rb +++ b/app/models/data_digest_templates/invoice.rb @@ -76,7 +76,8 @@ def filter_class end def base_scope - @base_scope ||= ::Invoice.joins(:booking).where(bookings: { organisation_id: organisation }).kept + @base_scope ||= ::Invoice.joins(:booking).where(bookings: { organisation_id: organisation }) + .includes(booking: :organisation).order(issued_at: :ASC).kept end end end diff --git a/app/models/data_digest_templates/invoice_part.rb b/app/models/data_digest_templates/invoice_part.rb index e65a383ed..5aac493bc 100644 --- a/app/models/data_digest_templates/invoice_part.rb +++ b/app/models/data_digest_templates/invoice_part.rb @@ -25,7 +25,7 @@ module DataDigestTemplates class InvoicePart < Tabular - ::DataDigestTemplate.register_subtype self + # ::DataDigestTemplate.register_subtype self DEFAULT_COLUMN_CONFIG = [ { diff --git a/app/models/data_digest_templates/accounting_journal_entry.rb b/app/models/data_digest_templates/journal_entry.rb similarity index 54% rename from app/models/data_digest_templates/accounting_journal_entry.rb rename to app/models/data_digest_templates/journal_entry.rb index b81ba4110..9f66f6920 100644 --- a/app/models/data_digest_templates/accounting_journal_entry.rb +++ b/app/models/data_digest_templates/journal_entry.rb @@ -24,44 +24,44 @@ # module DataDigestTemplates - class AccountingJournalEntry < Tabular + class JournalEntry < Tabular ::DataDigestTemplate.register_subtype self DEFAULT_COLUMN_CONFIG = [ { - header: ::Accounting::JournalEntry.human_attribute_name(:date), + header: ::JournalEntry.human_attribute_name(:date), body: '{{ journal_entry.date | date_format }}' }, { - header: ::Accounting::JournalEntry.human_attribute_name(:reference), - body: '{{ journal_entry.reference }}' + header: ::JournalEntry.human_attribute_name(:source_document_ref), + body: '{{ journal_entry.source_document_ref }}' }, { - header: ::Accounting::JournalEntry.human_attribute_name(:text), + header: ::JournalEntry.human_attribute_name(:text), body: '{{ journal_entry.text }}' }, { - header: ::Accounting::JournalEntry.human_attribute_name(:soll_account), + header: ::JournalEntry.human_attribute_name(:soll_account), body: '{{ journal_entry.soll_account }}' }, { - header: ::Accounting::JournalEntry.human_attribute_name(:haben_account), + header: ::JournalEntry.human_attribute_name(:haben_account), body: '{{ journal_entry.haben_account }}' }, { - header: ::Accounting::JournalEntry.human_attribute_name(:amount), + header: ::JournalEntry.human_attribute_name(:amount), body: '{{ journal_entry.amount | round: 2 }}' }, { - header: ::Accounting::JournalEntry.human_attribute_name(:tax_code), + header: ::JournalEntry.human_attribute_name(:tax_code), body: '{{ journal_entry.tax_code }}' }, { - header: ::Accounting::JournalEntry.human_attribute_name(:cost_center), + header: ::JournalEntry.human_attribute_name(:cost_center), body: '{{ journal_entry.cost_center }}' }, { - header: ::Accounting::JournalEntry.model_name.human, + header: ::JournalEntry.model_name.human, body: '{{ journal_entry.to_s }}' }, { @@ -79,39 +79,25 @@ class AccountingJournalEntry < Tabular end end - def records(period) - invoice_filter = ::Invoice::Filter.new(issued_at_after: period&.begin, issued_at_before: period&.end) - invoices = invoice_filter.apply(::Invoice.joins(:booking).where(bookings: { organisation: organisation }).kept) - invoices.index_with(&:journal_entries) - end - - def crunch(records) - records.values.flatten.compact.map do |record| - template_context_cache = {} - columns.map { |column| column.body(record, template_context_cache) } - end - end - formatter(:taf) do |_options = {}| - records.keys.map do |source| + records.map do |source| TafBlock::Collection.new { derive(source) } end.join("\n\n") end - protected - def periodfilter(period = nil) - raise NotImplementedError - # filter_class.new(issued_at_after: period&.begin, issued_at_before: period&.end) + filter_class.new(date_after: period&.begin, date_before: period&.end) end - def base_scope - raise NotImplementedError - # @base_scope ||= ::Invoice.joins(:booking).where(bookings: { organisation_id: organisation }).kept + def filter_class + ::JournalEntry::Filter end - def prefilter - raise NotImplementedError + protected + + def base_scope + @base_scope ||= ::JournalEntry.joins(:booking).where(bookings: { organisation_id: organisation }) + .includes(booking: :organisation).order(date: :ASC) end end end diff --git a/app/models/data_digest_templates/payment.rb b/app/models/data_digest_templates/payment.rb index 347dce76e..7dbe3a392 100644 --- a/app/models/data_digest_templates/payment.rb +++ b/app/models/data_digest_templates/payment.rb @@ -76,7 +76,7 @@ def record_order end def base_scope - @base_scope ||= organisation.payments + @base_scope ||= organisation.payments.includes(:invoice, booking: :organisation).order(paid_at: :ASC) end end end diff --git a/app/models/data_digest_templates/tenant.rb b/app/models/data_digest_templates/tenant.rb index 65bef1f8d..04736e2bc 100644 --- a/app/models/data_digest_templates/tenant.rb +++ b/app/models/data_digest_templates/tenant.rb @@ -82,7 +82,7 @@ class Tenant < Tabular end def base_scope - @base_scope ||= organisation.tenants.ordered + @base_scope ||= organisation.tenants.ordered.includes(:organisation) end end end diff --git a/app/models/invoice.rb b/app/models/invoice.rb index f919e3c05..398182adb 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -49,6 +49,7 @@ class Invoice < ApplicationRecord has_many :superseded_by_invoices, class_name: :Invoice, dependent: :nullify, foreign_key: :supersede_invoice_id, inverse_of: :supersede_invoice has_many :payments, dependent: :nullify + has_many :journal_entries, as: :source, dependent: :destroy has_one :organisation, through: :booking has_one_attached :pdf @@ -72,6 +73,8 @@ class Invoice < ApplicationRecord after_create :supersede! before_update :generate_pdf, if: :generate_pdf? after_save :recalculate! + after_save :generate_journal_entries + after_discard :generate_journal_entries delegate :currency, to: :organisation @@ -110,11 +113,16 @@ def generate_ref(force: false) self.ref = RefBuilders::Invoice.new(self).generate if ref.blank? || force end - # this should never be forced def generate_payment_ref + # this should never be forced self.payment_ref = RefBuilders::InvoicePayment.new(self).generate if payment_ref.blank? end + def generate_journal_entries + # GenerateJournalEntriesJob.perform_later(self) + JournalEntry.generate(self) + end + def paid? refund? || amount_open.zero? end @@ -187,17 +195,4 @@ def to_attachable def vat_amounts invoice_parts.group_by(&:vat_category).except(nil).transform_values { _1.sum(&:calculated_amount) } end - - def journal_entries - [debitor_journal_entry] + invoice_parts.map(&:journal_entries) - end - - def debitor_journal_entry - Accounting::JournalEntry.new( - account: organisation.accounting_settings.debitor_account_nr, - date: issued_at, amount:, amount_type: :brutto, side: :soll, - reference: ref, source: self, currency:, booking:, - text: "#{self.class.model_name.human} #{ref} - #{booking.tenant.last_name}" - ) - end end diff --git a/app/models/invoice_part.rb b/app/models/invoice_part.rb index f834f9d93..e85a2e08d 100644 --- a/app/models/invoice_part.rb +++ b/app/models/invoice_part.rb @@ -66,10 +66,6 @@ def to_sum(sum) sum + calculated_amount end - def journal_entries - nil - end - def self.from_usage(usage, **attributes) return unless usage diff --git a/app/models/invoice_parts/add.rb b/app/models/invoice_parts/add.rb index ac9ab78db..dc62dedf8 100644 --- a/app/models/invoice_parts/add.rb +++ b/app/models/invoice_parts/add.rb @@ -30,16 +30,5 @@ class Add < InvoicePart def calculated_amount amount end - - def journal_entries # rubocop:disable Metrics/AbcSize - [ - Accounting::JournalEntry.new( - account: tarif&.accounting_account_nr, date: invoice.issued_at, amount: amount.abs, amount_type: :brutto, - side: :haben, tax_code: vat_category&.accounting_vat_code, reference: invoice.ref, source: self, - currency: organisation.currency, booking:, cost_center: tarif&.accounting_profit_center_nr, - text: "#{invoice.class.model_name.human} #{invoice.ref} #{label}" - ) - ] - end end end diff --git a/app/models/journal_entry.rb b/app/models/journal_entry.rb new file mode 100644 index 000000000..55cfe931d --- /dev/null +++ b/app/models/journal_entry.rb @@ -0,0 +1,182 @@ +# == Schema Information +# +# Table name: journal_entries +# +# id :integer not null, primary key +# booking_id :uuid not null +# source_type :string not null +# source_id :integer not null +# account_nr :string not null +# vat_category_id :integer +# date :date not null +# amount :decimal(, ) not null +# side :integer not null +# currency :string not null +# text :string +# ordinal :integer +# source_document_ref :string +# cost_center :string +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_journal_entries_on_source (source_type,source_id) +# index_journal_entries_on_vat_category_id (vat_category_id) +# + +# frozen_string_literal: true + +class JournalEntry < ApplicationRecord + belongs_to :booking + belongs_to :source, polymorphic: true + belongs_to :vat_category, optional: true + + has_one :organisation, through: :booking + + enum :side, { soll: 1, haben: -1 } + + validates :account_nr, :side, :amount, :source_document_ref, presence: true + + def invert + return self.side = :soll if haben? + return self.side = :haben if soll? + + nil + end + + def soll_account + account_nr if soll? + end + + def haben_account + account_nr if haben? + end + + def soll_amount + amount if soll? + end + + def haben_amount + amount if haben? + end + + def self.balanced?(journal_entries) + journal_entries.map { _1.soll_amount || 0 }.sum == journal_entries.map { _1.haben_amount || 0 }.sum + end + + def to_s + [ + (id || index).presence&.then { "[#{_1}]" }, + soll_account, + '->', + haben_account, + ActiveSupport::NumberHelper.number_to_currency(amount, unit: currency), + ':', + text + ].compact.join(' ') + end + + def self.generate(source, booking: source.booking) + existing_journal_entry_ids = source.reload.journal_entry_ids + new_journal_entries = Array.wrap(JournalEntry::Factory.new.source(source)).compact + + # raise ActiveRecord::Rollback unless + new_journal_entries.all?(&:valid?) && balanced?(new_journal_entries) && + new_journal_entries.all?(&:save) && + where(id: existing_journal_entry_ids, booking:).destroy_all + end + + class Filter < ApplicationFilter + attribute :date_after, :date + attribute :date_before, :date + + filter :date do |journal_entries| + next unless date_before.present? || date_after.present? + + journal_entries.where(JournalEntry.arel_table[:date].between(date_after..date_before)) + end + end + + class Factory + def source(source) + case source + when Invoice + source.kept? ? kept_invoice(source) : discarded_invoice(source) + when Payment + source.kept? ? kept_payment(source) : discarded_payment(source) + end + end + + def kept_invoice(invoice) + return unless invoice.is_a?(Invoices::Deposit) || invoice.is_a?(Invoices::Invoice) + + [invoice_debitor(invoice)] + invoice.invoice_parts.map { invoice_part(_1) }.compact + end + + def discarded_invoice(invoice) + previous_journal_entries = kept_invoice(invoice) + return if previous_journal_entries.blank? + + previous_journal_entries + kept_invoice(invoice).each do |journal_entry| + journal_entry.invert + journal_entry.date = invoice.discarded_at.to_date + end + end + + def invoice_debitor(invoice) + invoice.instance_eval do + account_nr = organisation.accounting_settings.debitor_account_nr + return if account_nr.blank? + + JournalEntry.new(account_nr:, date: issued_at, side: :soll, amount:, source_document_ref: ref, + source: self, currency:, booking:, + text: "#{ref} - #{booking.tenant.last_name}") + end + end + + def invoice_part(invoice_part) + case invoice_part + when InvoiceParts::Add + invoice_part_add(invoice_part) + when InvoiceParts::Deposit + invoice_part_deposit(invoice_part) + end + end + + def invoice_part_add(invoice_part) # rubocop:disable Metrics/AbcSize + invoice_part.instance_eval do + account_nr = tarif&.accounting_account_nr + return if account_nr.blank? + + JournalEntry.new(account_nr:, date: invoice.issued_at, side: :haben, + amount:, vat_category:, source_document_ref: invoice.ref, source: invoice, + currency: organisation.currency, booking:, cost_center: tarif&.accounting_profit_center_nr, + text: "#{invoice.ref} #{label}") + end + end + + def invoice_part_deposit(invoice_part) # rubocop:disable Metrics/AbcSize + invoice_part.instance_eval do + account_nr = tarif&.accounting_account_nr + return if account_nr.blank? + + JournalEntry.new(account_nr:, date: invoice.issued_at, side: :soll, + amount:, source_document_ref: invoice.ref, source: invoice, + currency: organisation.currency, booking:, cost_center: tarif&.accounting_profit_center_nr, + text: "#{invoice.ref} #{label}") + end + end + + def kept_payment(payment) # rubocop:disable Metrics/AbcSize + payment.instance_eval do + account_nr = organisation.accounting_settings.payment_account_nr + return if account_nr.blank? + + JournalEntry.new(account_nr:, date: paid_at, side: :soll, amount:, + source_document_ref: invoice.ref, source: self, currency: organisation.currency, booking:, + text: "#{invoice.ref} #{self.class.model_name.human}") + end + end + end +end diff --git a/app/serializers/manage/journal_entry_serializer.rb b/app/serializers/manage/journal_entry_serializer.rb index 5a86b72ce..2bac71c0a 100644 --- a/app/serializers/manage/journal_entry_serializer.rb +++ b/app/serializers/manage/journal_entry_serializer.rb @@ -2,12 +2,8 @@ module Manage class JournalEntrySerializer < ApplicationSerializer - fields :id, :account, :date, :tax_code, :text, :amount, :side, :cost_center, - :index, :amount_type, :reference, :currency, :to_s, :soll_account, :haben_account - - field :booking_id do |journal_entry| - journal_entry.booking&.id - end + fields :id, :account_nr, :date, :text, :amount, :side, :cost_center, :ordinal, :source_document_ref, :currency, + :soll_amount, :haben_amount, :soll_account, :haben_account field :source do |journal_entry| { @@ -15,5 +11,9 @@ class JournalEntrySerializer < ApplicationSerializer id: journal_entry.source&.id } end + + field :tax_code do |journal_entry| + journal_entry.vat_category&.accounting_vat_code + end end end diff --git a/app/services/taf_block.rb b/app/services/taf_block.rb index 142dd7167..7fd4e1487 100644 --- a/app/services/taf_block.rb +++ b/app/services/taf_block.rb @@ -109,19 +109,19 @@ def self.derive(value, **override) instance_exec(value, **override, &derive_block) if derive_block.present? end - derive_from Accounting::JournalEntry do |journal_entry, **override| + derive_from JournalEntry do |journal_entry, **override| new(:Bk, **{ # The Id of a book keeping account. [Fibu-Konto] - AccId: journal_entry.account, + AccId: Value.cast(journal_entry.account_nr, as: :symbol), # Integer; Booking type: 1=cost booking, 2=tax booking - BType: journal_entry.amount_type&.to_sym == :tax || 1, + BType: 1, # String[13], This is the cost type account CAcc: journal_entry.cost_center, # Integer; This is the index of the booking that represents the cost booking which is attached to this booking - CIdx: journal_entry.index, + # CIdx: journal_entry.index, # String[9]; A user definable code. Code: nil, @@ -138,9 +138,9 @@ def self.derive(value, **override) Flags: nil, # String[5]; The Id of the tax. [MWSt-Kürzel] - TaxId: journal_entry.tax_code, + TaxId: journal_entry.vat_category&.accounting_vat_code, - MkTxB: journal_entry.tax_code.present?, + MkTxB: journal_entry.vat_category&.accounting_vat_code.present?, # String[61*]; This string specifies the first line of the booking text. Text: journal_entry.text&.slice(0..59)&.lines&.first&.strip || '-', # rubocop:disable Style/SafeNavigationChainLength @@ -156,28 +156,28 @@ def self.derive(value, **override) # Integer; This is the index of the booking that represents the tax booking # which is attached to this booking. - TIdx: (journal_entry.amount_type&.to_sym == :tax && journal_entry.index) || nil, + # TIdx: (journal_entry.amount_type&.to_sym == :tax && journal_entry.index) || nil, # Boolean; Booking type. # 0 a debit booking [Soll] # 1 a credit booking [Haben] - Type: { soll: 0, haben: 1 }[journal_entry.side], + Type: { soll: 0, haben: 1 }[journal_entry.side&.to_sym], # Currency; The net amount for this booking. [Netto-Betrag] - ValNt: journal_entry.amount_type&.to_sym == :netto ? journal_entry.amount : nil, + # ValNt: journal_entry.amount_type&.to_sym == :netto ? journal_entry.amount : nil, # Currency; The tax amount for this booking. [Brutto-Betrag] - ValBt: journal_entry.amount_type&.to_sym == :brutto ? journal_entry.amount : nil, + ValBt: journal_entry.amount, # Currency; The tax amount for this booking. [Steuer-Betrag] - ValTx: journal_entry.amount_type&.to_sym == :tax ? journal_entry.amount : nil, + # ValTx: journal_entry.amount_type&.to_sym == :tax ? journal_entry.amount : nil, # Currency; The gross amount for this booking in the foreign currency specified # by currency of the account AccId. [FW-Betrag] # ValFW : not implemented # String[13]The OP id of this booking. - OpId: journal_entry.reference, + OpId: journal_entry.source_document_ref, # The PK number of this booking. PkKey: nil @@ -191,7 +191,7 @@ def self.derive(value, **override) pk_key = [invoice.booking.tenant.accounting_debitor_account_nr, invoice.organisation.accounting_settings.currency_account_nr].then { "[#{_1.join(',')}]" } - journal_entries = invoice.journal_entries.flatten.compact + journal_entries = invoice.journal_entries.to_a [ derive(invoice.booking.tenant), diff --git a/app/services/template_context.rb b/app/services/template_context.rb index 57c5fecd0..30f4079b7 100644 --- a/app/services/template_context.rb +++ b/app/services/template_context.rb @@ -8,7 +8,7 @@ class TemplateContext Payment => Manage::PaymentSerializer, Invoice => Manage::InvoiceSerializer, InvoicePart => Manage::InvoicePartSerializer, - Accounting::JournalEntry => Manage::JournalEntrySerializer, + JournalEntry => Manage::JournalEntrySerializer, Tenant => Manage::TenantSerializer, Usage => Manage::UsageSerializer, PaymentInfo => Manage::PaymentInfoSerializer, diff --git a/app/views/renderables/data_digest_templates/accounting_journal_entry/_form_fields.html.slim b/app/views/renderables/data_digest_templates/journal_entry/_form_fields.html.slim similarity index 100% rename from app/views/renderables/data_digest_templates/accounting_journal_entry/_form_fields.html.slim rename to app/views/renderables/data_digest_templates/journal_entry/_form_fields.html.slim diff --git a/app/views/renderables/data_digest_templates/accounting_journal_entry/_show_data_digest.html.slim b/app/views/renderables/data_digest_templates/journal_entry/_show_data_digest.html.slim similarity index 100% rename from app/views/renderables/data_digest_templates/accounting_journal_entry/_show_data_digest.html.slim rename to app/views/renderables/data_digest_templates/journal_entry/_show_data_digest.html.slim diff --git a/app/views/renderables/journal_entry/filter/_filter_fields.html.slim b/app/views/renderables/journal_entry/filter/_filter_fields.html.slim new file mode 100644 index 000000000..5f10b6481 --- /dev/null +++ b/app/views/renderables/journal_entry/filter/_filter_fields.html.slim @@ -0,0 +1,4 @@ +.row + .col + = f.date_field :date_before, include_blank: true + = f.date_field :date_after, include_blank: true diff --git a/config/locales/de.yml b/config/locales/de.yml index 3ff976d4f..adfd470d6 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -231,6 +231,18 @@ de: ordinal_position: Ordnungsnummer type: Art der Rechnungsposition vat: inkl. MwSt. in Prozent + journal_entry: + amount: Betrag + booking_id: Buchung + cost_center: Kostenstelle + date: Datum + haben_account: Haben Konto + haben_amount: Haben Betrag + side: Seite + soll_account: Soll Konto + soll_amount: Soll Betrag + source_document_ref: Beleg + text: Text mail_template: meter_reading_period: begins_at: Beginn @@ -479,6 +491,10 @@ de: paid: only_paid: Nur beglichene only_unpaid: Nur offene + journal_entry: + side: + haben: Haben + soll: Soll mail_template: attachable_booking_documents: contract: Vertrag @@ -627,6 +643,9 @@ de: data_digest_templates/invoice_part: one: Rechnungspositionen Auszug other: Rechnungspositionen Auszüge + data_digest_templates/journal_entry: + one: Buchungssätze Auszug + other: Buchungssätze Auszüge data_digest_templates/meter_reading_period: one: Zählerstand Auszug other: Zählerstände Auszüge diff --git a/db/migrate/20241212191319_create_journal_entries.rb b/db/migrate/20241212191319_create_journal_entries.rb new file mode 100644 index 000000000..f609f62ec --- /dev/null +++ b/db/migrate/20241212191319_create_journal_entries.rb @@ -0,0 +1,20 @@ +class CreateJournalEntries < ActiveRecord::Migration[8.0] + def change + create_table :journal_entries do |t| + t.uuid :booking_id, null: false + t.references :source, polymorphic: true, null: false + t.string :account_nr, null: false + t.references :vat_category, null: true, foreign_key: true + t.date :date, null: false + t.decimal :amount, null: false + t.integer :side, null: false + t.string :currency, null: false + t.string :text + t.integer :ordinal + t.string :source_document_ref + t.string :cost_center + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 74052b383..c8e0dfe8d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2024_12_12_150434) do +ActiveRecord::Schema[8.0].define(version: 2024_12_12_191319) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "pgcrypto" @@ -342,6 +342,26 @@ t.index ["type"], name: "index_invoices_on_type" end + create_table "journal_entries", force: :cascade do |t| + t.uuid "booking_id", null: false + t.string "source_type", null: false + t.bigint "source_id", null: false + t.string "account_nr", null: false + t.bigint "vat_category_id" + t.date "date", null: false + t.decimal "amount", null: false + t.integer "side", null: false + t.string "currency", null: false + t.string "text" + t.integer "ordinal" + t.string "source_document_ref" + t.string "cost_center" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["source_type", "source_id"], name: "index_journal_entries_on_source" + t.index ["vat_category_id"], name: "index_journal_entries_on_vat_category_id" + end + create_table "key_sequences", force: :cascade do |t| t.string "key", null: false t.bigint "organisation_id", null: false @@ -688,6 +708,7 @@ add_foreign_key "invoice_parts", "vat_categories" add_foreign_key "invoices", "bookings" add_foreign_key "invoices", "invoices", column: "supersede_invoice_id" + add_foreign_key "journal_entries", "vat_categories" add_foreign_key "key_sequences", "organisations" add_foreign_key "mail_template_designated_documents", "designated_documents" add_foreign_key "mail_template_designated_documents", "rich_text_templates", column: "mail_template_id" diff --git a/spec/factories/journal_entries.rb b/spec/factories/journal_entries.rb new file mode 100644 index 000000000..8acb0cd5a --- /dev/null +++ b/spec/factories/journal_entries.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: journal_entries +# +# id :integer not null, primary key +# booking_id :uuid not null +# source_type :string not null +# source_id :integer not null +# account_nr :string not null +# vat_category_id :integer +# date :date not null +# amount :decimal(, ) not null +# side :integer not null +# currency :string not null +# text :string +# ordinal :integer +# source_document_ref :string +# cost_center :string +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_journal_entries_on_source (source_type,source_id) +# index_journal_entries_on_vat_category_id (vat_category_id) +# + +FactoryBot.define do + factory :journal_entry do + booking { nil } + source { nil } + account_nr { 'MyString' } + date { '2024-12-12' } + vat_category { nil } + text { 'MyString' } + amount { '9.99' } + ordinal { 1 } + side { 1 } + source_document_ref { 'MyString' } + currency { 'MyString' } + cost_center { 'MyString' } + end +end diff --git a/spec/models/journal_entry_spec.rb b/spec/models/journal_entry_spec.rb new file mode 100644 index 000000000..3937f6924 --- /dev/null +++ b/spec/models/journal_entry_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: journal_entries +# +# id :integer not null, primary key +# booking_id :uuid not null +# source_type :string not null +# source_id :integer not null +# account_nr :string not null +# vat_category_id :integer +# date :date not null +# amount :decimal(, ) not null +# side :integer not null +# currency :string not null +# text :string +# ordinal :integer +# source_document_ref :string +# cost_center :string +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_journal_entries_on_source (source_type,source_id) +# index_journal_entries_on_vat_category_id (vat_category_id) +# + +require 'rails_helper' + +RSpec.describe JournalEntry, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/services/taf_block_spec.rb b/spec/services/taf_block_spec.rb index e3fd65a69..6ccf56e23 100644 --- a/spec/services/taf_block_spec.rb +++ b/spec/services/taf_block_spec.rb @@ -39,14 +39,17 @@ end context '::derive' do - let(:booking) { create(:booking) } - let(:currency) { booking.organisation.currency } - describe 'Accounting::JournalEntry' do + let(:organisation) { create(:organisation) } + let(:booking) { create(:booking, organisation:) } + let(:vat_category) { create(:vat_category, percentage: 3.8, accounting_vat_code: 'MwSt38', organisation:) } + let(:currency) { organisation.currency } + + describe 'JournalEntry' do subject(:taf_block) { described_class.derive(journal_entry) } let(:journal_entry) do - Accounting::JournalEntry.new(account: 1050, amount: 2091.75, date: Date.new(2024, 10, 5), reference: '1234', - amount_type: :netto, side: :soll, tax_code: 'MwSt38', booking:, currency:, - text: "Lorem ipsum\nSecond Line, but its longer than sixty \"chars\", OMG!") + JournalEntry.new(account_nr: 1050, amount: 2091.75, date: Date.new(2024, 10, 5), source_document_ref: '1234', + side: :soll, vat_category:, booking:, currency:, + text: "Lorem ipsum\nSecond Line, but its longer than sixty \"chars\", OMG!") end it 'builds correctly' do @@ -61,7 +64,7 @@ Text="Lorem ipsum" Text2="Second Line, but its longer than sixty ""chars"", " Type=0 - ValNt=2091.75 + ValBt=2091.75 OpId="1234" } @@ -69,12 +72,12 @@ end end - describe 'Accounting::JournalEntry' do + describe 'JournalEntry' do subject(:taf_block) { described_class.derive(journal_entry) } let(:journal_entry) do - Accounting::JournalEntry.new(account: 1050, amount: 2091.75, date: Date.new(2024, 10, 5), reference: '1234', - amount_type: :netto, side: :soll, tax_code: 'MwSt38', booking:, currency:, - text: "Lorem ipsum\nSecond Line, but its longer than sixty \"chars\", OMG!") + JournalEntry.new(account_nr: 1050, amount: 2091.75, date: Date.new(2024, 10, 5), source_document_ref: '1234', + side: :soll, vat_category:, booking:, currency:, + text: "Lorem ipsum\nSecond Line, but its longer than sixty \"chars\", OMG!") end it 'builds correctly' do @@ -89,7 +92,7 @@ Text="Lorem ipsum" Text2="Second Line, but its longer than sixty ""chars"", " Type=0 - ValNt=2091.75 + ValBt=2091.75 OpId="1234" } From e7ccde6b76b704cc8aa1f46a59b0d924ca9dfddd Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Mon, 16 Dec 2024 10:40:10 +0000 Subject: [PATCH 12/30] feat: journal_entries also for vat and cost_centers --- Gemfile | 2 +- Gemfile.lock | 162 ++++++------- app/concerns/subtypeable.rb | 4 +- app/models/accounting_settings.rb | 7 +- .../data_digest_templates/journal_entry.rb | 16 +- app/models/invoice.rb | 14 +- app/models/invoice_part.rb | 37 ++- app/models/invoice_part/factory.rb | 42 ++-- app/models/invoice_parts/add.rb | 24 +- app/models/invoice_parts/deposit.rb | 41 +++- app/models/invoice_parts/percentage.rb | 24 +- app/models/invoice_parts/text.rb | 24 +- app/models/invoices/deposit.rb | 4 +- app/models/journal_entry.rb | 213 +++++++++++------- app/models/vat_category.rb | 8 +- app/params/manage/invoice_part_params.rb | 3 +- app/params/manage/tarif_params.rb | 2 +- .../manage/journal_entry_serializer.rb | 15 +- app/services/export/pdf/invoice_pdf.rb | 3 +- .../invoice/invoice_parts_table.rb | 2 +- app/services/taf_block.rb | 25 +- app/services/template_context.rb | 1 + .../manage/invoice_parts/_form.html.slim | 4 + app/views/manage/tarifs/_form.html.slim | 2 +- .../manage/vat_categories/index.html.slim | 2 +- .../invoice_parts/add/_form_fields.html.slim | 2 + .../deposit/_form_fields.html.slim | 2 + .../percentage/_form_fields.html.slim | 2 + config/locales/de.yml | 9 +- .../20241212191319_create_journal_entries.rb | 17 +- ..._profit_center_to_cost_center_on_tarifs.rb | 5 + ...unting_nr_to_invoice_parts_and_payments.rb | 20 ++ db/schema.rb | 27 ++- spec/factories/invoice_parts.rb | 24 +- spec/factories/journal_entries.rb | 19 +- spec/models/invoice_part_spec.rb | 24 +- spec/models/journal_entry_spec.rb | 19 +- 37 files changed, 486 insertions(+), 365 deletions(-) create mode 100644 db/migrate/20241213072004_rename_profit_center_to_cost_center_on_tarifs.rb create mode 100644 db/migrate/20241215174132_add_accounting_nr_to_invoice_parts_and_payments.rb diff --git a/Gemfile b/Gemfile index 11e08397f..54f87f4ab 100644 --- a/Gemfile +++ b/Gemfile @@ -37,7 +37,7 @@ gem 'net-pop', require: false gem 'net-smtp', require: false gem 'pg' gem 'prawn', '~> 2.4.0' # https://github.com/prawnpdf/prawn/issues/1346 -gem 'prawn-markup' +gem 'prawn-markup', git: 'https://github.com/puzzle/prawn-markup' gem 'prawn-table' gem 'puma' gem 'rack-mini-profiler' diff --git a/Gemfile.lock b/Gemfile.lock index 295e18299..e3875b81e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,29 +1,38 @@ +GIT + remote: https://github.com/puzzle/prawn-markup + revision: 1279dd4a636e534a1752ca0b60de89b1c6f31ae9 + specs: + prawn-markup (1.0.0) + nokogiri + prawn + prawn-table + GEM remote: https://rubygems.org/ specs: - actioncable (8.0.0.1) - actionpack (= 8.0.0.1) - activesupport (= 8.0.0.1) + actioncable (8.0.1) + actionpack (= 8.0.1) + activesupport (= 8.0.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.0.0.1) - actionpack (= 8.0.0.1) - activejob (= 8.0.0.1) - activerecord (= 8.0.0.1) - activestorage (= 8.0.0.1) - activesupport (= 8.0.0.1) + actionmailbox (8.0.1) + actionpack (= 8.0.1) + activejob (= 8.0.1) + activerecord (= 8.0.1) + activestorage (= 8.0.1) + activesupport (= 8.0.1) mail (>= 2.8.0) - actionmailer (8.0.0.1) - actionpack (= 8.0.0.1) - actionview (= 8.0.0.1) - activejob (= 8.0.0.1) - activesupport (= 8.0.0.1) + actionmailer (8.0.1) + actionpack (= 8.0.1) + actionview (= 8.0.1) + activejob (= 8.0.1) + activesupport (= 8.0.1) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.0.0.1) - actionview (= 8.0.0.1) - activesupport (= 8.0.0.1) + actionpack (8.0.1) + actionview (= 8.0.1) + activesupport (= 8.0.1) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -31,40 +40,40 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.0.0.1) - actionpack (= 8.0.0.1) - activerecord (= 8.0.0.1) - activestorage (= 8.0.0.1) - activesupport (= 8.0.0.1) + actiontext (8.0.1) + actionpack (= 8.0.1) + activerecord (= 8.0.1) + activestorage (= 8.0.1) + activesupport (= 8.0.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.0.0.1) - activesupport (= 8.0.0.1) + actionview (8.0.1) + activesupport (= 8.0.1) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - active_flag (2.0.2) + active_flag (2.0.3) activerecord (>= 5) bigdecimal logger mutex_m - activejob (8.0.0.1) - activesupport (= 8.0.0.1) + activejob (8.0.1) + activesupport (= 8.0.1) globalid (>= 0.3.6) - activemodel (8.0.0.1) - activesupport (= 8.0.0.1) - activerecord (8.0.0.1) - activemodel (= 8.0.0.1) - activesupport (= 8.0.0.1) + activemodel (8.0.1) + activesupport (= 8.0.1) + activerecord (8.0.1) + activemodel (= 8.0.1) + activesupport (= 8.0.1) timeout (>= 0.4.0) - activestorage (8.0.0.1) - actionpack (= 8.0.0.1) - activejob (= 8.0.0.1) - activerecord (= 8.0.0.1) - activesupport (= 8.0.0.1) + activestorage (8.0.1) + actionpack (= 8.0.1) + activejob (= 8.0.1) + activerecord (= 8.0.1) + activesupport (= 8.0.1) marcel (~> 1.0) - activesupport (8.0.0.1) + activesupport (8.0.1) base64 benchmark (>= 0.3) bigdecimal @@ -84,7 +93,7 @@ GEM rake (>= 0.8.7) ast (2.4.2) aws-eventstream (1.3.0) - aws-partitions (1.1021.0) + aws-partitions (1.1023.0) aws-sdk-core (3.214.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -93,7 +102,7 @@ GEM aws-sdk-kms (1.96.0) aws-sdk-core (~> 3, >= 3.210.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.176.0) + aws-sdk-s3 (1.176.1) aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) @@ -151,7 +160,7 @@ GEM cronex (0.15.0) tzinfo unicode (>= 0.4.4.5) - csv (3.3.0) + csv (3.3.1) database_cleaner (2.1.0) database_cleaner-active_record (>= 2, < 3) database_cleaner-active_record (2.2.0) @@ -175,9 +184,9 @@ GEM discard (1.4.0) activerecord (>= 4.2, < 9.0) docile (1.4.1) - dotenv (3.1.4) - dotenv-rails (3.1.4) - dotenv (= 3.1.4) + dotenv (3.1.6) + dotenv-rails (3.1.6) + dotenv (= 3.1.6) railties (>= 6.1) drb (2.2.1) dry-cli (1.2.0) @@ -283,7 +292,7 @@ GEM rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) locale (2.1.4) - logger (1.6.2) + logger (1.6.3) loofah (2.23.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) @@ -318,7 +327,7 @@ GEM net-smtp (0.5.0) net-protocol nio4r (2.7.4) - nokogiri (1.17.1-x86_64-linux) + nokogiri (1.17.2-x86_64-linux) racc (~> 1.4) orm_adapter (0.5.0) ostruct (0.6.1) @@ -331,10 +340,6 @@ GEM prawn (2.4.0) pdf-core (~> 0.9.0) ttfunk (~> 1.7) - prawn-markup (1.0.0) - nokogiri - prawn - prawn-table prawn-table (0.2.2) prawn (>= 1.3.0, < 3.0.0) prime (0.1.3) @@ -368,33 +373,33 @@ GEM rack (>= 1.3) rackup (2.2.1) rack (>= 3) - rails (8.0.0.1) - actioncable (= 8.0.0.1) - actionmailbox (= 8.0.0.1) - actionmailer (= 8.0.0.1) - actionpack (= 8.0.0.1) - actiontext (= 8.0.0.1) - actionview (= 8.0.0.1) - activejob (= 8.0.0.1) - activemodel (= 8.0.0.1) - activerecord (= 8.0.0.1) - activestorage (= 8.0.0.1) - activesupport (= 8.0.0.1) + rails (8.0.1) + actioncable (= 8.0.1) + actionmailbox (= 8.0.1) + actionmailer (= 8.0.1) + actionpack (= 8.0.1) + actiontext (= 8.0.1) + actionview (= 8.0.1) + activejob (= 8.0.1) + activemodel (= 8.0.1) + activerecord (= 8.0.1) + activestorage (= 8.0.1) + activesupport (= 8.0.1) bundler (>= 1.15.0) - railties (= 8.0.0.1) + railties (= 8.0.1) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.1) + rails-html-sanitizer (1.6.2) loofah (~> 2.21) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) rails-i18n (8.0.1) i18n (>= 0.7, < 2) railties (>= 8.0.0, < 9) - railties (8.0.0.1) - actionpack (= 8.0.0.1) - activesupport (= 8.0.0.1) + railties (8.0.1) + actionpack (= 8.0.1) + activesupport (= 8.0.1) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -409,7 +414,7 @@ GEM ffi (~> 1.0) rbs (3.7.0) logger - rdoc (6.8.1) + rdoc (6.9.0) psych (>= 4.0.0) react-rails (3.2.1) babel-transpiler (>= 0.7.0) @@ -429,7 +434,7 @@ GEM responders (3.1.1) actionpack (>= 5.2) railties (>= 5.2) - rexml (3.3.9) + rexml (3.4.0) rqrcode (2.2.0) chunky_png (~> 1.0) rqrcode_core (~> 1.0) @@ -451,7 +456,7 @@ GEM rspec-mocks (~> 3.13) rspec-support (~> 3.13) rspec-support (3.13.2) - rubocop (1.69.1) + rubocop (1.69.2) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) @@ -461,7 +466,7 @@ GEM rubocop-ast (>= 1.36.2, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.36.2) + rubocop-ast (1.37.0) parser (>= 3.3.1.0) rubocop-performance (1.23.0) rubocop (>= 1.48.1, < 2.0) @@ -482,7 +487,7 @@ GEM ruby-lsp (>= 0.22.0, < 0.23.0) ruby-progressbar (1.13.0) rubyzip (2.3.2) - securerandom (0.4.0) + securerandom (0.4.1) selenium-webdriver (4.27.0) base64 (~> 0.2) logger (~> 1.4) @@ -514,19 +519,18 @@ GEM actionpack (>= 3.1) railties (>= 3.1) slim (>= 3.0, < 6.0, != 5.0.0) - sorbet-runtime (0.5.11691) + sorbet-runtime (0.5.11694) squasher (0.8.0) statesman (12.1.0) statsd-ruby (1.5.0) stringio (3.1.2) temple (0.10.3) - terminal-table (3.0.2) - unicode-display_width (>= 1.1.1, < 3) + terminal-table (1.6.0) text (1.3.1) thor (1.3.2) thruster (0.1.9-x86_64-linux) tilt (2.4.0) - timeout (0.4.2) + timeout (0.4.3) translation (1.41) gettext (~> 3.2, >= 3.2.5, <= 3.4.9) ttfunk (1.7.0) @@ -534,7 +538,9 @@ GEM concurrent-ruby (~> 1.0) unaccent (0.4.0) unicode (0.4.4.5) - unicode-display_width (2.6.0) + unicode-display_width (3.1.2) + unicode-emoji (~> 4.0, >= 4.0.4) + unicode-emoji (4.0.4) uri (1.0.2) useragent (0.16.11) vite_rails (3.0.19) @@ -605,7 +611,7 @@ DEPENDENCIES net-smtp pg prawn (~> 2.4.0) - prawn-markup + prawn-markup! prawn-table pry-rails pry-rescue diff --git a/app/concerns/subtypeable.rb b/app/concerns/subtypeable.rb index 3e58bffde..355dbdd52 100644 --- a/app/concerns/subtypeable.rb +++ b/app/concerns/subtypeable.rb @@ -9,8 +9,10 @@ module Subtypeable # end class_methods do - def register_subtype(klass, name: klass.to_s.to_sym) + def register_subtype(klass, name: klass.to_s.to_sym, &block) subtypes[name] = klass + instance_eval(&block) if block.present? + subtypes end def subtypes diff --git a/app/models/accounting_settings.rb b/app/models/accounting_settings.rb index fafc724b4..a8421420c 100644 --- a/app/models/accounting_settings.rb +++ b/app/models/accounting_settings.rb @@ -2,7 +2,10 @@ class AccountingSettings < Settings attribute :tenant_debitor_account_nr_base, :integer, default: -> { 0 } - attribute :debitor_account_nr, :string + attribute :debitor_account_nr, :string, default: -> { 1050 } + attribute :rental_yield_account_nr, :string, default: -> { 6000 } + attribute :rental_yield_vat_category_id, :integer attribute :currency_account_nr, :string - attribute :default_payment_account_nr, :string + attribute :vat_account_nr, :string, default: -> { 2016 } + attribute :default_payment_account_nr, :string, default: -> { 2512 } end diff --git a/app/models/data_digest_templates/journal_entry.rb b/app/models/data_digest_templates/journal_entry.rb index 9f66f6920..60a422ecd 100644 --- a/app/models/data_digest_templates/journal_entry.rb +++ b/app/models/data_digest_templates/journal_entry.rb @@ -53,16 +53,12 @@ class JournalEntry < Tabular body: '{{ journal_entry.amount | round: 2 }}' }, { - header: ::JournalEntry.human_attribute_name(:tax_code), - body: '{{ journal_entry.tax_code }}' + header: ::JournalEntry.human_attribute_name(:vat_code), + body: '{{ journal_entry.vat_code }}' }, { - header: ::JournalEntry.human_attribute_name(:cost_center), - body: '{{ journal_entry.cost_center }}' - }, - { - header: ::JournalEntry.model_name.human, - body: '{{ journal_entry.to_s }}' + header: ::JournalEntry.human_attribute_name(:book_type), + body: '{{ journal_entry.book_type }}' }, { header: ::Booking.human_attribute_name(:ref), @@ -80,8 +76,8 @@ class JournalEntry < Tabular end formatter(:taf) do |_options = {}| - records.map do |source| - TafBlock::Collection.new { derive(source) } + records.to_a.group_by(&:invoice).map do |invoice, _journal_entries| + TafBlock::Collection.new { derive(invoice) } end.join("\n\n") end diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 398182adb..af795369c 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -49,7 +49,7 @@ class Invoice < ApplicationRecord has_many :superseded_by_invoices, class_name: :Invoice, dependent: :nullify, foreign_key: :supersede_invoice_id, inverse_of: :supersede_invoice has_many :payments, dependent: :nullify - has_many :journal_entries, as: :source, dependent: :destroy + has_many :journal_entries, dependent: :destroy has_one :organisation, through: :booking has_one_attached :pdf @@ -73,8 +73,8 @@ class Invoice < ApplicationRecord after_create :supersede! before_update :generate_pdf, if: :generate_pdf? after_save :recalculate! - after_save :generate_journal_entries - after_discard :generate_journal_entries + # after_save :process_journal_entries + # after_discard :process_journal_entries delegate :currency, to: :organisation @@ -84,7 +84,7 @@ class Invoice < ApplicationRecord end def generate_pdf? - kept? && payment_ref.present? && !skip_generate_pdf && (pdf.blank? || changed?) + kept? && !skip_generate_pdf && (pdf.blank? || changed?) end def supersede! @@ -118,9 +118,9 @@ def generate_payment_ref self.payment_ref = RefBuilders::InvoicePayment.new(self).generate if payment_ref.blank? end - def generate_journal_entries + def process_journal_entries # GenerateJournalEntriesJob.perform_later(self) - JournalEntry.generate(self) + JournalEntry.process_invoice(self) end def paid? @@ -154,7 +154,7 @@ def recalculate! end def filename - "#{self.class.model_name.human} #{booking.ref}_#{id}.pdf" + "#{self.class.model_name.human} #{ref}.pdf" end def amount_paid diff --git a/app/models/invoice_part.rb b/app/models/invoice_part.rb index e85a2e08d..5124fa3cf 100644 --- a/app/models/invoice_part.rb +++ b/app/models/invoice_part.rb @@ -4,17 +4,19 @@ # # Table name: invoice_parts # -# id :integer not null, primary key -# invoice_id :integer -# usage_id :integer -# type :string -# amount :decimal(, ) -# label :string -# breakdown :string -# ordinal :integer -# created_at :datetime not null -# updated_at :datetime not null -# vat_category_id :integer +# id :integer not null, primary key +# invoice_id :integer +# usage_id :integer +# type :string +# amount :decimal(, ) +# label :string +# breakdown :string +# ordinal :integer +# created_at :datetime not null +# updated_at :datetime not null +# vat_category_id :integer +# accounting_account_nr :string +# accounting_cost_center_nr :string # # Indexes # @@ -54,6 +56,10 @@ def calculated_amount amount end + def vat_breakdown + @vat_breakdown ||= vat_category&.breakdown(amount) || { brutto: amount, netto: amount, vat: 0 } + end + def sum_of_predecessors invoice.invoice_parts.ordered.inject(0) do |sum, invoice_part| break sum if invoice_part == self @@ -66,15 +72,6 @@ def to_sum(sum) sum + calculated_amount end - def self.from_usage(usage, **attributes) - return unless usage - - new(attributes.reverse_merge( - usage:, label: usage.tarif.label, ordinal: usage.tarif.ordinal, vat_category: usage.tarif.vat_category, - breakdown: usage.remarks.presence || usage.breakdown, amount: usage.price - )) - end - class Filter < ApplicationFilter attribute :homes, default: -> { [] } attribute :issued_at_after, :datetime diff --git a/app/models/invoice_part/factory.rb b/app/models/invoice_part/factory.rb index 37ea4034f..33b92875d 100644 --- a/app/models/invoice_part/factory.rb +++ b/app/models/invoice_part/factory.rb @@ -4,7 +4,8 @@ class InvoicePart class Factory attr_reader :invoice - delegate :booking, to: :invoice + delegate :booking, :organisation, to: :invoice + delegate :accounting_settings, to: :organisation def initialize(invoice) @invoice = invoice @@ -13,7 +14,7 @@ def initialize(invoice) def call I18n.with_locale(invoice.locale || I18n.locale) do [ - from_payments.presence, + from_unassigned_payments.presence, from_deposits.presence, from_supersede_invoice.presence, from_usages.presence @@ -33,29 +34,35 @@ def from_usages(usages = booking.usages.ordered.where.not(id: invoice.invoice_pa end.flatten end - def from_payments - payed_amount = @invoice.booking.payments.where(invoice: nil, write_off: false).sum(:amount) - return [] unless payed_amount.positive? && @invoice.new_record? + def from_unassigned_payments # rubocop:disable Metrics/MethodLength + unassigned_payments = booking.payments.where(invoice: nil, write_off: false) + payed_amount = unassigned_payments.sum(:amount) + return [] unless payed_amount.positive? && invoice.new_record? apply = invoice.invoice_parts.none? [ - InvoiceParts::Text.new(apply:, label: Invoices::Deposit.model_name.human), - InvoiceParts::Add.new(apply:, label: I18n.t('invoice_parts.deposited_amount'), amount: - payed_amount) + InvoiceParts::Deposit.new(apply:, label: I18n.t('invoice_parts.unassigned_payments_amount'), + amount: - payed_amount, unassigned_payments:, + vat_category_id: accounting_settings.rental_yield_vat_category_id, + accounting_account_nr: accounting_settings.rental_yield_account_nr, + accounting_cost_center_nr: 9009) ] end - def from_deposits - deposits = Invoices::Deposit.of(@invoice.booking).kept + def from_deposits # rubocop:disable Metrics/AbcSize,Metrics/MethodLength + deposits = booking.invoices.deposits.kept deposited_amount = deposits.sum(&:amount_paid) + return [] unless deposited_amount.positive? && invoice.new_record? && invoice.is_a?(Invoices::Invoice) + apply = invoice.invoice_parts.none? - return [] unless deposited_amount.positive? && @invoice.new_record? && - @invoice.is_a?(Invoices::Invoice) [ InvoiceParts::Text.new(apply:, label: Invoices::Deposit.model_name.human), - InvoiceParts::Deposit.new(apply:, label: I18n.t('invoice_parts.deposited_amount'), - amount: - deposited_amount) + InvoiceParts::Deposit.new(apply:, label: I18n.t('invoice_parts.deposited_amount'), amount: - deposited_amount, + vat_category_id: accounting_settings.rental_yield_vat_category_id, + accounting_account_nr: accounting_settings.rental_yield_account_nr, + accounting_cost_center_nr: 9009) ] end @@ -63,12 +70,17 @@ def from_supersede_invoice @invoice.supersede_invoice&.invoice_parts&.map(&:dup) if @invoice.new_record? end - def usages_to_invoice_parts(usages) + def usages_to_invoice_parts(usages) # rubocop:disable Metrics/AbcSize usages.filter_map do |usage| next unless usage.tarif&.associated_types&.include?(Tarif::ASSOCIATED_TYPES.key(invoice.class)) apply = invoice.invoice_parts.none? && usage.tarif.apply_usage_to_invoice?(usage, invoice) - InvoiceParts::Add.from_usage(usage, apply:) + usage.instance_eval do + InvoiceParts::Add.new(usage: self, apply:, label: tarif.label, ordinal: tarif.ordinal, + vat_category: tarif.vat_category, breakdown: remarks.presence || breakdown, + amount: price, accounting_account_nr: tarif.accounting_account_nr, + accounting_cost_center_nr: tarif.accounting_cost_center_nr) + end end end diff --git a/app/models/invoice_parts/add.rb b/app/models/invoice_parts/add.rb index dc62dedf8..9a8f3bf2d 100644 --- a/app/models/invoice_parts/add.rb +++ b/app/models/invoice_parts/add.rb @@ -4,17 +4,19 @@ # # Table name: invoice_parts # -# id :integer not null, primary key -# invoice_id :integer -# usage_id :integer -# type :string -# amount :decimal(, ) -# label :string -# breakdown :string -# ordinal :integer -# created_at :datetime not null -# updated_at :datetime not null -# vat_category_id :integer +# id :integer not null, primary key +# invoice_id :integer +# usage_id :integer +# type :string +# amount :decimal(, ) +# label :string +# breakdown :string +# ordinal :integer +# created_at :datetime not null +# updated_at :datetime not null +# vat_category_id :integer +# accounting_account_nr :string +# accounting_cost_center_nr :string # # Indexes # diff --git a/app/models/invoice_parts/deposit.rb b/app/models/invoice_parts/deposit.rb index 8fdd905f8..455d539e7 100644 --- a/app/models/invoice_parts/deposit.rb +++ b/app/models/invoice_parts/deposit.rb @@ -4,17 +4,19 @@ # # Table name: invoice_parts # -# id :integer not null, primary key -# invoice_id :integer -# usage_id :integer -# type :string -# amount :decimal(, ) -# label :string -# breakdown :string -# ordinal :integer -# created_at :datetime not null -# updated_at :datetime not null -# vat_category_id :integer +# id :integer not null, primary key +# invoice_id :integer +# usage_id :integer +# type :string +# amount :decimal(, ) +# label :string +# breakdown :string +# ordinal :integer +# created_at :datetime not null +# updated_at :datetime not null +# vat_category_id :integer +# accounting_account_nr :string +# accounting_cost_center_nr :string # # Indexes # @@ -26,5 +28,22 @@ module InvoiceParts class Deposit < Add InvoicePart.register_subtype self + + attr_accessor :unassigned_payments + + after_save :assign_payments + + attribute :vat_category_id, default: lambda { |invoice_part| + invoice_part.organisation&.accounting_settings&.rental_yield_vat_category_id + } + attribute :accounting_account_nr, default: -> { organisation&.accounting_settings&.rental_yield_account_nr } + + def assign_payments + return unless valid? && apply + + unassigned_payments&.each do |payment| + payment&.update(invoice:) + end + end end end diff --git a/app/models/invoice_parts/percentage.rb b/app/models/invoice_parts/percentage.rb index 56f1aafad..043c9832a 100644 --- a/app/models/invoice_parts/percentage.rb +++ b/app/models/invoice_parts/percentage.rb @@ -4,17 +4,19 @@ # # Table name: invoice_parts # -# id :integer not null, primary key -# invoice_id :integer -# usage_id :integer -# type :string -# amount :decimal(, ) -# label :string -# breakdown :string -# ordinal :integer -# created_at :datetime not null -# updated_at :datetime not null -# vat_category_id :integer +# id :integer not null, primary key +# invoice_id :integer +# usage_id :integer +# type :string +# amount :decimal(, ) +# label :string +# breakdown :string +# ordinal :integer +# created_at :datetime not null +# updated_at :datetime not null +# vat_category_id :integer +# accounting_account_nr :string +# accounting_cost_center_nr :string # # Indexes # diff --git a/app/models/invoice_parts/text.rb b/app/models/invoice_parts/text.rb index 15d55e24b..899f40351 100644 --- a/app/models/invoice_parts/text.rb +++ b/app/models/invoice_parts/text.rb @@ -4,17 +4,19 @@ # # Table name: invoice_parts # -# id :integer not null, primary key -# invoice_id :integer -# usage_id :integer -# type :string -# amount :decimal(, ) -# label :string -# breakdown :string -# ordinal :integer -# created_at :datetime not null -# updated_at :datetime not null -# vat_category_id :integer +# id :integer not null, primary key +# invoice_id :integer +# usage_id :integer +# type :string +# amount :decimal(, ) +# label :string +# breakdown :string +# ordinal :integer +# created_at :datetime not null +# updated_at :datetime not null +# vat_category_id :integer +# accounting_account_nr :string +# accounting_cost_center_nr :string # # Indexes # diff --git a/app/models/invoices/deposit.rb b/app/models/invoices/deposit.rb index 9632719f2..1fa76322d 100644 --- a/app/models/invoices/deposit.rb +++ b/app/models/invoices/deposit.rb @@ -38,6 +38,8 @@ module Invoices class Deposit < ::Invoice - ::Invoice.register_subtype self + ::Invoice.register_subtype(self) do + scope :deposits, -> { where(type: Invoices::Deposit) } + end end end diff --git a/app/models/journal_entry.rb b/app/models/journal_entry.rb index 55cfe931d..0ad3e7d1b 100644 --- a/app/models/journal_entry.rb +++ b/app/models/journal_entry.rb @@ -3,14 +3,14 @@ # Table name: journal_entries # # id :integer not null, primary key -# booking_id :uuid not null -# source_type :string not null -# source_id :integer not null -# account_nr :string not null +# invoice_id :integer not null +# source_type :string +# source_id :integer # vat_category_id :integer -# date :date not null +# account_nr :string not null # amount :decimal(, ) not null # side :integer not null +# date :date not null # currency :string not null # text :string # ordinal :integer @@ -21,6 +21,7 @@ # # Indexes # +# index_journal_entries_on_invoice_id (invoice_id) # index_journal_entries_on_source (source_type,source_id) # index_journal_entries_on_vat_category_id (vat_category_id) # @@ -28,13 +29,14 @@ # frozen_string_literal: true class JournalEntry < ApplicationRecord - belongs_to :booking - belongs_to :source, polymorphic: true - belongs_to :vat_category, optional: true + belongs_to :invoice + # belongs_to :vat_category, optional: true + has_one :booking, through: :invoice has_one :organisation, through: :booking enum :side, { soll: 1, haben: -1 } + enum :book_type, { main: 0, cost: 1, vat: 2 }, prefix: true, default: :main validates :account_nr, :side, :amount, :source_document_ref, presence: true @@ -61,30 +63,62 @@ def haben_amount amount if haben? end - def self.balanced?(journal_entries) - journal_entries.map { _1.soll_amount || 0 }.sum == journal_entries.map { _1.haben_amount || 0 }.sum + def self.collect(**defaults, &) + Collection.new(**defaults).tap(&) end - def to_s - [ - (id || index).presence&.then { "[#{_1}]" }, - soll_account, - '->', - haben_account, - ActiveSupport::NumberHelper.number_to_currency(amount, unit: currency), - ':', - text - ].compact.join(' ') + def self.process_invoice(invoice) + existing_journal_entry_ids = invoice.reload.journal_entry_ids + new_journal_entries = JournalEntry::Factory.new.invoice(invoice) + + # raise ActiveRecord::Rollback unless + new_journal_entries.save && where(id: existing_journal_entry_ids, invoice:).destroy_all end - def self.generate(source, booking: source.booking) - existing_journal_entry_ids = source.reload.journal_entry_ids - new_journal_entries = Array.wrap(JournalEntry::Factory.new.source(source)).compact + class Collection + delegate :[], :to_a, to: :journal_entries - # raise ActiveRecord::Rollback unless - new_journal_entries.all?(&:valid?) && balanced?(new_journal_entries) && - new_journal_entries.all?(&:save) && - where(id: existing_journal_entry_ids, booking:).destroy_all + attr_reader :journal_entries + + def initialize(**defaults) + @journal_entries = [] + @defaults = defaults + end + + def collect(journal_entry) + journal_entry = JournalEntry.new(**@defaults, **journal_entry) if journal_entry.is_a?(Hash) + return if journal_entry.account_nr.blank? || journal_entry.amount.blank? || journal_entry.amount.zero? + + @journal_entries << journal_entry + end + + def haben(**args) + collect(side: :haben, **args) + end + + def soll(**args) + collect(side: :soll, **args) + end + + def soll_amount + journal_entries.map { _1.soll_amount || 0 }.sum + end + + def haben_amount + amount if haben? + end + + def balanced? + soll_amount == haben_amount + end + + def valid? + journal_entries.all?(&:valid?) # && balanced? + end + + def save + valid? && journal_entries.all?(&:save) + end end class Filter < ApplicationFilter @@ -99,84 +133,89 @@ class Filter < ApplicationFilter end class Factory - def source(source) - case source - when Invoice - source.kept? ? kept_invoice(source) : discarded_invoice(source) - when Payment - source.kept? ? kept_payment(source) : discarded_payment(source) + def invoice(invoice) + JournalEntry.collect(currency: invoice.currency, source_document_ref: invoice.ref, date: invoice.issued_at, + invoice:, text: "#{invoice.ref} - #{invoice.booking.tenant.last_name}") do |collection| + next unless invoice.is_a?(Invoices::Deposit) || invoice.is_a?(Invoices::Invoice) + next unless invoice.kept? + + invoice_debitor(invoice, collection) + invoice.invoice_parts.map { invoice_part(_1, collection) } end end - def kept_invoice(invoice) - return unless invoice.is_a?(Invoices::Deposit) || invoice.is_a?(Invoices::Invoice) - - [invoice_debitor(invoice)] + invoice.invoice_parts.map { invoice_part(_1) }.compact - end + # def discarded_invoice(invoice) + # previous_journal_entries = kept_invoice(invoice) + # return if previous_journal_entries.blank? - def discarded_invoice(invoice) - previous_journal_entries = kept_invoice(invoice) - return if previous_journal_entries.blank? + # previous_journal_entries + kept_invoice(invoice).each do |journal_entry| + # journal_entry.invert + # journal_entry.date = invoice.discarded_at.to_date + # end + # end - previous_journal_entries + kept_invoice(invoice).each do |journal_entry| - journal_entry.invert - journal_entry.date = invoice.discarded_at.to_date - end - end - - def invoice_debitor(invoice) + def invoice_debitor(invoice, collection) invoice.instance_eval do - account_nr = organisation.accounting_settings.debitor_account_nr - return if account_nr.blank? + defaults = { source_type: ::Invoice.sti_name, source_id: id } - JournalEntry.new(account_nr:, date: issued_at, side: :soll, amount:, source_document_ref: ref, - source: self, currency:, booking:, - text: "#{ref} - #{booking.tenant.last_name}") + # Der Betrag, welcher der Debitor noch schuldig ist. (inkl. MwSt.). Jak: «Erlösbuchung» + collection.soll(**defaults, account_nr: organisation&.accounting_settings&.debitor_account_nr, amount: amount) end end - def invoice_part(invoice_part) + def invoice_part(invoice_part, collection) case invoice_part - when InvoiceParts::Add - invoice_part_add(invoice_part) - when InvoiceParts::Deposit - invoice_part_deposit(invoice_part) + when InvoiceParts::Add, InvoiceParts::Deposit + invoice_part_add(invoice_part, collection) end end - def invoice_part_add(invoice_part) # rubocop:disable Metrics/AbcSize + def invoice_part_add(invoice_part, collection) # rubocop:disable Metrics/AbcSize invoice_part.instance_eval do - account_nr = tarif&.accounting_account_nr - return if account_nr.blank? + defaults = { source_type: self.class.sti_name, source_id: id, text: "#{invoice.ref} #{label}" } - JournalEntry.new(account_nr:, date: invoice.issued_at, side: :haben, - amount:, vat_category:, source_document_ref: invoice.ref, source: invoice, - currency: organisation.currency, booking:, cost_center: tarif&.accounting_profit_center_nr, - text: "#{invoice.ref} #{label}") + collection.haben(**defaults, account_nr: accounting_account_nr, amount: vat_breakdown[:netto]) + collection.haben(**defaults, account_nr: accounting_cost_center_nr, book_type: :cost, + amount: vat_breakdown[:netto]) + collection.haben(**defaults, account_nr: vat_category&.organisation&.accounting_settings&.vat_account_nr, + book_type: :vat, amount: vat_breakdown[:vat]) end end - def invoice_part_deposit(invoice_part) # rubocop:disable Metrics/AbcSize - invoice_part.instance_eval do - account_nr = tarif&.accounting_account_nr - return if account_nr.blank? - - JournalEntry.new(account_nr:, date: invoice.issued_at, side: :soll, - amount:, source_document_ref: invoice.ref, source: invoice, - currency: organisation.currency, booking:, cost_center: tarif&.accounting_profit_center_nr, - text: "#{invoice.ref} #{label}") - end - end - - def kept_payment(payment) # rubocop:disable Metrics/AbcSize - payment.instance_eval do - account_nr = organisation.accounting_settings.payment_account_nr - return if account_nr.blank? - - JournalEntry.new(account_nr:, date: paid_at, side: :soll, amount:, - source_document_ref: invoice.ref, source: self, currency: organisation.currency, booking:, - text: "#{invoice.ref} #{self.class.model_name.human}") - end - end + # def invoice_part_deposit(invoice_part) + # invoice = invoice_part.invoice + # cost_center_nr = invoice_part.accounting_cost_center_nr.presence + # vat_account_nr = invoice.organisation.accounting_settings.vat_account_nr.presence + + # JournalEntry.collect(date: invoice.issued_at, invoice: invoice_part.invoice, source_document_ref: invoice.ref, + # source_type: invoice_part.class.sti_name, source_id: id, + # text: "#{invoice.ref} #{invoice_part.label}") do + # haben(account_nr:, amount: breakdown[:netto]) if accounting_account_nr.present? + # haben(account_nr: cost_center_nr, book_type: :cost, amount: breakdown[:netto]) if cost_center_nr + # haben(account_nr:, book_type: :vat, amount: breakdown[:vat]) if vat_category.present? && vat_account_nr + # end + # end + + # def invoice_part_deposit(invoice_part) + # invoice_part.instance_eval do + # account_nr = tarif&.accounting_account_nr + # return if account_nr.blank? + + # invoice_part_add(invoice_part).tap do |journal_entry| + # journal_entry.assign_attributes(account_nr:, side: :haben, amount: -amount) + # end + # end + # end + + # def kept_payment(payment) + # payment.instance_eval do + # account_nr = organisation.accounting_settings.payment_account_nr + # return if account_nr.blank? || cost_account_nr.blank? + + # JournalEntry.new(account_nr:, date: paid_at, side: :soll, amount:, invoice:, currency: organisation.currency, + # source_document_ref: invoice.ref, text: "#{invoice.ref} #{self.class.model_name.human}", + # source_type: ::Payment.sti_name, source_id: id) + # end + # end end end diff --git a/app/models/vat_category.rb b/app/models/vat_category.rb index 12266b891..0c368c9cf 100644 --- a/app/models/vat_category.rb +++ b/app/models/vat_category.rb @@ -14,15 +14,15 @@ class VatCategory < ApplicationRecord scope :ordered, -> { order(percentage: :ASC) } def to_s - formatted_percentage = ActiveSupport::NumberHelper.number_to_percentage(percentage, precision: 2) + formatted_percentage = ActiveSupport::NumberHelper.number_to_percentage(percentage) return formatted_percentage if label.blank? "#{label} (#{formatted_percentage})" end def breakdown(amount) - tax = 0 - tax = (amount / (100 + percentage)) * percentage if percentage.present? - { tax:, brutto: amount, netto: (amount - tax) } + vat = 0 + vat = (amount / (100 + percentage)) * percentage if percentage.present? + { vat:, brutto: amount, netto: (amount - vat) } end end diff --git a/app/params/manage/invoice_part_params.rb b/app/params/manage/invoice_part_params.rb index d5ad00a20..29f81b13b 100644 --- a/app/params/manage/invoice_part_params.rb +++ b/app/params/manage/invoice_part_params.rb @@ -3,7 +3,8 @@ module Manage class InvoicePartParams < ApplicationParams def self.permitted_keys - %i[usage_id label breakdown amount type ordinal_position vat_category_id] + %i[usage_id label breakdown amount type ordinal_position vat_category_id + accounting_account_nr accounting_cost_center_nr] end end end diff --git a/app/params/manage/tarif_params.rb b/app/params/manage/tarif_params.rb index a14feb1a6..a56db7324 100644 --- a/app/params/manage/tarif_params.rb +++ b/app/params/manage/tarif_params.rb @@ -3,7 +3,7 @@ module Manage class TarifParams < ApplicationParams def self.permitted_keys - %i[type label unit price_per_unit ordinal tarif_group accounting_account_nr accounting_profit_center_nr + %i[type label unit price_per_unit ordinal tarif_group accounting_account_nr accounting_cost_center_nr prefill_usage_method prefill_usage_booking_question_id vat_category_id pin minimum_usage_per_night minimum_usage_total minimum_price_per_night minimum_price_total] + I18n.available_locales.map { |locale| ["label_#{locale}", "unit_#{locale}"] }.flatten + diff --git a/app/serializers/manage/journal_entry_serializer.rb b/app/serializers/manage/journal_entry_serializer.rb index 2bac71c0a..9a05ff165 100644 --- a/app/serializers/manage/journal_entry_serializer.rb +++ b/app/serializers/manage/journal_entry_serializer.rb @@ -2,18 +2,7 @@ module Manage class JournalEntrySerializer < ApplicationSerializer - fields :id, :account_nr, :date, :text, :amount, :side, :cost_center, :ordinal, :source_document_ref, :currency, - :soll_amount, :haben_amount, :soll_account, :haben_account - - field :source do |journal_entry| - { - type: journal_entry.source&.class&.sti_name, - id: journal_entry.source&.id - } - end - - field :tax_code do |journal_entry| - journal_entry.vat_category&.accounting_vat_code - end + fields :id, :account_nr, :date, :text, :amount, :side, :ordinal, :source_document_ref, :currency, + :soll_amount, :haben_amount, :soll_account, :haben_account, :book_type end end diff --git a/app/services/export/pdf/invoice_pdf.rb b/app/services/export/pdf/invoice_pdf.rb index 96c4239d5..13061cc89 100644 --- a/app/services/export/pdf/invoice_pdf.rb +++ b/app/services/export/pdf/invoice_pdf.rb @@ -50,7 +50,8 @@ def initialize(invoice) to_render do special_tokens = { TARIFS: Renderables::Invoice::InvoicePartsTable.new(invoice) } - Renderables::RichText.split(invoice.text, special_tokens).each { render _1 } + slices = Renderables::RichText.split(invoice.text, special_tokens) + slices.each { render _1 } end to_render do diff --git a/app/services/export/pdf/renderables/invoice/invoice_parts_table.rb b/app/services/export/pdf/renderables/invoice/invoice_parts_table.rb index 561e3450c..1ca1cf900 100644 --- a/app/services/export/pdf/renderables/invoice/invoice_parts_table.rb +++ b/app/services/export/pdf/renderables/invoice/invoice_parts_table.rb @@ -96,7 +96,7 @@ def vat_table_data number_to_percentage(vat_category.percentage, precision: 2), organisation.currency, number_to_currency(amount, unit: ''), - number_to_currency(vat_category.breakdown(amount)[:tax], unit: '') + number_to_currency(vat_category.breakdown(amount)[:vat], unit: '') ] end end diff --git a/app/services/taf_block.rb b/app/services/taf_block.rb index 7fd4e1487..c16444edb 100644 --- a/app/services/taf_block.rb +++ b/app/services/taf_block.rb @@ -115,10 +115,10 @@ def self.derive(value, **override) AccId: Value.cast(journal_entry.account_nr, as: :symbol), # Integer; Booking type: 1=cost booking, 2=tax booking - BType: 1, + # BType: 1, # default # String[13], This is the cost type account - CAcc: journal_entry.cost_center, + CAcc: (Value.cast(journal_entry.cost_account_nr, as: :symbol) if journal_entry.cost_account_nr), # Integer; This is the index of the booking that represents the cost booking which is attached to this booking # CIdx: journal_entry.index, @@ -188,35 +188,38 @@ def self.derive(value, **override) next unless invoice.is_a?(Invoices::Invoice) || invoice.is_a?(Invoices::Deposit) op_id = Value.cast(invoice.ref, as: :symbol) - pk_key = [invoice.booking.tenant.accounting_debitor_account_nr, - invoice.organisation.accounting_settings.currency_account_nr].then { "[#{_1.join(',')}]" } - + pk_key = Value.cast(invoice.booking.tenant.accounting_debitor_account_nr, as: :symbol) journal_entries = invoice.journal_entries.to_a [ derive(invoice.booking.tenant), new(:OPd, **{ PkKey: pk_key, OpId: op_id, ZabId: '15T' }), new(:Blg, **{ Date: invoice.issued_at, Orig: true }) do - derive(journal_entries.shift, Flags: 1, OpId: op_id, PkKey: pk_key, CAcc: :div) + # TODO: check if invoice == source + derive(journal_entries.shift, Flags: 1, OpId: op_id, PkKey: pk_key) journal_entries.each { derive(_1, OpId: op_id) } end ] end derive_from Tenant do |tenant, **_override| + account_nr = Value.cast(tenant.accounting_debitor_account_nr, as: :symbol) [ new(:Adr, **{ - AdrId: tenant.accounting_debitor_account_nr, - Line1: tenant.full_name, + AdrId: account_nr, + Sort: I18n.transliterate(tenant.full_name).gsub(/\s/, '').upcase, + Corp: tenant.full_name, + Lang: 'D', Road: tenant.street_address, CCode: tenant.country_code, ACode: tenant.zipcode, City: tenant.city }), new(:PKd, **{ - PkKey: Value.cast(tenant.accounting_debitor_account_nr, as: :symbol), - AdrId: Value.cast(tenant.accounting_debitor_account_nr, as: :symbol), - AccId: Value.cast(tenant.organisation.accounting_settings.currency_account_nr, as: :symbol) + PkKey: account_nr, + AdrId: account_nr, + AccId: Value.cast(tenant.organisation.accounting_settings.debitor_account_nr, as: :symbol), + ZabId: '15T' }) ] diff --git a/app/services/template_context.rb b/app/services/template_context.rb index 30f4079b7..0d3a59e0e 100644 --- a/app/services/template_context.rb +++ b/app/services/template_context.rb @@ -33,6 +33,7 @@ def to_h def self.serialize_value(value, serializer: serializer_for(value)) return value.map { serialize_value(_1) } if value.is_a?(Array) || value.is_a?(ActiveRecord::Relation) + Rails.logger.debug(value.inspect) serializer.try(:render_as_hash, value) || value.try(:to_h) || value.try(:to_s) || value.presence end diff --git a/app/views/manage/invoice_parts/_form.html.slim b/app/views/manage/invoice_parts/_form.html.slim index 147bea9e9..3844d4834 100644 --- a/app/views/manage/invoice_parts/_form.html.slim +++ b/app/views/manage/invoice_parts/_form.html.slim @@ -8,6 +8,10 @@ = f.text_field :breakdown = f.text_field :amount, inputmode: 'numeric' + fieldset + .row + .col-md-6= f.text_field :accounting_account_nr + .col-md-6= f.text_field :accounting_cost_center_nr - if current_organisation.vat_categories.any? = f.collection_select :vat_category_id, current_organisation.vat_categories, :id, :to_s, include_blank: true diff --git a/app/views/manage/tarifs/_form.html.slim b/app/views/manage/tarifs/_form.html.slim index 31ba72370..0dbd301f7 100644 --- a/app/views/manage/tarifs/_form.html.slim +++ b/app/views/manage/tarifs/_form.html.slim @@ -51,7 +51,7 @@ fieldset .row .col-md-6= f.text_field :accounting_account_nr - .col-md-6= f.text_field :accounting_profit_center_nr + .col-md-6= f.text_field :accounting_cost_center_nr - if current_organisation.vat_categories.any? = f.collection_select :vat_category_id, current_organisation.vat_categories, :id, :to_s, include_blank: true diff --git a/app/views/manage/vat_categories/index.html.slim b/app/views/manage/vat_categories/index.html.slim index 56790f278..2d023161b 100644 --- a/app/views/manage/vat_categories/index.html.slim +++ b/app/views/manage/vat_categories/index.html.slim @@ -9,7 +9,7 @@ h1.mt-0.mb-5= VatCategory.model_name.human(count: 2) td = link_to vat_category.to_s, edit_manage_vat_category_path(vat_category) td - = number_to_percentage(vat_category.percentage, precision: 2) + = number_to_percentage(vat_category.percentage) td = vat_category.accounting_vat_code td.p-1.text-end diff --git a/app/views/renderables/invoice_parts/add/_form_fields.html.slim b/app/views/renderables/invoice_parts/add/_form_fields.html.slim index 25fde7e25..151f08d16 100644 --- a/app/views/renderables/invoice_parts/add/_form_fields.html.slim +++ b/app/views/renderables/invoice_parts/add/_form_fields.html.slim @@ -3,6 +3,8 @@ div = f.hidden_field :usage_id = f.hidden_field :type = f.hidden_field :vat_category_id + = f.hidden_field :accounting_account_nr + = f.hidden_field :accounting_cost_center_nr .row .col-1.py-2 diff --git a/app/views/renderables/invoice_parts/deposit/_form_fields.html.slim b/app/views/renderables/invoice_parts/deposit/_form_fields.html.slim index 25fde7e25..151f08d16 100644 --- a/app/views/renderables/invoice_parts/deposit/_form_fields.html.slim +++ b/app/views/renderables/invoice_parts/deposit/_form_fields.html.slim @@ -3,6 +3,8 @@ div = f.hidden_field :usage_id = f.hidden_field :type = f.hidden_field :vat_category_id + = f.hidden_field :accounting_account_nr + = f.hidden_field :accounting_cost_center_nr .row .col-1.py-2 diff --git a/app/views/renderables/invoice_parts/percentage/_form_fields.html.slim b/app/views/renderables/invoice_parts/percentage/_form_fields.html.slim index 3686c2793..7d962964d 100644 --- a/app/views/renderables/invoice_parts/percentage/_form_fields.html.slim +++ b/app/views/renderables/invoice_parts/percentage/_form_fields.html.slim @@ -3,6 +3,8 @@ div = f.hidden_field :usage_id = f.hidden_field :type = f.hidden_field :vat_category_id + = f.hidden_field :accounting_account_nr + = f.hidden_field :accounting_cost_center_nr .row .col-1.py-2 diff --git a/config/locales/de.yml b/config/locales/de.yml index adfd470d6..e1304b1b8 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -219,7 +219,7 @@ de: payment_info_type: Zahlungsteil payment_required: Zahlung für Statusübergang zwingend percentage_paid: "% bezahlt" - ref: Referenznummer + ref: Rechnungsnummer sent_at: Rechnungsdatum text: Text type: Rechnungsart @@ -233,8 +233,8 @@ de: vat: inkl. MwSt. in Prozent journal_entry: amount: Betrag + book_type: Buch booking_id: Buchung - cost_center: Kostenstelle date: Datum haben_account: Haben Konto haben_amount: Haben Betrag @@ -348,7 +348,7 @@ de: title: Titel tarif: accounting_account_nr: Buchhaltungskonto - accounting_profit_center_nr: Profit-Center + accounting_cost_center_nr: Profit-Center associated_types: Ausgewiesen in Dokumenten enabling_conditions: Bedingungen für erlaubte Auswahl label: Tarifbezeichnung @@ -1338,7 +1338,8 @@ de: default: "%{used_units} × %{unit} à %{price_per_unit}" flat: Pauschale à %{price_per_unit} minimum: Mindestbetrag %{minimum} - deposited_amount: Gutschrift aus Anzahlung + deposited_amount: Gutschrift aus An-/Akontozahlung + unassigned_payments_amount: Saldo invoices: total: Total vat_title: in den Preisen enhaltene MwSt. diff --git a/db/migrate/20241212191319_create_journal_entries.rb b/db/migrate/20241212191319_create_journal_entries.rb index f609f62ec..4b67982c4 100644 --- a/db/migrate/20241212191319_create_journal_entries.rb +++ b/db/migrate/20241212191319_create_journal_entries.rb @@ -1,18 +1,21 @@ class CreateJournalEntries < ActiveRecord::Migration[8.0] def change create_table :journal_entries do |t| - t.uuid :booking_id, null: false - t.references :source, polymorphic: true, null: false + t.references :invoice, null: false, foreign_key: true + t.references :source, polymorphic: true, null: true + # t.references :vat_category, null: true, foreign_key: true + t.string :account_nr, null: false - t.references :vat_category, null: true, foreign_key: true - t.date :date, null: false - t.decimal :amount, null: false t.integer :side, null: false - t.string :currency, null: false + t.decimal :amount, null: false + t.date :date, null: false t.string :text + t.string :currency, null: false t.integer :ordinal t.string :source_document_ref - t.string :cost_center + # t.string :cost_center + + t.integer :book_type t.timestamps end diff --git a/db/migrate/20241213072004_rename_profit_center_to_cost_center_on_tarifs.rb b/db/migrate/20241213072004_rename_profit_center_to_cost_center_on_tarifs.rb new file mode 100644 index 000000000..71100664f --- /dev/null +++ b/db/migrate/20241213072004_rename_profit_center_to_cost_center_on_tarifs.rb @@ -0,0 +1,5 @@ +class RenameProfitCenterToCostCenterOnTarifs < ActiveRecord::Migration[8.0] + def change + rename_column :tarifs, :accounting_profit_center_nr, :accounting_cost_center_nr + end +end diff --git a/db/migrate/20241215174132_add_accounting_nr_to_invoice_parts_and_payments.rb b/db/migrate/20241215174132_add_accounting_nr_to_invoice_parts_and_payments.rb new file mode 100644 index 000000000..169ed4a68 --- /dev/null +++ b/db/migrate/20241215174132_add_accounting_nr_to_invoice_parts_and_payments.rb @@ -0,0 +1,20 @@ +class AddAccountingNrToInvoicePartsAndPayments < ActiveRecord::Migration[8.0] + def change + add_column :invoice_parts, :accounting_account_nr, :string, null: true + add_column :invoice_parts, :accounting_cost_center_nr, :string, null: true + add_column :payments, :accounting_account_nr, :string, null: true + add_column :payments, :accounting_cost_center_nr, :string, null: true + + reversible do |direction| + direction.up do + Tarif.find_each do |tarif| + next unless tarif.accounting_account_nr.present? || tarif.accounting_cost_center_nr.present? + + InvoicePart.joins(:usage).where(usage: { tarif: tarif }) + .update_all(accounting_account_nr: tarif.accounting_account_nr.presence, + accounting_cost_center_nr: tarif.accounting_cost_center_nr.presence) + end + end + end + end +end diff --git a/db/schema.rb b/db/schema.rb index c8e0dfe8d..46dac3d1b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2024_12_12_191319) do +ActiveRecord::Schema[8.0].define(version: 2024_12_15_174132) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "pgcrypto" @@ -310,6 +310,8 @@ t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.bigint "vat_category_id" + t.string "accounting_account_nr" + t.string "accounting_cost_center_nr" t.index ["invoice_id"], name: "index_invoice_parts_on_invoice_id" t.index ["usage_id"], name: "index_invoice_parts_on_usage_id" t.index ["vat_category_id"], name: "index_invoice_parts_on_vat_category_id" @@ -343,23 +345,22 @@ end create_table "journal_entries", force: :cascade do |t| - t.uuid "booking_id", null: false - t.string "source_type", null: false - t.bigint "source_id", null: false + t.bigint "invoice_id", null: false + t.string "source_type" + t.bigint "source_id" t.string "account_nr", null: false - t.bigint "vat_category_id" - t.date "date", null: false - t.decimal "amount", null: false t.integer "side", null: false - t.string "currency", null: false + t.decimal "amount", null: false + t.date "date", null: false t.string "text" + t.string "currency", null: false t.integer "ordinal" t.string "source_document_ref" - t.string "cost_center" + t.integer "book_type" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["invoice_id"], name: "index_journal_entries_on_invoice_id" t.index ["source_type", "source_id"], name: "index_journal_entries_on_source" - t.index ["vat_category_id"], name: "index_journal_entries_on_vat_category_id" end create_table "key_sequences", force: :cascade do |t| @@ -533,6 +534,8 @@ t.datetime "updated_at", precision: nil, null: false t.boolean "write_off", default: false, null: false t.string "camt_instr_id" + t.string "accounting_account_nr" + t.string "accounting_cost_center_nr" t.index ["booking_id"], name: "index_payments_on_booking_id" t.index ["invoice_id"], name: "index_payments_on_invoice_id" end @@ -581,7 +584,7 @@ t.bigint "prefill_usage_booking_question_id" t.decimal "minimum_price_per_night" t.decimal "minimum_price_total" - t.string "accounting_profit_center_nr" + t.string "accounting_cost_center_nr" t.bigint "vat_category_id" t.index ["discarded_at"], name: "index_tarifs_on_discarded_at" t.index ["organisation_id"], name: "index_tarifs_on_organisation_id" @@ -708,7 +711,7 @@ add_foreign_key "invoice_parts", "vat_categories" add_foreign_key "invoices", "bookings" add_foreign_key "invoices", "invoices", column: "supersede_invoice_id" - add_foreign_key "journal_entries", "vat_categories" + add_foreign_key "journal_entries", "invoices" add_foreign_key "key_sequences", "organisations" add_foreign_key "mail_template_designated_documents", "designated_documents" add_foreign_key "mail_template_designated_documents", "rich_text_templates", column: "mail_template_id" diff --git a/spec/factories/invoice_parts.rb b/spec/factories/invoice_parts.rb index d355f5dda..e91a8abdf 100644 --- a/spec/factories/invoice_parts.rb +++ b/spec/factories/invoice_parts.rb @@ -4,17 +4,19 @@ # # Table name: invoice_parts # -# id :integer not null, primary key -# invoice_id :integer -# usage_id :integer -# type :string -# amount :decimal(, ) -# label :string -# breakdown :string -# ordinal :integer -# created_at :datetime not null -# updated_at :datetime not null -# vat_category_id :integer +# id :integer not null, primary key +# invoice_id :integer +# usage_id :integer +# type :string +# amount :decimal(, ) +# label :string +# breakdown :string +# ordinal :integer +# created_at :datetime not null +# updated_at :datetime not null +# vat_category_id :integer +# accounting_account_nr :string +# accounting_cost_center_nr :string # # Indexes # diff --git a/spec/factories/journal_entries.rb b/spec/factories/journal_entries.rb index 8acb0cd5a..1baa3b454 100644 --- a/spec/factories/journal_entries.rb +++ b/spec/factories/journal_entries.rb @@ -5,26 +5,25 @@ # Table name: journal_entries # # id :integer not null, primary key -# booking_id :uuid not null -# source_type :string not null -# source_id :integer not null +# invoice_id :integer not null +# source_type :string +# source_id :integer # account_nr :string not null -# vat_category_id :integer -# date :date not null -# amount :decimal(, ) not null # side :integer not null -# currency :string not null +# amount :decimal(, ) not null +# date :date not null # text :string +# currency :string not null # ordinal :integer # source_document_ref :string -# cost_center :string +# book_type :integer # created_at :datetime not null # updated_at :datetime not null # # Indexes # -# index_journal_entries_on_source (source_type,source_id) -# index_journal_entries_on_vat_category_id (vat_category_id) +# index_journal_entries_on_invoice_id (invoice_id) +# index_journal_entries_on_source (source_type,source_id) # FactoryBot.define do diff --git a/spec/models/invoice_part_spec.rb b/spec/models/invoice_part_spec.rb index c3600b75a..3ad818c20 100644 --- a/spec/models/invoice_part_spec.rb +++ b/spec/models/invoice_part_spec.rb @@ -4,17 +4,19 @@ # # Table name: invoice_parts # -# id :integer not null, primary key -# invoice_id :integer -# usage_id :integer -# type :string -# amount :decimal(, ) -# label :string -# breakdown :string -# ordinal :integer -# created_at :datetime not null -# updated_at :datetime not null -# vat_category_id :integer +# id :integer not null, primary key +# invoice_id :integer +# usage_id :integer +# type :string +# amount :decimal(, ) +# label :string +# breakdown :string +# ordinal :integer +# created_at :datetime not null +# updated_at :datetime not null +# vat_category_id :integer +# accounting_account_nr :string +# accounting_cost_center_nr :string # # Indexes # diff --git a/spec/models/journal_entry_spec.rb b/spec/models/journal_entry_spec.rb index 3937f6924..d66e5ae88 100644 --- a/spec/models/journal_entry_spec.rb +++ b/spec/models/journal_entry_spec.rb @@ -5,26 +5,25 @@ # Table name: journal_entries # # id :integer not null, primary key -# booking_id :uuid not null -# source_type :string not null -# source_id :integer not null +# invoice_id :integer not null +# source_type :string +# source_id :integer # account_nr :string not null -# vat_category_id :integer -# date :date not null -# amount :decimal(, ) not null # side :integer not null -# currency :string not null +# amount :decimal(, ) not null +# date :date not null # text :string +# currency :string not null # ordinal :integer # source_document_ref :string -# cost_center :string +# book_type :integer # created_at :datetime not null # updated_at :datetime not null # # Indexes # -# index_journal_entries_on_source (source_type,source_id) -# index_journal_entries_on_vat_category_id (vat_category_id) +# index_journal_entries_on_invoice_id (invoice_id) +# index_journal_entries_on_source (source_type,source_id) # require 'rails_helper' From 09b9d303b2bd9524cb2bf790370b53a6dfdd8dea Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Tue, 17 Dec 2024 16:16:15 +0000 Subject: [PATCH 13/30] fix: migrations and specs --- README.md | 2 +- .../booking_actions/manage/email_contract.rb | 2 +- .../booking_actions/manage/email_invoices.rb | 3 ++- app/mailers/organisation_mailer.rb | 3 ++- app/models/invoice.rb | 2 +- app/models/invoice_part/factory.rb | 21 +++++++++--------- app/models/invoices/deposit.rb | 2 +- app/models/invoices/invoice.rb | 4 +++- app/models/invoices/late_notice.rb | 4 +++- app/models/invoices/offer.rb | 6 +++-- app/models/journal_entry.rb | 8 +++---- app/models/key_sequence.rb | 13 ++++++----- app/models/tenant.rb | 4 ---- .../export/pdf/renderables/page_header.rb | 2 ++ app/services/taf_block.rb | 22 +++++++++---------- app/views/manage/tenants/index.html.slim | 6 ++--- .../20241212191319_create_journal_entries.rb | 2 +- ...241217125938_back_fill_sequence_numbers.rb | 9 ++++++++ spec/factories/journal_entries.rb | 6 +++-- spec/models/journal_entry_spec.rb | 6 +++-- spec/services/taf_block_spec.rb | 10 ++------- 21 files changed, 75 insertions(+), 62 deletions(-) create mode 100644 db/migrate/20241217125938_back_fill_sequence_numbers.rb diff --git a/README.md b/README.md index cf1533779..37f048e2e 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,7 @@ cat data.csv | bin/rails r Import::Csv::TarifImporter.new(home).read ## Backup & Restore ```bash -cat ./path/to/backup.dump | docker exec -i $(docker ps -q --filter name=heimv-db-) pg_restore -U postgres -d heimv_development --host=localhost --no-privileges --no-owner +cat ./path/to/backup.dump | docker exec -i $(docker ps -q --filter name=heimv-db-) pg_restore -U postgres --dbname heimv_development --host=localhost --no-privileges --no-owner --no-acl --clean --create --verbose ``` ## Copyright & License diff --git a/app/domain/booking_actions/manage/email_contract.rb b/app/domain/booking_actions/manage/email_contract.rb index e096f4acc..1b1854d53 100644 --- a/app/domain/booking_actions/manage/email_contract.rb +++ b/app/domain/booking_actions/manage/email_contract.rb @@ -32,7 +32,7 @@ def confirm protected def deposits - @deposits ||= booking.invoices.kept.unsent.where(type: [Invoices::Deposit.to_s]) + @deposits ||= booking.invoices.kept.unsent.deposits end def send_tenant_notification(deposits) diff --git a/app/domain/booking_actions/manage/email_invoices.rb b/app/domain/booking_actions/manage/email_invoices.rb index b480157d2..c51121da3 100644 --- a/app/domain/booking_actions/manage/email_invoices.rb +++ b/app/domain/booking_actions/manage/email_invoices.rb @@ -39,7 +39,8 @@ def send_operator_notification(invoices) end def unsent_invoices - booking.invoices.unsent.where(type: [Invoices::Deposit, Invoices::Invoice, Invoices::LateNotice].map(&:to_s)) + booking.invoices.unsent.where(type: [Invoices::Deposit, Invoices::Invoice, + Invoices::LateNotice].map(&:sti_name)) end def operator diff --git a/app/mailers/organisation_mailer.rb b/app/mailers/organisation_mailer.rb index 8429c6702..df3a6bed0 100644 --- a/app/mailers/organisation_mailer.rb +++ b/app/mailers/organisation_mailer.rb @@ -45,7 +45,8 @@ def set_headers end def set_delivery_options - return unless Rails.env.production? && @organisation.smtp_settings.present? + return unless Rails.env.production? && ENV.fetch('USE_ORGANISATION_SMTP', false) && + @organisation.smtp_settings.present? mail.delivery_method.settings.merge!(@organisation.smtp_settings.to_h) end diff --git a/app/models/invoice.rb b/app/models/invoice.rb index af795369c..a1c1bdf8d 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -84,7 +84,7 @@ class Invoice < ApplicationRecord end def generate_pdf? - kept? && !skip_generate_pdf && (pdf.blank? || changed?) + kept? && !skip_generate_pdf && (changed? || pdf.blank?) end def supersede! diff --git a/app/models/invoice_part/factory.rb b/app/models/invoice_part/factory.rb index 33b92875d..b74e313b8 100644 --- a/app/models/invoice_part/factory.rb +++ b/app/models/invoice_part/factory.rb @@ -50,20 +50,19 @@ def from_unassigned_payments # rubocop:disable Metrics/MethodLength ] end - def from_deposits # rubocop:disable Metrics/AbcSize,Metrics/MethodLength - deposits = booking.invoices.deposits.kept - deposited_amount = deposits.sum(&:amount_paid) - return [] unless deposited_amount.positive? && invoice.new_record? && invoice.is_a?(Invoices::Invoice) + def from_deposits # rubocop:disable Metrics/AbcSize + deposits = booking.invoices.deposits.kept.where(Invoice.arel_table[:amount].gt(0)) + return [] unless deposits.any? && invoice.new_record? && invoice.is_a?(Invoices::Invoice) apply = invoice.invoice_parts.none? - [ - InvoiceParts::Text.new(apply:, label: Invoices::Deposit.model_name.human), - InvoiceParts::Deposit.new(apply:, label: I18n.t('invoice_parts.deposited_amount'), amount: - deposited_amount, - vat_category_id: accounting_settings.rental_yield_vat_category_id, - accounting_account_nr: accounting_settings.rental_yield_account_nr, - accounting_cost_center_nr: 9009) - ] + [InvoiceParts::Text.new(apply:, label: Invoices::Deposit.model_name.human)] + + [ + InvoiceParts::Deposit.new(apply:, label: I18n.t('invoice_parts.deposited_amount'), amount: - deposited_amount, + vat_category_id: accounting_settings.rental_yield_vat_category_id, + accounting_account_nr: accounting_settings.rental_yield_account_nr, + accounting_cost_center_nr: nil) + ] end def from_supersede_invoice diff --git a/app/models/invoices/deposit.rb b/app/models/invoices/deposit.rb index 1fa76322d..0e53bf5b1 100644 --- a/app/models/invoices/deposit.rb +++ b/app/models/invoices/deposit.rb @@ -39,7 +39,7 @@ module Invoices class Deposit < ::Invoice ::Invoice.register_subtype(self) do - scope :deposits, -> { where(type: Invoices::Deposit) } + scope :deposits, -> { where(type: Invoices::Deposit.sti_name) } end end end diff --git a/app/models/invoices/invoice.rb b/app/models/invoices/invoice.rb index 614b46ee2..0c6d9fa1c 100644 --- a/app/models/invoices/invoice.rb +++ b/app/models/invoices/invoice.rb @@ -38,6 +38,8 @@ module Invoices class Invoice < ::Invoice - ::Invoice.register_subtype self + ::Invoice.register_subtype self do + scope :invoices, -> { where(type: Invoices::Invoice.sti_name) } + end end end diff --git a/app/models/invoices/late_notice.rb b/app/models/invoices/late_notice.rb index ab965532d..053812498 100644 --- a/app/models/invoices/late_notice.rb +++ b/app/models/invoices/late_notice.rb @@ -38,6 +38,8 @@ module Invoices class LateNotice < ::Invoice - ::Invoice.register_subtype self + ::Invoice.register_subtype self do + scope :late_notices, -> { where(type: Invoices::LateNotice.sti_name) } + end end end diff --git a/app/models/invoices/offer.rb b/app/models/invoices/offer.rb index fa8359f08..baa6826da 100644 --- a/app/models/invoices/offer.rb +++ b/app/models/invoices/offer.rb @@ -38,7 +38,9 @@ module Invoices class Offer < ::Invoice - ::Invoice.register_subtype self + ::Invoice.register_subtype self do + scope :offers, -> { where(type: Invoices::Offer.sti_name) } + end def amount_open 0 @@ -53,7 +55,7 @@ def payment_required end def sequence_number - @sequence_number ||= organisation.key_sequences.key(self.class.sti_name, year: sequence_year).lease! + self[:sequence_number] ||= organisation.key_sequences.key(Offer.sti_name, year: sequence_year).lease! end end end diff --git a/app/models/journal_entry.rb b/app/models/journal_entry.rb index 0ad3e7d1b..7ecbab037 100644 --- a/app/models/journal_entry.rb +++ b/app/models/journal_entry.rb @@ -8,14 +8,14 @@ # source_id :integer # vat_category_id :integer # account_nr :string not null -# amount :decimal(, ) not null # side :integer not null +# amount :decimal(, ) not null # date :date not null -# currency :string not null # text :string +# currency :string not null # ordinal :integer # source_document_ref :string -# cost_center :string +# book_type :integer # created_at :datetime not null # updated_at :datetime not null # @@ -30,7 +30,7 @@ class JournalEntry < ApplicationRecord belongs_to :invoice - # belongs_to :vat_category, optional: true + belongs_to :vat_category, optional: true has_one :booking, through: :invoice has_one :organisation, through: :booking diff --git a/app/models/key_sequence.rb b/app/models/key_sequence.rb index 0be78a86e..163ffdc3b 100644 --- a/app/models/key_sequence.rb +++ b/app/models/key_sequence.rb @@ -11,24 +11,27 @@ def lease! increment!(:value).value # rubocop:disable Rails/SkipsModelValidations end - def self.backfill_invoices(organisation) + def self.backfill_invoices(organisation, generate_ref: false) organisation.invoices.order(created_at: :ASC).each do |invoice| - invoice.sequence_number invoice.skip_generate_pdf = true + invoice.sequence_number + invoice.generate_ref if generate_ref invoice.save end end - def self.backfill_tenants(organisation) + def self.backfill_tenants(organisation, generate_ref: false) organisation.tenants.order(created_at: :ASC).each do |tenant| tenant.sequence_number + tenant.generate_ref if generate_ref tenant.save end end - def self.backfill_bookings(organisation) - organisation.bookings.order(created_at: :ASC).each do |tenant| + def self.backfill_bookings(organisation, generate_ref: false) + organisation.bookings.order(begins_at: :ASC).each do |tenant| tenant.sequence_number + tenant.generate_ref if generate_ref tenant.save end end diff --git a/app/models/tenant.rb b/app/models/tenant.rb index d79abf547..dcad9c0d2 100644 --- a/app/models/tenant.rb +++ b/app/models/tenant.rb @@ -155,8 +155,4 @@ def merge_with_new(tenant) assign_attributes(tenant&.changed_values&.except(:email, :organisation_id) || {}) self end - - def accounting_debitor_account_nr - @accounting_debitor_account_nr ||= (organisation.accounting_settings&.tenant_debitor_account_nr_base || 0) + id - end end diff --git a/app/services/export/pdf/renderables/page_header.rb b/app/services/export/pdf/renderables/page_header.rb index 9028efe8f..f14fd8263 100644 --- a/app/services/export/pdf/renderables/page_header.rb +++ b/app/services/export/pdf/renderables/page_header.rb @@ -30,6 +30,8 @@ def render_logo image_source ||= Rails.root.join('app/javascript/images/logo.png') image image_source, at: [bounds.left, bounds.top + 80], width: 200, height: 45, fit: [200, 45] + rescue Aws::Errors::MissingRegionError + nil end end end diff --git a/app/services/taf_block.rb b/app/services/taf_block.rb index c16444edb..e2d9c676d 100644 --- a/app/services/taf_block.rb +++ b/app/services/taf_block.rb @@ -109,16 +109,16 @@ def self.derive(value, **override) instance_exec(value, **override, &derive_block) if derive_block.present? end - derive_from JournalEntry do |journal_entry, **override| + derive_from JournalEntry do |journal_entry, _index: nil, **override| new(:Bk, **{ # The Id of a book keeping account. [Fibu-Konto] AccId: Value.cast(journal_entry.account_nr, as: :symbol), # Integer; Booking type: 1=cost booking, 2=tax booking - # BType: 1, # default + BType: { main: nil, cost: 1, vat: 2 }[journal_entry.book_type&.to_sym], # String[13], This is the cost type account - CAcc: (Value.cast(journal_entry.cost_account_nr, as: :symbol) if journal_entry.cost_account_nr), + # CAcc: (Value.cast(journal_entry.cost_account_nr, as: :symbol) if journal_entry.cost_account_nr), # Integer; This is the index of the booking that represents the cost booking which is attached to this booking # CIdx: journal_entry.index, @@ -140,7 +140,7 @@ def self.derive(value, **override) # String[5]; The Id of the tax. [MWSt-Kürzel] TaxId: journal_entry.vat_category&.accounting_vat_code, - MkTxB: journal_entry.vat_category&.accounting_vat_code.present?, + # MkTxB: journal_entry.vat_category&.accounting_vat_code.present?, # String[61*]; This string specifies the first line of the booking text. Text: journal_entry.text&.slice(0..59)&.lines&.first&.strip || '-', # rubocop:disable Style/SafeNavigationChainLength @@ -164,10 +164,10 @@ def self.derive(value, **override) Type: { soll: 0, haben: 1 }[journal_entry.side&.to_sym], # Currency; The net amount for this booking. [Netto-Betrag] - # ValNt: journal_entry.amount_type&.to_sym == :netto ? journal_entry.amount : nil, + ValNt: journal_entry.amount, # Currency; The tax amount for this booking. [Brutto-Betrag] - ValBt: journal_entry.amount, + # ValBt: journal_entry.amount, # Currency; The tax amount for this booking. [Steuer-Betrag] # ValTx: journal_entry.amount_type&.to_sym == :tax ? journal_entry.amount : nil, @@ -177,7 +177,7 @@ def self.derive(value, **override) # ValFW : not implemented # String[13]The OP id of this booking. - OpId: journal_entry.source_document_ref, + # OpId: journal_entry.source_document_ref, # The PK number of this booking. PkKey: nil @@ -188,7 +188,7 @@ def self.derive(value, **override) next unless invoice.is_a?(Invoices::Invoice) || invoice.is_a?(Invoices::Deposit) op_id = Value.cast(invoice.ref, as: :symbol) - pk_key = Value.cast(invoice.booking.tenant.accounting_debitor_account_nr, as: :symbol) + pk_key = Value.cast(invoice.booking.tenant.ref, as: :symbol) journal_entries = invoice.journal_entries.to_a [ @@ -196,14 +196,14 @@ def self.derive(value, **override) new(:OPd, **{ PkKey: pk_key, OpId: op_id, ZabId: '15T' }), new(:Blg, **{ Date: invoice.issued_at, Orig: true }) do # TODO: check if invoice == source - derive(journal_entries.shift, Flags: 1, OpId: op_id, PkKey: pk_key) - journal_entries.each { derive(_1, OpId: op_id) } + derive(journal_entries.shift, index: 1, Flags: 1, OpId: op_id, PkKey: pk_key) + journal_entries.each_with_index { |journal_entry, index| derive(journal_entry, index: index + 1) } end ] end derive_from Tenant do |tenant, **_override| - account_nr = Value.cast(tenant.accounting_debitor_account_nr, as: :symbol) + account_nr = Value.cast(tenant.ref, as: :symbol) [ new(:Adr, **{ AdrId: account_nr, diff --git a/app/views/manage/tenants/index.html.slim b/app/views/manage/tenants/index.html.slim index 46099eb67..749a4b496 100644 --- a/app/views/manage/tenants/index.html.slim +++ b/app/views/manage/tenants/index.html.slim @@ -16,10 +16,8 @@ h1.mt-0.mb-5= Tenant.model_name.human(count: 2) tr[data-href=manage_tenant_path(tenant) data-id=tenant.to_param] td = link_to manage_tenant_path(tenant) - - if tenant.first_name.present? || tenant.last_name.present? - = "#{tenant.last_name}, #{tenant.first_name}" - - else - = tenant.to_s + = tenant.full_name if tenant.full_name.present? + .text-muted= tenant.ref.presence td == tenant.full_address_lines.join('
    '.html_safe) td diff --git a/db/migrate/20241212191319_create_journal_entries.rb b/db/migrate/20241212191319_create_journal_entries.rb index 4b67982c4..dcfb53eb8 100644 --- a/db/migrate/20241212191319_create_journal_entries.rb +++ b/db/migrate/20241212191319_create_journal_entries.rb @@ -3,7 +3,7 @@ def change create_table :journal_entries do |t| t.references :invoice, null: false, foreign_key: true t.references :source, polymorphic: true, null: true - # t.references :vat_category, null: true, foreign_key: true + t.references :vat_category, null: true, foreign_key: true t.string :account_nr, null: false t.integer :side, null: false diff --git a/db/migrate/20241217125938_back_fill_sequence_numbers.rb b/db/migrate/20241217125938_back_fill_sequence_numbers.rb new file mode 100644 index 000000000..80bfefe83 --- /dev/null +++ b/db/migrate/20241217125938_back_fill_sequence_numbers.rb @@ -0,0 +1,9 @@ +class BackFillSequenceNumbers < ActiveRecord::Migration[8.0] + def up + Organisation.find_each do |organisation| + KeySequence.backfill_tenants(organisation, generate_ref: true) + KeySequence.backfill_bookings(organisation, generate_ref: false) + KeySequence.backfill_invoices(organisation, generate_ref: true) + end + end +end diff --git a/spec/factories/journal_entries.rb b/spec/factories/journal_entries.rb index 1baa3b454..ef2c546ca 100644 --- a/spec/factories/journal_entries.rb +++ b/spec/factories/journal_entries.rb @@ -8,6 +8,7 @@ # invoice_id :integer not null # source_type :string # source_id :integer +# vat_category_id :integer # account_nr :string not null # side :integer not null # amount :decimal(, ) not null @@ -22,8 +23,9 @@ # # Indexes # -# index_journal_entries_on_invoice_id (invoice_id) -# index_journal_entries_on_source (source_type,source_id) +# index_journal_entries_on_invoice_id (invoice_id) +# index_journal_entries_on_source (source_type,source_id) +# index_journal_entries_on_vat_category_id (vat_category_id) # FactoryBot.define do diff --git a/spec/models/journal_entry_spec.rb b/spec/models/journal_entry_spec.rb index d66e5ae88..2318d5b89 100644 --- a/spec/models/journal_entry_spec.rb +++ b/spec/models/journal_entry_spec.rb @@ -8,6 +8,7 @@ # invoice_id :integer not null # source_type :string # source_id :integer +# vat_category_id :integer # account_nr :string not null # side :integer not null # amount :decimal(, ) not null @@ -22,8 +23,9 @@ # # Indexes # -# index_journal_entries_on_invoice_id (invoice_id) -# index_journal_entries_on_source (source_type,source_id) +# index_journal_entries_on_invoice_id (invoice_id) +# index_journal_entries_on_source (source_type,source_id) +# index_journal_entries_on_vat_category_id (vat_category_id) # require 'rails_helper' diff --git a/spec/services/taf_block_spec.rb b/spec/services/taf_block_spec.rb index 6ccf56e23..8424edf37 100644 --- a/spec/services/taf_block_spec.rb +++ b/spec/services/taf_block_spec.rb @@ -57,15 +57,12 @@ expect(taf_block.to_s).to eq(<<~TAF.chomp) {Bk AccId=1050 - BType=1 Date=05.10.2024 TaxId="MwSt38" - MkTxB=1 Text="Lorem ipsum" Text2="Second Line, but its longer than sixty ""chars"", " Type=0 - ValBt=2091.75 - OpId="1234" + ValNt=2091.75 } TAF @@ -85,15 +82,12 @@ expect(taf_block.to_s).to eq(<<~TAF.chomp) {Bk AccId=1050 - BType=1 Date=05.10.2024 TaxId="MwSt38" - MkTxB=1 Text="Lorem ipsum" Text2="Second Line, but its longer than sixty ""chars"", " Type=0 - ValBt=2091.75 - OpId="1234" + ValNt=2091.75 } TAF From 7ed5318dd1e05238eabfeac725f95471d11a4b39 Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Wed, 18 Dec 2024 09:44:52 +0000 Subject: [PATCH 14/30] fix: serializers and importers --- app/models/invoice.rb | 2 +- app/models/organisation.rb | 4 + app/serializers/manage/deadline_serializer.rb | 3 +- .../manage/designated_document_serializer.rb | 1 + .../manage/invoice_part_serializer.rb | 5 +- .../manage/journal_entry_serializer.rb | 3 +- app/serializers/manage/operator_serializer.rb | 1 + .../manage/organisation_serializer.rb | 10 +- app/serializers/manage/payment_serializer.rb | 1 + .../manage/rich_text_template_serializer.rb | 1 + app/serializers/manage/tarif_serializer.rb | 6 +- app/serializers/manage/tenant_serializer.rb | 3 +- .../public/agent_booking_serializer.rb | 1 + .../public/booking_category_serializer.rb | 1 + .../public/booking_question_serializer.rb | 1 + app/serializers/public/home_serializer.rb | 1 + .../public/vat_category_serializer.rb | 3 +- .../invoice/invoice_parts_table.rb | 4 +- .../import/hash/organisation_importer.rb | 9 +- app/services/import/hash/tarif_importer.rb | 4 +- app/services/import_seeder.rb | 2 +- db/schema.rb | 8 +- db/seeds/demo.json | 182 +++++++++++++++--- db/seeds/development.json | 180 ++++++++++++++--- 24 files changed, 366 insertions(+), 70 deletions(-) diff --git a/app/models/invoice.rb b/app/models/invoice.rb index a1c1bdf8d..654c63761 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -192,7 +192,7 @@ def to_attachable { io: StringIO.new(pdf.blob.download), filename:, content_type: pdf.content_type } if pdf&.blob.present? end - def vat_amounts + def vat_breakdown invoice_parts.group_by(&:vat_category).except(nil).transform_values { _1.sum(&:calculated_amount) } end end diff --git a/app/models/organisation.rb b/app/models/organisation.rb index 7a1c69a0d..57b52badf 100644 --- a/app/models/organisation.rb +++ b/app/models/organisation.rb @@ -96,6 +96,10 @@ class Organisation < ApplicationRecord attribute :accounting_settings, Settings::Type.new(AccountingSettings), default: -> { AccountingSettings.new } attribute :smtp_settings, Settings::Type.new(SmtpSettings) attribute :iban, IBAN::Type.new + attribute :tenant_ref_template, default: -> { RefBuilders::Tenant::DEFAULT_TEMPLATE } + attribute :booking_ref_template, default: -> { RefBuilders::Booking::DEFAULT_TEMPLATE } + attribute :invoice_ref_template, default: -> { RefBuilders::Invoice::DEFAULT_TEMPLATE } + attribute :invoice_payment_ref_template, default: -> { RefBuilders::InvoicePayment::DEFAULT_TEMPLATE } def booking_flow_class @booking_flow_class ||= BookingFlows.const_get(booking_flow_type) diff --git a/app/serializers/manage/deadline_serializer.rb b/app/serializers/manage/deadline_serializer.rb index 9ce2de805..2eed861b3 100644 --- a/app/serializers/manage/deadline_serializer.rb +++ b/app/serializers/manage/deadline_serializer.rb @@ -2,6 +2,7 @@ module Manage class DeadlineSerializer < ApplicationSerializer - fields :at, :postponable_for + identifier :id + fields :at, :postponable_for, :armed end end diff --git a/app/serializers/manage/designated_document_serializer.rb b/app/serializers/manage/designated_document_serializer.rb index c630b9a39..e462cac1a 100644 --- a/app/serializers/manage/designated_document_serializer.rb +++ b/app/serializers/manage/designated_document_serializer.rb @@ -2,6 +2,7 @@ module Manage class DesignatedDocumentSerializer < ApplicationSerializer + identifier :id fields :designation, :locale, :name, :send_with_accepted, :send_with_contract, :send_with_last_infos field :file_url do |designated_document| url.url_for(designated_document.file) diff --git a/app/serializers/manage/invoice_part_serializer.rb b/app/serializers/manage/invoice_part_serializer.rb index ac571d8a9..f4313a3b4 100644 --- a/app/serializers/manage/invoice_part_serializer.rb +++ b/app/serializers/manage/invoice_part_serializer.rb @@ -2,11 +2,14 @@ module Manage class InvoicePartSerializer < ApplicationSerializer + identifier :id + association :tarif, blueprint: Manage::TarifSerializer association :usage, blueprint: Manage::UsageSerializer association :vat_category, blueprint: Public::VatCategorySerializer - fields :amount, :label, :breakdown, :usage_id + fields :amount, :label, :breakdown, :usage_id, + :accounting_account_nr, :accounting_cost_center_nr, :vat_breakdown field :tarif_id do |invoice_part| invoice_part.tarif&.id diff --git a/app/serializers/manage/journal_entry_serializer.rb b/app/serializers/manage/journal_entry_serializer.rb index 9a05ff165..6f0ae01fa 100644 --- a/app/serializers/manage/journal_entry_serializer.rb +++ b/app/serializers/manage/journal_entry_serializer.rb @@ -2,7 +2,8 @@ module Manage class JournalEntrySerializer < ApplicationSerializer - fields :id, :account_nr, :date, :text, :amount, :side, :ordinal, :source_document_ref, :currency, + identifier :id + fields :account_nr, :date, :text, :amount, :side, :ordinal, :source_document_ref, :currency, :soll_amount, :haben_amount, :soll_account, :haben_account, :book_type end end diff --git a/app/serializers/manage/operator_serializer.rb b/app/serializers/manage/operator_serializer.rb index bb12720fa..62a59dd86 100644 --- a/app/serializers/manage/operator_serializer.rb +++ b/app/serializers/manage/operator_serializer.rb @@ -2,6 +2,7 @@ module Manage class OperatorSerializer < ApplicationSerializer + identifier :id fields :name, :email, :contact_info end end diff --git a/app/serializers/manage/organisation_serializer.rb b/app/serializers/manage/organisation_serializer.rb index e1bae3df9..e41921d46 100644 --- a/app/serializers/manage/organisation_serializer.rb +++ b/app/serializers/manage/organisation_serializer.rb @@ -9,9 +9,11 @@ class OrganisationSerializer < Public::OrganisationSerializer # association :designated_documents, blueprint: DesignatedDocumentSerializer association :tarifs, blueprint: TarifSerializer association :booking_questions, blueprint: Public::BookingQuestionSerializer + association :vat_categories, blueprint: Public::VatCategorySerializer - fields :esr_beneficiary_account, :iban, :mail_from, :booking_ref_template, - :booking_flow_type, :invoice_payment_ref_template, :notifications_enabled, :location, :nickname_label_i18n + fields :esr_beneficiary_account, :iban, :mail_from, :booking_ref_template, :tenant_ref_template, + :invoice_ref_template, :booking_flow_type, :invoice_payment_ref_template, + :notifications_enabled, :location, :nickname_label_i18n field :designated_documents do |organisation| organisation.designated_documents.pluck(:designation).map do |designation| @@ -26,6 +28,10 @@ class OrganisationSerializer < Public::OrganisationSerializer organisation.settings.to_h end + field :accounting_settings do |organisation| + organisation.accounting_settings.to_h + end + view :export do include_view :default diff --git a/app/serializers/manage/payment_serializer.rb b/app/serializers/manage/payment_serializer.rb index beef3f972..a3ecf4c83 100644 --- a/app/serializers/manage/payment_serializer.rb +++ b/app/serializers/manage/payment_serializer.rb @@ -2,6 +2,7 @@ module Manage class PaymentSerializer < ApplicationSerializer + identifier :id association :invoice, blueprint: Manage::InvoiceSerializer fields :paid_at, :amount, :write_off, :remarks diff --git a/app/serializers/manage/rich_text_template_serializer.rb b/app/serializers/manage/rich_text_template_serializer.rb index 915515feb..07d2f538b 100644 --- a/app/serializers/manage/rich_text_template_serializer.rb +++ b/app/serializers/manage/rich_text_template_serializer.rb @@ -2,6 +2,7 @@ module Manage class RichTextTemplateSerializer < ApplicationSerializer + identifier :id fields :key, :title_i18n, :body_i18n view :export do diff --git a/app/serializers/manage/tarif_serializer.rb b/app/serializers/manage/tarif_serializer.rb index 52742ab1f..03b477ab1 100644 --- a/app/serializers/manage/tarif_serializer.rb +++ b/app/serializers/manage/tarif_serializer.rb @@ -2,11 +2,13 @@ module Manage class TarifSerializer < ApplicationSerializer + identifier :id association :vat_category, blueprint: Public::VatCategorySerializer fields :label, :pin, :prefill_usage_method, :price_per_unit, :tarif_group, :type, :unit, :ordinal, - :label_i18n, :unit_i18n, :valid_from, :valid_until, :accounting_account_nr, - :minimum_usage_per_night, :minimum_usage_total + :label_i18n, :unit_i18n, :valid_from, :valid_until, :vat_category_id, + :accounting_account_nr, :accounting_cost_center_nr, + :minimum_usage_per_night, :minimum_usage_total, :minimum_price_per_night, :minimum_price_total field :associated_types do |tarif| tarif.associated_types.to_a diff --git a/app/serializers/manage/tenant_serializer.rb b/app/serializers/manage/tenant_serializer.rb index 47bbc495f..c5f508316 100644 --- a/app/serializers/manage/tenant_serializer.rb +++ b/app/serializers/manage/tenant_serializer.rb @@ -2,9 +2,10 @@ module Manage class TenantSerializer < ApplicationSerializer + identifier :id fields :salutation, :salutation_form, :first_name, :last_name, :street_address, :nickname, :name, :address_addon, :zipcode, :city, :email, :full_name, :contact_info, :phone, :names, :salutations, - :address_lines, :full_address_lines, :birth_date, :country_code, :locale + :address_lines, :full_address_lines, :birth_date, :country_code, :locale, :ref field :salutation_name do |tenant| tenant.salutations[:informal_neutral] diff --git a/app/serializers/public/agent_booking_serializer.rb b/app/serializers/public/agent_booking_serializer.rb index 86a867286..041332d4c 100644 --- a/app/serializers/public/agent_booking_serializer.rb +++ b/app/serializers/public/agent_booking_serializer.rb @@ -2,6 +2,7 @@ module Public class AgentBookingSerializer < ApplicationSerializer + identifier :id fields :booking_agent_ref, :booking_agent_id # association :booking_agent, blueprint: Public::AgentBookingSerializer diff --git a/app/serializers/public/booking_category_serializer.rb b/app/serializers/public/booking_category_serializer.rb index 1bb966cd9..3c9eab8c8 100644 --- a/app/serializers/public/booking_category_serializer.rb +++ b/app/serializers/public/booking_category_serializer.rb @@ -2,6 +2,7 @@ module Public class BookingCategorySerializer < ApplicationSerializer + identifier :id fields :key, :title_i18n, :title, :description_i18n, :ordinal end end diff --git a/app/serializers/public/booking_question_serializer.rb b/app/serializers/public/booking_question_serializer.rb index b076b1b93..0e5c117ca 100644 --- a/app/serializers/public/booking_question_serializer.rb +++ b/app/serializers/public/booking_question_serializer.rb @@ -2,6 +2,7 @@ module Public class BookingQuestionSerializer < ApplicationSerializer + identifier :id fields :key, :label_i18n, :ordinal, :description_i18n, :required, :type, :label, :description end end diff --git a/app/serializers/public/home_serializer.rb b/app/serializers/public/home_serializer.rb index ad108c3c1..82ed6e582 100644 --- a/app/serializers/public/home_serializer.rb +++ b/app/serializers/public/home_serializer.rb @@ -2,6 +2,7 @@ module Public class HomeSerializer < OccupiableSerializer + # fields :accounting_account_nr association :occupiables, blueprint: Public::OccupiableSerializer do |home| home.occupiables.kept end diff --git a/app/serializers/public/vat_category_serializer.rb b/app/serializers/public/vat_category_serializer.rb index e44fe2698..4f8d770d5 100644 --- a/app/serializers/public/vat_category_serializer.rb +++ b/app/serializers/public/vat_category_serializer.rb @@ -2,6 +2,7 @@ module Public class VatCategorySerializer < ApplicationSerializer - fields :label_i18n, :percentage, :accounting_vat_code, :to_s + identifier :id + fields :label, :label_i18n, :percentage, :accounting_vat_code, :to_s end end diff --git a/app/services/export/pdf/renderables/invoice/invoice_parts_table.rb b/app/services/export/pdf/renderables/invoice/invoice_parts_table.rb index 1ca1cf900..4c96de8f0 100644 --- a/app/services/export/pdf/renderables/invoice/invoice_parts_table.rb +++ b/app/services/export/pdf/renderables/invoice/invoice_parts_table.rb @@ -44,7 +44,7 @@ def render_invoice_total_table end def render_invoice_vat_table - return if invoice.vat_amounts.none? + return if invoice.vat_breakdown.none? move_down 10 start_new_page if cursor < (vat_table_data.count + 1) * 9 @@ -90,7 +90,7 @@ def invoice_part_table_row_data(invoice_part) end def vat_table_data - invoice.vat_amounts.map do |vat_category, amount| + invoice.vat_breakdown.map do |vat_category, amount| [ vat_category.label, number_to_percentage(vat_category.percentage, precision: 2), diff --git a/app/services/import/hash/organisation_importer.rb b/app/services/import/hash/organisation_importer.rb index af17711ef..1d62d43fe 100644 --- a/app/services/import/hash/organisation_importer.rb +++ b/app/services/import/hash/organisation_importer.rb @@ -3,9 +3,10 @@ module Import module Hash class OrganisationImporter < Base - use_attributes(*%w[name email address booking_flow_type currency location mail_from - iban invoice_payment_ref_template booking_ref_template notifications_enabled slug - settings currency country_code]) + use_attributes(*%w[slug address booking_flow_type settings accounting_settings + booking_ref_template country_code currency email esr_beneficiary_account iban + invoice_payment_ref_template invoice_ref_template location + mail_from name nickname_label_i18n notifications_enabled tenant_ref_template]) def initialize_record(_hash) Organisation.new @@ -38,6 +39,8 @@ def initialize_record(_hash) importer = TarifImporter.new(organisation, **options) hash['tarifs'].each { |tarif| organisation.tarifs << importer.import(tarif) } end + + # TODO: booking_agents, vat_categories end end end diff --git a/app/services/import/hash/tarif_importer.rb b/app/services/import/hash/tarif_importer.rb index 68ee7ad5c..63ec91dd6 100644 --- a/app/services/import/hash/tarif_importer.rb +++ b/app/services/import/hash/tarif_importer.rb @@ -6,7 +6,9 @@ class TarifImporter < Base attr_reader :organisation use_attributes(*%w[associated_types label_i18n ordinal prefill_usage_method price_per_unit tarif_group - pin type unit_i18n valid_from valid_until minimum_usage_per_night minimum_usage_total]) + pin type unit_i18n vat_category_id valid_from valid_until + accounting_account_nr accounting_cost_center_nr + minimum_price_per_night minimum_price_total minimum_usage_per_night minimum_usage_total]) def initialize(organisation, **) super(**) diff --git a/app/services/import_seeder.rb b/app/services/import_seeder.rb index fd2735a21..d3716e14b 100644 --- a/app/services/import_seeder.rb +++ b/app/services/import_seeder.rb @@ -44,7 +44,7 @@ def organisation(file) def users(organisation, users: nil) onboarding = OnboardingService.new(organisation) users ||= [ - { email: 'supermanager@heimv.local', role: :admin, password: 'heimverwaltung' }, + { email: 'admin@heimv.local', role: :admin, password: 'heimverwaltung' }, { email: 'manager@heimv.local', role: :admin, password: 'heimverwaltung' }, { email: 'reader@heimv.local', role: :readonly, password: 'heimverwaltung' } ] diff --git a/db/schema.rb b/db/schema.rb index 46dac3d1b..965f81646 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,9 +10,10 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2024_12_15_174132) do +ActiveRecord::Schema[8.0].define(version: 2024_12_17_125938) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" + enable_extension "pg_stat_statements" enable_extension "pgcrypto" enable_extension "uuid-ossp" @@ -159,7 +160,7 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "tenant_mode", default: 0, null: false - t.integer "booking_agent_mode" + t.integer "booking_agent_mode", default: 0 t.index ["discarded_at"], name: "index_booking_questions_on_discarded_at" t.index ["organisation_id"], name: "index_booking_questions_on_organisation_id" t.index ["type"], name: "index_booking_questions_on_type" @@ -348,6 +349,7 @@ t.bigint "invoice_id", null: false t.string "source_type" t.bigint "source_id" + t.bigint "vat_category_id" t.string "account_nr", null: false t.integer "side", null: false t.decimal "amount", null: false @@ -361,6 +363,7 @@ t.datetime "updated_at", null: false t.index ["invoice_id"], name: "index_journal_entries_on_invoice_id" t.index ["source_type", "source_id"], name: "index_journal_entries_on_source" + t.index ["vat_category_id"], name: "index_journal_entries_on_vat_category_id" end create_table "key_sequences", force: :cascade do |t| @@ -712,6 +715,7 @@ add_foreign_key "invoices", "bookings" add_foreign_key "invoices", "invoices", column: "supersede_invoice_id" add_foreign_key "journal_entries", "invoices" + add_foreign_key "journal_entries", "vat_categories" add_foreign_key "key_sequences", "organisations" add_foreign_key "mail_template_designated_documents", "designated_documents" add_foreign_key "mail_template_designated_documents", "rich_text_templates", column: "mail_template_id" diff --git a/db/seeds/demo.json b/db/seeds/demo.json index 5bbf6b183..678e97adf 100644 --- a/db/seeds/demo.json +++ b/db/seeds/demo.json @@ -22,27 +22,32 @@ } ], "booking_flow_type": "BookingFlows::Default", + "booking_ref_template": "%s%04d%02d%02d%s", + "country_code": "CH", "currency": "CHF", - "designated_documents": [], "email": "demo1@heimv.local", "homes": [ { - "description": "9876 Einsiedeln", "id": 1, + "description": "9876 Einsiedeln", + "home_id": 1, "name": "Pfadiheim Stolz", "occupiable": true, - "home_id": 1 + "ordinal": 0 } ], "iban": "CH75 3000 0002 1571 4845 3", + "invoice_payment_ref_template": "%s%06d%04d%05d", + "invoice_ref_template": "%02d%04d", "location": "", "mail_from": "info@heimv.local", - "name": "Heimverein Musterhaus", + "name": "Heimverein Development", + "nickname_label_i18n": {}, "notifications_enabled": true, - "settings": {}, - "slug": null, "tarifs": [ { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "deposit", "offer", @@ -52,6 +57,10 @@ "label_i18n": { "de": "Anzahlung (1 Nacht)" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 25, "pin": true, "prefill_usage_method": "flat", @@ -63,9 +72,13 @@ "de": "Pauschale" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "id": 74, + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "deposit", "offer", @@ -75,6 +88,10 @@ "label_i18n": { "de": "Anzahlung (2 Nächte)" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 38, "pin": true, "prefill_usage_method": "flat", @@ -86,9 +103,12 @@ "de": "Pauschale" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "deposit", "offer", @@ -98,6 +118,10 @@ "label_i18n": { "de": "Anzahlung (3 Nächte)" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 44, "pin": true, "prefill_usage_method": "flat", @@ -109,9 +133,12 @@ "de": "Pauschale" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "deposit", "offer", @@ -121,6 +148,10 @@ "label_i18n": { "de": "Anzahlung (4 Nächte)" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 61, "pin": true, "prefill_usage_method": "flat", @@ -132,9 +163,12 @@ "de": "Pauschale" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "invoice", "offer", @@ -144,6 +178,10 @@ "label_i18n": { "de": "Lagertarif (unter 25 jährig)" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 186, "pin": true, "prefill_usage_method": null, @@ -155,9 +193,12 @@ "de": "Übernachtung" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "invoice", "offer", @@ -167,6 +208,10 @@ "label_i18n": { "de": "Lagertarif (über 25 jährig)" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 187, "pin": true, "prefill_usage_method": null, @@ -178,9 +223,12 @@ "de": "Übernachtung" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "invoice", "offer", @@ -190,6 +238,10 @@ "label_i18n": { "de": "Mindestbelegung Lagertarif" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": "12.0", + "minimum_usage_total": null, "ordinal": 188, "pin": true, "prefill_usage_method": "nights", @@ -202,9 +254,11 @@ }, "valid_from": "2019-05-28T11:03:52+02:00", "valid_until": null, - "minimum_usage_per_night": 12 + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "invoice", "offer", @@ -214,6 +268,10 @@ "label_i18n": { "de": "Kurtaxe (ab 12 -18jährig)" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 196, "pin": false, "prefill_usage_method": null, @@ -225,14 +283,21 @@ "de": "Übernachtung" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [], "label": "Festtarif 1 Nacht", "label_i18n": { "de": "Festtarif 1 Nacht" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 197, "pin": true, "prefill_usage_method": "nights", @@ -244,9 +309,12 @@ "de": "pro Tag" }, "valid_from": "2019-12-07T12:04:59+01:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "invoice", "offer", @@ -256,6 +324,10 @@ "label_i18n": { "de": "Kurtaxe (ab 18 Jahren)" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 198, "pin": false, "prefill_usage_method": null, @@ -267,14 +339,21 @@ "de": "Übernachtung" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [], "label": "Festtarrif 2 Nächte", "label_i18n": { "de": "Festtarrif 2 Nächte" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 199, "pin": true, "prefill_usage_method": "nights", @@ -286,9 +365,12 @@ "de": "pro Tag" }, "valid_from": "2019-12-07T12:06:55+01:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "invoice", "offer", @@ -298,6 +380,10 @@ "label_i18n": { "de": "Festtarrif 0 Nächte" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 228, "pin": true, "prefill_usage_method": "nights", @@ -309,9 +395,12 @@ "de": "pro Tag" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "invoice" ], @@ -319,20 +408,27 @@ "label_i18n": { "de": "Schäden" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 277, "pin": true, "prefill_usage_method": null, "price_per_unit": null, "tarif_group": "Schaden", "type": "Tarifs::Price", - "unit": "CHF", + "unit": "nach Betrag", "unit_i18n": { "de": "nach Betrag" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "invoice", "offer", @@ -342,6 +438,10 @@ "label_i18n": { "de": "Strom (Hochtarif)" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 284, "pin": false, "prefill_usage_method": null, @@ -353,9 +453,12 @@ "de": "kWh" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "invoice", "offer", @@ -365,6 +468,10 @@ "label_i18n": { "de": "Reservationsgebühr" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 285, "pin": true, "prefill_usage_method": "flat", @@ -376,9 +483,12 @@ "de": "Pauschale" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "invoice", "offer", @@ -388,6 +498,10 @@ "label_i18n": { "de": "Strom (Niedertarif)" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 286, "pin": false, "prefill_usage_method": null, @@ -399,9 +513,12 @@ "de": "kWh" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "invoice", "offer", @@ -411,6 +528,10 @@ "label_i18n": { "de": "Brennholz" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 287, "pin": false, "prefill_usage_method": null, @@ -422,9 +543,12 @@ "de": "Harass" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "invoice", "offer", @@ -434,6 +558,10 @@ "label_i18n": { "de": "Abfall" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 288, "pin": false, "prefill_usage_method": null, @@ -445,7 +573,9 @@ "de": "pro 60 L Sack" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null } - ] + ], + "tenant_ref_template": "%d" } diff --git a/db/seeds/development.json b/db/seeds/development.json index 19ed02b30..86479edf9 100644 --- a/db/seeds/development.json +++ b/db/seeds/development.json @@ -22,27 +22,32 @@ } ], "booking_flow_type": "BookingFlows::Default", + "booking_ref_template": "%s%04d%02d%02d%s", + "country_code": "CH", "currency": "CHF", - "designated_documents": [], "email": "info@heimv.local", "homes": [ { - "description": "9876 Einsiedeln", "id": 1, + "description": "9876 Einsiedeln", + "home_id": 1, "name": "Pfadiheim Stolz", "occupiable": true, - "home_id": 1 + "ordinal": 0 } ], "iban": "CH75 3000 0002 1571 4845 3", + "invoice_payment_ref_template": "%s%06d%04d%05d", + "invoice_ref_template": "%02d%04d", "location": "", "mail_from": "info@heimv.local", "name": "Heimverein Development", + "nickname_label_i18n": {}, "notifications_enabled": true, - "settings": {}, - "slug": null, "tarifs": [ { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "deposit", "offer", @@ -52,6 +57,10 @@ "label_i18n": { "de": "Anzahlung (1 Nacht)" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 25, "pin": true, "prefill_usage_method": "flat", @@ -63,9 +72,13 @@ "de": "Pauschale" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "id": 74, + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "deposit", "offer", @@ -75,6 +88,10 @@ "label_i18n": { "de": "Anzahlung (2 Nächte)" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 38, "pin": true, "prefill_usage_method": "flat", @@ -86,9 +103,12 @@ "de": "Pauschale" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "deposit", "offer", @@ -98,6 +118,10 @@ "label_i18n": { "de": "Anzahlung (3 Nächte)" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 44, "pin": true, "prefill_usage_method": "flat", @@ -109,9 +133,12 @@ "de": "Pauschale" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "deposit", "offer", @@ -121,6 +148,10 @@ "label_i18n": { "de": "Anzahlung (4 Nächte)" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 61, "pin": true, "prefill_usage_method": "flat", @@ -132,9 +163,12 @@ "de": "Pauschale" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "invoice", "offer", @@ -144,6 +178,10 @@ "label_i18n": { "de": "Lagertarif (unter 25 jährig)" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 186, "pin": true, "prefill_usage_method": null, @@ -155,9 +193,12 @@ "de": "Übernachtung" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "invoice", "offer", @@ -167,6 +208,10 @@ "label_i18n": { "de": "Lagertarif (über 25 jährig)" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 187, "pin": true, "prefill_usage_method": null, @@ -178,9 +223,12 @@ "de": "Übernachtung" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "invoice", "offer", @@ -190,6 +238,10 @@ "label_i18n": { "de": "Mindestbelegung Lagertarif" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": "12.0", + "minimum_usage_total": null, "ordinal": 188, "pin": true, "prefill_usage_method": "nights", @@ -202,9 +254,11 @@ }, "valid_from": "2019-05-28T11:03:52+02:00", "valid_until": null, - "minimum_usage_per_night": 12 + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "invoice", "offer", @@ -214,6 +268,10 @@ "label_i18n": { "de": "Kurtaxe (ab 12 -18jährig)" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 196, "pin": false, "prefill_usage_method": null, @@ -225,14 +283,21 @@ "de": "Übernachtung" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [], "label": "Festtarif 1 Nacht", "label_i18n": { "de": "Festtarif 1 Nacht" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 197, "pin": true, "prefill_usage_method": "nights", @@ -244,9 +309,12 @@ "de": "pro Tag" }, "valid_from": "2019-12-07T12:04:59+01:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "invoice", "offer", @@ -256,6 +324,10 @@ "label_i18n": { "de": "Kurtaxe (ab 18 Jahren)" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 198, "pin": false, "prefill_usage_method": null, @@ -267,14 +339,21 @@ "de": "Übernachtung" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [], "label": "Festtarrif 2 Nächte", "label_i18n": { "de": "Festtarrif 2 Nächte" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 199, "pin": true, "prefill_usage_method": "nights", @@ -286,9 +365,12 @@ "de": "pro Tag" }, "valid_from": "2019-12-07T12:06:55+01:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "invoice", "offer", @@ -298,6 +380,10 @@ "label_i18n": { "de": "Festtarrif 0 Nächte" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 228, "pin": true, "prefill_usage_method": "nights", @@ -309,9 +395,12 @@ "de": "pro Tag" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "invoice" ], @@ -319,20 +408,27 @@ "label_i18n": { "de": "Schäden" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 277, "pin": true, "prefill_usage_method": null, "price_per_unit": null, "tarif_group": "Schaden", "type": "Tarifs::Price", - "unit": "CHF", + "unit": "nach Betrag", "unit_i18n": { "de": "nach Betrag" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "invoice", "offer", @@ -342,6 +438,10 @@ "label_i18n": { "de": "Strom (Hochtarif)" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 284, "pin": false, "prefill_usage_method": null, @@ -353,9 +453,12 @@ "de": "kWh" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "invoice", "offer", @@ -365,6 +468,10 @@ "label_i18n": { "de": "Reservationsgebühr" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 285, "pin": true, "prefill_usage_method": "flat", @@ -376,9 +483,12 @@ "de": "Pauschale" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "invoice", "offer", @@ -388,6 +498,10 @@ "label_i18n": { "de": "Strom (Niedertarif)" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 286, "pin": false, "prefill_usage_method": null, @@ -399,9 +513,12 @@ "de": "kWh" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "invoice", "offer", @@ -411,6 +528,10 @@ "label_i18n": { "de": "Brennholz" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 287, "pin": false, "prefill_usage_method": null, @@ -422,9 +543,12 @@ "de": "Harass" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null }, { + "accounting_account_nr": null, + "accounting_cost_center_nr": null, "associated_types": [ "invoice", "offer", @@ -434,6 +558,10 @@ "label_i18n": { "de": "Abfall" }, + "minimum_price_per_night": null, + "minimum_price_total": null, + "minimum_usage_per_night": null, + "minimum_usage_total": null, "ordinal": 288, "pin": false, "prefill_usage_method": null, @@ -445,7 +573,9 @@ "de": "pro 60 L Sack" }, "valid_from": "2019-05-28T11:03:52+02:00", - "valid_until": null + "valid_until": null, + "vat_category": null } - ] + ], + "tenant_ref_template": "%d" } From de8dcbebd668b6f338d011a3d7f443bb989820a5 Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Wed, 18 Dec 2024 16:57:08 +0000 Subject: [PATCH 15/30] feat: add journal entries controller --- Gemfile.lock | 26 ++++----- .../manage/journal_entries_controller.rb | 45 +++++++++++++++ app/models/ability.rb | 16 ++++++ .../data_digest_templates/journal_entry.rb | 2 +- app/models/invoice.rb | 21 ++++--- app/models/invoice_part.rb | 8 +++ app/models/invoice_part/factory.rb | 22 +++---- app/models/invoice_parts/deposit.rb | 16 +++--- app/models/journal_entry.rb | 57 +++++-------------- app/models/occupiable_settings.rb | 1 + app/models/organisation_user.rb | 2 +- app/models/vat_category.rb | 14 ++++- app/params/manage/occupiable_params.rb | 2 +- app/services/taf_block.rb | 15 +++-- .../manage/journal_entries/_form.html.slim | 26 +++++++++ .../manage/journal_entries/edit.html.slim | 5 ++ .../manage/journal_entries/index.html.slim | 38 +++++++++++++ .../manage/journal_entries/new.html.slim | 5 ++ app/views/manage/occupiables/_form.html.slim | 1 + app/views/manage/payments/_form.html.slim | 2 +- app/views/manage/payments/import.html.slim | 4 +- config/locales/de.yml | 16 +++++- config/routes.rb | 1 + 23 files changed, 248 insertions(+), 97 deletions(-) create mode 100644 app/controllers/manage/journal_entries_controller.rb create mode 100644 app/views/manage/journal_entries/_form.html.slim create mode 100644 app/views/manage/journal_entries/edit.html.slim create mode 100644 app/views/manage/journal_entries/index.html.slim create mode 100644 app/views/manage/journal_entries/new.html.slim diff --git a/Gemfile.lock b/Gemfile.lock index e3875b81e..2376f6d9c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,8 +1,8 @@ GIT remote: https://github.com/puzzle/prawn-markup - revision: 1279dd4a636e534a1752ca0b60de89b1c6f31ae9 + revision: 76d90979b9880e10716b76ec9c929f56cbc529d0 specs: - prawn-markup (1.0.0) + prawn-markup (1.0.1) nokogiri prawn prawn-table @@ -93,7 +93,7 @@ GEM rake (>= 0.8.7) ast (2.4.2) aws-eventstream (1.3.0) - aws-partitions (1.1023.0) + aws-partitions (1.1024.0) aws-sdk-core (3.214.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -184,9 +184,9 @@ GEM discard (1.4.0) activerecord (>= 4.2, < 9.0) docile (1.4.1) - dotenv (3.1.6) - dotenv-rails (3.1.6) - dotenv (= 3.1.6) + dotenv (3.1.7) + dotenv-rails (3.1.7) + dotenv (= 3.1.7) railties (>= 6.1) drb (2.2.1) dry-cli (1.2.0) @@ -276,7 +276,7 @@ GEM ice_cube (0.17.0) interception (0.5) io-console (0.8.0) - irb (1.14.2) + irb (1.14.3) rdoc (>= 4.0.0) reline (>= 0.4.2) jmespath (1.6.2) @@ -317,7 +317,7 @@ GEM mutex_m (0.3.0) net-http (0.6.0) uri - net-imap (0.5.1) + net-imap (0.5.2) date net-protocol net-pop (0.1.2) @@ -354,7 +354,7 @@ GEM pry-rescue (1.6.0) interception (>= 0.5) pry (>= 0.12.0) - psych (5.2.1) + psych (5.2.2) date stringio public_suffix (6.0.1) @@ -414,7 +414,7 @@ GEM ffi (~> 1.0) rbs (3.7.0) logger - rdoc (6.9.0) + rdoc (6.10.0) psych (>= 4.0.0) react-rails (3.2.1) babel-transpiler (>= 0.7.0) @@ -427,7 +427,7 @@ GEM redis-client (0.23.0) connection_pool regexp_parser (2.9.3) - reline (0.5.12) + reline (0.6.0) io-console (~> 0.5) request_store (1.7.0) rack (>= 1.4) @@ -494,7 +494,7 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - sidekiq (7.3.6) + sidekiq (7.3.7) connection_pool (>= 2.3.0) logger rack (>= 2.2.4) @@ -519,7 +519,7 @@ GEM actionpack (>= 3.1) railties (>= 3.1) slim (>= 3.0, < 6.0, != 5.0.0) - sorbet-runtime (0.5.11694) + sorbet-runtime (0.5.11704) squasher (0.8.0) statesman (12.1.0) statsd-ruby (1.5.0) diff --git a/app/controllers/manage/journal_entries_controller.rb b/app/controllers/manage/journal_entries_controller.rb new file mode 100644 index 000000000..7d910a297 --- /dev/null +++ b/app/controllers/manage/journal_entries_controller.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Manage + class JournalEntriesController < BaseController + load_and_authorize_resource :journal_entry + + def index + @journal_entries = @journal_entries.joins(invoice: :booking).ordered + .where(invoices: { bookings: { organisation: current_organisation } }) + respond_with :manage, @journal_entries + end + + def new + respond_with :manage, @journal_entry + end + + def edit + respond_with :manage, @journal_entry + end + + def create + @journal_entry.assign_attributes(journal_entry_params) + @journal_entry.currency ||= current_organisation.currency + @journal_entry.save + respond_with :manage, location: manage_journal_entries_path + end + + def update + @journal_entry.update(journal_entry_params) + respond_with :manage, location: manage_journal_entries_path + end + + def destroy + @journal_entry.destroy + respond_with :manage, @journal_entry, location: manage_journal_entries_path + end + + private + + def journal_entry_params + params.require(:journal_entry).permit(*%i[invoice_id source_type source_id vat_category_id account_nr side amount + date text currency ordinal source_document_ref book_type]) + end + end +end diff --git a/app/models/ability.rb b/app/models/ability.rb index 871393e4c..217afa952 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -43,6 +43,8 @@ class Manage < Base can(:manage, OrganisationUser, organisation:) can(:manage, Tarif, organisation:) can(:manage, VatCategory, organisation:) + can(:manage, JournalEntry, invoice: { booking: { organisation: } }) + can(:new, JournalEntry) end role :manager do |user, organisation| @@ -66,6 +68,20 @@ class Manage < Base can(:manage, Usage, booking: { organisation: }) can(:manage, RichTextTemplate, organisation:) can(:read, PlanBBackup, organisation:) + can(:read, JournalEntry, invoice: { booking: { organisation: } }) + end + + role :treasurer do |user, organisation| + next unless user.in_organisation?(organisation) + + abilities_for_role(:readonly, user, organisation) + can(:manage, Payment, booking: { organisation: }) + can(:manage, Invoice, booking: { organisation: }) + can(:manage, InvoicePart, invoice: { booking: { organisation: } }) + can(:manage, Tenant, organisation:) + can(:manage, VatCategory, organisation:) + can(:manage, JournalEntry, invoice: { booking: { organisation: } }) + can(:new, JournalEntry) end role :readonly do |user, organisation| diff --git a/app/models/data_digest_templates/journal_entry.rb b/app/models/data_digest_templates/journal_entry.rb index 60a422ecd..3ee2a2ea9 100644 --- a/app/models/data_digest_templates/journal_entry.rb +++ b/app/models/data_digest_templates/journal_entry.rb @@ -93,7 +93,7 @@ def filter_class def base_scope @base_scope ||= ::JournalEntry.joins(:booking).where(bookings: { organisation_id: organisation }) - .includes(booking: :organisation).order(date: :ASC) + .includes(booking: :organisation).ordered end end end diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 654c63761..0843518d0 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -49,12 +49,12 @@ class Invoice < ApplicationRecord has_many :superseded_by_invoices, class_name: :Invoice, dependent: :nullify, foreign_key: :supersede_invoice_id, inverse_of: :supersede_invoice has_many :payments, dependent: :nullify - has_many :journal_entries, dependent: :destroy + has_many :journal_entries, dependent: :destroy, inverse_of: :invoice has_one :organisation, through: :booking has_one_attached :pdf - attr_accessor :skip_generate_pdf + attr_accessor :skip_generate_pdf, :skip_generate_journal_entries scope :ordered, -> { order(payable_until: :ASC, created_at: :ASC) } scope :unpaid, -> { kept.where(arel_table[:amount_open].gt(0)) } @@ -73,8 +73,8 @@ class Invoice < ApplicationRecord after_create :supersede! before_update :generate_pdf, if: :generate_pdf? after_save :recalculate! - # after_save :process_journal_entries - # after_discard :process_journal_entries + after_save :generate_journal_entries, if: :generate_journal_entries? + after_discard :generate_journal_entries, if: :generate_journal_entries? delegate :currency, to: :organisation @@ -118,9 +118,16 @@ def generate_payment_ref self.payment_ref = RefBuilders::InvoicePayment.new(self).generate if payment_ref.blank? end - def process_journal_entries - # GenerateJournalEntriesJob.perform_later(self) - JournalEntry.process_invoice(self) + def generate_journal_entries? + !skip_generate_journal_entries && (changed? || journal_entries.none?) + end + + def generate_journal_entries + existing_journal_entry_ids = reload.journal_entry_ids + new_journal_entries = JournalEntry::Factory.new.invoice(self) + + # raise ActiveRecord::Rollback unless + new_journal_entries.save && JournalEntry.where(id: existing_journal_entry_ids, invoice: self).destroy_all end def paid? diff --git a/app/models/invoice_part.rb b/app/models/invoice_part.rb index 5124fa3cf..2b8dbb3ef 100644 --- a/app/models/invoice_part.rb +++ b/app/models/invoice_part.rb @@ -60,6 +60,14 @@ def vat_breakdown @vat_breakdown ||= vat_category&.breakdown(amount) || { brutto: amount, netto: amount, vat: 0 } end + def accounting_cost_center_nr + @accounting_cost_center_nr ||= if super.to_s == 'home' + invoice.booking.home&.settings&.accounting_cost_center_nr.presence + else + super.presence + end + end + def sum_of_predecessors invoice.invoice_parts.ordered.inject(0) do |sum, invoice_part| break sum if invoice_part == self diff --git a/app/models/invoice_part/factory.rb b/app/models/invoice_part/factory.rb index b74e313b8..793696060 100644 --- a/app/models/invoice_part/factory.rb +++ b/app/models/invoice_part/factory.rb @@ -43,26 +43,26 @@ def from_unassigned_payments # rubocop:disable Metrics/MethodLength [ InvoiceParts::Deposit.new(apply:, label: I18n.t('invoice_parts.unassigned_payments_amount'), - amount: - payed_amount, unassigned_payments:, + amount: - payed_amount, reassign_payments: unassigned_payments, vat_category_id: accounting_settings.rental_yield_vat_category_id, accounting_account_nr: accounting_settings.rental_yield_account_nr, - accounting_cost_center_nr: 9009) + accounting_cost_center_nr: :home) ] end def from_deposits # rubocop:disable Metrics/AbcSize - deposits = booking.invoices.deposits.kept.where(Invoice.arel_table[:amount].gt(0)) - return [] unless deposits.any? && invoice.new_record? && invoice.is_a?(Invoices::Invoice) + deposited_payments = Payment.joins(:invoice).where(invoice: { type: Invoices::Deposit.sti_name, + discarded_at: nil }, booking:, write_off: false) + deposited_amount = deposited_payments.sum(:amount) + return [] unless deposited_amount.positive? && invoice.new_record? && invoice.is_a?(Invoices::Invoice) apply = invoice.invoice_parts.none? - [InvoiceParts::Text.new(apply:, label: Invoices::Deposit.model_name.human)] + - [ - InvoiceParts::Deposit.new(apply:, label: I18n.t('invoice_parts.deposited_amount'), amount: - deposited_amount, - vat_category_id: accounting_settings.rental_yield_vat_category_id, - accounting_account_nr: accounting_settings.rental_yield_account_nr, - accounting_cost_center_nr: nil) - ] + [InvoiceParts::Text.new(apply:, label: Invoices::Deposit.model_name.human), + InvoiceParts::Deposit.new(apply:, label: I18n.t('invoice_parts.deposited_amount'), amount: - deposited_amount, + vat_category_id: accounting_settings.rental_yield_vat_category_id, + accounting_account_nr: accounting_settings.rental_yield_account_nr, + accounting_cost_center_nr: :home)] end def from_supersede_invoice diff --git a/app/models/invoice_parts/deposit.rb b/app/models/invoice_parts/deposit.rb index 455d539e7..0faf9c950 100644 --- a/app/models/invoice_parts/deposit.rb +++ b/app/models/invoice_parts/deposit.rb @@ -29,21 +29,19 @@ module InvoiceParts class Deposit < Add InvoicePart.register_subtype self - attr_accessor :unassigned_payments + attr_accessor :reassign_payments - after_save :assign_payments + after_save :reassign_payments! - attribute :vat_category_id, default: lambda { |invoice_part| - invoice_part.organisation&.accounting_settings&.rental_yield_vat_category_id - } attribute :accounting_account_nr, default: -> { organisation&.accounting_settings&.rental_yield_account_nr } + attribute :vat_category_id, default: (lambda do |invoice_part| + invoice_part.organisation&.accounting_settings&.rental_yield_vat_category_id + end) - def assign_payments + def reassign_payments!(payments = reassign_payments) return unless valid? && apply - unassigned_payments&.each do |payment| - payment&.update(invoice:) - end + payments&.each { |payment| payment&.update!(invoice:) } end end end diff --git a/app/models/journal_entry.rb b/app/models/journal_entry.rb index 7ecbab037..ff3bc869c 100644 --- a/app/models/journal_entry.rb +++ b/app/models/journal_entry.rb @@ -29,7 +29,7 @@ # frozen_string_literal: true class JournalEntry < ApplicationRecord - belongs_to :invoice + belongs_to :invoice, inverse_of: :journal_entries belongs_to :vat_category, optional: true has_one :booking, through: :invoice @@ -38,7 +38,9 @@ class JournalEntry < ApplicationRecord enum :side, { soll: 1, haben: -1 } enum :book_type, { main: 0, cost: 1, vat: 2 }, prefix: true, default: :main - validates :account_nr, :side, :amount, :source_document_ref, presence: true + validates :account_nr, :side, :amount, :source_document_ref, :currency, presence: true + + scope :ordered, -> { order(date: :ASC, created_at: :ASC) } def invert return self.side = :soll if haben? @@ -63,16 +65,12 @@ def haben_amount amount if haben? end - def self.collect(**defaults, &) - Collection.new(**defaults).tap(&) + def parallels + JournalEntry.where(invoice:, source_type:, source_id:).index_by(&:book_type).symbolize_keys end - def self.process_invoice(invoice) - existing_journal_entry_ids = invoice.reload.journal_entry_ids - new_journal_entries = JournalEntry::Factory.new.invoice(invoice) - - # raise ActiveRecord::Rollback unless - new_journal_entries.save && where(id: existing_journal_entry_ids, invoice:).destroy_all + def self.collect(**defaults, &) + Collection.new(**defaults).tap(&) end class Collection @@ -100,12 +98,12 @@ def soll(**args) collect(side: :soll, **args) end - def soll_amount - journal_entries.map { _1.soll_amount || 0 }.sum + def soll_amount(book_type: %i[main vat]) + journal_entries.filter_map { _1.soll_amount || 0 if Array.wrap(book_type).include?(_1.book_type) }.sum end - def haben_amount - amount if haben? + def haben_amount(book_type: %i[main vat]) + journal_entries.filter_map { _1.haben_amount || 0 if Array.wrap(book_type).include?(_1.book_type) }.sum end def balanced? @@ -172,41 +170,16 @@ def invoice_part(invoice_part, collection) def invoice_part_add(invoice_part, collection) # rubocop:disable Metrics/AbcSize invoice_part.instance_eval do - defaults = { source_type: self.class.sti_name, source_id: id, text: "#{invoice.ref} #{label}" } + defaults = { source_type: self.class.sti_name, source_id: id, vat_category:, text: "#{invoice.ref} #{label}" } collection.haben(**defaults, account_nr: accounting_account_nr, amount: vat_breakdown[:netto]) - collection.haben(**defaults, account_nr: accounting_cost_center_nr, book_type: :cost, - amount: vat_breakdown[:netto]) + collection.haben(**defaults, account_nr: accounting_cost_center_nr, + book_type: :cost, amount: vat_breakdown[:netto]) collection.haben(**defaults, account_nr: vat_category&.organisation&.accounting_settings&.vat_account_nr, book_type: :vat, amount: vat_breakdown[:vat]) end end - # def invoice_part_deposit(invoice_part) - # invoice = invoice_part.invoice - # cost_center_nr = invoice_part.accounting_cost_center_nr.presence - # vat_account_nr = invoice.organisation.accounting_settings.vat_account_nr.presence - - # JournalEntry.collect(date: invoice.issued_at, invoice: invoice_part.invoice, source_document_ref: invoice.ref, - # source_type: invoice_part.class.sti_name, source_id: id, - # text: "#{invoice.ref} #{invoice_part.label}") do - # haben(account_nr:, amount: breakdown[:netto]) if accounting_account_nr.present? - # haben(account_nr: cost_center_nr, book_type: :cost, amount: breakdown[:netto]) if cost_center_nr - # haben(account_nr:, book_type: :vat, amount: breakdown[:vat]) if vat_category.present? && vat_account_nr - # end - # end - - # def invoice_part_deposit(invoice_part) - # invoice_part.instance_eval do - # account_nr = tarif&.accounting_account_nr - # return if account_nr.blank? - - # invoice_part_add(invoice_part).tap do |journal_entry| - # journal_entry.assign_attributes(account_nr:, side: :haben, amount: -amount) - # end - # end - # end - # def kept_payment(payment) # payment.instance_eval do # account_nr = organisation.accounting_settings.payment_account_nr diff --git a/app/models/occupiable_settings.rb b/app/models/occupiable_settings.rb index e24132a76..8401db36a 100644 --- a/app/models/occupiable_settings.rb +++ b/app/models/occupiable_settings.rb @@ -2,6 +2,7 @@ class OccupiableSettings < Settings attribute :booking_margin, DurationType.new, default: 0 + attribute :accounting_cost_center_nr validates :booking_margin, numericality: { less_than_or_equal: 5.years, greater_than_or_equal: 0 } end diff --git a/app/models/organisation_user.rb b/app/models/organisation_user.rb index a6b0c9132..0718f9099 100644 --- a/app/models/organisation_user.rb +++ b/app/models/organisation_user.rb @@ -23,7 +23,7 @@ class OrganisationUser < ApplicationRecord belongs_to :organisation, inverse_of: :organisation_users belongs_to :user, inverse_of: :organisation_users, autosave: true - enum :role, { none: 0, readonly: 1, admin: 2, manager: 3 }, prefix: :role + enum :role, { none: 0, readonly: 1, admin: 2, manager: 3, treasurer: 4 }, prefix: :role has_secure_token :token, length: 48 diff --git a/app/models/vat_category.rb b/app/models/vat_category.rb index 0c368c9cf..4a383a924 100644 --- a/app/models/vat_category.rb +++ b/app/models/vat_category.rb @@ -20,9 +20,17 @@ def to_s "#{label} (#{formatted_percentage})" end - def breakdown(amount) + def breakdown(brutto) vat = 0 - vat = (amount / (100 + percentage)) * percentage if percentage.present? - { vat:, brutto: amount, netto: (amount - vat) } + vat = (brutto / (100 + percentage)) * percentage if percentage.present? + { vat:, brutto: brutto, netto: (brutto - vat) } + end + + def breakup(brutto: nil, netto: nil, vat: nil) + return breakdown(brutto) if brutto.present? + return breakdown((netto / 100) * (100 + percentage)) if netto.present? + return breakdown((vat / percentage) * (100 + percentage)) if vat.present? && percentage&.positive? + + nil end end diff --git a/app/params/manage/occupiable_params.rb b/app/params/manage/occupiable_params.rb index 652f15a4a..5640cd8e9 100644 --- a/app/params/manage/occupiable_params.rb +++ b/app/params/manage/occupiable_params.rb @@ -5,7 +5,7 @@ class OccupiableParams < ApplicationParams def self.permitted_keys %i[name description occupiable type ref home_id ordinal_position] + I18n.available_locales.map { |locale| ["name_#{locale}", "description_#{locale}"] }.flatten + - [{ settings: %i[booking_margin] }] + [{ settings: %i[booking_margin accounting_cost_center_nr] }] end end end diff --git a/app/services/taf_block.rb b/app/services/taf_block.rb index e2d9c676d..94db405d7 100644 --- a/app/services/taf_block.rb +++ b/app/services/taf_block.rb @@ -109,7 +109,7 @@ def self.derive(value, **override) instance_exec(value, **override, &derive_block) if derive_block.present? end - derive_from JournalEntry do |journal_entry, _index: nil, **override| + derive_from JournalEntry do |journal_entry, **override| new(:Bk, **{ # The Id of a book keeping account. [Fibu-Konto] AccId: Value.cast(journal_entry.account_nr, as: :symbol), @@ -138,7 +138,7 @@ def self.derive(value, **override) Flags: nil, # String[5]; The Id of the tax. [MWSt-Kürzel] - TaxId: journal_entry.vat_category&.accounting_vat_code, + TaxId: (journal_entry.book_type_main? && journal_entry.vat_category&.accounting_vat_code) || nil, # MkTxB: journal_entry.vat_category&.accounting_vat_code.present?, @@ -170,7 +170,8 @@ def self.derive(value, **override) # ValBt: journal_entry.amount, # Currency; The tax amount for this booking. [Steuer-Betrag] - # ValTx: journal_entry.amount_type&.to_sym == :tax ? journal_entry.amount : nil, + ValTx: journal_entry.book_type_vat? && + journal_entry.vat_category&.breakup(vat: journal_entry.amount)&.[](:netto), # Currency; The gross amount for this booking in the foreign currency specified # by currency of the account AccId. [FW-Betrag] @@ -196,8 +197,12 @@ def self.derive(value, **override) new(:OPd, **{ PkKey: pk_key, OpId: op_id, ZabId: '15T' }), new(:Blg, **{ Date: invoice.issued_at, Orig: true }) do # TODO: check if invoice == source - derive(journal_entries.shift, index: 1, Flags: 1, OpId: op_id, PkKey: pk_key) - journal_entries.each_with_index { |journal_entry, index| derive(journal_entry, index: index + 1) } + derive(journal_entries.shift, Idx: 1, Flags: 1, OpId: op_id, PkKey: pk_key) + journal_entries.each_with_index do |journal_entry, index| + cost_index = (journal_entry.book_type_main? && journal_entries.index(journal_entry.parallels[:cost])) || nil + vat_index = (journal_entry.book_type_main? && journal_entries.index(journal_entry.parallels[:vat])) || nil + derive(journal_entry, Idx: index + 2, CIdx: cost_index&.+(index + 2), TIdx: vat_index&.+(index + 2)) + end end ] end diff --git a/app/views/manage/journal_entries/_form.html.slim b/app/views/manage/journal_entries/_form.html.slim new file mode 100644 index 000000000..eea62d2ee --- /dev/null +++ b/app/views/manage/journal_entries/_form.html.slim @@ -0,0 +1,26 @@ += form_with(model: [:manage, @journal_entry], local: true) do |f| + + fieldset + = f.collection_select :invoice_id, @current_organisation.invoices, :id, :ref, include_blank: true, required: false + + fieldset + = f.text_field :account_nr + = f.select :side, JournalEntry.sides.keys.map { [JournalEntry.human_enum(:side, _1), _1] }, include_blank: true + = f.text_field :amount, step: 0.1, inputmode: "numeric" + = f.date_field :date + = f.text_field :source_document_ref + = f.text_area :text, rows: 2 + = f.select :book_type, JournalEntry.book_types.keys.map { [JournalEntry.human_enum(:book_types, _1), _1] }, include_blank: true + + - if current_organisation.vat_categories.any? + = f.collection_select :vat_category_id, current_organisation.vat_categories, :id, :to_s, include_blank: true + + fieldset.d-none + = f.text_field :source_type + = f.text_field :source_id + = f.text_field :ordinal + = f.hidden_field :currency + + .form-actions.pt-4.mt-3 + = f.submit + = link_to t(:back), manage_journal_entries_path, class: 'btn btn-default' diff --git a/app/views/manage/journal_entries/edit.html.slim b/app/views/manage/journal_entries/edit.html.slim new file mode 100644 index 000000000..f6dd6ddc9 --- /dev/null +++ b/app/views/manage/journal_entries/edit.html.slim @@ -0,0 +1,5 @@ +.row.justify-content-center + .col-lg-8 + .card.shadow-sm + .card-body + == render 'form' diff --git a/app/views/manage/journal_entries/index.html.slim b/app/views/manage/journal_entries/index.html.slim new file mode 100644 index 000000000..5e3f46d61 --- /dev/null +++ b/app/views/manage/journal_entries/index.html.slim @@ -0,0 +1,38 @@ + +h1.mt-0.mb-5= JournalEntry.model_name.human(count: 2) + +.table-responsive + table.table.table-hover.align-middle + thead + tr + th=JournalEntry.human_attribute_name(:date) + th=JournalEntry.human_attribute_name(:source_document_ref) + th=JournalEntry.human_attribute_name(:soll_account) + th=JournalEntry.human_attribute_name(:haben_account) + th=JournalEntry.human_attribute_name(:amount) + th=JournalEntry.human_attribute_name(:book_type) + th=Invoice.model_name.human + th + + tbody.shadow-sm + - @journal_entries.each do |journal_entry| + tr + td= l(journal_entry.date, format: :default) + td= journal_entry.source_document_ref + td= journal_entry.soll_account + td= journal_entry.haben_account + td= number_to_currency(journal_entry.amount, unit: journal_entry.currency) + td= JournalEntry.human_enum(:book_types, journal_entry.book_type) + td= link_to journal_entry.invoice.ref, manage_invoice_path(journal_entry.invoice) + td.p-1.text-end + .btn-group + = link_to edit_manage_journal_entry_path(journal_entry), class: 'btn btn-default' do + span.fa.fa-edit + = link_to manage_journal_entry_path(journal_entry), data: { confirm: t(:confirm) }, method: :delete, title: t(:destroy), class: 'btn btn-default' do + span.fa.fa-trash + +br += link_to new_manage_journal_entry_path, class: 'btn btn-primary' do + span.fa.fa-file-o + ' + = t(:add_record, model_name: JournalEntry.model_name.human) diff --git a/app/views/manage/journal_entries/new.html.slim b/app/views/manage/journal_entries/new.html.slim new file mode 100644 index 000000000..f6dd6ddc9 --- /dev/null +++ b/app/views/manage/journal_entries/new.html.slim @@ -0,0 +1,5 @@ +.row.justify-content-center + .col-lg-8 + .card.shadow-sm + .card-body + == render 'form' diff --git a/app/views/manage/occupiables/_form.html.slim b/app/views/manage/occupiables/_form.html.slim index c26a39808..95120aafe 100644 --- a/app/views/manage/occupiables/_form.html.slim +++ b/app/views/manage/occupiables/_form.html.slim @@ -26,6 +26,7 @@ = f.fields_for :settings, @occupiable.settings || OccupiableSettings.new do |sf| = sf.text_field :booking_margin, value: sf.object.booking_margin&.iso8601 + = sf.text_field :accounting_cost_center_nr, value: sf.object.accounting_cost_center_nr .form-actions.pt-4.mt-3 = f.submit diff --git a/app/views/manage/payments/_form.html.slim b/app/views/manage/payments/_form.html.slim index b0e4037bd..a44d80393 100644 --- a/app/views/manage/payments/_form.html.slim +++ b/app/views/manage/payments/_form.html.slim @@ -2,7 +2,7 @@ fieldset - invoices = @payment.organisation.invoices.includes(:organisation).kept.unsettled - = f.collection_select :invoice_id, [@payment.invoice, invoices].flatten.compact.uniq, :id, :to_s, include_blank: true + = f.collection_select :invoice_id, [@payment.invoice, invoices].flatten.compact.uniq, :id, :ref, include_blank: true = f.text_field :amount, inputmode: "numeric" = f.date_field :paid_at, lang: I18n.locale = f.text_area :remarks diff --git a/app/views/manage/payments/import.html.slim b/app/views/manage/payments/import.html.slim index 6e921080b..1a849ce4c 100644 --- a/app/views/manage/payments/import.html.slim +++ b/app/views/manage/payments/import.html.slim @@ -29,9 +29,9 @@ = pf.hidden_field :camt_instr_id = pf.hidden_field :data td.py-2 - = pf.collection_select :invoice_id, @invoices, :id, :to_s, skip_label: true, include_blank: true, required: false + = pf.collection_select :invoice_id, @invoices, :id, :ref, skip_label: true, include_blank: true, required: false td.py-2 - = pf.collection_select :booking_id, @bookings, :id, :to_s, skip_label: true, include_blank: true, required: false + = pf.collection_select :booking_id, @bookings, :id, :ref, skip_label: true, include_blank: true, required: false td.py-2 = pf.text_field :amount, skip_label: true, inputmode: "numeric" td.py-2 diff --git a/config/locales/de.yml b/config/locales/de.yml index e1304b1b8..db76c3bbf 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -224,6 +224,8 @@ de: text: Text type: Rechnungsart invoice_part: + accounting_account_nr: Buchhaltungskonto + accounting_cost_center_nr: Kostenstelle amount: Betrag apply: Anwenden breakdown: Aufschlüsselung @@ -231,18 +233,22 @@ de: ordinal_position: Ordnungsnummer type: Art der Rechnungsposition vat: inkl. MwSt. in Prozent + vat_category_id: MwSt. Kategorie journal_entry: + account_nr: Konto amount: Betrag book_type: Buch booking_id: Buchung date: Datum haben_account: Haben Konto haben_amount: Haben Betrag + invoice_id: Rechnung side: Seite soll_account: Soll Konto soll_amount: Soll Betrag source_document_ref: Beleg text: Text + vat_category_id: MwSt. Kategorie mail_template: meter_reading_period: begins_at: Beginn @@ -348,7 +354,7 @@ de: title: Titel tarif: accounting_account_nr: Buchhaltungskonto - accounting_cost_center_nr: Profit-Center + accounting_cost_center_nr: Kostenstelle associated_types: Ausgewiesen in Dokumenten enabling_conditions: Bedingungen für erlaubte Auswahl label: Tarifbezeichnung @@ -492,6 +498,10 @@ de: only_paid: Nur beglichene only_unpaid: Nur offene journal_entry: + book_types: + cost: Kostenstellen + main: Hauptbuch + vat: Steuern side: haben: Haben soll: Soll @@ -528,6 +538,7 @@ de: manager: Verwaltung none: "-" readonly: Lesend + treasurer: Kassier tarif: prefill_usage_methods: days: "× Tage" @@ -691,6 +702,9 @@ de: invoices/offer: one: Offerte other: Offerten + journal_entry: + one: Buchungssatz + other: Buchungssätze mail_template: one: Mailvorlage other: Mailvorlagen diff --git a/config/routes.rb b/config/routes.rb index b79e60c21..76da3fcc7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -60,6 +60,7 @@ resources :booking_agents resources :booking_categories, except: :show resources :vat_categories, except: :show + resources :journal_entries, except: %i[show] resources :notifications, only: %i[index] resources :rich_text_templates do post :create_missing, on: :collection From c5ae1c19bad1a8c18b663b70adb2673ebc380fef Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Thu, 19 Dec 2024 09:04:47 +0000 Subject: [PATCH 16/30] fix: ci --- Dockerfile | 2 +- app/models/accounting_settings.rb | 14 +++++++------- app/models/invoice.rb | 13 ++++++++----- app/services/taf_block.rb | 7 ++++--- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2d2eacc39..c28965931 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ ### === base === ### FROM ruby:3.3.6-alpine AS base -RUN apk add --no-cache --update postgresql-dev tzdata nodejs npm +RUN apk add --no-cache --update postgresql-dev tzdata nodejs npm git RUN gem install bundler WORKDIR /rails diff --git a/app/models/accounting_settings.rb b/app/models/accounting_settings.rb index a8421420c..7c3de5d1e 100644 --- a/app/models/accounting_settings.rb +++ b/app/models/accounting_settings.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true class AccountingSettings < Settings - attribute :tenant_debitor_account_nr_base, :integer, default: -> { 0 } - attribute :debitor_account_nr, :string, default: -> { 1050 } - attribute :rental_yield_account_nr, :string, default: -> { 6000 } - attribute :rental_yield_vat_category_id, :integer - attribute :currency_account_nr, :string - attribute :vat_account_nr, :string, default: -> { 2016 } - attribute :default_payment_account_nr, :string, default: -> { 2512 } + attribute :enabled, :boolean, default: -> { false } + attribute :debitor_account_nr, :string # , default: -> { 1050 } + attribute :rental_yield_account_nr, :string # , default: -> { 6000 } + attribute :rental_yield_vat_category_id # , :integer + attribute :currency_account_nr # , :string + attribute :vat_account_nr, :string # , default: -> { 2016 } + attribute :default_payment_account_nr # , :string, default: -> { 2512 } end diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 0843518d0..59d10cf92 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -73,8 +73,8 @@ class Invoice < ApplicationRecord after_create :supersede! before_update :generate_pdf, if: :generate_pdf? after_save :recalculate! - after_save :generate_journal_entries, if: :generate_journal_entries? - after_discard :generate_journal_entries, if: :generate_journal_entries? + after_save :generate_journal_entries!, if: :generate_journal_entries? + after_discard :generate_journal_entries!, if: :generate_journal_entries? delegate :currency, to: :organisation @@ -119,15 +119,17 @@ def generate_payment_ref end def generate_journal_entries? - !skip_generate_journal_entries && (changed? || journal_entries.none?) + organisation.accounting_settings.enabled && !skip_generate_journal_entries && (changed? || journal_entries.none?) end - def generate_journal_entries + def generate_journal_entries! + return unless organisation.accounting_settings.enabled + existing_journal_entry_ids = reload.journal_entry_ids new_journal_entries = JournalEntry::Factory.new.invoice(self) # raise ActiveRecord::Rollback unless - new_journal_entries.save && JournalEntry.where(id: existing_journal_entry_ids, invoice: self).destroy_all + new_journal_entries.save! && JournalEntry.where(id: existing_journal_entry_ids, invoice: self).destroy_all! end def paid? @@ -155,6 +157,7 @@ def recalculate self.amount_open = amount - amount_paid end + # TODO: describe why this is needed def recalculate! recalculate save! if amount_changed? || amount_open_changed? diff --git a/app/services/taf_block.rb b/app/services/taf_block.rb index 94db405d7..574ea0c8c 100644 --- a/app/services/taf_block.rb +++ b/app/services/taf_block.rb @@ -124,7 +124,7 @@ def self.derive(value, **override) # CIdx: journal_entry.index, # String[9]; A user definable code. - Code: nil, + Code: journal_entry.id, # Date; The date of the booking. Date: journal_entry.date, @@ -197,11 +197,12 @@ def self.derive(value, **override) new(:OPd, **{ PkKey: pk_key, OpId: op_id, ZabId: '15T' }), new(:Blg, **{ Date: invoice.issued_at, Orig: true }) do # TODO: check if invoice == source - derive(journal_entries.shift, Idx: 1, Flags: 1, OpId: op_id, PkKey: pk_key) + derive(journal_entries.shift, Flags: 1, OpId: op_id, PkKey: pk_key) + journal_entries.each_with_index do |journal_entry, index| cost_index = (journal_entry.book_type_main? && journal_entries.index(journal_entry.parallels[:cost])) || nil vat_index = (journal_entry.book_type_main? && journal_entries.index(journal_entry.parallels[:vat])) || nil - derive(journal_entry, Idx: index + 2, CIdx: cost_index&.+(index + 2), TIdx: vat_index&.+(index + 2)) + derive(journal_entry, CIdx: cost_index&.+(index + 2), TIdx: vat_index&.+(index + 2)) end end ] From 9f4153d448f4cc9afade9577332adf2e30be55fd Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Thu, 19 Dec 2024 10:09:43 +0000 Subject: [PATCH 17/30] fix: refactor code from PR --- app/models/journal_entry.rb | 11 +++++++++++ app/models/payment.rb | 17 +++++++++++++++++ app/models/payment_infos/qr_bill.rb | 2 +- app/serializers/public/home_serializer.rb | 1 - .../public/vat_category_serializer.rb | 2 +- .../export/pdf/renderables/page_header.rb | 2 -- 6 files changed, 30 insertions(+), 5 deletions(-) diff --git a/app/models/journal_entry.rb b/app/models/journal_entry.rb index ff3bc869c..aebdd7c36 100644 --- a/app/models/journal_entry.rb +++ b/app/models/journal_entry.rb @@ -180,6 +180,17 @@ def invoice_part_add(invoice_part, collection) # rubocop:disable Metrics/AbcSize end end + def payment(payment) # rubocop:disable Metrics/AbcSize + payment.instance_eval do + JournalEntry.collect(currency: organisation.currency, source_document_ref: invoice.ref, + date: paid_at, invoice:, source_type: Payment.st_name, source_id: id, + text: "#{Payment.model_name.human} #{invoice.ref}") do |collection| + collection.haben(account_nr: organisation&.accounting_settings&.debitor_account_nr, amount:) + collection.soll(account_nr: organisation&.accounting_settings&.rental_yield_account_nr, amount: amount) + end + end + end + # def kept_payment(payment) # payment.instance_eval do # account_nr = organisation.accounting_settings.payment_account_nr diff --git a/app/models/payment.rb b/app/models/payment.rb index 09d74f2f8..b2953595e 100644 --- a/app/models/payment.rb +++ b/app/models/payment.rb @@ -46,6 +46,9 @@ class Payment < ApplicationRecord scope :ordered, -> { order(paid_at: :DESC) } scope :recent, -> { where(arel_table[:paid_at].gt(3.months.ago)) } + attr_accessor :skip_generate_journal_entries + + before_save :generate_journal_entries, if: :generate_journal_entries? after_create :confirm!, if: :confirm? after_destroy :recalculate_invoice after_save :recalculate_invoice @@ -66,6 +69,20 @@ def confirm! MailTemplate.use(:payment_confirmation_notification, booking, to: :tenant, context:, &:autodeliver!) end + def generate_journal_entries? + organisation.accounting_settings.enabled && !skip_generate_journal_entries && (changed? || journal_entries.none?) + end + + def generate_journal_entries! + return unless organisation.accounting_settings.enabled + + existing_journal_entry_ids = reload.journal_entry_ids + new_journal_entries = JournalEntry::Factory.new.payment(self) + + # raise ActiveRecord::Rollback unless + new_journal_entries.save! && JournalEntry.where(id: existing_journal_entry_ids).destroy_all! + end + def recalculate_invoice return if invoice.blank? diff --git a/app/models/payment_infos/qr_bill.rb b/app/models/payment_infos/qr_bill.rb index 4ffe19ac1..4a19703c9 100644 --- a/app/models/payment_infos/qr_bill.rb +++ b/app/models/payment_infos/qr_bill.rb @@ -107,7 +107,7 @@ def scor_ref end def qrr_ref - invoice.payment_ref.rjust(27, '0') + RefBuilders::InvoicePayment.with_checksum(invoice.payment_ref).rjust(27, '0') end def ref diff --git a/app/serializers/public/home_serializer.rb b/app/serializers/public/home_serializer.rb index 82ed6e582..ad108c3c1 100644 --- a/app/serializers/public/home_serializer.rb +++ b/app/serializers/public/home_serializer.rb @@ -2,7 +2,6 @@ module Public class HomeSerializer < OccupiableSerializer - # fields :accounting_account_nr association :occupiables, blueprint: Public::OccupiableSerializer do |home| home.occupiables.kept end diff --git a/app/serializers/public/vat_category_serializer.rb b/app/serializers/public/vat_category_serializer.rb index 4f8d770d5..f8bd2528d 100644 --- a/app/serializers/public/vat_category_serializer.rb +++ b/app/serializers/public/vat_category_serializer.rb @@ -3,6 +3,6 @@ module Public class VatCategorySerializer < ApplicationSerializer identifier :id - fields :label, :label_i18n, :percentage, :accounting_vat_code, :to_s + fields :label, :label_i18n, :percentage, :accounting_vat_code end end diff --git a/app/services/export/pdf/renderables/page_header.rb b/app/services/export/pdf/renderables/page_header.rb index f14fd8263..9028efe8f 100644 --- a/app/services/export/pdf/renderables/page_header.rb +++ b/app/services/export/pdf/renderables/page_header.rb @@ -30,8 +30,6 @@ def render_logo image_source ||= Rails.root.join('app/javascript/images/logo.png') image image_source, at: [bounds.left, bounds.top + 80], width: 200, height: 45, fit: [200, 45] - rescue Aws::Errors::MissingRegionError - nil end end end From 1b5a6db89f9679f6718ddf2db91b0616a0dbb911 Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Thu, 19 Dec 2024 10:44:41 +0000 Subject: [PATCH 18/30] feat: add journal_entries for payments --- app/models/accounting_settings.rb | 11 +- app/models/invoice.rb | 5 +- app/models/journal_entry.rb | 29 +- app/models/organisation.rb | 1 + app/models/payment.rb | 6 +- db/schema.rb | 3 +- yarn.lock | 897 ++++++++++++++++-------------- 7 files changed, 498 insertions(+), 454 deletions(-) diff --git a/app/models/accounting_settings.rb b/app/models/accounting_settings.rb index 7c3de5d1e..beec1ba1f 100644 --- a/app/models/accounting_settings.rb +++ b/app/models/accounting_settings.rb @@ -2,10 +2,9 @@ class AccountingSettings < Settings attribute :enabled, :boolean, default: -> { false } - attribute :debitor_account_nr, :string # , default: -> { 1050 } - attribute :rental_yield_account_nr, :string # , default: -> { 6000 } - attribute :rental_yield_vat_category_id # , :integer - attribute :currency_account_nr # , :string - attribute :vat_account_nr, :string # , default: -> { 2016 } - attribute :default_payment_account_nr # , :string, default: -> { 2512 } + attribute :debitor_account_nr, :string + attribute :rental_yield_account_nr, :string + attribute :rental_yield_vat_category_id + attribute :vat_account_nr, :string + attribute :payment_account_nr end diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 59d10cf92..688e73ccb 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -125,11 +125,12 @@ def generate_journal_entries? def generate_journal_entries! return unless organisation.accounting_settings.enabled - existing_journal_entry_ids = reload.journal_entry_ids + existing_ids = organisation.journal_entry_ids.where(invoice: self).pluck(:id) new_journal_entries = JournalEntry::Factory.new.invoice(self) # raise ActiveRecord::Rollback unless - new_journal_entries.save! && JournalEntry.where(id: existing_journal_entry_ids, invoice: self).destroy_all! + new_journal_entries.save! && organisation.where(id: existing_ids, invoice: self).destroy_all + payments.each(&:generate_journal_entries!) end def paid? diff --git a/app/models/journal_entry.rb b/app/models/journal_entry.rb index aebdd7c36..4c176c394 100644 --- a/app/models/journal_entry.rb +++ b/app/models/journal_entry.rb @@ -114,8 +114,8 @@ def valid? journal_entries.all?(&:valid?) # && balanced? end - def save - valid? && journal_entries.all?(&:save) + def save! + valid? && journal_entries.all?(&:save!) end end @@ -142,16 +142,6 @@ def invoice(invoice) end end - # def discarded_invoice(invoice) - # previous_journal_entries = kept_invoice(invoice) - # return if previous_journal_entries.blank? - - # previous_journal_entries + kept_invoice(invoice).each do |journal_entry| - # journal_entry.invert - # journal_entry.date = invoice.discarded_at.to_date - # end - # end - def invoice_debitor(invoice, collection) invoice.instance_eval do defaults = { source_type: ::Invoice.sti_name, source_id: id } @@ -183,23 +173,12 @@ def invoice_part_add(invoice_part, collection) # rubocop:disable Metrics/AbcSize def payment(payment) # rubocop:disable Metrics/AbcSize payment.instance_eval do JournalEntry.collect(currency: organisation.currency, source_document_ref: invoice.ref, - date: paid_at, invoice:, source_type: Payment.st_name, source_id: id, + date: paid_at, invoice:, source_type: Payment.sti_name, source_id: id, text: "#{Payment.model_name.human} #{invoice.ref}") do |collection| + collection.soll(account_nr: organisation&.accounting_settings&.payment_account_nr, amount:) collection.haben(account_nr: organisation&.accounting_settings&.debitor_account_nr, amount:) - collection.soll(account_nr: organisation&.accounting_settings&.rental_yield_account_nr, amount: amount) end end end - - # def kept_payment(payment) - # payment.instance_eval do - # account_nr = organisation.accounting_settings.payment_account_nr - # return if account_nr.blank? || cost_account_nr.blank? - - # JournalEntry.new(account_nr:, date: paid_at, side: :soll, amount:, invoice:, currency: organisation.currency, - # source_document_ref: invoice.ref, text: "#{invoice.ref} #{self.class.model_name.human}", - # source_type: ::Payment.sti_name, source_id: id) - # end - # end end end diff --git a/app/models/organisation.rb b/app/models/organisation.rb index 57b52badf..2e0d067ea 100644 --- a/app/models/organisation.rb +++ b/app/models/organisation.rb @@ -57,6 +57,7 @@ class Organisation < ApplicationRecord has_many :booking_questions, dependent: :destroy, inverse_of: :organisation has_many :payments, through: :bookings has_many :invoices, through: :bookings + has_many :journal_entries, through: :invoices has_many :notifications, through: :bookings has_many :organisation_users, dependent: :destroy has_many :occupiables, dependent: :destroy diff --git a/app/models/payment.rb b/app/models/payment.rb index b2953595e..5b21625b3 100644 --- a/app/models/payment.rb +++ b/app/models/payment.rb @@ -48,7 +48,7 @@ class Payment < ApplicationRecord attr_accessor :skip_generate_journal_entries - before_save :generate_journal_entries, if: :generate_journal_entries? + before_save :generate_journal_entries!, if: :generate_journal_entries? after_create :confirm!, if: :confirm? after_destroy :recalculate_invoice after_save :recalculate_invoice @@ -76,11 +76,11 @@ def generate_journal_entries? def generate_journal_entries! return unless organisation.accounting_settings.enabled - existing_journal_entry_ids = reload.journal_entry_ids + existing_ids = organisation.journal_entries.where(source_type: self.class.sti_name, source_id: id).pluck(:id) new_journal_entries = JournalEntry::Factory.new.payment(self) # raise ActiveRecord::Rollback unless - new_journal_entries.save! && JournalEntry.where(id: existing_journal_entry_ids).destroy_all! + new_journal_entries.save! && organisation.journal_entries.where(id: existing_ids).destroy_all end def recalculate_invoice diff --git a/db/schema.rb b/db/schema.rb index 965f81646..0b077b57a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -13,7 +13,6 @@ ActiveRecord::Schema[8.0].define(version: 2024_12_17_125938) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" - enable_extension "pg_stat_statements" enable_extension "pgcrypto" enable_extension "uuid-ossp" @@ -160,7 +159,7 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "tenant_mode", default: 0, null: false - t.integer "booking_agent_mode", default: 0 + t.integer "booking_agent_mode" t.index ["discarded_at"], name: "index_booking_questions_on_discarded_at" t.index ["organisation_id"], name: "index_booking_questions_on_organisation_id" t.index ["type"], name: "index_booking_questions_on_type" diff --git a/yarn.lock b/yarn.lock index 0606db9a9..a88b676af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1528,10 +1528,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@9.16.0": - version "9.16.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.16.0.tgz#3df2b2dd3b9163056616886c86e4082f45dbf3f4" - integrity sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg== +"@eslint/js@9.17.0": + version "9.17.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.17.0.tgz#1523e586791f80376a6f8398a3964455ecc651ec" + integrity sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w== "@eslint/object-schema@^2.1.5": version "2.1.5" @@ -1593,9 +1593,9 @@ chalk "^4.0.0" "@jridgewell/gen-mapping@^0.3.5": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" - integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + version "0.3.8" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz#4f0e06362e01362f823d348f1872b08f666d8142" + integrity sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA== dependencies: "@jridgewell/set-array" "^1.2.1" "@jridgewell/sourcemap-codec" "^1.4.10" @@ -1754,9 +1754,9 @@ integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== "@rails/ujs@^7.0.2-2": - version "7.1.500" - resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-7.1.500.tgz#84bb037b6a823ec7fb7782a2ac03452deb505128" - integrity sha512-PfPtQrxFOn55RlJ4ygXkcIHFkoPO1JMTRJcSdPb0EavcSZ3Ekl9ujAAPrrqj2zKwkbe0pGGR79RJhnUr5ngQjw== + version "7.1.501" + resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-7.1.501.tgz#e560a7b6885a12a659c4beb47f4336c8a9353056" + integrity sha512-7EDRGUlgns12IgP3SXVSaxA3CwRzbLOypPXn1EqEZiZ/NS/PwaQ/oa7Z2VRO4B46JifoVr0PYg+G5ERSGQJHxQ== "@react-aria/ssr@^3.5.0": version "3.9.7" @@ -1779,10 +1779,10 @@ dependencies: dequal "^2.0.3" -"@restart/ui@^1.9.0": - version "1.9.1" - resolved "https://registry.yarnpkg.com/@restart/ui/-/ui-1.9.1.tgz#f0f22d255470e02f6fee6dcc449e56f0f1b8ebc1" - integrity sha512-qghR21ynHiUrpcIkKCoKYB+3rJtezY5Y7ikrwradCL+7hZHdQ2Ozc5ffxtpmpahoAGgc31gyXaSx2sXXaThmqA== +"@restart/ui@^1.9.2": + version "1.9.2" + resolved "https://registry.yarnpkg.com/@restart/ui/-/ui-1.9.2.tgz#dad8f084e6a56f87f9799dbc248af04168ffc03b" + integrity sha512-MWWqJqSyqUWWPBOOiRQrX57CBc/9CoYONg7sE+uag72GCAuYrHGU5c49vU5s4BUSBgiKNY6rL7TULqGDrouUaA== dependencies: "@babel/runtime" "^7.26.0" "@popperjs/core" "^2.11.8" @@ -1985,9 +1985,9 @@ integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== "@types/node@*": - version "22.10.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.1.tgz#41ffeee127b8975a05f8c4f83fb89bcb2987d766" - integrity sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ== + version "22.10.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.2.tgz#a485426e6d1fdafc7b0d4c7b24e2c78182ddabb9" + integrity sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ== dependencies: undici-types "~6.20.0" @@ -1996,7 +1996,7 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== -"@types/prop-types@*": +"@types/prop-types@*", "@types/prop-types@^15.7.12": version "15.7.14" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.14.tgz#1433419d73b2a7ebfc6918dcefd2ec0d5cd698f2" integrity sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ== @@ -2007,23 +2007,21 @@ integrity sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q== "@types/react-transition-group@^4.4.6": - version "4.4.11" - resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.11.tgz#d963253a611d757de01ebb241143b1017d5d63d5" - integrity sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA== - dependencies: - "@types/react" "*" + version "4.4.12" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044" + integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w== -"@types/react@*", "@types/react@>=16.9.11": - version "19.0.1" - resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.1.tgz#a000d5b78f473732a08cecbead0f3751e550b3df" - integrity sha512-YW6614BDhqbpR5KtUYzTA+zlA7nayzJRA9ljz9CQoxthR0sDisYZLuvSMsil36t4EH/uAt8T52Xb4sVw17G+SQ== +"@types/react@>=16.9.11": + version "19.0.2" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.2.tgz#9363e6b3ef898c471cb182dd269decc4afc1b4f6" + integrity sha512-USU8ZI/xyKJwFTpjSVIrSeHBVAGagkHQKPNbxeWwql/vDmnTIBgx+TJnhFnj1NXgz8XfprU0egV2dROLGpsBEg== dependencies: csstype "^3.0.2" "@types/react@^18.0.0": - version "18.3.16" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.16.tgz#5326789125fac98b718d586ad157442ceb44ff28" - integrity sha512-oh8AMIC4Y2ciKufU8hnKgs+ufgbA/dhPTACaZPM86AbwX9QwnFtSoPWEeRUj8fge+v6kFt78BXcDhAU1SrrAsw== + version "18.3.17" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.17.tgz#d86ca0e081c7a5e979b7db175f9515a41038cea7" + integrity sha512-opAQ5no6LqJNo9TqnxBKsgnkIYHozW9KSTlFVoSUJYh1Fl/sswkEoqIugRSm7tbh6pABtYjGAjW+GOS23j8qbw== dependencies: "@types/prop-types" "*" csstype "^3.0.2" @@ -2063,61 +2061,61 @@ "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^8.0.1": - version "8.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.0.tgz#0901933326aea4443b81df3f740ca7dfc45c7bea" - integrity sha512-NR2yS7qUqCL7AIxdJUQf2MKKNDVNaig/dEB0GBLU7D+ZdHgK1NoH/3wsgO3OnPVipn51tG3MAwaODEGil70WEw== + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.1.tgz#992e5ac1553ce20d0d46aa6eccd79dc36dedc805" + integrity sha512-Ncvsq5CT3Gvh+uJG0Lwlho6suwDfUXH0HztslDf5I+F2wAFAZMRwYLEorumpKLzmO2suAXZ/td1tBg4NZIi9CQ== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.18.0" - "@typescript-eslint/type-utils" "8.18.0" - "@typescript-eslint/utils" "8.18.0" - "@typescript-eslint/visitor-keys" "8.18.0" + "@typescript-eslint/scope-manager" "8.18.1" + "@typescript-eslint/type-utils" "8.18.1" + "@typescript-eslint/utils" "8.18.1" + "@typescript-eslint/visitor-keys" "8.18.1" graphemer "^1.4.0" ignore "^5.3.1" natural-compare "^1.4.0" ts-api-utils "^1.3.0" "@typescript-eslint/parser@^8.0.1": - version "8.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.18.0.tgz#a1c9456cbb6a089730bf1d3fc47946c5fb5fe67b" - integrity sha512-hgUZ3kTEpVzKaK3uNibExUYm6SKKOmTU2BOxBSvOYwtJEPdVQ70kZJpPjstlnhCHcuc2WGfSbpKlb/69ttyN5Q== - dependencies: - "@typescript-eslint/scope-manager" "8.18.0" - "@typescript-eslint/types" "8.18.0" - "@typescript-eslint/typescript-estree" "8.18.0" - "@typescript-eslint/visitor-keys" "8.18.0" + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.18.1.tgz#c258bae062778b7696793bc492249027a39dfb95" + integrity sha512-rBnTWHCdbYM2lh7hjyXqxk70wvon3p2FyaniZuey5TrcGBpfhVp0OxOa6gxr9Q9YhZFKyfbEnxc24ZnVbbUkCA== + dependencies: + "@typescript-eslint/scope-manager" "8.18.1" + "@typescript-eslint/types" "8.18.1" + "@typescript-eslint/typescript-estree" "8.18.1" + "@typescript-eslint/visitor-keys" "8.18.1" debug "^4.3.4" -"@typescript-eslint/scope-manager@8.18.0": - version "8.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.18.0.tgz#30b040cb4557804a7e2bcc65cf8fdb630c96546f" - integrity sha512-PNGcHop0jkK2WVYGotk/hxj+UFLhXtGPiGtiaWgVBVP1jhMoMCHlTyJA+hEj4rszoSdLTK3fN4oOatrL0Cp+Xw== +"@typescript-eslint/scope-manager@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.18.1.tgz#52cedc3a8178d7464a70beffed3203678648e55b" + integrity sha512-HxfHo2b090M5s2+/9Z3gkBhI6xBH8OJCFjH9MhQ+nnoZqxU3wNxkLT+VWXWSFWc3UF3Z+CfPAyqdCTdoXtDPCQ== dependencies: - "@typescript-eslint/types" "8.18.0" - "@typescript-eslint/visitor-keys" "8.18.0" + "@typescript-eslint/types" "8.18.1" + "@typescript-eslint/visitor-keys" "8.18.1" -"@typescript-eslint/type-utils@8.18.0": - version "8.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.18.0.tgz#6f0d12cf923b6fd95ae4d877708c0adaad93c471" - integrity sha512-er224jRepVAVLnMF2Q7MZJCq5CsdH2oqjP4dT7K6ij09Kyd+R21r7UVJrF0buMVdZS5QRhDzpvzAxHxabQadow== +"@typescript-eslint/type-utils@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.18.1.tgz#10f41285475c0bdee452b79ff7223f0e43a7781e" + integrity sha512-jAhTdK/Qx2NJPNOTxXpMwlOiSymtR2j283TtPqXkKBdH8OAMmhiUfP0kJjc/qSE51Xrq02Gj9NY7MwK+UxVwHQ== dependencies: - "@typescript-eslint/typescript-estree" "8.18.0" - "@typescript-eslint/utils" "8.18.0" + "@typescript-eslint/typescript-estree" "8.18.1" + "@typescript-eslint/utils" "8.18.1" debug "^4.3.4" ts-api-utils "^1.3.0" -"@typescript-eslint/types@8.18.0": - version "8.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.18.0.tgz#3afcd30def8756bc78541268ea819a043221d5f3" - integrity sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA== +"@typescript-eslint/types@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.18.1.tgz#d7f4f94d0bba9ebd088de840266fcd45408a8fff" + integrity sha512-7uoAUsCj66qdNQNpH2G8MyTFlgerum8ubf21s3TSM3XmKXuIn+H2Sifh/ES2nPOPiYSRJWAk0fDkW0APBWcpfw== -"@typescript-eslint/typescript-estree@8.18.0": - version "8.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.0.tgz#d8ca785799fbb9c700cdff1a79c046c3e633c7f9" - integrity sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg== +"@typescript-eslint/typescript-estree@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.1.tgz#2a86cd64b211a742f78dfa7e6f4860413475367e" + integrity sha512-z8U21WI5txzl2XYOW7i9hJhxoKKNG1kcU4RzyNvKrdZDmbjkmLBo8bgeiOJmA06kizLI76/CCBAAGlTlEeUfyg== dependencies: - "@typescript-eslint/types" "8.18.0" - "@typescript-eslint/visitor-keys" "8.18.0" + "@typescript-eslint/types" "8.18.1" + "@typescript-eslint/visitor-keys" "8.18.1" debug "^4.3.4" fast-glob "^3.3.2" is-glob "^4.0.3" @@ -2125,22 +2123,22 @@ semver "^7.6.0" ts-api-utils "^1.3.0" -"@typescript-eslint/utils@8.18.0": - version "8.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.18.0.tgz#48f67205d42b65d895797bb7349d1be5c39a62f7" - integrity sha512-p6GLdY383i7h5b0Qrfbix3Vc3+J2k6QWw6UMUeY5JGfm3C5LbZ4QIZzJNoNOfgyRe0uuYKjvVOsO/jD4SJO+xg== +"@typescript-eslint/utils@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.18.1.tgz#c4199ea23fc823c736e2c96fd07b1f7235fa92d5" + integrity sha512-8vikiIj2ebrC4WRdcAdDcmnu9Q/MXXwg+STf40BVfT8exDqBCUPdypvzcUPxEqRGKg9ALagZ0UWcYCtn+4W2iQ== dependencies: "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.18.0" - "@typescript-eslint/types" "8.18.0" - "@typescript-eslint/typescript-estree" "8.18.0" + "@typescript-eslint/scope-manager" "8.18.1" + "@typescript-eslint/types" "8.18.1" + "@typescript-eslint/typescript-estree" "8.18.1" -"@typescript-eslint/visitor-keys@8.18.0": - version "8.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.0.tgz#7b6d33534fa808e33a19951907231ad2ea5c36dd" - integrity sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw== +"@typescript-eslint/visitor-keys@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.1.tgz#344b4f6bc83f104f514676facf3129260df7610a" + integrity sha512-Vj0WLm5/ZsD013YeUKn+K0y8p1M0jPpxOkKdbD1wB0ns53a5piVY02zjf072TblEweAbcYiFiPoSMF3kp+VhhQ== dependencies: - "@typescript-eslint/types" "8.18.0" + "@typescript-eslint/types" "8.18.1" eslint-visitor-keys "^4.2.0" "@vitejs/plugin-react@^4.2.1": @@ -2243,24 +2241,24 @@ array.prototype.findlast@^1.2.5: es-shim-unscopables "^1.0.2" array.prototype.flat@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" - integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== + version "1.3.3" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz#534aaf9e6e8dd79fb6b9a9917f839ef1ec63afe5" + integrity sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - es-shim-unscopables "^1.0.0" + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-shim-unscopables "^1.0.2" array.prototype.flatmap@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" - integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== + version "1.3.3" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz#712cc792ae70370ae40586264629e33aab5dd38b" + integrity sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - es-shim-unscopables "^1.0.0" + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-shim-unscopables "^1.0.2" array.prototype.tosorted@^1.1.4: version "1.1.4" @@ -2273,19 +2271,18 @@ array.prototype.tosorted@^1.1.4: es-errors "^1.3.0" es-shim-unscopables "^1.0.2" -arraybuffer.prototype.slice@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" - integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== +arraybuffer.prototype.slice@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz#9d760d84dbdd06d0cbf92c8849615a1a7ab3183c" + integrity sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ== dependencies: array-buffer-byte-length "^1.0.1" - call-bind "^1.0.5" + call-bind "^1.0.8" define-properties "^1.2.1" - es-abstract "^1.22.3" - es-errors "^1.2.1" - get-intrinsic "^1.2.3" + es-abstract "^1.23.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.6" is-array-buffer "^3.0.4" - is-shared-array-buffer "^1.0.2" asynckit@^0.4.0: version "0.4.0" @@ -2396,16 +2393,16 @@ braces@^3.0.3: fill-range "^7.1.1" browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.23.1, browserslist@^4.23.3, browserslist@^4.24.0, browserslist@^4.24.2: - version "4.24.2" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580" - integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg== + version "4.24.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.3.tgz#5fc2725ca8fb3c1432e13dac278c7cc103e026d2" + integrity sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA== dependencies: - caniuse-lite "^1.0.30001669" - electron-to-chromium "^1.5.41" - node-releases "^2.0.18" + caniuse-lite "^1.0.30001688" + electron-to-chromium "^1.5.73" + node-releases "^2.0.19" update-browserslist-db "^1.1.1" -call-bind-apply-helpers@^1.0.0: +call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz#32e5892e6361b29b0b545ba6f7763378daca2840" integrity sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g== @@ -2413,7 +2410,7 @@ call-bind-apply-helpers@^1.0.0: es-errors "^1.3.0" function-bind "^1.1.2" -call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7, call-bind@^1.0.8: +call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7, call-bind@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== @@ -2423,6 +2420,14 @@ call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7, call-bin get-intrinsic "^1.2.4" set-function-length "^1.2.2" +call-bound@^1.0.2, call-bound@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.3.tgz#41cfd032b593e39176a71533ab4f384aa04fd681" + integrity sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA== + dependencies: + call-bind-apply-helpers "^1.0.1" + get-intrinsic "^1.2.6" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -2438,10 +2443,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001669: - version "1.0.30001687" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001687.tgz#d0ac634d043648498eedf7a3932836beba90ebae" - integrity sha512-0S/FDhf4ZiqrTUiQ39dKeUjYRjkv7lOZU1Dgif2rIqrTzX/1wV2hfKu9TOm1IHkdSijfLswxTFzl/cvir+SLSQ== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001688: + version "1.0.30001690" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz#f2d15e3aaf8e18f76b2b8c1481abde063b8104c8" + integrity sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w== chalk@^4.0.0: version "4.1.2" @@ -2452,9 +2457,9 @@ chalk@^4.0.0: supports-color "^7.1.0" chokidar@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.1.tgz#4a6dff66798fb0f72a94f616abbd7e1a19f31d41" - integrity sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA== + version "4.0.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" + integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== dependencies: readdirp "^4.0.1" @@ -2535,7 +2540,7 @@ cosmiconfig@^7.0.0: path-type "^4.0.0" yaml "^1.10.0" -cross-spawn@^7.0.5: +cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -2556,10 +2561,10 @@ css-declaration-sorter@^7.2.0: resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz#6dec1c9523bc4a643e088aab8f09e67a54961024" integrity sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow== -css-has-pseudo@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-7.0.1.tgz#adbb51821e51f7a7c1d2df4d12827870cc311137" - integrity sha512-EOcoyJt+OsuKfCADgLT7gADZI5jMzIe/AeI6MeAYKiFBDmNmM7kk46DtSfMj5AohUJisqVzopBpnQTlvbyaBWg== +css-has-pseudo@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-7.0.2.tgz#fb42e8de7371f2896961e1f6308f13c2c7019b72" + integrity sha512-nzol/h+E0bId46Kn2dQH5VElaknX2Sr0hFuB/1EomdC7j+OISt2ZzK7EHX9DZDY53WbIVAR7FYKSO2XnSf07MQ== dependencies: "@csstools/selector-specificity" "^5.0.0" postcss-selector-parser "^7.0.0" @@ -2628,7 +2633,7 @@ css-what@^6.1.0: resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== -cssdb@^8.2.1: +cssdb@^8.2.3: version "8.2.3" resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-8.2.3.tgz#7e6980bb5a785a9b4eb2a21bd38d50624b56cb46" integrity sha512-9BDG5XmJrJQQnJ51VFxXCAtpZ5ebDlAREmO8sxMOVU0aSxN/gocbctjIG5LMh3WBUq+xTlb/jw2LoljBEqraTA== @@ -2718,11 +2723,11 @@ data-view-byte-length@^1.0.1: is-data-view "^1.0.1" data-view-byte-offset@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" - integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz#068307f9b71ab76dbbe10291389e020856606191" + integrity sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ== dependencies: - call-bind "^1.0.6" + call-bound "^1.0.2" es-errors "^1.3.0" is-data-view "^1.0.1" @@ -2752,7 +2757,7 @@ define-data-property@^1.0.1, define-data-property@^1.1.4: es-errors "^1.3.0" gopd "^1.0.1" -define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: +define-properties@^1.1.3, define-properties@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== @@ -2821,19 +2826,19 @@ domutils@^3.0.1: domelementtype "^2.3.0" domhandler "^5.0.3" -dunder-proto@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.0.tgz#c2fce098b3c8f8899554905f4377b6d85dabaa80" - integrity sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A== +dunder-proto@^1.0.0, dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== dependencies: - call-bind-apply-helpers "^1.0.0" + call-bind-apply-helpers "^1.0.1" es-errors "^1.3.0" gopd "^1.2.0" -electron-to-chromium@^1.5.41: - version "1.5.72" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.72.tgz#a732805986d3a5b5fedd438ddf4616c7d78ac2df" - integrity sha512-ZpSAUOZ2Izby7qnZluSrAlGgGQzucmFbN0n64dYzocYxnxV5ufurpj3VgEe4cUp7ir9LmeLxNYo8bVnlM8bQHw== +electron-to-chromium@^1.5.73: + version "1.5.74" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.74.tgz#cb886b504a6467e4c00bea3317edb38393c53413" + integrity sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw== emojis-list@^3.0.0: version "3.0.0" @@ -2852,64 +2857,66 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.2, es-abstract@^1.23.3, es-abstract@^1.23.5: - version "1.23.5" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.5.tgz#f4599a4946d57ed467515ed10e4f157289cd52fb" - integrity sha512-vlmniQ0WNPwXqA0BnmwV3Ng7HxiGlh6r5U6JcTMNx8OilcAGqVJBHJcPjqOMaczU9fRuRK5Px2BdVyPRnKMMVQ== +es-abstract@^1.17.5, es-abstract@^1.23.2, es-abstract@^1.23.3, es-abstract@^1.23.5, es-abstract@^1.23.6: + version "1.23.6" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.6.tgz#55f0e1ce7128995cc04ace0a57d7dca348345108" + integrity sha512-Ifco6n3yj2tMZDWNLyloZrytt9lqqlwvS83P3HtaETR0NUOYnIULGGHpktqYGObGy+8wc1okO25p8TjemhImvA== dependencies: array-buffer-byte-length "^1.0.1" - arraybuffer.prototype.slice "^1.0.3" + arraybuffer.prototype.slice "^1.0.4" available-typed-arrays "^1.0.7" - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.3" data-view-buffer "^1.0.1" data-view-byte-length "^1.0.1" data-view-byte-offset "^1.0.0" - es-define-property "^1.0.0" + es-define-property "^1.0.1" es-errors "^1.3.0" es-object-atoms "^1.0.0" es-set-tostringtag "^2.0.3" - es-to-primitive "^1.2.1" - function.prototype.name "^1.1.6" - get-intrinsic "^1.2.4" + es-to-primitive "^1.3.0" + function.prototype.name "^1.1.7" + get-intrinsic "^1.2.6" get-symbol-description "^1.0.2" globalthis "^1.0.4" - gopd "^1.0.1" + gopd "^1.2.0" has-property-descriptors "^1.0.2" - has-proto "^1.0.3" - has-symbols "^1.0.3" + has-proto "^1.2.0" + has-symbols "^1.1.0" hasown "^2.0.2" - internal-slot "^1.0.7" + internal-slot "^1.1.0" is-array-buffer "^3.0.4" is-callable "^1.2.7" - is-data-view "^1.0.1" + is-data-view "^1.0.2" is-negative-zero "^2.0.3" - is-regex "^1.1.4" + is-regex "^1.2.1" is-shared-array-buffer "^1.0.3" - is-string "^1.0.7" + is-string "^1.1.1" is-typed-array "^1.1.13" - is-weakref "^1.0.2" + is-weakref "^1.1.0" + math-intrinsics "^1.0.0" object-inspect "^1.13.3" object-keys "^1.1.1" object.assign "^4.1.5" regexp.prototype.flags "^1.5.3" - safe-array-concat "^1.1.2" - safe-regex-test "^1.0.3" - string.prototype.trim "^1.2.9" - string.prototype.trimend "^1.0.8" + safe-array-concat "^1.1.3" + safe-regex-test "^1.1.0" + string.prototype.trim "^1.2.10" + string.prototype.trimend "^1.0.9" string.prototype.trimstart "^1.0.8" typed-array-buffer "^1.0.2" typed-array-byte-length "^1.0.1" - typed-array-byte-offset "^1.0.2" - typed-array-length "^1.0.6" + typed-array-byte-offset "^1.0.3" + typed-array-length "^1.0.7" unbox-primitive "^1.0.2" - which-typed-array "^1.1.15" + which-typed-array "^1.1.16" es-define-property@^1.0.0, es-define-property@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== -es-errors@^1.2.1, es-errors@^1.3.0: +es-errors@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== @@ -2951,14 +2958,14 @@ es-set-tostringtag@^2.0.3: has-tostringtag "^1.0.2" hasown "^2.0.1" -es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2: +es-shim-unscopables@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== dependencies: hasown "^2.0.0" -es-to-primitive@^1.2.1: +es-to-primitive@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz#96c89c82cc49fd8794a24835ba3e1ff87f214e18" integrity sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g== @@ -3063,16 +3070,16 @@ eslint-visitor-keys@^4.2.0: integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== eslint@^9.1.1: - version "9.16.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.16.0.tgz#66832e66258922ac0a626f803a9273e37747f2a6" - integrity sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA== + version "9.17.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.17.0.tgz#faa1facb5dd042172fdc520106984b5c2421bb0c" + integrity sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.12.1" "@eslint/config-array" "^0.19.0" "@eslint/core" "^0.9.0" "@eslint/eslintrc" "^3.2.0" - "@eslint/js" "9.16.0" + "@eslint/js" "9.17.0" "@eslint/plugin-kit" "^0.2.3" "@humanfs/node" "^0.16.6" "@humanwhocodes/module-importer" "^1.0.1" @@ -3081,7 +3088,7 @@ eslint@^9.1.1: "@types/json-schema" "^7.0.15" ajv "^6.12.4" chalk "^4.0.0" - cross-spawn "^7.0.5" + cross-spawn "^7.0.6" debug "^4.3.2" escape-string-regexp "^4.0.0" eslint-scope "^8.2.0" @@ -3264,15 +3271,16 @@ function-bind@^1.1.2: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== -function.prototype.name@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" - integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== +function.prototype.name@^1.1.6, function.prototype.name@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.7.tgz#9df48ea5f746bf577d7e15b5da89df8952a98e7b" + integrity sha512-2g4x+HqTJKM9zcJqBSpjoRmdcPFtJM60J3xJisTQSXBWka5XqyBN/2tNUgma1mztTXyDuUsEtYe5qcs7xYzYQA== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" + call-bind "^1.0.8" + define-properties "^1.2.1" functions-have-names "^1.2.3" + hasown "^2.0.2" + is-callable "^1.2.7" functions-have-names@^1.2.3: version "1.2.3" @@ -3284,28 +3292,30 @@ gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== -get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: - version "1.2.5" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.5.tgz#dfe7dd1b30761b464fe51bf4bb00ac7c37b681e7" - integrity sha512-Y4+pKa7XeRUPWFNvOOYHkRYrfzW07oraURSvjDmRVOJ748OrVmeXtpE4+GCEHncjCjkTxPNRt8kEbxDhsn6VTg== +get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.6.tgz#43dd3dd0e7b49b82b2dfcad10dc824bf7fc265d5" + integrity sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA== dependencies: - call-bind-apply-helpers "^1.0.0" + call-bind-apply-helpers "^1.0.1" dunder-proto "^1.0.0" es-define-property "^1.0.1" es-errors "^1.3.0" + es-object-atoms "^1.0.0" function-bind "^1.1.2" gopd "^1.2.0" has-symbols "^1.1.0" hasown "^2.0.2" + math-intrinsics "^1.0.0" get-symbol-description@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" - integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== + version "1.1.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz#7bdd54e0befe8ffc9f3b4e203220d9f1e881b6ee" + integrity sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg== dependencies: - call-bind "^1.0.5" + call-bound "^1.0.3" es-errors "^1.3.0" - get-intrinsic "^1.2.4" + get-intrinsic "^1.2.6" glob-parent@^5.1.2: version "5.1.2" @@ -3339,7 +3349,7 @@ globalthis@^1.0.4: define-properties "^1.2.1" gopd "^1.0.1" -gopd@^1.0.1, gopd@^1.1.0, gopd@^1.2.0: +gopd@^1.0.1, gopd@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== @@ -3355,9 +3365,9 @@ graphemer@^1.4.0: integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== has-bigints@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" - integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.1.0.tgz#28607e965ac967e03cd2a2c70a2636a1edad49fe" + integrity sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg== has-flag@^4.0.0: version "4.0.0" @@ -3371,7 +3381,7 @@ has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: dependencies: es-define-property "^1.0.0" -has-proto@^1.0.3: +has-proto@^1.0.3, has-proto@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.2.0.tgz#5de5a6eabd95fdffd9818b43055e8065e39fe9d5" integrity sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ== @@ -3412,9 +3422,9 @@ html-parse-stringify@^3.0.1: void-elements "3.1.0" i18next@^24.0.2: - version "24.0.5" - resolved "https://registry.yarnpkg.com/i18next/-/i18next-24.0.5.tgz#2678986eca46411cae0329542a84dd4cd7e5f2f0" - integrity sha512-1jSdEzgFPGLZRsQwydoMFCBBaV+PmrVEO5WhANllZPX4y2JSGTxUjJ+xVklHIsiS95uR8gYc/y0hYZWevucNjg== + version "24.1.2" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-24.1.2.tgz#6dd0bcf6d50d8d61227b0a3aefb653885d267d09" + integrity sha512-th/075GW0Ub1gYDMHLiZXMGSfGv1aP1VqjT3fma/12hNHCNlH8oJMftvlDzycT/R+KoULWk+xLU8H1JRwV85qw== dependencies: "@babel/runtime" "^7.23.2" @@ -3446,14 +3456,14 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== -internal-slot@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" - integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== +internal-slot@^1.0.7, internal-slot@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961" + integrity sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw== dependencies: es-errors "^1.3.0" - hasown "^2.0.0" - side-channel "^1.0.4" + hasown "^2.0.2" + side-channel "^1.1.0" invariant@^2.2.4: version "2.2.4" @@ -3463,12 +3473,13 @@ invariant@^2.2.4: loose-envify "^1.0.0" is-array-buffer@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" - integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== + version "3.0.5" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280" + integrity sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A== dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.2.1" + call-bind "^1.0.8" + call-bound "^1.0.3" + get-intrinsic "^1.2.6" is-arrayish@^0.2.1: version "0.2.1" @@ -3489,12 +3500,12 @@ is-bigint@^1.1.0: dependencies: has-bigints "^1.0.2" -is-boolean-object@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.2.0.tgz#9743641e80a62c094b5941c5bb791d66a88e497a" - integrity sha512-kR5g0+dXf/+kXnqI+lu0URKYPKgICtHGGNCDSB10AaUFj3o/HkB3u7WfpRBJGFopxxY0oH3ux7ZsDjLtK7xqvw== +is-boolean-object@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.2.1.tgz#c20d0c654be05da4fbc23c562635c019e93daf89" + integrity sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng== dependencies: - call-bind "^1.0.7" + call-bound "^1.0.2" has-tostringtag "^1.0.2" is-callable@^1.1.3, is-callable@^1.2.7: @@ -3502,26 +3513,29 @@ is-callable@^1.1.3, is-callable@^1.2.7: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-core-module@^2.13.0: - version "2.15.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" - integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== +is-core-module@^2.13.0, is-core-module@^2.16.0: + version "2.16.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.0.tgz#6c01ffdd5e33c49c1d2abfa93334a85cb56bd81c" + integrity sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g== dependencies: hasown "^2.0.2" -is-data-view@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" - integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== +is-data-view@^1.0.1, is-data-view@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.2.tgz#bae0a41b9688986c2188dda6657e56b8f9e63b8e" + integrity sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw== dependencies: + call-bound "^1.0.2" + get-intrinsic "^1.2.6" is-typed-array "^1.1.13" -is-date-object@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" - integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== +is-date-object@^1.0.5, is-date-object@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.1.0.tgz#ad85541996fc7aa8b2729701d27b7319f95d82f7" + integrity sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg== dependencies: - has-tostringtag "^1.0.0" + call-bound "^1.0.2" + has-tostringtag "^1.0.2" is-extglob@^2.1.1: version "2.1.1" @@ -3529,11 +3543,11 @@ is-extglob@^2.1.1: integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== is-finalizationregistry@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.1.0.tgz#d74a7d0c5f3578e34a20729e69202e578d495dc2" - integrity sha512-qfMdqbAQEwBw78ZyReKnlA8ezmPdb9BemzIIip/JkjaZUhitfXDkkr+3QTboW0JrSXT1QWyYShpvnNHGZ4c4yA== + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz#eefdcdc6c94ddd0674d9c85887bf93f944a97c90" + integrity sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg== dependencies: - call-bind "^1.0.7" + call-bound "^1.0.3" is-generator-function@^1.0.10: version "1.0.10" @@ -3559,12 +3573,12 @@ is-negative-zero@^2.0.3: resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== -is-number-object@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.1.0.tgz#5a867e9ecc3d294dda740d9f127835857af7eb05" - integrity sha512-KVSZV0Dunv9DTPkhXwcZ3Q+tUc9TsaE1ZwX5J2WMvsSGS6Md8TFPun5uwh0yRdrNerI6vf/tbJxqSx4c1ZI1Lw== +is-number-object@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.1.1.tgz#144b21e95a1bc148205dcc2814a9134ec41b2541" + integrity sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw== dependencies: - call-bind "^1.0.7" + call-bound "^1.0.3" has-tostringtag "^1.0.2" is-number@^7.0.0: @@ -3572,13 +3586,13 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-regex@^1.1.4: - version "1.2.0" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.0.tgz#41b9d266e7eb7451312c64efc37e8a7d453077cf" - integrity sha512-B6ohK4ZmoftlUe+uvenXSbPJFo6U37BH7oO1B3nQH8f/7h27N56s85MhUtbFJAziz5dcmuR3i8ovUl35zp8pFA== +is-regex@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" + integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== dependencies: - call-bind "^1.0.7" - gopd "^1.1.0" + call-bound "^1.0.2" + gopd "^1.2.0" has-tostringtag "^1.0.2" hasown "^2.0.2" @@ -3587,56 +3601,56 @@ is-set@^2.0.3: resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== -is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" - integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== +is-shared-array-buffer@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz#9b67844bd9b7f246ba0708c3a93e34269c774f6f" + integrity sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A== dependencies: - call-bind "^1.0.7" + call-bound "^1.0.3" -is-string@^1.0.7, is-string@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.1.0.tgz#8cb83c5d57311bf8058bc6c8db294711641da45d" - integrity sha512-PlfzajuF9vSo5wErv3MJAKD/nqf9ngAs1NFQYm16nUYFO2IzxJ2hcm+IOCg+EEopdykNNUhVq5cz35cAUxU8+g== +is-string@^1.0.7, is-string@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.1.1.tgz#92ea3f3d5c5b6e039ca8677e5ac8d07ea773cbb9" + integrity sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA== dependencies: - call-bind "^1.0.7" + call-bound "^1.0.3" has-tostringtag "^1.0.2" -is-symbol@^1.0.4, is-symbol@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.1.0.tgz#ae993830a56d4781886d39f9f0a46b3e89b7b60b" - integrity sha512-qS8KkNNXUZ/I+nX6QT8ZS1/Yx0A444yhzdTKxCzKkNjQ9sHErBxJnJAgh+f5YhusYECEcjo4XcyH87hn6+ks0A== +is-symbol@^1.0.4, is-symbol@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.1.1.tgz#f47761279f532e2b05a7024a7506dbbedacd0634" + integrity sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w== dependencies: - call-bind "^1.0.7" - has-symbols "^1.0.3" - safe-regex-test "^1.0.3" + call-bound "^1.0.2" + has-symbols "^1.1.0" + safe-regex-test "^1.1.0" -is-typed-array@^1.1.13: - version "1.1.13" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" - integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== +is-typed-array@^1.1.13, is-typed-array@^1.1.14, is-typed-array@^1.1.15: + version "1.1.15" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b" + integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== dependencies: - which-typed-array "^1.1.14" + which-typed-array "^1.1.16" is-weakmap@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== -is-weakref@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" - integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== +is-weakref@^1.0.2, is-weakref@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.1.0.tgz#47e3472ae95a63fa9cf25660bcf0c181c39770ef" + integrity sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q== dependencies: - call-bind "^1.0.2" + call-bound "^1.0.2" is-weakset@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.3.tgz#e801519df8c0c43e12ff2834eead84ec9e624007" - integrity sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ== + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.4.tgz#c9f5deb0bc1906c6d6f1027f284ddf459249daca" + integrity sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ== dependencies: - call-bind "^1.0.7" - get-intrinsic "^1.2.4" + call-bound "^1.0.3" + get-intrinsic "^1.2.6" isarray@^2.0.5: version "2.0.5" @@ -3649,15 +3663,16 @@ isexe@^2.0.0: integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== iterator.prototype@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.3.tgz#016c2abe0be3bbdb8319852884f60908ac62bf9c" - integrity sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ== + version "1.1.4" + resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.4.tgz#4ae6cf98b97fdc717b7e159d79dc25f8fc9482f1" + integrity sha512-x4WH0BWmrMmg4oHHl+duwubhrvczGlyuGAZu3nvrf0UXOfPu8IhZObFEr7DE/iv01YgVZrsOiRcqw2srkKEDIA== dependencies: - define-properties "^1.2.1" - get-intrinsic "^1.2.1" - has-symbols "^1.0.3" - reflect.getprototypeof "^1.0.4" - set-function-name "^2.0.1" + define-data-property "^1.1.4" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.6" + has-symbols "^1.1.0" + reflect.getprototypeof "^1.0.8" + set-function-name "^2.0.2" javascript-stringify@^2.0.1: version "2.1.0" @@ -3698,7 +3713,12 @@ js-yaml@4.1.0, js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -jsesc@^3.0.2, jsesc@~3.0.2: +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + +jsesc@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== @@ -3818,6 +3838,11 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +math-intrinsics@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + mdn-data@2.0.28: version "2.0.28" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.28.tgz#5ec48e7bef120654539069e1ae4ddc81ca490eba" @@ -3900,7 +3925,7 @@ node-addon-api@^7.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== -node-releases@^2.0.18: +node-releases@^2.0.19: version "2.0.19" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== @@ -3922,7 +3947,7 @@ object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-inspect@^1.13.1, object-inspect@^1.13.3: +object-inspect@^1.13.3: version "1.13.3" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.3.tgz#f14c183de51130243d6d18ae149375ff50ea488a" integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA== @@ -3933,13 +3958,15 @@ object-keys@^1.1.1: integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== object.assign@^4.1.4, object.assign@^4.1.5: - version "4.1.5" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" - integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== + version "4.1.7" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.7.tgz#8c14ca1a424c6a561b0bb2a22f66f5049a945d3d" + integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw== dependencies: - call-bind "^1.0.5" + call-bind "^1.0.8" + call-bound "^1.0.3" define-properties "^1.2.1" - has-symbols "^1.0.3" + es-object-atoms "^1.0.0" + has-symbols "^1.1.0" object-keys "^1.1.1" object.entries@^1.1.8: @@ -3962,11 +3989,12 @@ object.fromentries@^2.0.8: es-object-atoms "^1.0.0" object.values@^1.1.6, object.values@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" - integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== + version "1.2.1" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.1.tgz#deed520a50809ff7f75a7cfd4bc64c7a038c6216" + integrity sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.3" define-properties "^1.2.1" es-object-atoms "^1.0.0" @@ -4307,9 +4335,9 @@ postcss-modules-extract-imports@^3.1.0: integrity sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q== postcss-modules-local-by-default@^4.0.5: - version "4.1.0" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.1.0.tgz#b0db6bc81ffc7bdc52eb0f84d6ca0bedf0e36d21" - integrity sha512-rm0bdSv4jC3BDma3s9H19ZddW0aHX6EoqwDYU2IfZhRN+53QrufTRo2IdkAbRqLx4R2IYbZnbjKKxg4VN5oU9Q== + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz#d150f43837831dae25e4085596e84f6f5d6ec368" + integrity sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw== dependencies: icss-utils "^5.0.0" postcss-selector-parser "^7.0.0" @@ -4433,9 +4461,9 @@ postcss-place@^10.0.0: postcss-value-parser "^4.2.0" postcss-preset-env@^10.0.0: - version "10.1.1" - resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-10.1.1.tgz#6ee631272353fb1c4a9711943e9b80a178ffce44" - integrity sha512-wqqsnBFD6VIwcHHRbhjTOcOi4qRVlB26RwSr0ordPj7OubRRxdWebv/aLjKLRR8zkZrbxZyuus03nOIgC5elMQ== + version "10.1.2" + resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-10.1.2.tgz#ea9c25d92045ef06edd78f9945d2586107aab3e3" + integrity sha512-OqUBZ9ByVfngWhMNuBEMy52Izj07oIFA6K/EOGBlaSv+P12MiE1+S2cqXtS1VuW82demQ/Tzc7typYk3uHunkA== dependencies: "@csstools/postcss-cascade-layers" "^5.0.1" "@csstools/postcss-color-function" "^4.0.6" @@ -4472,9 +4500,9 @@ postcss-preset-env@^10.0.0: autoprefixer "^10.4.19" browserslist "^4.23.1" css-blank-pseudo "^7.0.1" - css-has-pseudo "^7.0.1" + css-has-pseudo "^7.0.2" css-prefers-color-scheme "^10.0.0" - cssdb "^8.2.1" + cssdb "^8.2.3" postcss-attribute-case-insensitive "^7.0.1" postcss-clamp "^4.1.0" postcss-color-functional-notation "^7.0.6" @@ -4637,13 +4665,14 @@ randombytes@^2.1.0: safe-buffer "^5.1.0" react-bootstrap@^2.3.1: - version "2.10.6" - resolved "https://registry.yarnpkg.com/react-bootstrap/-/react-bootstrap-2.10.6.tgz#cb8b6f3604480b99b1e3cfa09cf53446e760bba7" - integrity sha512-fNvKytSp0nHts1WRnRBJeBEt+I9/ZdrnhIjWOucEduRNvFRU1IXjZueDdWnBiqsTSJ7MckQJi9i/hxGolaRq+g== + version "2.10.7" + resolved "https://registry.yarnpkg.com/react-bootstrap/-/react-bootstrap-2.10.7.tgz#63954e8faa6f0d28d9c755e5f8fbd27b5b09764a" + integrity sha512-w6mWb3uytB5A18S2oTZpYghcOUK30neMBBvZ/bEfA+WIF2dF4OGqjzoFVMpVXBjtyf92gkmRToHlddiMAVhQqQ== dependencies: "@babel/runtime" "^7.24.7" "@restart/hooks" "^0.4.9" - "@restart/ui" "^1.9.0" + "@restart/ui" "^1.9.2" + "@types/prop-types" "^15.7.12" "@types/react-transition-group" "^4.4.6" classnames "^2.3.2" dom-helpers "^5.2.1" @@ -4663,14 +4692,14 @@ react-dom@^18.0.0, react-dom@^18.2.0: scheduler "^0.23.2" react-hook-form@^7.51.3: - version "7.54.0" - resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.54.0.tgz#46bd9142d65fd16ac064a2bbf4dc0333e2d6840d" - integrity sha512-PS05+UQy/IdSbJNojBypxAo9wllhHgGmyr8/dyGQcPoiMf3e7Dfb9PWYVRco55bLbxH9S+1yDDJeTdlYCSxO3A== + version "7.54.1" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.54.1.tgz#e99c2a55a5e4859fb47a8f55adf66b34d6ac331d" + integrity sha512-PUNzFwQeQ5oHiiTUO7GO/EJXGEtuun2Y1A59rLnZBBj+vNEOWt/3ERTiG1/zt7dVeJEM+4vDX/7XQ/qanuvPMg== react-i18next@^15.0.0: - version "15.1.4" - resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.1.4.tgz#65c03c31a5e42202000652e163f22f23a9306a60" - integrity sha512-2tai71gmehbvl9ZIqPMqlCCkm/cbeV1G4STpmM3C8Uzo6T2l8jDvZxEVSsQKt8blP9X34iRFP/k1ROqG2296MQ== + version "15.2.0" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.2.0.tgz#6b51650e1e93eb4d235a4d533fcf61b3bbf4ea10" + integrity sha512-iJNc8111EaDtVTVMKigvBtPHyrJV+KblWG73cUxqp+WmJCcwkzhWNFXmkAD5pwP2Z4woeDj/oXDdbjDsb3Gutg== dependencies: "@babel/runtime" "^7.25.0" html-parse-stringify "^3.0.1" @@ -4752,19 +4781,19 @@ readdirp@^4.0.1: resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.0.2.tgz#388fccb8b75665da3abffe2d8f8ed59fe74c230a" integrity sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA== -reflect.getprototypeof@^1.0.4, reflect.getprototypeof@^1.0.6: - version "1.0.8" - resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.8.tgz#c58afb17a4007b4d1118c07b92c23fca422c5d82" - integrity sha512-B5dj6usc5dkk8uFliwjwDHM8To5/QwdKz9JcBZ8Ic4G1f0YmeeJTtE/ZTdgRFPAfxZFiUaPhZ1Jcs4qeagItGQ== +reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.8, reflect.getprototypeof@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.9.tgz#c905f3386008de95a62315f3ea8630404be19e2f" + integrity sha512-r0Ay04Snci87djAsI4U+WNRcSw5S4pOH7qFjd/veA5gC7TbqESR3tcj28ia95L/fYUDw11JKP7uqUKUAfVvV5Q== dependencies: call-bind "^1.0.8" define-properties "^1.2.1" - dunder-proto "^1.0.0" - es-abstract "^1.23.5" + dunder-proto "^1.0.1" + es-abstract "^1.23.6" es-errors "^1.3.0" - get-intrinsic "^1.2.4" + get-intrinsic "^1.2.6" gopd "^1.2.0" - which-builtin-type "^1.2.0" + which-builtin-type "^1.2.1" regenerate-unicode-properties@^10.2.0: version "10.2.0" @@ -4835,11 +4864,11 @@ resolve-from@^4.0.0: integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== resolve@^1.1.7, resolve@^1.14.2, resolve@^1.19.0: - version "1.22.8" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" - integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + version "1.22.9" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.9.tgz#6da76e4cdc57181fa4471231400e8851d0a924f3" + integrity sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A== dependencies: - is-core-module "^2.13.0" + is-core-module "^2.16.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" @@ -4897,14 +4926,15 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-array-concat@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" - integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== +safe-array-concat@^1.1.2, safe-array-concat@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz#c9e54ec4f603b0bbb8e7e5007a5ee7aecd1538c3" + integrity sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q== dependencies: - call-bind "^1.0.7" - get-intrinsic "^1.2.4" - has-symbols "^1.0.3" + call-bind "^1.0.8" + call-bound "^1.0.2" + get-intrinsic "^1.2.6" + has-symbols "^1.1.0" isarray "^2.0.5" safe-buffer@^5.1.0: @@ -4912,19 +4942,19 @@ safe-buffer@^5.1.0: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-regex-test@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" - integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== +safe-regex-test@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1" + integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== dependencies: - call-bind "^1.0.6" + call-bound "^1.0.2" es-errors "^1.3.0" - is-regex "^1.1.4" + is-regex "^1.2.1" sass@^1.75.0: - version "1.82.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.82.0.tgz#30da277af3d0fa6042e9ceabd0d984ed6d07df70" - integrity sha512-j4GMCTa8elGyN9A7x7bEglx0VgSpNUG4W4wNedQ33wSMdnkqQCT8HTwOaVSV4e6yQovcu/3Oc4coJP/l0xhL2Q== + version "1.83.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.83.0.tgz#e36842c0b88a94ed336fd16249b878a0541d536f" + integrity sha512-qsSxlayzoOjdvXMVLkzF84DJFc2HZEL/rFyGIKbbilYtAvlCxyuzUeff9LawTn4btVnLKg75Z8MMr1lxU1lfGw== dependencies: chokidar "^4.0.0" immutable "^5.0.2" @@ -4940,9 +4970,9 @@ scheduler@^0.23.2: loose-envify "^1.1.0" schema-utils@^4.0.0, schema-utils@^4.0.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.2.0.tgz#70d7c93e153a273a805801882ebd3bff20d89c8b" - integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw== + version "4.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.0.tgz#3b669f04f71ff2dfb5aba7ce2d5a9d79b35622c0" + integrity sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g== dependencies: "@types/json-schema" "^7.0.9" ajv "^8.9.0" @@ -4978,7 +5008,7 @@ set-function-length@^1.2.2: gopd "^1.0.1" has-property-descriptors "^1.0.2" -set-function-name@^2.0.1, set-function-name@^2.0.2: +set-function-name@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== @@ -5000,15 +5030,45 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -side-channel@^1.0.4, side-channel@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" - integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== dependencies: - call-bind "^1.0.7" es-errors "^1.3.0" - get-intrinsic "^1.2.4" - object-inspect "^1.13.1" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.0.6, side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" sortablejs@^1.15.1: version "1.15.6" @@ -5056,22 +5116,26 @@ string.prototype.repeat@^1.0.0: define-properties "^1.1.3" es-abstract "^1.17.5" -string.prototype.trim@^1.2.9: - version "1.2.9" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" - integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== +string.prototype.trim@^1.2.10: + version "1.2.10" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz#40b2dd5ee94c959b4dcfb1d65ce72e90da480c81" + integrity sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.2" + define-data-property "^1.1.4" define-properties "^1.2.1" - es-abstract "^1.23.0" + es-abstract "^1.23.5" es-object-atoms "^1.0.0" + has-property-descriptors "^1.0.2" -string.prototype.trimend@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" - integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== +string.prototype.trimend@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz#62e2731272cd285041b36596054e9f66569b6942" + integrity sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.2" define-properties "^1.2.1" es-object-atoms "^1.0.0" @@ -5158,9 +5222,9 @@ tiny-invariant@1.2.0: integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg== tinymce@^7.2.1: - version "7.5.1" - resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-7.5.1.tgz#4c207ab930d3a073bf851ddd3a8aa44d8d94d7bd" - integrity sha512-GRXJUB0BEIOUHUEC+q9IjsgWGIAQ4Tn5t5hfpB/YR7No3oPgKHG03v1d3nbov9aqdyVW7Be+UD4I3ZerQG30VQ== + version "7.6.0" + resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-7.6.0.tgz#476069a3e98f46f3f6e7875ca878313d6b3345b9" + integrity sha512-kUrklnD7H8JbpSDEGRh51GKK6Mrf+pR9neSDzUHvXKV+2oRtMB7sqfAtEOnM0/WKdstwaX0qoNCZNo2H1Y0EFA== to-regex-range@^5.0.1: version "5.0.1" @@ -5192,39 +5256,39 @@ type-check@^0.4.0, type-check@~0.4.0: prelude-ls "^1.2.1" typed-array-buffer@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" - integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz#a72395450a4869ec033fd549371b47af3a2ee536" + integrity sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw== dependencies: - call-bind "^1.0.7" + call-bound "^1.0.3" es-errors "^1.3.0" - is-typed-array "^1.1.13" + is-typed-array "^1.1.14" typed-array-byte-length@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" - integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz#8407a04f7d78684f3d252aa1a143d2b77b4160ce" + integrity sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" for-each "^0.3.3" - gopd "^1.0.1" - has-proto "^1.0.3" - is-typed-array "^1.1.13" + gopd "^1.2.0" + has-proto "^1.2.0" + is-typed-array "^1.1.14" -typed-array-byte-offset@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.3.tgz#3fa9f22567700cc86aaf86a1e7176f74b59600f2" - integrity sha512-GsvTyUHTriq6o/bHcTd0vM7OQ9JEdlvluu9YISaA7+KzDzPaIzEeDFNkTfhdE3MYcNhNi0vq/LlegYgIs5yPAw== +typed-array-byte-offset@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz#ae3698b8ec91a8ab945016108aef00d5bff12355" + integrity sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ== dependencies: available-typed-arrays "^1.0.7" - call-bind "^1.0.7" + call-bind "^1.0.8" for-each "^0.3.3" - gopd "^1.0.1" - has-proto "^1.0.3" - is-typed-array "^1.1.13" - reflect.getprototypeof "^1.0.6" + gopd "^1.2.0" + has-proto "^1.2.0" + is-typed-array "^1.1.15" + reflect.getprototypeof "^1.0.9" -typed-array-length@^1.0.6: +typed-array-length@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.7.tgz#ee4deff984b64be1e118b0de8c9c877d5ce73d3d" integrity sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg== @@ -5242,14 +5306,14 @@ typescript@5.7.2: integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg== unbox-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" - integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + version "1.1.0" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz#8d9d2c9edeea8460c7f35033a88867944934d1e2" + integrity sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw== dependencies: - call-bind "^1.0.2" + call-bound "^1.0.3" has-bigints "^1.0.2" - has-symbols "^1.0.3" - which-boxed-primitive "^1.0.2" + has-symbols "^1.1.0" + which-boxed-primitive "^1.1.1" uncontrollable@^7.2.1: version "7.2.1" @@ -5361,9 +5425,9 @@ vite-plugin-stimulus-hmr@^3.0.0: stimulus-vite-helpers "^3.0.0" vite@^6.0.1: - version "6.0.3" - resolved "https://registry.yarnpkg.com/vite/-/vite-6.0.3.tgz#cc01f403e326a9fc1e064235df8a6de084c8a491" - integrity sha512-Cmuo5P0ENTN6HxLSo6IHsjCLn/81Vgrp81oaiFFMRa8gGDj5xEjIcEpf2ZymZtZR8oU0P2JX5WuUp/rlXcHkAw== + version "6.0.4" + resolved "https://registry.yarnpkg.com/vite/-/vite-6.0.4.tgz#fe7cfaedff7c701d5582be5c4ed6a2150538ea9d" + integrity sha512-zwlH6ar+6o6b4Wp+ydhtIKLrGM/LoqZzcdVmkGAFun0KHTzIzjh+h0kungEx7KJg/PYnC80I4TII9WkjciSR6Q== dependencies: esbuild "^0.24.0" postcss "^8.4.49" @@ -5383,35 +5447,35 @@ warning@^4.0.0, warning@^4.0.3: dependencies: loose-envify "^1.0.0" -which-boxed-primitive@^1.0.2: - version "1.1.0" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.0.tgz#2d850d6c4ac37b95441a67890e19f3fda8b6c6d9" - integrity sha512-Ei7Miu/AXe2JJ4iNF5j/UphAgRoma4trE6PtisM09bPygb3egMH3YLW/befsWb1A1AxvNSFidOFTB18XtnIIng== +which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e" + integrity sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA== dependencies: is-bigint "^1.1.0" - is-boolean-object "^1.2.0" - is-number-object "^1.1.0" - is-string "^1.1.0" - is-symbol "^1.1.0" + is-boolean-object "^1.2.1" + is-number-object "^1.1.1" + is-string "^1.1.1" + is-symbol "^1.1.1" -which-builtin-type@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.2.0.tgz#58042ac9602d78a6d117c7e811349df1268ba63c" - integrity sha512-I+qLGQ/vucCby4tf5HsLmGueEla4ZhwTBSqaooS+Y0BuxN4Cp+okmGuV+8mXZ84KDI9BA+oklo+RzKg0ONdSUA== +which-builtin-type@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz#89183da1b4907ab089a6b02029cc5d8d6574270e" + integrity sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q== dependencies: - call-bind "^1.0.7" + call-bound "^1.0.2" function.prototype.name "^1.1.6" has-tostringtag "^1.0.2" is-async-function "^2.0.0" - is-date-object "^1.0.5" + is-date-object "^1.1.0" is-finalizationregistry "^1.1.0" is-generator-function "^1.0.10" - is-regex "^1.1.4" + is-regex "^1.2.1" is-weakref "^1.0.2" isarray "^2.0.5" - which-boxed-primitive "^1.0.2" + which-boxed-primitive "^1.1.0" which-collection "^1.0.2" - which-typed-array "^1.1.15" + which-typed-array "^1.1.16" which-collection@^1.0.2: version "1.0.2" @@ -5423,15 +5487,16 @@ which-collection@^1.0.2: is-weakmap "^2.0.2" is-weakset "^2.0.3" -which-typed-array@^1.1.14, which-typed-array@^1.1.15: - version "1.1.16" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.16.tgz#db4db429c4706feca2f01677a144278e4a8c216b" - integrity sha512-g+N+GAWiRj66DngFwHvISJd+ITsyphZvD1vChfVg6cEdnzy53GzB3oy0fUNlvhz7H7+MiqhYr26qxQShCpKTTQ== +which-typed-array@^1.1.16: + version "1.1.18" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.18.tgz#df2389ebf3fbb246a71390e90730a9edb6ce17ad" + integrity sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA== dependencies: available-typed-arrays "^1.0.7" - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.3" for-each "^0.3.3" - gopd "^1.0.1" + gopd "^1.2.0" has-tostringtag "^1.0.2" which@^2.0.1: From b58cd4c52a61f6ca52a3fcb808cfe42e5368de34 Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Sun, 22 Dec 2024 14:14:35 +0000 Subject: [PATCH 19/30] fix: error regression --- Gemfile | 2 +- Gemfile.lock | 15 +++++---------- app/javascript/stylesheets/calendar.scss | 16 ++++++++++++++++ app/models/tarifs/group_minimum.rb | 1 + app/services/export/pdf/renderables/rich_text.rb | 2 +- spec/models/payment_infos/qr_bill_spec.rb | 2 +- 6 files changed, 25 insertions(+), 13 deletions(-) diff --git a/Gemfile b/Gemfile index 54f87f4ab..11e08397f 100644 --- a/Gemfile +++ b/Gemfile @@ -37,7 +37,7 @@ gem 'net-pop', require: false gem 'net-smtp', require: false gem 'pg' gem 'prawn', '~> 2.4.0' # https://github.com/prawnpdf/prawn/issues/1346 -gem 'prawn-markup', git: 'https://github.com/puzzle/prawn-markup' +gem 'prawn-markup' gem 'prawn-table' gem 'puma' gem 'rack-mini-profiler' diff --git a/Gemfile.lock b/Gemfile.lock index 2376f6d9c..5bef10156 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,12 +1,3 @@ -GIT - remote: https://github.com/puzzle/prawn-markup - revision: 76d90979b9880e10716b76ec9c929f56cbc529d0 - specs: - prawn-markup (1.0.1) - nokogiri - prawn - prawn-table - GEM remote: https://rubygems.org/ specs: @@ -340,6 +331,10 @@ GEM prawn (2.4.0) pdf-core (~> 0.9.0) ttfunk (~> 1.7) + prawn-markup (1.0.1) + nokogiri + prawn + prawn-table prawn-table (0.2.2) prawn (>= 1.3.0, < 3.0.0) prime (0.1.3) @@ -611,7 +606,7 @@ DEPENDENCIES net-smtp pg prawn (~> 2.4.0) - prawn-markup! + prawn-markup prawn-table pry-rails pry-rescue diff --git a/app/javascript/stylesheets/calendar.scss b/app/javascript/stylesheets/calendar.scss index a66e98fa5..85b07de8d 100644 --- a/app/javascript/stylesheets/calendar.scss +++ b/app/javascript/stylesheets/calendar.scss @@ -1,5 +1,15 @@ @import "./variables"; +@keyframes fadein { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + .calendar { position: relative; container: calendar / inline-size; @@ -266,9 +276,15 @@ } svg { + cursor: inherit; overflow: clip; + rect, + polygon { + animation: fadein 500ms; + } + rect:not([fill]), polygon:not([fill]) { fill: transparent diff --git a/app/models/tarifs/group_minimum.rb b/app/models/tarifs/group_minimum.rb index 1195ffc0a..d1d64b1ea 100644 --- a/app/models/tarifs/group_minimum.rb +++ b/app/models/tarifs/group_minimum.rb @@ -48,6 +48,7 @@ def usages_in_group(usage) usage.booking.usages.joins(:tarif) .where(tarifs: { tarif_group: usage.tarif.tarif_group }) .where.not(id: usage.id) + .where.not(tarifs: { type: Tarifs::GroupMinimum.sti_name }) end def group_price(usage) diff --git a/app/services/export/pdf/renderables/rich_text.rb b/app/services/export/pdf/renderables/rich_text.rb index 65b56fdd2..61697ba75 100644 --- a/app/services/export/pdf/renderables/rich_text.rb +++ b/app/services/export/pdf/renderables/rich_text.rb @@ -21,7 +21,7 @@ def markup_options heading4: { style: :bold, size: 10, margin_top: 10, margin_bottom: 2 }, heading5: { style: :bold, margin_top: 10, margin_bottom: 2 }, heading6: { style: :bold, margin_top: 10, margin_bottom: 2 }, - table: { cell: { border_width: 0 } } + table: { header: { style: :bold, align: :left }, cell: { border_width: 0, padding: [0, 4, 4, 0] } } } end diff --git a/spec/models/payment_infos/qr_bill_spec.rb b/spec/models/payment_infos/qr_bill_spec.rb index 9e26ee7aa..d296f0f1e 100644 --- a/spec/models/payment_infos/qr_bill_spec.rb +++ b/spec/models/payment_infos/qr_bill_spec.rb @@ -48,7 +48,7 @@ context 'with QRR Ref' do let(:iban) { qr_iban } - it { is_expected.to eq('00 00000 00012 34567 89101 11213') } + it { is_expected.to eq('00 00000 00123 45678 91011 12138') } end context 'with SCOR Ref' do From 0c299106371e80fdabf4c7d7b32278098cf970e9 Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Sun, 22 Dec 2024 15:02:07 +0000 Subject: [PATCH 20/30] fix: changes from pr --- Gemfile.lock | 25 +++++++++++-------- .../data_digest_templates/invoice_part.rb | 2 +- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 5bef10156..501a2a005 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -84,7 +84,7 @@ GEM rake (>= 0.8.7) ast (2.4.2) aws-eventstream (1.3.0) - aws-partitions (1.1024.0) + aws-partitions (1.1027.0) aws-sdk-core (3.214.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -151,7 +151,7 @@ GEM cronex (0.15.0) tzinfo unicode (>= 0.4.4.5) - csv (3.3.1) + csv (3.3.2) database_cleaner (2.1.0) database_cleaner-active_record (>= 2, < 3) database_cleaner-active_record (2.2.0) @@ -159,7 +159,7 @@ GEM database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) date (3.4.1) - debug (1.9.2) + debug (1.10.0) irb (~> 1.10) reline (>= 0.3.8) devise (4.9.4) @@ -211,7 +211,7 @@ GEM zeitwerk (~> 2.6) email_address (0.2.4) simpleidn - erubi (1.13.0) + erubi (1.13.1) et-orbi (1.2.11) tzinfo exception_notification (4.1.1) @@ -271,19 +271,21 @@ GEM rdoc (>= 4.0.0) reline (>= 0.4.2) jmespath (1.6.2) - json (2.9.0) + json (2.9.1) kramdown (2.5.1) rexml (>= 3.3.9) language_server-protocol (3.17.0.3) launchy (3.0.1) addressable (~> 2.8) childprocess (~> 5.0) - liquid (5.5.1) + liquid (5.6.0) + bigdecimal + strscan listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) locale (2.1.4) - logger (1.6.3) + logger (1.6.4) loofah (2.23.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) @@ -308,7 +310,7 @@ GEM mutex_m (0.3.0) net-http (0.6.0) uri - net-imap (0.5.2) + net-imap (0.5.3) date net-protocol net-pop (0.1.2) @@ -340,7 +342,7 @@ GEM prime (0.1.3) forwardable singleton - prism (1.2.0) + prism (1.3.0) pry (0.15.0) coderay (~> 1.1) method_source (~> 1.0) @@ -514,17 +516,18 @@ GEM actionpack (>= 3.1) railties (>= 3.1) slim (>= 3.0, < 6.0, != 5.0.0) - sorbet-runtime (0.5.11704) + sorbet-runtime (0.5.11708) squasher (0.8.0) statesman (12.1.0) statsd-ruby (1.5.0) stringio (3.1.2) + strscan (3.1.2) temple (0.10.3) terminal-table (1.6.0) text (1.3.1) thor (1.3.2) thruster (0.1.9-x86_64-linux) - tilt (2.4.0) + tilt (2.5.0) timeout (0.4.3) translation (1.41) gettext (~> 3.2, >= 3.2.5, <= 3.4.9) diff --git a/app/models/data_digest_templates/invoice_part.rb b/app/models/data_digest_templates/invoice_part.rb index 5aac493bc..e65a383ed 100644 --- a/app/models/data_digest_templates/invoice_part.rb +++ b/app/models/data_digest_templates/invoice_part.rb @@ -25,7 +25,7 @@ module DataDigestTemplates class InvoicePart < Tabular - # ::DataDigestTemplate.register_subtype self + ::DataDigestTemplate.register_subtype self DEFAULT_COLUMN_CONFIG = [ { From 49fa3cf7db7bae8ae1c59eb93034fcb39a1054e9 Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Sun, 22 Dec 2024 15:46:46 +0000 Subject: [PATCH 21/30] fix: import payments --- app/services/camt_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/camt_service.rb b/app/services/camt_service.rb index 22d078649..57eeedf05 100644 --- a/app/services/camt_service.rb +++ b/app/services/camt_service.rb @@ -39,7 +39,7 @@ def payment_from_transaction(transaction, entry) end def find_invoice_by_ref(ref) - @organisation.invoices.kept.where(normalized_ref_condition(ref)).first + @organisation.invoices.kept.where(self.class.normalized_ref_condition(ref)).first end def self.normalized_ref_condition(ref) From 50054a016e3d45bf40c1fc7b8c3c831349485bef Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Sun, 22 Dec 2024 19:46:30 +0000 Subject: [PATCH 22/30] fix: payment_ref finder --- app/models/invoice.rb | 8 ++++---- app/models/journal_entry.rb | 15 ++++++++------- app/services/camt_service.rb | 2 +- app/services/taf_block.rb | 14 +++++++++----- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 688e73ccb..60393b55a 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -119,18 +119,18 @@ def generate_payment_ref end def generate_journal_entries? - organisation.accounting_settings.enabled && !skip_generate_journal_entries && (changed? || journal_entries.none?) + organisation.accounting_settings.enabled && !skip_generate_journal_entries end def generate_journal_entries! return unless organisation.accounting_settings.enabled - existing_ids = organisation.journal_entry_ids.where(invoice: self).pluck(:id) + existing_ids = organisation.journal_entries.where(invoice: self).pluck(:id) new_journal_entries = JournalEntry::Factory.new.invoice(self) # raise ActiveRecord::Rollback unless - new_journal_entries.save! && organisation.where(id: existing_ids, invoice: self).destroy_all - payments.each(&:generate_journal_entries!) + new_journal_entries.save! && organisation.journal_entries.where(id: existing_ids, invoice: self).destroy_all + # payments.each(&:generate_journal_entries!) end def paid? diff --git a/app/models/journal_entry.rb b/app/models/journal_entry.rb index 4c176c394..7fcc71e8c 100644 --- a/app/models/journal_entry.rb +++ b/app/models/journal_entry.rb @@ -40,7 +40,7 @@ class JournalEntry < ApplicationRecord validates :account_nr, :side, :amount, :source_document_ref, :currency, presence: true - scope :ordered, -> { order(date: :ASC, created_at: :ASC) } + scope :ordered, -> { order(date: :ASC, ordinal: :ASC, created_at: :ASC) } def invert return self.side = :soll if haben? @@ -65,8 +65,9 @@ def haben_amount amount if haben? end - def parallels - JournalEntry.where(invoice:, source_type:, source_id:).index_by(&:book_type).symbolize_keys + def related + @related ||= JournalEntry.where(invoice:, source_type:, source_id:, date:, source_document_ref:) + .index_by(&:book_type).symbolize_keys end def self.collect(**defaults, &) @@ -115,7 +116,7 @@ def valid? end def save! - valid? && journal_entries.all?(&:save!) + valid? && journal_entries.each_with_index { |journal_entry, ordinal| journal_entry.update!(ordinal:) } end end @@ -170,11 +171,11 @@ def invoice_part_add(invoice_part, collection) # rubocop:disable Metrics/AbcSize end end - def payment(payment) # rubocop:disable Metrics/AbcSize + def payment(payment) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity payment.instance_eval do - JournalEntry.collect(currency: organisation.currency, source_document_ref: invoice.ref, + JournalEntry.collect(currency: organisation.currency, source_document_ref: invoice&.ref, date: paid_at, invoice:, source_type: Payment.sti_name, source_id: id, - text: "#{Payment.model_name.human} #{invoice.ref}") do |collection| + text: "#{Payment.model_name.human} #{invoice&.ref}") do |collection| collection.soll(account_nr: organisation&.accounting_settings&.payment_account_nr, amount:) collection.haben(account_nr: organisation&.accounting_settings&.debitor_account_nr, amount:) end diff --git a/app/services/camt_service.rb b/app/services/camt_service.rb index 57eeedf05..4a0257e22 100644 --- a/app/services/camt_service.rb +++ b/app/services/camt_service.rb @@ -43,7 +43,7 @@ def find_invoice_by_ref(ref) end def self.normalized_ref_condition(ref) - Arel::Nodes::NamedFunction.new('LPAD', [Invoice.arel_table[:ref], 27, Arel::Nodes.build_quoted('0')]) + Arel::Nodes::NamedFunction.new('LPAD', [Invoice.arel_table[:payment_ref], 27, Arel::Nodes.build_quoted('0')]) .eq(normalize_ref(ref)) end diff --git a/app/services/taf_block.rb b/app/services/taf_block.rb index 574ea0c8c..44363798a 100644 --- a/app/services/taf_block.rb +++ b/app/services/taf_block.rb @@ -138,7 +138,8 @@ def self.derive(value, **override) Flags: nil, # String[5]; The Id of the tax. [MWSt-Kürzel] - TaxId: (journal_entry.book_type_main? && journal_entry.vat_category&.accounting_vat_code) || nil, + TaxId: (journal_entry.book_type_main? && journal_entry.vat_category&.percentage&.positive? && + journal_entry.vat_category&.accounting_vat_code) || nil, # MkTxB: journal_entry.vat_category&.accounting_vat_code.present?, @@ -197,12 +198,15 @@ def self.derive(value, **override) new(:OPd, **{ PkKey: pk_key, OpId: op_id, ZabId: '15T' }), new(:Blg, **{ Date: invoice.issued_at, Orig: true }) do # TODO: check if invoice == source - derive(journal_entries.shift, Flags: 1, OpId: op_id, PkKey: pk_key) + creation_journal_entry = journal_entries.shift + derive(creation_journal_entry, Flags: 1, OpId: op_id, PkKey: pk_key, CAcc: :div) journal_entries.each_with_index do |journal_entry, index| - cost_index = (journal_entry.book_type_main? && journal_entries.index(journal_entry.parallels[:cost])) || nil - vat_index = (journal_entry.book_type_main? && journal_entries.index(journal_entry.parallels[:vat])) || nil - derive(journal_entry, CIdx: cost_index&.+(index + 2), TIdx: vat_index&.+(index + 2)) + cost_index = (journal_entry.book_type_main? && journal_entries.index(journal_entry.related[:cost])) || nil + vat_index = (journal_entry.book_type_main? && journal_entries.index(journal_entry.related[:vat])) || nil + taf_index = index + 2 + derive(journal_entry, CIdx: cost_index&.+(taf_index), TIdx: vat_index&.+(taf_index), + CAcc: Value.cast(creation_journal_entry.account_nr, as: :symbol)) end end ] From 0c1717c4db53095d06e740c675aa2e17f5184985 Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Mon, 23 Dec 2024 13:16:05 +0000 Subject: [PATCH 23/30] fix: upgrade liquid --- app/models/data_digest_template.rb | 7 ++++ app/models/data_digest_templates/tabular.rb | 4 ++- app/models/rich_text_template.rb | 9 ++++- app/services/template_environment.rb | 40 +++++++++++++++++++++ config/initializers/liquid.rb | 40 --------------------- 5 files changed, 58 insertions(+), 42 deletions(-) create mode 100644 app/services/template_environment.rb delete mode 100644 config/initializers/liquid.rb diff --git a/app/models/data_digest_template.rb b/app/models/data_digest_template.rb index fd7559003..6990576e0 100644 --- a/app/models/data_digest_template.rb +++ b/app/models/data_digest_template.rb @@ -70,4 +70,11 @@ def crunch(_records); end def prefilter @prefilter ||= filter_class&.new(prefilter_params.presence || {}) end + + def self.template_environment + @template_environment ||= Liquid::Environment.build do |environment| + environment.error_mode = :strict unless Rails.env.production? + environment.register_filter(TemplateEnvironment::Default) + end + end end diff --git a/app/models/data_digest_templates/tabular.rb b/app/models/data_digest_templates/tabular.rb index 9115c09ed..f53ee3263 100644 --- a/app/models/data_digest_templates/tabular.rb +++ b/app/models/data_digest_templates/tabular.rb @@ -112,7 +112,9 @@ class Column def initialize(config, header: nil, footer: nil, body: nil) @config = config.symbolize_keys @blocks = { header:, footer:, body: } - @templates = @config.slice(*@blocks.keys).transform_values { |template| Liquid::Template.parse(template) } + @templates = @config.slice(*@blocks.keys).transform_values do |template| + Liquid::Template.parse(template, environment: DataDigestTemplate.template_environment) + end end def column_type diff --git a/app/models/rich_text_template.rb b/app/models/rich_text_template.rb index bc986fd3d..21c43f0b7 100644 --- a/app/models/rich_text_template.rb +++ b/app/models/rich_text_template.rb @@ -100,7 +100,7 @@ def interpolate(context, locale: I18n.locale) I18n.with_locale(locale) do context = TemplateContext.new(context) unless context.is_a?(TemplateContext) parts = [title, body].map do |part| - template = Liquid::Template.parse(part) + template = Liquid::Template.parse(part, environment: self.class.template_environment) RichTextSanitizer.sanitize(template.render!(context.to_h)) end InterpolationResult.new(*parts) @@ -144,4 +144,11 @@ def self.defaults_for_key(key:, locales: I18n.available_locales) end end end + + def self.template_environment + @template_environment ||= Liquid::Environment.build do |environment| + environment.error_mode = :strict unless Rails.env.production? + environment.register_filter(TemplateEnvironment::Default) + end + end end diff --git a/app/services/template_environment.rb b/app/services/template_environment.rb new file mode 100644 index 000000000..14ddc38ab --- /dev/null +++ b/app/services/template_environment.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class TemplateEnvironment + module Default + def i18n_translate(value, scope = nil) + I18n.t(value, scope:, default: nil) + end + + def date_format(value, format = I18n.t('date.formats.default')) + value = Date.iso8601(value) unless value.respond_to?(:strftime) + I18n.l(value, format:) + rescue Date::Error, TypeError + nil + end + + def datetime_format(value, format = I18n.t('time.formats.default')) + value = DateTime.iso8601(value) unless value.respond_to?(:strftime) + I18n.l(value, format:) + rescue Date::Error, TypeError + nil + end + + def currency(value, unit = nil) + ActiveSupport::NumberHelper.number_to_currency(value, **({ unit: } if unit.present?)) + end + + def as_liquid(value) + "{{ #{value} }}" + end + + def booking_condition(value, type, *args) + # compare_value = args.pop + # compare_operator = args.pop + # compare_attribute = args.pop + # booking_condition = BookingConditions.const_get(type)&.new(compare_attribute:, compare_operator:, + # compare_value:) + # booking_condition&.evaluate(Booking.find(value.fetch('id'))) + end + end +end diff --git a/config/initializers/liquid.rb b/config/initializers/liquid.rb deleted file mode 100644 index b59bc4df1..000000000 --- a/config/initializers/liquid.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -Liquid::Template.error_mode = :strict unless Rails.env.production? -Liquid::Template.register_filter(Module.new do - def i18n_translate(value, scope = nil) - I18n.t(value, scope:, default: nil) - end - - def date_format(value, format = I18n.t('date.formats.default')) - value = Date.iso8601(value) unless value.respond_to?(:strftime) - I18n.l(value, format:) - rescue Date::Error, TypeError - nil - end - - def datetime_format(value, format = I18n.t('time.formats.default')) - value = DateTime.iso8601(value) unless value.respond_to?(:strftime) - I18n.l(value, format:) - rescue Date::Error, TypeError - nil - end - - def currency(value, unit = nil) - return ActiveSupport::NumberHelper.number_to_currency(value, unit:) if unit.present? - - ActiveSupport::NumberHelper.number_to_currency(value) - end - - def as_liquid(value) - "{{ #{value} }}" - end - - def booking_condition(value, type, *args) - # compare_value = args.pop - # compare_operator = args.pop - # compare_attribute = args.pop - # booking_condition = BookingConditions.const_get(type)&.new(compare_attribute:, compare_operator:, compare_value:) - # booking_condition&.evaluate(Booking.find(value.fetch('id'))) - end -end) From 277853186a75c89b7cd8090c51951f46d0816439 Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Fri, 3 Jan 2025 14:11:40 +0000 Subject: [PATCH 24/30] stashcommit --- Gemfile.lock | 24 +-- .../manage/journal_entries_controller.rb | 22 ++- app/domain/booking_actions/public/cancel.rb | 13 +- app/models/ability.rb | 2 +- .../data_digest_templates/journal_entry.rb | 16 +- app/models/data_digest_templates/payment.rb | 4 +- app/models/invoice_part.rb | 2 + app/models/journal_entry.rb | 153 ++++++++++++------ app/models/organisation_user.rb | 2 +- app/models/payment.rb | 3 + app/serializers/manage/booking_serializer.rb | 16 +- .../manage/journal_entry_serializer.rb | 2 +- app/services/taf_block.rb | 12 +- .../layouts/_manage_navigation.html.slim | 123 +++++++------- .../manage/journal_entries/_form.html.slim | 2 +- .../manage/journal_entries/index.html.slim | 20 ++- .../filter/_filter_fields.html.slim | 8 + config/locales/de.yml | 24 ++- ...30084234_add_sources_to_journal_entries.rb | 19 +++ db/schema.rb | 17 +- spec/factories/journal_entries.rb | 41 ++--- spec/models/journal_entry_spec.rb | 95 ++++++++--- spec/services/taf_block_spec.rb | 4 +- 23 files changed, 422 insertions(+), 202 deletions(-) create mode 100644 db/migrate/20241230084234_add_sources_to_journal_entries.rb diff --git a/Gemfile.lock b/Gemfile.lock index 501a2a005..705761e4a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -84,8 +84,8 @@ GEM rake (>= 0.8.7) ast (2.4.2) aws-eventstream (1.3.0) - aws-partitions (1.1027.0) - aws-sdk-core (3.214.0) + aws-partitions (1.1029.0) + aws-sdk-core (3.214.1) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -109,7 +109,7 @@ GEM base64 (0.2.0) bcrypt (3.1.20) benchmark (0.4.0) - bigdecimal (3.1.8) + bigdecimal (3.1.9) blueprinter (1.1.2) bootsnap (1.18.4) msgpack (~> 1.2) @@ -310,7 +310,7 @@ GEM mutex_m (0.3.0) net-http (0.6.0) uri - net-imap (0.5.3) + net-imap (0.5.4) date net-protocol net-pop (0.1.2) @@ -320,7 +320,7 @@ GEM net-smtp (0.5.0) net-protocol nio4r (2.7.4) - nokogiri (1.17.2-x86_64-linux) + nokogiri (1.18.1-x86_64-linux-musl) racc (~> 1.4) orm_adapter (0.5.0) ostruct (0.6.1) @@ -343,7 +343,7 @@ GEM forwardable singleton prism (1.3.0) - pry (0.15.0) + pry (0.15.2) coderay (~> 1.1) method_source (~> 1.0) pry-rails (0.3.11) @@ -366,7 +366,7 @@ GEM rack rack-session (2.0.0) rack (>= 3.0.0) - rack-test (2.1.0) + rack-test (2.2.0) rack (>= 1.3) rackup (2.2.1) rack (>= 3) @@ -409,7 +409,7 @@ GEM rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) - rbs (3.7.0) + rbs (3.8.1) logger rdoc (6.10.0) psych (>= 4.0.0) @@ -423,7 +423,7 @@ GEM redis-client (>= 0.22.0) redis-client (0.23.0) connection_pool - regexp_parser (2.9.3) + regexp_parser (2.10.0) reline (0.6.0) io-console (~> 0.5) request_store (1.7.0) @@ -468,7 +468,7 @@ GEM rubocop-performance (1.23.0) rubocop (>= 1.48.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rails (2.27.0) + rubocop-rails (2.28.0) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.52.0, < 2.0) @@ -512,7 +512,7 @@ GEM slim (5.2.1) temple (~> 0.10.0) tilt (>= 2.1.0) - slim-rails (3.6.3) + slim-rails (3.7.0) actionpack (>= 3.1) railties (>= 3.1) slim (>= 3.0, < 6.0, != 5.0.0) @@ -536,7 +536,7 @@ GEM concurrent-ruby (~> 1.0) unaccent (0.4.0) unicode (0.4.4.5) - unicode-display_width (3.1.2) + unicode-display_width (3.1.3) unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) uri (1.0.2) diff --git a/app/controllers/manage/journal_entries_controller.rb b/app/controllers/manage/journal_entries_controller.rb index 7d910a297..5fd268aca 100644 --- a/app/controllers/manage/journal_entries_controller.rb +++ b/app/controllers/manage/journal_entries_controller.rb @@ -3,10 +3,12 @@ module Manage class JournalEntriesController < BaseController load_and_authorize_resource :journal_entry + before_action :set_filter, only: :index def index @journal_entries = @journal_entries.joins(invoice: :booking).ordered .where(invoices: { bookings: { organisation: current_organisation } }) + @journal_entries = @filter.apply(@journal_entries, cached: false) if @filter.any? respond_with :manage, @journal_entries end @@ -30,6 +32,16 @@ def update respond_with :manage, location: manage_journal_entries_path end + def update_many + @tarifs = Tarif.where(organisation: current_organisation) + @updated_tarifs = (tarifs_params[:tarifs]&.values || []).map do |tarif_params| + @tarifs.find_by(id: tarif_params[:id])&.tap do |tarif| + tarif.update(tarif_params) + end + end + respond_with :manage, @updated_tarifs, location: manage_tarifs_path + end + def destroy @journal_entry.destroy respond_with :manage, @journal_entry, location: manage_journal_entries_path @@ -37,9 +49,17 @@ def destroy private + def set_filter + @filter = JournalEntry::Filter.new(processed: false, **(journal_entry_filter_params || {})) + end + + def journal_entry_filter_params + params[:filter]&.permit(%w[processed_at_after processed_at_before date_after date_before processed]) + end + def journal_entry_params params.require(:journal_entry).permit(*%i[invoice_id source_type source_id vat_category_id account_nr side amount - date text currency ordinal source_document_ref book_type]) + date text currency ordinal ref book_type]) end end end diff --git a/app/domain/booking_actions/public/cancel.rb b/app/domain/booking_actions/public/cancel.rb index c01f22a2d..d1b9137ab 100644 --- a/app/domain/booking_actions/public/cancel.rb +++ b/app/domain/booking_actions/public/cancel.rb @@ -5,12 +5,13 @@ module Public class Cancel < BookingActions::Base def invoke! booking.reload - transition_to = if booking.can_transition_to?(:cancelled_request) - :cancelled_request - elsif booking.can_transition_to?(:cancelation_pending) - :cancelation_pending - end - Result.new success: booking.update(transition_to:) + booking.cancellation_reason ||= I18n.t('.cancellation_reason') + booking.transition_to = if booking.can_transition_to?(:cancelled_request) + :cancelled_request + elsif booking.can_transition_to?(:cancelation_pending) + :cancelation_pending + end + Result.new success: booking.save end def allowed? diff --git a/app/models/ability.rb b/app/models/ability.rb index 217afa952..bea90ae3d 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -71,7 +71,7 @@ class Manage < Base can(:read, JournalEntry, invoice: { booking: { organisation: } }) end - role :treasurer do |user, organisation| + role :finance do |user, organisation| next unless user.in_organisation?(organisation) abilities_for_role(:readonly, user, organisation) diff --git a/app/models/data_digest_templates/journal_entry.rb b/app/models/data_digest_templates/journal_entry.rb index 3ee2a2ea9..c04b56003 100644 --- a/app/models/data_digest_templates/journal_entry.rb +++ b/app/models/data_digest_templates/journal_entry.rb @@ -33,8 +33,8 @@ class JournalEntry < Tabular body: '{{ journal_entry.date | date_format }}' }, { - header: ::JournalEntry.human_attribute_name(:source_document_ref), - body: '{{ journal_entry.source_document_ref }}' + header: ::JournalEntry.human_attribute_name(:ref), + body: '{{ journal_entry.ref }}' }, { header: ::JournalEntry.human_attribute_name(:text), @@ -52,10 +52,6 @@ class JournalEntry < Tabular header: ::JournalEntry.human_attribute_name(:amount), body: '{{ journal_entry.amount | round: 2 }}' }, - { - header: ::JournalEntry.human_attribute_name(:vat_code), - body: '{{ journal_entry.vat_code }}' - }, { header: ::JournalEntry.human_attribute_name(:book_type), body: '{{ journal_entry.book_type }}' @@ -67,17 +63,17 @@ class JournalEntry < Tabular ].freeze column_type :default do - body do |journal_entry, tempalte_context_cache| + body do |journal_entry, template_context_cache| booking = journal_entry.booking - context = tempalte_context_cache[cache_key(journal_entry)] ||= + context = template_context_cache[cache_key(journal_entry)] ||= TemplateContext.new(booking:, organisation: booking.organisation, journal_entry:).to_h @templates[:body]&.render!(context) end end formatter(:taf) do |_options = {}| - records.to_a.group_by(&:invoice).map do |invoice, _journal_entries| - TafBlock::Collection.new { derive(invoice) } + JournalEntry::Compound.group(records) do |compound| + # TafBlock::Collection.new { derive(compound) } end.join("\n\n") end diff --git a/app/models/data_digest_templates/payment.rb b/app/models/data_digest_templates/payment.rb index 7dbe3a392..f6f025e7e 100644 --- a/app/models/data_digest_templates/payment.rb +++ b/app/models/data_digest_templates/payment.rb @@ -55,9 +55,9 @@ class Payment < Tabular ].freeze column_type :default do - body do |payment, tempalte_context_cache| + body do |payment, template_context_cache| booking = payment.booking - context = tempalte_context_cache[cache_key(payment)] ||= + context = template_context_cache[cache_key(payment)] ||= TemplateContext.new(booking:, organisation: booking.organisation, payment:).to_h @templates[:body]&.render!(context) end diff --git a/app/models/invoice_part.rb b/app/models/invoice_part.rb index 2b8dbb3ef..46e444150 100644 --- a/app/models/invoice_part.rb +++ b/app/models/invoice_part.rb @@ -38,6 +38,8 @@ class InvoicePart < ApplicationRecord has_one :tarif, through: :usage has_one :booking, through: :usage + has_many :journal_entries, inverse_of: :invoice_part, dependent: :destroy + attribute :apply, :boolean, default: true delegate :booking, :organisation, to: :invoice diff --git a/app/models/journal_entry.rb b/app/models/journal_entry.rb index 7fcc71e8c..ee7efefb8 100644 --- a/app/models/journal_entry.rb +++ b/app/models/journal_entry.rb @@ -2,45 +2,57 @@ # # Table name: journal_entries # -# id :integer not null, primary key -# invoice_id :integer not null -# source_type :string -# source_id :integer -# vat_category_id :integer -# account_nr :string not null -# side :integer not null -# amount :decimal(, ) not null -# date :date not null -# text :string -# currency :string not null -# ordinal :integer -# source_document_ref :string -# book_type :integer -# created_at :datetime not null -# updated_at :datetime not null +# id :integer not null, primary key +# invoice_id :integer +# vat_category_id :integer +# account_nr :string not null +# side :integer not null +# amount :decimal(, ) not null +# date :date not null +# text :string +# currency :string not null +# ordinal :integer +# ref :string +# book_type :integer +# created_at :datetime not null +# updated_at :datetime not null +# invoice_part_id :integer +# payment_id :integer +# trigger :integer not null +# booking_id :uuid not null +# processed_at :datetime # # Indexes # +# index_journal_entries_on_booking_id (booking_id) # index_journal_entries_on_invoice_id (invoice_id) -# index_journal_entries_on_source (source_type,source_id) +# index_journal_entries_on_invoice_part_id (invoice_part_id) +# index_journal_entries_on_payment_id (payment_id) # index_journal_entries_on_vat_category_id (vat_category_id) # # frozen_string_literal: true class JournalEntry < ApplicationRecord - belongs_to :invoice, inverse_of: :journal_entries + belongs_to :booking + belongs_to :invoice, inverse_of: :journal_entries, optional: true + belongs_to :invoice_part, inverse_of: :journal_entries, optional: true + belongs_to :payment, inverse_of: :journal_entries, optional: true belongs_to :vat_category, optional: true - has_one :booking, through: :invoice has_one :organisation, through: :booking enum :side, { soll: 1, haben: -1 } enum :book_type, { main: 0, cost: 1, vat: 2 }, prefix: true, default: :main + enum :trigger, { manual: 0, invoice_created: 1, payment_created: 2 }, prefix: true - validates :account_nr, :side, :amount, :source_document_ref, :currency, presence: true + before_validation :set_currency + + validates :account_nr, :side, :amount, :ref, :currency, :date, :trigger, presence: true scope :ordered, -> { order(date: :ASC, ordinal: :ASC, created_at: :ASC) } + scope :processed, -> { where.not(processed_at: nil) } + scope :unprocessed, -> { where(processed_at: nil) } def invert return self.side = :soll if haben? @@ -65,27 +77,35 @@ def haben_amount amount if haben? end + def set_currency + self.currency ||= organisation&.currency + end + def related - @related ||= JournalEntry.where(invoice:, source_type:, source_id:, date:, source_document_ref:) + @related ||= JournalEntry.where(booking:, invoice:, payment:, invoice_part:, trigger:, date:, ref:) .index_by(&:book_type).symbolize_keys end def self.collect(**defaults, &) - Collection.new(**defaults).tap(&) + Compound.new(**defaults).tap(&) end - class Collection - delegate :[], :to_a, to: :journal_entries + class Compound + COMPOUND_ATTRIBUTES = %i[booking_id invoice_id payment_id date trigger].freeze - attr_reader :journal_entries + delegate :[], :to_a, :<<, to: :journal_entries - def initialize(**defaults) - @journal_entries = [] + attr_reader :common, :defaults + attr_accessor :journal_entries + + def initialize(journal_entries = [], **defaults) + @journal_entries = journal_entries @defaults = defaults + @common = defaults.slice(*COMPOUND_ATTRIBUTES) end def collect(journal_entry) - journal_entry = JournalEntry.new(**@defaults, **journal_entry) if journal_entry.is_a?(Hash) + journal_entry = JournalEntry.new(**defaults, **journal_entry) if journal_entry.is_a?(Hash) return if journal_entry.account_nr.blank? || journal_entry.amount.blank? || journal_entry.amount.zero? @journal_entries << journal_entry @@ -112,72 +132,101 @@ def balanced? end def valid? - journal_entries.all?(&:valid?) # && balanced? + journal_entries.all?(&:valid?) && balanced? end def save! - valid? && journal_entries.each_with_index { |journal_entry, ordinal| journal_entry.update!(ordinal:) } + raise ActiveRecord::RecordInvalid unless valid? + + journal_entries.each_with_index { |journal_entry, ordinal| journal_entry.update!(ordinal:) } + end + + def ==(other) + common == other.common + end + + def self.group(journal_entries) + journal_entries.each_with_object([]) do |journal_entry, compounds| + common = journal_entry.attributes.symbolize_keys.slice(*COMPOUND_ATTRIBUTES) + compound = compounds.find { _1.common == common } + compounds << compound = new(**common) if compound.nil? + compound.journal_entries << journal_entry + end end end class Filter < ApplicationFilter attribute :date_after, :date attribute :date_before, :date + attribute :processed_at_after, :date + attribute :processed_at_before, :date + attribute :processed, :boolean filter :date do |journal_entries| next unless date_before.present? || date_after.present? journal_entries.where(JournalEntry.arel_table[:date].between(date_after..date_before)) end + + filter :processed do |journal_entries| + next if processed.nil? + + processed ? journal_entries.processed : journal_entries.unprocessed + end + + filter :processed_at do |journal_entries| + next unless processed_at_before.present? || processed_at_after.present? + + journal_entries.where(JournalEntry.arel_table[:processed_at].between(processed_at_after..processed_at_before)) + end end class Factory def invoice(invoice) - JournalEntry.collect(currency: invoice.currency, source_document_ref: invoice.ref, date: invoice.issued_at, - invoice:, text: "#{invoice.ref} - #{invoice.booking.tenant.last_name}") do |collection| + text = "#{invoice.ref} - #{invoice.booking.tenant.last_name}" + JournalEntry.collect(ref: invoice.ref, date: invoice.issued_at, invoice:, + booking: invoice.booking, trigger: :invoice_created, text:) do |compound| next unless invoice.is_a?(Invoices::Deposit) || invoice.is_a?(Invoices::Invoice) next unless invoice.kept? - invoice_debitor(invoice, collection) - invoice.invoice_parts.map { invoice_part(_1, collection) } + invoice_debitor(invoice, compound) + invoice.invoice_parts.map { invoice_part(_1, compound) } end end - def invoice_debitor(invoice, collection) + def invoice_debitor(invoice, compound) invoice.instance_eval do - defaults = { source_type: ::Invoice.sti_name, source_id: id } - # Der Betrag, welcher der Debitor noch schuldig ist. (inkl. MwSt.). Jak: «Erlösbuchung» - collection.soll(**defaults, account_nr: organisation&.accounting_settings&.debitor_account_nr, amount: amount) + compound.soll(account_nr: organisation&.accounting_settings&.debitor_account_nr, amount:) end end - def invoice_part(invoice_part, collection) + def invoice_part(invoice_part, compound) case invoice_part when InvoiceParts::Add, InvoiceParts::Deposit - invoice_part_add(invoice_part, collection) + invoice_part_add(invoice_part, compound) end end - def invoice_part_add(invoice_part, collection) # rubocop:disable Metrics/AbcSize + def invoice_part_add(invoice_part, compound) # rubocop:disable Metrics/AbcSize invoice_part.instance_eval do - defaults = { source_type: self.class.sti_name, source_id: id, vat_category:, text: "#{invoice.ref} #{label}" } + defaults = { invoice_part:, vat_category:, text: "#{invoice.ref} #{label}" } - collection.haben(**defaults, account_nr: accounting_account_nr, amount: vat_breakdown[:netto]) - collection.haben(**defaults, account_nr: accounting_cost_center_nr, - book_type: :cost, amount: vat_breakdown[:netto]) - collection.haben(**defaults, account_nr: vat_category&.organisation&.accounting_settings&.vat_account_nr, - book_type: :vat, amount: vat_breakdown[:vat]) + compound.haben(**defaults, account_nr: accounting_account_nr, amount: vat_breakdown[:netto]) + compound.haben(**defaults, account_nr: accounting_cost_center_nr, + book_type: :cost, amount: vat_breakdown[:netto]) + compound.haben(**defaults, account_nr: vat_category&.organisation&.accounting_settings&.vat_account_nr, + book_type: :vat, amount: vat_breakdown[:vat]) end end def payment(payment) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity payment.instance_eval do - JournalEntry.collect(currency: organisation.currency, source_document_ref: invoice&.ref, - date: paid_at, invoice:, source_type: Payment.sti_name, source_id: id, - text: "#{Payment.model_name.human} #{invoice&.ref}") do |collection| - collection.soll(account_nr: organisation&.accounting_settings&.payment_account_nr, amount:) - collection.haben(account_nr: organisation&.accounting_settings&.debitor_account_nr, amount:) + text = "#{Payment.model_name.human} #{invoice&.ref}" + JournalEntry.collect(currency: organisation.currency, ref: invoice&.ref, date: paid_at, invoice:, + payment:, text:, booking:, trigger: :payment_created) do |compound| + compound.soll(account_nr: organisation&.accounting_settings&.payment_account_nr, amount:) + compound.haben(account_nr: organisation&.accounting_settings&.debitor_account_nr, amount:) end end end diff --git a/app/models/organisation_user.rb b/app/models/organisation_user.rb index 0718f9099..5a8acf35f 100644 --- a/app/models/organisation_user.rb +++ b/app/models/organisation_user.rb @@ -23,7 +23,7 @@ class OrganisationUser < ApplicationRecord belongs_to :organisation, inverse_of: :organisation_users belongs_to :user, inverse_of: :organisation_users, autosave: true - enum :role, { none: 0, readonly: 1, admin: 2, manager: 3, treasurer: 4 }, prefix: :role + enum :role, { none: 0, readonly: 1, admin: 2, manager: 3, finance: 4 }, prefix: :role has_secure_token :token, length: 48 diff --git a/app/models/payment.rb b/app/models/payment.rb index 5b21625b3..902292f1c 100644 --- a/app/models/payment.rb +++ b/app/models/payment.rb @@ -32,8 +32,11 @@ class Payment < ApplicationRecord MailTemplate.define(:payment_confirmation_notification, context: %i[booking payment]) belongs_to :invoice, optional: true belongs_to :booking, touch: true + has_one :organisation, through: :booking + has_many :journal_entries, inverse_of: :payment, dependent: :destroy + attribute :applies, :boolean, default: true attribute :confirm, :boolean, default: true diff --git a/app/serializers/manage/booking_serializer.rb b/app/serializers/manage/booking_serializer.rb index 97b1fa78b..8c380f302 100644 --- a/app/serializers/manage/booking_serializer.rb +++ b/app/serializers/manage/booking_serializer.rb @@ -6,14 +6,14 @@ class BookingSerializer < Public::BookingSerializer identifier :id - association :home, blueprint: Manage::HomeSerializer - association :occupancies, blueprint: Manage::OccupancySerializer - association :occupiables, blueprint: Manage::OccupiableSerializer - association :tenant, blueprint: Manage::TenantSerializer - association :deadline, blueprint: Manage::DeadlineSerializer - association :category, blueprint: Manage::BookingCategorySerializer - association :contract, blueprint: Manage::ContractSerializer - association :usages, blueprint: Manage::UsageSerializer + association :home, blueprint: Manage::HomeSerializer + association :occupancies, blueprint: Manage::OccupancySerializer + association :occupiables, blueprint: Manage::OccupiableSerializer + association :tenant, blueprint: Manage::TenantSerializer + association :deadline, blueprint: Manage::DeadlineSerializer + association :category, blueprint: Manage::BookingCategorySerializer + association :contract, blueprint: Manage::ContractSerializer + association :usages, blueprint: Manage::UsageSerializer fields :tenant_organisation, :cancellation_reason, :invoice_address, :ref, :committed_request, :tenant_id, :locale, :purpose_description, :approximate_headcount, :remarks diff --git a/app/serializers/manage/journal_entry_serializer.rb b/app/serializers/manage/journal_entry_serializer.rb index 6f0ae01fa..ccded44d7 100644 --- a/app/serializers/manage/journal_entry_serializer.rb +++ b/app/serializers/manage/journal_entry_serializer.rb @@ -3,7 +3,7 @@ module Manage class JournalEntrySerializer < ApplicationSerializer identifier :id - fields :account_nr, :date, :text, :amount, :side, :ordinal, :source_document_ref, :currency, + fields :account_nr, :date, :text, :amount, :side, :ordinal, :ref, :currency, :soll_amount, :haben_amount, :soll_account, :haben_account, :book_type end end diff --git a/app/services/taf_block.rb b/app/services/taf_block.rb index 44363798a..bbd743314 100644 --- a/app/services/taf_block.rb +++ b/app/services/taf_block.rb @@ -179,13 +179,23 @@ def self.derive(value, **override) # ValFW : not implemented # String[13]The OP id of this booking. - # OpId: journal_entry.source_document_ref, + # OpId: journal_entry.ref, # The PK number of this booking. PkKey: nil }, **override) end + derive_from Payment do |payment, **_override| + raise StandardError, 'Abschreibung is not yet supported' if payment.write_off + + # op_id = Value.cast(payment.invoice.ref, as: :symbol) + # pk_key = Value.cast(payment.invoice.booking.tenant.ref, as: :symbol) + payment.journal_entries.to_a.map do |journal_entry| + derive(journal_entry) + end + end + derive_from Invoice do |invoice, **_override| next unless invoice.is_a?(Invoices::Invoice) || invoice.is_a?(Invoices::Deposit) diff --git a/app/views/layouts/_manage_navigation.html.slim b/app/views/layouts/_manage_navigation.html.slim index f5fc4a4fd..81faa2472 100644 --- a/app/views/layouts/_manage_navigation.html.slim +++ b/app/views/layouts/_manage_navigation.html.slim @@ -17,10 +17,6 @@ nav.px-3 = link_to Booking.model_name.human(count: 2), manage_bookings_path, class: 'nav-link' li.nav-item = link_to Tenant.model_name.human(count: 2), manage_tenants_path, class: 'nav-link' - li.nav-item - = link_to Invoice.model_name.human(count: 2), manage_invoices_path, class: 'nav-link' - li.nav-item - = link_to Payment.model_name.human(count: 2), manage_payments_path, class: 'nav-link' - outbox_count = current_organisation.notifications.unsent.count - if outbox_count.positive? @@ -33,59 +29,74 @@ nav.px-3 li.nav-item = link_to DataDigest.model_name.human(count: 2), manage_data_digest_templates_path, class: 'nav-link' - li.nav-item - a.nav-link.dropdown-toggle href="#settingsNav" role="button" data-bs-toggle="collapse" aria-expanded="false" - = t('.settings') - ul.list-unstyled.ps-3.collapse#settingsNav - li - = link_to edit_manage_organisation_path, class: 'nav-link' - span.fa.fa-cog.pe-2[style="width: 1.5rem;"] - = Organisation.model_name.human - li - = link_to manage_occupiables_path, class: 'nav-link' do - span.fa.fa-home.pe-2[style="width: 1.5rem;"] - = Occupiable.model_name.human(count: 2) - li - = link_to manage_tarifs_path, class: 'nav-link' do - span.fa.fa-usd.pe-2[style="width: 1.5rem;"] - = Tarif.model_name.human(count: 2) - li - = link_to manage_booking_categories_path, class: 'nav-link' do - span.fa.fa-bed.pe-2[style="width: 1.5rem;"] - = BookingCategory.model_name.human(count: 2) - li - = link_to manage_rich_text_templates_path, class: 'nav-link' do - span.fa.fa-file-text-o.pe-2[style="width: 1.5rem;"] - = RichTextTemplate.model_name.human(count: 2) - li - = link_to manage_designated_documents_path, class: 'nav-link' do - span.fa.fa-file-text-o.pe-2[style="width: 1.5rem;"] - = DesignatedDocument.model_name.human(count: 2) - li - = link_to manage_booking_agents_path, class: 'nav-link' do - span.fa.fa-user-secret.pe-2[style="width: 1.5rem;"] - = BookingAgent.model_name.human(count: 2) - li - = link_to manage_organisation_users_path, class: 'nav-link' - span.fa.fa-users.pe-2[style="width: 1.5rem;"] - = OrganisationUser.model_name.human(count: 2) - li - = link_to manage_operators_path, class: 'nav-link' do - span.fa.fa-users.pe-2[style="width: 1.5rem;"] - = Operator.model_name.human(count: 2) - li - = link_to manage_booking_questions_path, class: 'nav-link' do - span.fa.fa-question-circle.pe-2[style="width: 1.5rem;"] - = BookingQuestion.model_name.human(count: 2) - li - = link_to manage_booking_validations_path, class: 'nav-link' do - span.fa.fa-check-square-o.pe-2[style="width: 1.5rem;"] - = BookingValidation.model_name.human(count: 2) - li - = link_to manage_plan_b_backups_path, class: 'nav-link' do - span.fa.fa-download.pe-2[style="width: 1.5rem;"] - = PlanBBackup.model_name.human(count: 2) + li.nav-item + a.nav-link.dropdown-toggle href="#financesNav" role="button" data-bs-toggle="collapse" aria-expanded="false" + = t('.finances') + + ul.list-unstyled.ps-3.collapse#financesNav + li.nav-item + = link_to Invoice.model_name.human(count: 2), manage_invoices_path, class: 'nav-link' + li.nav-item + = link_to Payment.model_name.human(count: 2), manage_payments_path, class: 'nav-link' + + - if current_user.role_admin? + li.nav-item + = link_to JournalEntry.model_name.human(count: 2), manage_journal_entries_path, class: 'nav-link' + + li.nav-item + a.nav-link.dropdown-toggle href="#settingsNav" role="button" data-bs-toggle="collapse" aria-expanded="false" + = t('.settings') + + ul.list-unstyled.ps-3.collapse#settingsNav + li + = link_to edit_manage_organisation_path, class: 'nav-link' + span.fa.fa-cog.pe-2[style="width: 1.5rem;"] + = Organisation.model_name.human + li + = link_to manage_occupiables_path, class: 'nav-link' do + span.fa.fa-home.pe-2[style="width: 1.5rem;"] + = Occupiable.model_name.human(count: 2) + li + = link_to manage_tarifs_path, class: 'nav-link' do + span.fa.fa-usd.pe-2[style="width: 1.5rem;"] + = Tarif.model_name.human(count: 2) + li + = link_to manage_booking_categories_path, class: 'nav-link' do + span.fa.fa-bed.pe-2[style="width: 1.5rem;"] + = BookingCategory.model_name.human(count: 2) + li + = link_to manage_rich_text_templates_path, class: 'nav-link' do + span.fa.fa-file-text-o.pe-2[style="width: 1.5rem;"] + = RichTextTemplate.model_name.human(count: 2) + li + = link_to manage_designated_documents_path, class: 'nav-link' do + span.fa.fa-file-text-o.pe-2[style="width: 1.5rem;"] + = DesignatedDocument.model_name.human(count: 2) + li + = link_to manage_booking_agents_path, class: 'nav-link' do + span.fa.fa-user-secret.pe-2[style="width: 1.5rem;"] + = BookingAgent.model_name.human(count: 2) + li + = link_to manage_organisation_users_path, class: 'nav-link' + span.fa.fa-users.pe-2[style="width: 1.5rem;"] + = OrganisationUser.model_name.human(count: 2) + li + = link_to manage_operators_path, class: 'nav-link' do + span.fa.fa-users.pe-2[style="width: 1.5rem;"] + = Operator.model_name.human(count: 2) + li + = link_to manage_booking_questions_path, class: 'nav-link' do + span.fa.fa-question-circle.pe-2[style="width: 1.5rem;"] + = BookingQuestion.model_name.human(count: 2) + li + = link_to manage_booking_validations_path, class: 'nav-link' do + span.fa.fa-check-square-o.pe-2[style="width: 1.5rem;"] + = BookingValidation.model_name.human(count: 2) + li + = link_to manage_plan_b_backups_path, class: 'nav-link' do + span.fa.fa-download.pe-2[style="width: 1.5rem;"] + = PlanBBackup.model_name.human(count: 2) li.nav-item = link_to manage_flow_path(org: current_organisation, locale: I18n.locale), class: 'nav-link' diff --git a/app/views/manage/journal_entries/_form.html.slim b/app/views/manage/journal_entries/_form.html.slim index eea62d2ee..19a71a66d 100644 --- a/app/views/manage/journal_entries/_form.html.slim +++ b/app/views/manage/journal_entries/_form.html.slim @@ -8,7 +8,7 @@ = f.select :side, JournalEntry.sides.keys.map { [JournalEntry.human_enum(:side, _1), _1] }, include_blank: true = f.text_field :amount, step: 0.1, inputmode: "numeric" = f.date_field :date - = f.text_field :source_document_ref + = f.text_field :ref = f.text_area :text, rows: 2 = f.select :book_type, JournalEntry.book_types.keys.map { [JournalEntry.human_enum(:book_types, _1), _1] }, include_blank: true diff --git a/app/views/manage/journal_entries/index.html.slim b/app/views/manage/journal_entries/index.html.slim index 5e3f46d61..d53cb2bbd 100644 --- a/app/views/manage/journal_entries/index.html.slim +++ b/app/views/manage/journal_entries/index.html.slim @@ -1,12 +1,27 @@ h1.mt-0.mb-5= JournalEntry.model_name.human(count: 2) +.my-3 + button.btn.btn-outline-primary.bg-body[type="button" data-bs-toggle="modal" data-bs-target="#filter"] + i.fa.fa-filter<> + = t(:filter) + +#filter.modal.fade + .modal-dialog.modal-lg + .modal-content + = form_with(model: @filter, url: manage_journal_entries_path, scope: 'filter', method: :GET, local: true) do |f| + = hidden_field_tag :locale, I18n.locale + .modal-body + = render partial: @filter.to_partial_path('filter_fields'), locals: { f: f } + .modal-footer + = f.submit t(:filter), class: 'btn btn-primary' + .table-responsive table.table.table-hover.align-middle thead tr th=JournalEntry.human_attribute_name(:date) - th=JournalEntry.human_attribute_name(:source_document_ref) + th=JournalEntry.human_attribute_name(:ref) th=JournalEntry.human_attribute_name(:soll_account) th=JournalEntry.human_attribute_name(:haben_account) th=JournalEntry.human_attribute_name(:amount) @@ -18,12 +33,11 @@ h1.mt-0.mb-5= JournalEntry.model_name.human(count: 2) - @journal_entries.each do |journal_entry| tr td= l(journal_entry.date, format: :default) - td= journal_entry.source_document_ref + td= journal_entry.ref td= journal_entry.soll_account td= journal_entry.haben_account td= number_to_currency(journal_entry.amount, unit: journal_entry.currency) td= JournalEntry.human_enum(:book_types, journal_entry.book_type) - td= link_to journal_entry.invoice.ref, manage_invoice_path(journal_entry.invoice) td.p-1.text-end .btn-group = link_to edit_manage_journal_entry_path(journal_entry), class: 'btn btn-default' do diff --git a/app/views/renderables/journal_entry/filter/_filter_fields.html.slim b/app/views/renderables/journal_entry/filter/_filter_fields.html.slim index 5f10b6481..9963bc937 100644 --- a/app/views/renderables/journal_entry/filter/_filter_fields.html.slim +++ b/app/views/renderables/journal_entry/filter/_filter_fields.html.slim @@ -1,4 +1,12 @@ +.row + .col + = f.select :processed, { only_processed: true, only_unprocessed: false }.map { [t(_1, scope: 'activerecord.enums.journal_entry/filter.processed'), _2] }, include_blank: true + .col + / = f.select :invoice_type, subtype_options_for_select(Invoice.subtypes), { include_blank: true }, readonly: true .row .col = f.date_field :date_before, include_blank: true = f.date_field :date_after, include_blank: true + .col + = f.date_field :processed_at_before, include_blank: true + = f.date_field :processed_at_after, include_blank: true diff --git a/config/locales/de.yml b/config/locales/de.yml index db76c3bbf..9b0dea84e 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -26,6 +26,12 @@ de: homes: Hauptmietobjekte issued_at_after: Rechnungsdatum nach issued_at_before: Rechnungsdatum vor + journal_entry/filter: + date_after: Buchungsdatum nach + date_before: Buchungsdatum vor + processed: Verbucht + processed_at_after: Verbucht nach + processed_at_before: Verbucht vor meter_reading_period/filter: begins_at_after: Beginn des Zählerstandperiode ab begins_at_before: Beginn des Zählerstandperiode bis @@ -41,6 +47,7 @@ de: ends_at_after: Ende der Belegung ab ends_at_before: Ende der Belegung bis occupiable_settings: + accounting_cost_center_nr: Kostenstelle booking_margin: Abstand zwischen Buchungen organisation_settings: awaiting_contract_deadline: Frist für Vertragseingänge @@ -237,16 +244,19 @@ de: journal_entry: account_nr: Konto amount: Betrag - book_type: Buch + book_type: Typ booking_id: Buchung date: Datum haben_account: Haben Konto haben_amount: Haben Betrag invoice_id: Rechnung + invoice_part_id: Rechnungsposition + payment_id: Zahlung + processed_at: Verbucht am + ref: Beleg side: Seite soll_account: Soll Konto soll_amount: Soll Betrag - source_document_ref: Beleg text: Text vat_category_id: MwSt. Kategorie mail_template: @@ -505,6 +515,13 @@ de: side: haben: Haben soll: Soll + trigger: + invoice_created: Rechnung erstellt + payment_created: Zahlung erstellt + journal_entry/filter: + processed: + only_processed: Nur verbuchte + only_unprocessed: Nur unverbuchte mail_template: attachable_booking_documents: contract: Vertrag @@ -535,10 +552,10 @@ de: organisation_user: role: admin: Admin + finance: Kassier manager: Verwaltung none: "-" readonly: Lesend - treasurer: Kassier tarif: prefill_usage_methods: days: "× Tage" @@ -1375,6 +1392,7 @@ de: github: GitHub title: Software manage_navigation: + finances: Finanzen help: Hilfe outbox: Postausgang settings: Einstellungen diff --git a/db/migrate/20241230084234_add_sources_to_journal_entries.rb b/db/migrate/20241230084234_add_sources_to_journal_entries.rb new file mode 100644 index 000000000..c7a82a148 --- /dev/null +++ b/db/migrate/20241230084234_add_sources_to_journal_entries.rb @@ -0,0 +1,19 @@ +class AddSourcesToJournalEntries < ActiveRecord::Migration[8.0] + def change + reversible do |direction| + direction.up do + JournalEntry.delete_all + end + end + + rename_column :journal_entries, :source_document_ref, :ref + remove_column :journal_entries, :source_type, :string + remove_column :journal_entries, :source_id, :integer + add_reference :journal_entries, :invoice_part, null: true + add_reference :journal_entries, :payment, null: true + add_column :journal_entries, :trigger, :integer, null: false + add_reference :journal_entries, :booking, type: :uuid, null: false + add_column :journal_entries, :processed_at, :datetime, null: true + change_column_null :journal_entries, :invoice_id, true, false + end +end diff --git a/db/schema.rb b/db/schema.rb index 0b077b57a..d29c3a8fb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2024_12_17_125938) do +ActiveRecord::Schema[8.0].define(version: 2024_12_30_084234) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "pgcrypto" @@ -345,9 +345,7 @@ end create_table "journal_entries", force: :cascade do |t| - t.bigint "invoice_id", null: false - t.string "source_type" - t.bigint "source_id" + t.bigint "invoice_id" t.bigint "vat_category_id" t.string "account_nr", null: false t.integer "side", null: false @@ -356,12 +354,19 @@ t.string "text" t.string "currency", null: false t.integer "ordinal" - t.string "source_document_ref" + t.string "ref" t.integer "book_type" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.bigint "invoice_part_id" + t.bigint "payment_id" + t.integer "trigger", null: false + t.uuid "booking_id", null: false + t.datetime "processed_at" + t.index ["booking_id"], name: "index_journal_entries_on_booking_id" t.index ["invoice_id"], name: "index_journal_entries_on_invoice_id" - t.index ["source_type", "source_id"], name: "index_journal_entries_on_source" + t.index ["invoice_part_id"], name: "index_journal_entries_on_invoice_part_id" + t.index ["payment_id"], name: "index_journal_entries_on_payment_id" t.index ["vat_category_id"], name: "index_journal_entries_on_vat_category_id" end diff --git a/spec/factories/journal_entries.rb b/spec/factories/journal_entries.rb index ef2c546ca..6ddb2a1b5 100644 --- a/spec/factories/journal_entries.rb +++ b/spec/factories/journal_entries.rb @@ -4,27 +4,32 @@ # # Table name: journal_entries # -# id :integer not null, primary key -# invoice_id :integer not null -# source_type :string -# source_id :integer -# vat_category_id :integer -# account_nr :string not null -# side :integer not null -# amount :decimal(, ) not null -# date :date not null -# text :string -# currency :string not null -# ordinal :integer -# source_document_ref :string -# book_type :integer -# created_at :datetime not null -# updated_at :datetime not null +# id :integer not null, primary key +# invoice_id :integer +# vat_category_id :integer +# account_nr :string not null +# side :integer not null +# amount :decimal(, ) not null +# date :date not null +# text :string +# currency :string not null +# ordinal :integer +# ref :string +# book_type :integer +# created_at :datetime not null +# updated_at :datetime not null +# invoice_part_id :integer +# payment_id :integer +# trigger :integer not null +# booking_id :uuid not null +# processed_at :datetime # # Indexes # +# index_journal_entries_on_booking_id (booking_id) # index_journal_entries_on_invoice_id (invoice_id) -# index_journal_entries_on_source (source_type,source_id) +# index_journal_entries_on_invoice_part_id (invoice_part_id) +# index_journal_entries_on_payment_id (payment_id) # index_journal_entries_on_vat_category_id (vat_category_id) # @@ -39,7 +44,7 @@ amount { '9.99' } ordinal { 1 } side { 1 } - source_document_ref { 'MyString' } + ref { 'MyString' } currency { 'MyString' } cost_center { 'MyString' } end diff --git a/spec/models/journal_entry_spec.rb b/spec/models/journal_entry_spec.rb index 2318d5b89..3efb4c5e9 100644 --- a/spec/models/journal_entry_spec.rb +++ b/spec/models/journal_entry_spec.rb @@ -4,32 +4,91 @@ # # Table name: journal_entries # -# id :integer not null, primary key -# invoice_id :integer not null -# source_type :string -# source_id :integer -# vat_category_id :integer -# account_nr :string not null -# side :integer not null -# amount :decimal(, ) not null -# date :date not null -# text :string -# currency :string not null -# ordinal :integer -# source_document_ref :string -# book_type :integer -# created_at :datetime not null -# updated_at :datetime not null +# id :integer not null, primary key +# invoice_id :integer +# vat_category_id :integer +# account_nr :string not null +# side :integer not null +# amount :decimal(, ) not null +# date :date not null +# text :string +# currency :string not null +# ordinal :integer +# ref :string +# book_type :integer +# created_at :datetime not null +# updated_at :datetime not null +# invoice_part_id :integer +# payment_id :integer +# trigger :integer not null +# booking_id :uuid not null +# processed_at :datetime # # Indexes # +# index_journal_entries_on_booking_id (booking_id) # index_journal_entries_on_invoice_id (invoice_id) -# index_journal_entries_on_source (source_type,source_id) +# index_journal_entries_on_invoice_part_id (invoice_part_id) +# index_journal_entries_on_payment_id (payment_id) # index_journal_entries_on_vat_category_id (vat_category_id) # require 'rails_helper' RSpec.describe JournalEntry, type: :model do - pending "add some examples to (or delete) #{__FILE__}" + let(:booking) { create(:booking) } + let(:invoice) { create(:invoice, booking:) } + + describe '::collect' do + let(:amount) { 500 } + let(:ref) { 'test' } + let(:date) { Time.zone.today } + + subject(:compound) do + described_class.collect(booking:, ref:, date:, trigger: :manual) do |compound| + compound.soll(account_nr: 6000, amount:) + compound.haben(account_nr: 1000, amount:) + end + end + + it 'collects the journal entries as compound' do + expect(compound).to be_a(JournalEntry::Compound) + expect(compound.journal_entries).to contain_exactly( + have_attributes(side: 'soll', account_nr: '6000', book_type: 'main', amount:, booking:, ref:, date:), + have_attributes(side: 'haben', account_nr: '1000', book_type: 'main', amount:, booking:, ref:, date:) + ) + expect(compound).to be_valid + compound.save! + expect(compound.journal_entries).to all(be_persisted) + end + end + + describe '::compounds' do + subject(:compounds) { JournalEntry::Compound.group(described_class.all) } + before do + described_class.collect(booking:, ref: 'test 1', date: 2.weeks.ago, trigger: :manual) do |compound| + compound.soll(account_nr: 6000, amount: 1000) + compound.haben(account_nr: 1000, amount: 500) + compound.haben(account_nr: 2000, amount: 500) + end.save! + described_class.collect(booking:, ref: 'test 2', date: 1.week.ago, trigger: :manual) do |compound| + compound.soll(account_nr: 6000, amount: 800) + compound.haben(account_nr: 1000, amount: 800) + end.save! + end + + it 'groups the compounds back together' do + expect(compounds.map(&:journal_entries)).to contain_exactly( + contain_exactly( + have_attributes(side: 'soll', account_nr: '6000', amount: 1000, booking:, ref: 'test 1'), + have_attributes(side: 'haben', account_nr: '1000', amount: 500, booking:, ref: 'test 1'), + have_attributes(side: 'haben', account_nr: '2000', amount: 500, booking:, ref: 'test 1') + ), + contain_exactly( + have_attributes(side: 'soll', account_nr: '6000', amount: 800, booking:, ref: 'test 2'), + have_attributes(side: 'haben', account_nr: '1000', amount: 800, booking:, ref: 'test 2') + ) + ) + end + end end diff --git a/spec/services/taf_block_spec.rb b/spec/services/taf_block_spec.rb index 8424edf37..45a4b8a66 100644 --- a/spec/services/taf_block_spec.rb +++ b/spec/services/taf_block_spec.rb @@ -47,7 +47,7 @@ describe 'JournalEntry' do subject(:taf_block) { described_class.derive(journal_entry) } let(:journal_entry) do - JournalEntry.new(account_nr: 1050, amount: 2091.75, date: Date.new(2024, 10, 5), source_document_ref: '1234', + JournalEntry.new(account_nr: 1050, amount: 2091.75, date: Date.new(2024, 10, 5), ref: '1234', side: :soll, vat_category:, booking:, currency:, text: "Lorem ipsum\nSecond Line, but its longer than sixty \"chars\", OMG!") end @@ -72,7 +72,7 @@ describe 'JournalEntry' do subject(:taf_block) { described_class.derive(journal_entry) } let(:journal_entry) do - JournalEntry.new(account_nr: 1050, amount: 2091.75, date: Date.new(2024, 10, 5), source_document_ref: '1234', + JournalEntry.new(account_nr: 1050, amount: 2091.75, date: Date.new(2024, 10, 5), ref: '1234', side: :soll, vat_category:, booking:, currency:, text: "Lorem ipsum\nSecond Line, but its longer than sixty \"chars\", OMG!") end From 799fba57a3f6d5bbf7bd6811161f0964c26cdff9 Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Fri, 3 Jan 2025 15:56:39 +0000 Subject: [PATCH 25/30] refactor: taf journal_entries --- .../data_digest_templates/journal_entry.rb | 7 +- app/models/invoice.rb | 8 +- app/models/journal_entry.rb | 8 +- app/models/payment.rb | 13 +- app/services/export/taf/block.rb | 45 ++++ app/services/export/taf/builder.rb | 182 +++++++++++++ app/services/export/taf/document.rb | 23 ++ app/services/export/taf/value.rb | 38 +++ spec/factories/invoices.rb | 14 +- spec/factories/tarifs.rb | 6 + spec/services/export/taf/block_spec.rb | 27 ++ spec/services/export/taf/builder_spec.rb | 240 ++++++++++++++++++ spec/services/export/taf/document_spec.rb | 44 ++++ spec/services/taf_block_spec.rb | 97 ------- 14 files changed, 630 insertions(+), 122 deletions(-) create mode 100644 app/services/export/taf/block.rb create mode 100644 app/services/export/taf/builder.rb create mode 100644 app/services/export/taf/document.rb create mode 100644 app/services/export/taf/value.rb create mode 100644 spec/services/export/taf/block_spec.rb create mode 100644 spec/services/export/taf/builder_spec.rb create mode 100644 spec/services/export/taf/document_spec.rb delete mode 100644 spec/services/taf_block_spec.rb diff --git a/app/models/data_digest_templates/journal_entry.rb b/app/models/data_digest_templates/journal_entry.rb index c04b56003..59bb1bbd0 100644 --- a/app/models/data_digest_templates/journal_entry.rb +++ b/app/models/data_digest_templates/journal_entry.rb @@ -72,9 +72,10 @@ class JournalEntry < Tabular end formatter(:taf) do |_options = {}| - JournalEntry::Compound.group(records) do |compound| - # TafBlock::Collection.new { derive(compound) } - end.join("\n\n") + journal_entry_compounds = ::JournalEntry::Compound.group(records) + Export::Taf::Document.new do + journal_entry_compounds.each { build_with_journal_entry_compound(_1) } + end.serialize end def periodfilter(period = nil) diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 60393b55a..3024a1ba8 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -49,7 +49,7 @@ class Invoice < ApplicationRecord has_many :superseded_by_invoices, class_name: :Invoice, dependent: :nullify, foreign_key: :supersede_invoice_id, inverse_of: :supersede_invoice has_many :payments, dependent: :nullify - has_many :journal_entries, dependent: :destroy, inverse_of: :invoice + has_many :journal_entries, -> { ordered }, dependent: :nullify, inverse_of: :invoice has_one :organisation, through: :booking has_one_attached :pdf @@ -125,12 +125,12 @@ def generate_journal_entries? def generate_journal_entries! return unless organisation.accounting_settings.enabled - existing_ids = organisation.journal_entries.where(invoice: self).pluck(:id) + existing_ids = organisation.journal_entries.where(invoice: self, payment: nil).pluck(:id) new_journal_entries = JournalEntry::Factory.new.invoice(self) # raise ActiveRecord::Rollback unless - new_journal_entries.save! && organisation.journal_entries.where(id: existing_ids, invoice: self).destroy_all - # payments.each(&:generate_journal_entries!) + new_journal_entries.save! && organisation.journal_entries.where(id: existing_ids).destroy_all + journal_entries.reload end def paid? diff --git a/app/models/journal_entry.rb b/app/models/journal_entry.rb index ee7efefb8..12af32f5c 100644 --- a/app/models/journal_entry.rb +++ b/app/models/journal_entry.rb @@ -91,7 +91,7 @@ def self.collect(**defaults, &) end class Compound - COMPOUND_ATTRIBUTES = %i[booking_id invoice_id payment_id date trigger].freeze + COMPOUND_ATTRIBUTES = %i[booking_id invoice_id payment_id date trigger ref].freeze delegate :[], :to_a, :<<, to: :journal_entries @@ -222,9 +222,9 @@ def invoice_part_add(invoice_part, compound) # rubocop:disable Metrics/AbcSize def payment(payment) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity payment.instance_eval do - text = "#{Payment.model_name.human} #{invoice&.ref}" - JournalEntry.collect(currency: organisation.currency, ref: invoice&.ref, date: paid_at, invoice:, - payment:, text:, booking:, trigger: :payment_created) do |compound| + text = "#{Payment.model_name.human} #{invoice&.ref || paid_at}" + JournalEntry.collect(ref: id, date: paid_at, invoice:, + payment: self, text:, booking:, trigger: :payment_created) do |compound| compound.soll(account_nr: organisation&.accounting_settings&.payment_account_nr, amount:) compound.haben(account_nr: organisation&.accounting_settings&.debitor_account_nr, amount:) end diff --git a/app/models/payment.rb b/app/models/payment.rb index 902292f1c..d9471827d 100644 --- a/app/models/payment.rb +++ b/app/models/payment.rb @@ -51,10 +51,12 @@ class Payment < ApplicationRecord attr_accessor :skip_generate_journal_entries - before_save :generate_journal_entries!, if: :generate_journal_entries? + delegate :accounting_settings, to: :organisation + after_create :confirm!, if: :confirm? after_destroy :recalculate_invoice after_save :recalculate_invoice + after_save :generate_journal_entries!, if: :generate_journal_entries? before_validation do self.booking = invoice&.booking || booking @@ -73,17 +75,18 @@ def confirm! end def generate_journal_entries? - organisation.accounting_settings.enabled && !skip_generate_journal_entries && (changed? || journal_entries.none?) + organisation.accounting_settings.enabled && !skip_generate_journal_entries end - def generate_journal_entries! - return unless organisation.accounting_settings.enabled + def generate_journal_entries! # rubocop:disable Metrics/AbcSize + return unless accounting_settings.enabled && accounting_settings.payment_account_nr.present? - existing_ids = organisation.journal_entries.where(source_type: self.class.sti_name, source_id: id).pluck(:id) + existing_ids = organisation.journal_entries.where(payment: self).pluck(:id) new_journal_entries = JournalEntry::Factory.new.payment(self) # raise ActiveRecord::Rollback unless new_journal_entries.save! && organisation.journal_entries.where(id: existing_ids).destroy_all + journal_entries.reload end def recalculate_invoice diff --git a/app/services/export/taf/block.rb b/app/services/export/taf/block.rb new file mode 100644 index 000000000..048e05e09 --- /dev/null +++ b/app/services/export/taf/block.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Export + module Taf + class Block + INDENTOR = ' ' + SEPARATOR = "\n" + + attr_reader :type, :properties, :children + + def initialize(type, children = [], **properties) + @type = type + @properties = properties.transform_values { Value.cast(_1) } + @children = children + end + + def serialize(indent_level: 0, indent_with: ' ', separate_with: "\n") + indent = [indent_with * indent_level].join + separate_and_indent = [separate_with, indent, indent_with].join + serialized_children = serialize_children(indent_level:, indent_with:, separate_with:) + serialized_properties = properties.compact.map { |key, value| "#{key}=#{value.serialize}" } + + [ # tag_start + indent, "{#{type}", + # properties + separate_and_indent, serialized_properties.join(separate_and_indent), separate_with, + # children + (separate_with if children.present?), serialized_children, + # tag end + separate_with, indent, '}' + ].compact.join + end + + def serialize_children(indent_level: 0, indent_with: ' ', separate_with: "\n") + children.map do |child| + child.serialize(indent_level: indent_level + 1, indent_with:, separate_with:) if child.is_a?(Block) + end.compact.join(separate_with + separate_with) + end + + def to_s + serialize + end + end + end +end diff --git a/app/services/export/taf/builder.rb b/app/services/export/taf/builder.rb new file mode 100644 index 000000000..09738837f --- /dev/null +++ b/app/services/export/taf/builder.rb @@ -0,0 +1,182 @@ +# frozen_string_literal: true + +# rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity + +module Export + module Taf + class Builder + attr_reader :blocks + + delegate :to_a, :[], :<<, to: :blocks + + def initialize(&) + @blocks = [] + instance_exec(&) if block_given? + end + + def build_with_journal_entry(journal_entry, **override) + build(:Bk, **{ + # The Id of a book keeping account. [Fibu-Konto] + AccId: Value.cast(journal_entry.account_nr, as: :symbol), + + # Integer; Booking type: 1=cost booking, 2=tax booking + BType: { main: nil, cost: 1, vat: 2 }[journal_entry.book_type&.to_sym], + + # String[13], This is the cost type account + # CAcc: (Value.cast(journal_entry.cost_account_nr, as: :symbol) if journal_entry.cost_account_nr), + + # Integer; This is the index of the booking that represents the cost booking which is attached to t + # his booking + # CIdx: journal_entry.index, + + # String[9]; A user definable code. + Code: journal_entry.id, + + # Date; The date of the booking. + Date: journal_entry.date, + + # IntegerAuxilliary flags. This journal_entry consists of the sum of one or more of + # the following biases: + # 1 - The booking is the first one into the specified OP. + # 16 - This is a hidden booking. [Transitorische] + # 32 - This booking is the exit booking, as oposed to the return booking. + # Only valid if the hidden flag is set. + Flags: nil, + + # String[5]; The Id of the tax. [MWSt-Kürzel] + TaxId: (journal_entry.book_type_main? && journal_entry.vat_category&.percentage&.positive? && + journal_entry.vat_category&.accounting_vat_code) || nil, + + # MkTxB: journal_entry.vat_category&.accounting_vat_code.present?, + + # String[61*]; This string specifies the first line of the booking text. + Text: journal_entry.text&.slice(0..59)&.lines&.first&.strip || '-', # rubocop:disable Style/SafeNavigationChainLength + + # String[*]; This string specifies the second line of the booking text. + # (*)Both fields Text and Text2 are stored in the same memory location, + # which means their total length may not exceed 60 characters (1 char is + # required internally). + # Be careful not to put too many characters onto one single line, because + # most Reports are not designed to display a full string containing 60 + # characters. + Text2: journal_entry.text&.slice(0..59)&.lines&.[](1..-1)&.join("\n").presence, # rubocop:disable Style/SafeNavigationChainLength + + # Integer; This is the index of the booking that represents the tax booking + # which is attached to this booking. + # TIdx: (journal_entry.amount_type&.to_sym == :tax && journal_entry.index) || nil, + + # Boolean; Booking type. + # 0 a debit booking [Soll] + # 1 a credit booking [Haben] + Type: { soll: 0, haben: 1 }[journal_entry.side&.to_sym], + + # Currency; The net amount for this booking. [Netto-Betrag] + ValNt: journal_entry.amount, + + # Currency; The tax amount for this booking. [Brutto-Betrag] + # ValBt: journal_entry.amount, + + # Currency; The tax amount for this booking. [Steuer-Betrag] + ValTx: journal_entry.book_type_vat? && + journal_entry.vat_category&.breakup(vat: journal_entry.amount)&.[](:netto), + + # Currency; The gross amount for this booking in the foreign currency specified + # by currency of the account AccId. [FW-Betrag] + # ValFW : not implemented + + # String[13]The OP id of this booking. + # OpId: journal_entry.ref, + + # The PK number of this booking. + PkKey: nil + }, **override) + end + + def build_with_payment(payment, **_override) + raise StandardError, 'Abschreibung is not yet supported' if payment.write_off + + # op_id = Value.cast(payment.invoice.ref, as: :symbol) + # pk_key = Value.cast(payment.invoice.booking.tenant.ref, as: :symbol) + payment.journal_entries.to_a.map do |journal_entry| + build_with_journal_entry(journal_entry) + end + end + + def build_with_journal_entry_compound(compound) + case compound.common[:trigger]&.to_sym + when :invoice_created + build_with_invoice_created_journal_entry_compound(compound) + when :payment_created + build_with_payment_created_journal_entry_compound(compound) + end + end + + def build_with_payment_created_journal_entry_compound(compound) + build(:Blg, **{ Date: compound.common[:date] }) do + journal_entries = compound.journal_entries + compound.journal_entries.each_with_index do |journal_entry, index| + cost_index = (journal_entry.book_type_main? && journal_entries.index(journal_entry.related[:cost])) || nil + vat_index = (journal_entry.book_type_main? && journal_entries.index(journal_entry.related[:vat])) || nil + taf_index = index + 1 + build_with_journal_entry(journal_entry, CIdx: cost_index&.+(taf_index), TIdx: vat_index&.+(taf_index)) + end + end + end + + def build_with_invoice_created_journal_entry_compound(compound) + booking = Booking.find compound.common[:booking_id] + op_id = Value.cast(compound.common[:ref], as: :symbol) + pk_key = Value.cast(booking.tenant.ref, as: :symbol) + journal_entries = compound.journal_entries + + [ + build_with_tenant(booking.tenant), + build(:OPd, **{ PkKey: pk_key, OpId: op_id, ZabId: '15T' }), + build(:Blg, **{ Date: compound.common[:date], Orig: true }) do + # TODO: check if this is really the debitor_journal_entry + creation_journal_entry = journal_entries.shift + build_with_journal_entry(creation_journal_entry, Flags: 1, OpId: op_id, PkKey: pk_key, CAcc: :div) + + journal_entries.each_with_index do |journal_entry, index| + cost_index = (journal_entry.book_type_main? && journal_entries.index(journal_entry.related[:cost])) || nil + vat_index = (journal_entry.book_type_main? && journal_entries.index(journal_entry.related[:vat])) || nil + taf_index = index + 2 + build_with_journal_entry(journal_entry, CIdx: cost_index&.+(taf_index), TIdx: vat_index&.+(taf_index), + CAcc: Value.cast(creation_journal_entry.account_nr, as: :symbol)) + end + end + ] + end + + def build_with_tenant(tenant, **_override) + account_nr = Value.cast(tenant.ref, as: :symbol) + [ + build(:Adr, **{ + AdrId: account_nr, + Sort: I18n.transliterate(tenant.full_name).gsub(/\s/, '').upcase, + Corp: tenant.full_name, + Lang: 'D', + Road: tenant.street_address, + CCode: tenant.country_code, + ACode: tenant.zipcode, + City: tenant.city + }), + build(:PKd, **{ + PkKey: account_nr, + AdrId: account_nr, + AccId: Value.cast(tenant.organisation.accounting_settings.debitor_account_nr, as: :symbol), + ZabId: '15T' + }) + + ] + end + + def build(type, **properties, &) + children = (block_given? && Builder.new(&).blocks) || [] + Block.new(type, children, **properties).tap { blocks << _1 } + end + end + end +end + +# rubocop:enable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity diff --git a/app/services/export/taf/document.rb b/app/services/export/taf/document.rb new file mode 100644 index 000000000..813add267 --- /dev/null +++ b/app/services/export/taf/document.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Export + module Taf + class Document + attr_reader :blocks + + def initialize(blocks = nil, &) + @blocks = [blocks, (Builder.new(&).blocks if block_given?)].compact.flatten + end + + def serialize(indent_with: ' ', separate_with: "\n") + @blocks.map do |block| + block.serialize(indent_level: 0, indent_with:, separate_with:) if block.is_a?(Block) + end.compact.join(separate_with + separate_with) + end + + def to_s + serialize + end + end + end +end diff --git a/app/services/export/taf/value.rb b/app/services/export/taf/value.rb new file mode 100644 index 000000000..7ef878312 --- /dev/null +++ b/app/services/export/taf/value.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Export + module Taf + Value = Data.define(:value, :as) do + CAST_BLOCKS = { # rubocop:disable Lint/ConstantDefinitionInBlock + boolean: ->(value) { value ? '1' : '0' }, + decimal: ->(value) { format('%.2f', value) }, + number: ->(value) { value.to_i.to_s }, + date: ->(value) { value.strftime('%d.%m.%Y') }, + string: ->(value) { "\"#{value.gsub(/["']/, '""')}\"" }, + symbol: ->(value) { value.to_s }, + vector: ->(value) { "[#{value.to_a.map(&:to_s).join(',')}]" }, + value: ->(value) { value } + }.freeze + + CAST_CLASSES = { # rubocop:disable Lint/ConstantDefinitionInBlock + boolean: [::FalseClass, ::TrueClass], decimal: [::BigDecimal, ::Float], + number: [::Numeric], date: [::Date, ::DateTime, ::ActiveSupport::TimeWithZone], + string: [::String], symbol: [::Symbol] + }.freeze + + def self.cast(value, as: nil) + return value if value.is_a?(Value) + return nil if value.blank? + + as = CAST_CLASSES.find { |_key, klasses| klasses.any? { |klass| value.is_a?(klass) } }&.first if as.nil? + value = CAST_BLOCKS.fetch(as).call(value) + + new(value, as) + end + + def serialize + value.to_s + end + end + end +end diff --git a/spec/factories/invoices.rb b/spec/factories/invoices.rb index fa65604ea..b5d5cdb99 100644 --- a/spec/factories/invoices.rb +++ b/spec/factories/invoices.rb @@ -37,14 +37,15 @@ # FactoryBot.define do - factory :invoice do + factory :invoice, class: Invoices::Invoice.sti_name do booking issued_at { 1.week.ago } payable_until { 3.months.from_now } text { Faker::Lorem.sentences } - type { Invoices::Invoice } after(:build) do |invoice, evaluator| + next unless evaluator.invoice_parts.nil? + if evaluator.amount&.positive? build(:invoice_part, amount: evaluator.amount, invoice:) else @@ -52,12 +53,7 @@ end end - factory :deposit do - type { Invoices::Deposit } - end - - factory :offer do - type { Invoices::Offer } - end + factory :deposit, class: Invoices::Deposit.sti_name + factory :offer, class: Invoices::Offer.sti_name end end diff --git a/spec/factories/tarifs.rb b/spec/factories/tarifs.rb index c188f39c8..bbcea9f8f 100644 --- a/spec/factories/tarifs.rb +++ b/spec/factories/tarifs.rb @@ -51,5 +51,11 @@ organisation associated_types { Tarif.associated_types.keys } prefill_usage_method { nil } + + trait :with_vat do + after(:build) do |tarif, _evaluator| + tarif.vat_category ||= build(:vat_category, organisation: tarif.organisation) + end + end end end diff --git a/spec/services/export/taf/block_spec.rb b/spec/services/export/taf/block_spec.rb new file mode 100644 index 000000000..e40a4b40b --- /dev/null +++ b/spec/services/export/taf/block_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Export::Taf::Block, type: :model do + subject(:taf_block) do + described_class.new(:Blg, [described_class.new(:Bk, test: 2)], text: 'TAF is "great"', test: 1) + end + + describe '#serialize' do + subject(:serialize) { taf_block.serialize.chomp } + + it 'returns serialized string' do + is_expected.to eq(<<~TAF.chomp) + {Blg + text="TAF is ""great""" + test=1 + + {Bk + test=2 + + } + } + TAF + end + end +end diff --git a/spec/services/export/taf/builder_spec.rb b/spec/services/export/taf/builder_spec.rb new file mode 100644 index 000000000..0728457d8 --- /dev/null +++ b/spec/services/export/taf/builder_spec.rb @@ -0,0 +1,240 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Export::Taf::Builder, type: :model do + let(:organisation) { create(:organisation, accounting_settings:) } + let(:accounting_settings) { { enabled: true } } + let(:booking) { create(:booking, organisation:, tenant:) } + let(:vat_category) { create(:vat_category, percentage: 3.8, accounting_vat_code: 'MwSt38', organisation:) } + let(:tenant) do + create(:tenant, sequence_number: 200_002, first_name: 'Max', last_name: 'Müller', organisation:, + street_address: 'Bahnhofstr. 1', city: 'Bern', zipcode: 1234) + end + + subject(:builder) { described_class.new } + + describe 'build_with_journal_entry' do + subject(:taf_block) { builder.build_with_journal_entry(journal_entry) } + let(:journal_entry) do + JournalEntry.new(account_nr: 1050, amount: 2091.75, date: Date.new(2024, 10, 5), ref: '1234', + side: :soll, vat_category:, booking:, + text: "Lorem ipsum\nSecond Line, but its longer than sixty \"chars\", OMG!") + end + + it 'builds correctly' do + is_expected.to be_a(Export::Taf::Block) + expect(taf_block.to_s).to eq(<<~TAF.chomp) + {Bk + AccId=1050 + Date=05.10.2024 + TaxId="MwSt38" + Text="Lorem ipsum" + Text2="Second Line, but its longer than sixty ""chars"", " + Type=0 + ValNt=2091.75 + + } + TAF + end + end + + describe 'build_with_tenant' do + subject(:taf_blocks) { builder.build_with_tenant(tenant) } + + it 'builds correctly' do + is_expected.to contain_exactly(be_a(Export::Taf::Block), be_a(Export::Taf::Block)) + expect(taf_blocks.map(&:to_s).join("\n\n")).to eq(<<~TAF.chomp) + {Adr + AdrId=200002 + Sort="MAXMUELLER" + Corp="Max Müller" + Lang="D" + Road="Bahnhofstr. 1" + CCode="CH" + ACode="1234" + City="Bern" + + } + + {PKd + PkKey=200002 + AdrId=200002 + ZabId="15T" + + } + TAF + end + end + + describe 'build_with_invoice_created_journal_entry_compounds' do + subject(:taf_document) do + journal_entry_compounds = compounds + Export::Taf::Document.new do + journal_entry_compounds.each { build_with_journal_entry_compound(_1) } + end + end + subject(:compounds) { JournalEntry::Compound.group(invoice.journal_entries) } + + let(:accounting_settings) do + { enabled: true, debitor_account_nr: 1050, vat_account_nr: 6020, + payment_account_nr: 4000, rental_yield_account_nr: 6000 } + end + let(:vat_category) { create(:vat_category, percentage: 50, organisation:) } + let(:payment) { create(:payment, booking:, invoice: nil, amount: 300) } + let(:usages) { Usage::Factory.new(booking).build.each { _1.update(used_units: 48) } } + let(:invoice) do + tarif + usages + payment + build(:invoice, booking:, invoice_parts: []).tap do |invoice| + invoice.invoice_parts = InvoicePart::Factory.new(invoice).call + invoice.save! + invoice.recalculate! + end + end + let(:tarif) do + create(:tarif, organisation:, vat_category:, price_per_unit: 15, + accounting_account_nr: 6000, accounting_cost_center_nr: 9000) + end + + it 'creates to correct journal_entries' do + expect(compounds).to contain_exactly( + contain_exactly( + have_attributes(account_nr: '4000', soll_amount: 300.0, trigger: 'payment_created'), + have_attributes(account_nr: '1050', haben_amount: 300.0, trigger: 'payment_created') + ), + contain_exactly( + have_attributes(account_nr: '1050', soll_amount: 420.0, trigger: 'invoice_created'), + have_attributes(account_nr: '6000', haben_amount: -300.0, trigger: 'invoice_created'), + have_attributes(account_nr: '6000', haben_amount: 480.0, trigger: 'invoice_created'), + have_attributes(account_nr: '9000', haben_amount: 480.0, trigger: 'invoice_created'), + have_attributes(account_nr: '6020', haben_amount: 240.0, trigger: 'invoice_created') + ) + ) + end + + it 'exports to taf' do + expect(taf_document.to_s).to eq(<<~TAF.chomp) + {Blg + Date=11.10.2018 + + {Bk + AccId=4000 + Code=27 + Date=11.10.2018 + Text="Zahlung 250001" + Type=0 + ValNt=300.00 + + } + + {Bk + AccId=1050 + Code=28 + Date=11.10.2018 + Text="Zahlung 250001" + Type=1 + ValNt=300.00 + + } + } + + {Adr + AdrId=200002 + Sort="MAXMUELLER" + Corp="Max Müller" + Lang="D" + Road="Bahnhofstr. 1" + CCode="CH" + ACode="1234" + City="Bern" + + } + + {PKd + PkKey=200002 + AdrId=200002 + AccId=1050 + ZabId="15T" + + } + + {OPd + PkKey=200002 + OpId=250001 + ZabId="15T" + + } + + {Blg + Date=27.12.2024 + Orig=1 + + {Bk + AccId=1050 + Code=34 + Date=27.12.2024 + Flags=1 + Text="250001 - Müller" + Type=0 + ValNt=420.00 + PkKey=200002 + OpId=250001 + CAcc=div + + } + + {Bk + AccId=6000 + Code=35 + Date=27.12.2024 + Text="250001 Saldo" + Type=1 + ValNt=-300.00 + CAcc=1050 + + } + + {Bk + AccId=6000 + Code=36 + Date=27.12.2024 + Text="250001 Preis pro Übernachtung" + Type=1 + ValNt=480.00 + CIdx=5 + TIdx=6 + CAcc=1050 + + } + + {Bk + AccId=9000 + BType=1 + Code=37 + Date=27.12.2024 + Text="250001 Preis pro Übernachtung" + Type=1 + ValNt=480.00 + CAcc=1050 + + } + + {Bk + AccId=6020 + BType=2 + Code=38 + Date=27.12.2024 + Text="250001 Preis pro Übernachtung" + Type=1 + ValNt=240.00 + ValTx=480.00 + CAcc=1050 + + } + } + TAF + end + end +end diff --git a/spec/services/export/taf/document_spec.rb b/spec/services/export/taf/document_spec.rb new file mode 100644 index 000000000..d189b8fc1 --- /dev/null +++ b/spec/services/export/taf/document_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Export::Taf::Document, type: :model do + subject(:taf_document) do + described_class.new do + build(:Blg, text: 'TAF is "great"', test: 1) do + build(:Bk, test: 2) + end + end + end + + describe '#initialize' do + subject(:taf_block) { taf_document.blocks.first } + + it 'works as DSL' do + expect(taf_block.type).to eq(:Blg) + expect(taf_block.properties.transform_values(&:value)).to eq({ test: '1', text: '"TAF is ""great"""' }) + expect(taf_block.children.count).to eq(1) + expect(taf_block.children.first.type).to eq(:Bk) + expect(taf_block.children.first.properties.transform_values(&:value)).to eq({ test: '2' }) + expect(taf_block.children.first.children.count).to eq(0) + end + end + + describe '#serialize' do + subject(:serialized) { taf_document.serialize.chomp } + + it 'returns serialized string' do + is_expected.to eq(<<~TAF.chomp) + {Blg + text="TAF is ""great""" + test=1 + + {Bk + test=2 + + } + } + TAF + end + end +end diff --git a/spec/services/taf_block_spec.rb b/spec/services/taf_block_spec.rb deleted file mode 100644 index 45a4b8a66..000000000 --- a/spec/services/taf_block_spec.rb +++ /dev/null @@ -1,97 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe TafBlock, type: :model do - subject(:taf_block) do - described_class.new(:Blg, text: 'TAF is "great"', test: 1) do - block(:Bk, test: 2) - end - end - - describe '#initialize' do - it 'works as DSL' do - expect(taf_block.type).to eq(:Blg) - expect(taf_block.properties.transform_values(&:value)).to eq({ test: '1', text: '"TAF is ""great"""' }) - expect(taf_block.children.count).to eq(1) - expect(taf_block.children.first.type).to eq(:Bk) - expect(taf_block.children.first.properties.transform_values(&:value)).to eq({ test: '2' }) - expect(taf_block.children.first.children.count).to eq(0) - end - end - - describe '#serialize' do - subject(:serialize) { taf_block.serialize.chomp } - - it 'returns serialized string' do - is_expected.to eq(<<~TAF.chomp) - {Blg - text="TAF is ""great""" - test=1 - - {Bk - test=2 - - } - } - TAF - end - end - - context '::derive' do - let(:organisation) { create(:organisation) } - let(:booking) { create(:booking, organisation:) } - let(:vat_category) { create(:vat_category, percentage: 3.8, accounting_vat_code: 'MwSt38', organisation:) } - let(:currency) { organisation.currency } - - describe 'JournalEntry' do - subject(:taf_block) { described_class.derive(journal_entry) } - let(:journal_entry) do - JournalEntry.new(account_nr: 1050, amount: 2091.75, date: Date.new(2024, 10, 5), ref: '1234', - side: :soll, vat_category:, booking:, currency:, - text: "Lorem ipsum\nSecond Line, but its longer than sixty \"chars\", OMG!") - end - - it 'builds correctly' do - is_expected.to be_a described_class - expect(taf_block.to_s).to eq(<<~TAF.chomp) - {Bk - AccId=1050 - Date=05.10.2024 - TaxId="MwSt38" - Text="Lorem ipsum" - Text2="Second Line, but its longer than sixty ""chars"", " - Type=0 - ValNt=2091.75 - - } - TAF - end - end - - describe 'JournalEntry' do - subject(:taf_block) { described_class.derive(journal_entry) } - let(:journal_entry) do - JournalEntry.new(account_nr: 1050, amount: 2091.75, date: Date.new(2024, 10, 5), ref: '1234', - side: :soll, vat_category:, booking:, currency:, - text: "Lorem ipsum\nSecond Line, but its longer than sixty \"chars\", OMG!") - end - - it 'builds correctly' do - is_expected.to be_a described_class - expect(taf_block.to_s).to eq(<<~TAF.chomp) - {Bk - AccId=1050 - Date=05.10.2024 - TaxId="MwSt38" - Text="Lorem ipsum" - Text2="Second Line, but its longer than sixty ""chars"", " - Type=0 - ValNt=2091.75 - - } - TAF - end - end - end -end From 41c190cfd9c0c25376ca649b48052d91b2abb891 Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Sat, 4 Jan 2025 09:08:53 +0000 Subject: [PATCH 26/30] fix: specs --- app/services/export/taf/builder.rb | 2 +- spec/factories/invoices.rb | 5 ++++- spec/services/export/taf/builder_spec.rb | 16 ++++++++-------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/services/export/taf/builder.rb b/app/services/export/taf/builder.rb index 09738837f..93a1fd725 100644 --- a/app/services/export/taf/builder.rb +++ b/app/services/export/taf/builder.rb @@ -127,7 +127,7 @@ def build_with_invoice_created_journal_entry_compound(compound) booking = Booking.find compound.common[:booking_id] op_id = Value.cast(compound.common[:ref], as: :symbol) pk_key = Value.cast(booking.tenant.ref, as: :symbol) - journal_entries = compound.journal_entries + journal_entries = compound.journal_entries.dup [ build_with_tenant(booking.tenant), diff --git a/spec/factories/invoices.rb b/spec/factories/invoices.rb index b5d5cdb99..917697174 100644 --- a/spec/factories/invoices.rb +++ b/spec/factories/invoices.rb @@ -42,9 +42,12 @@ issued_at { 1.week.ago } payable_until { 3.months.from_now } text { Faker::Lorem.sentences } + transient do + skip_invoice_parts { false } + end after(:build) do |invoice, evaluator| - next unless evaluator.invoice_parts.nil? + next if evaluator.skip_invoice_parts if evaluator.amount&.positive? build(:invoice_part, amount: evaluator.amount, invoice:) diff --git a/spec/services/export/taf/builder_spec.rb b/spec/services/export/taf/builder_spec.rb index 0728457d8..815bd5ab2 100644 --- a/spec/services/export/taf/builder_spec.rb +++ b/spec/services/export/taf/builder_spec.rb @@ -87,7 +87,7 @@ tarif usages payment - build(:invoice, booking:, invoice_parts: []).tap do |invoice| + build(:invoice, booking:, skip_invoice_parts: true, issued_at: Date.new(2024, 12, 27)).tap do |invoice| invoice.invoice_parts = InvoicePart::Factory.new(invoice).call invoice.save! invoice.recalculate! @@ -121,7 +121,7 @@ {Bk AccId=4000 - Code=27 + Code=#{compounds[0].journal_entries[0]&.id} Date=11.10.2018 Text="Zahlung 250001" Type=0 @@ -131,7 +131,7 @@ {Bk AccId=1050 - Code=28 + Code=#{compounds[0].journal_entries[1]&.id} Date=11.10.2018 Text="Zahlung 250001" Type=1 @@ -173,7 +173,7 @@ {Bk AccId=1050 - Code=34 + Code=#{compounds[1].journal_entries[0]&.id} Date=27.12.2024 Flags=1 Text="250001 - Müller" @@ -187,7 +187,7 @@ {Bk AccId=6000 - Code=35 + Code=#{compounds[1].journal_entries[1]&.id} Date=27.12.2024 Text="250001 Saldo" Type=1 @@ -198,7 +198,7 @@ {Bk AccId=6000 - Code=36 + Code=#{compounds[1].journal_entries[2]&.id} Date=27.12.2024 Text="250001 Preis pro Übernachtung" Type=1 @@ -212,7 +212,7 @@ {Bk AccId=9000 BType=1 - Code=37 + Code=#{compounds[1].journal_entries[3]&.id} Date=27.12.2024 Text="250001 Preis pro Übernachtung" Type=1 @@ -224,7 +224,7 @@ {Bk AccId=6020 BType=2 - Code=38 + Code=#{compounds[1].journal_entries[4]&.id} Date=27.12.2024 Text="250001 Preis pro Übernachtung" Type=1 From d46c1850959b3fac66ac8d146cecb2a69e481dfd Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Sat, 4 Jan 2025 10:01:40 +0000 Subject: [PATCH 27/30] chore: update dependencies --- Gemfile | 1 + Gemfile.lock | 23 +++++++++++++---------- config/routes.rb | 6 ++++-- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/Gemfile b/Gemfile index 11e08397f..78cd576cb 100644 --- a/Gemfile +++ b/Gemfile @@ -52,6 +52,7 @@ gem 'rubyzip' gem 'sidekiq' gem 'sidekiq-cron' gem 'slim-rails' +gem 'stackprof' gem 'statesman' gem 'thruster' gem 'translation' diff --git a/Gemfile.lock b/Gemfile.lock index 705761e4a..532b27919 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -84,7 +84,7 @@ GEM rake (>= 0.8.7) ast (2.4.2) aws-eventstream (1.3.0) - aws-partitions (1.1029.0) + aws-partitions (1.1031.0) aws-sdk-core (3.214.1) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -93,7 +93,7 @@ GEM aws-sdk-kms (1.96.0) aws-sdk-core (~> 3, >= 3.210.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.176.1) + aws-sdk-s3 (1.177.0) aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) @@ -116,7 +116,7 @@ GEM bootstrap_form (5.4.0) actionpack (>= 6.1) activemodel (>= 6.1) - brakeman (6.2.2) + brakeman (7.0.0) racc builder (3.3.0) bundler-audit (0.9.2) @@ -189,7 +189,7 @@ GEM logger zeitwerk (~> 2.6) dry-inflector (1.1.0) - dry-initializer (3.1.1) + dry-initializer (3.2.0) dry-logic (1.5.0) concurrent-ruby (~> 1.0) dry-core (~> 1.0, < 2) @@ -231,7 +231,7 @@ GEM logger faraday-net_http (3.4.0) net-http (>= 0.5.0) - ffi (1.17.0-x86_64-linux-musl) + ffi (1.17.1-x86_64-linux-musl) forwardable (1.3.3) fugit (1.11.1) et-orbi (~> 1, >= 1.2.11) @@ -286,7 +286,7 @@ GEM rb-inotify (~> 0.9, >= 0.9.10) locale (2.1.4) logger (1.6.4) - loofah (2.23.1) + loofah (2.24.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -310,7 +310,7 @@ GEM mutex_m (0.3.0) net-http (0.6.0) uri - net-imap (0.5.4) + net-imap (0.5.5) date net-protocol net-pop (0.1.2) @@ -364,7 +364,8 @@ GEM rack (>= 1.2.0) rack-proxy (0.7.7) rack - rack-session (2.0.0) + rack-session (2.1.0) + base64 (>= 0.1.0) rack (>= 3.0.0) rack-test (2.2.0) rack (>= 1.3) @@ -465,7 +466,7 @@ GEM unicode-display_width (>= 2.4.0, < 4.0) rubocop-ast (1.37.0) parser (>= 3.3.1.0) - rubocop-performance (1.23.0) + rubocop-performance (1.23.1) rubocop (>= 1.48.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) rubocop-rails (2.28.0) @@ -516,8 +517,9 @@ GEM actionpack (>= 3.1) railties (>= 3.1) slim (>= 3.0, < 6.0, != 5.0.0) - sorbet-runtime (0.5.11708) + sorbet-runtime (0.5.11717) squasher (0.8.0) + stackprof (0.2.26) statesman (12.1.0) statsd-ruby (1.5.0) stringio (3.1.2) @@ -636,6 +638,7 @@ DEPENDENCIES simplecov slim-rails squasher + stackprof statesman thruster translation diff --git a/config/routes.rb b/config/routes.rb index 76da3fcc7..3aa1d5fee 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,6 +4,10 @@ devise_for :users, path: 'account', path_names: { sign_in: 'login', sign_out: 'logout' } resource :account, only: %i[edit update destroy] + get 'test', to: 'public/pages#test' + get 'health', to: 'public/pages#health' + get 'changelog', to: 'public/pages#changelog' + scope '(:org)' do namespace :manage do root to: 'bookings#calendar' @@ -96,6 +100,4 @@ root to: 'pages#home' end end - get 'health', to: 'pages#health' - get 'changelog', to: 'pages#changelog' end From e6d743ac4c8542500c81e3fcf47a74923fef0bbd Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Sat, 4 Jan 2025 10:09:02 +0000 Subject: [PATCH 28/30] chore: bump version --- CHANGELOG.md | 15 ++++++++++++--- VERSION | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83329323f..60e229187 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ # Changelog -## 25.12.1 +## 25.1.1 + +Released 04.01.2025 + +- Feature: Add key sequences for invoices, bookings and tenants +- Feature: Generate journal entries for accounting purposes +- Feature: Export journal entries to csv and taf formats +- Fix: Imporve data digest form + +## 24.12.1 Released 10.12.2024 @@ -10,12 +19,12 @@ Released 10.12.2024 Released 29.11.2024 -## 24.11.1 - - Fix: Fix manage occupancy calendar to include closedowns again - Feature: Allow setting check_on on BookingValidation - Chore: Update to Rails 8 +## 24.11.1 + Released 25.11.2024 - Fix: Refactor deadlines to fix out-of-order issues diff --git a/VERSION b/VERSION index a92c04bf8..549447cdc 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -V24.12.1 +V25.1.1 From f2cc83bb60d96b2d27f8662391baeaf7bf547819 Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Sat, 4 Jan 2025 10:15:46 +0000 Subject: [PATCH 29/30] chore: sync translations --- config/locales/.translation_io | 2 +- config/locales/fr.yml | 146 +++++++++++++++++++------ config/locales/it.yml | 187 +++++++++++++++++++++++++++++++++ 3 files changed, 300 insertions(+), 35 deletions(-) diff --git a/config/locales/.translation_io b/config/locales/.translation_io index 841d45ef6..9ef49dd83 100644 --- a/config/locales/.translation_io +++ b/config/locales/.translation_io @@ -5,4 +5,4 @@ # ignore the conflicts and "sync" again, it will fix this file for you. --- -timestamp: 1732884431 +timestamp: 1735985394 diff --git a/config/locales/fr.yml b/config/locales/fr.yml index a7f18e2d7..93e55b9f5 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -26,6 +26,12 @@ fr: homes: Chalets issued_at_after: Délai de paiement après le issued_at_before: Délai de paiement avant le + journal_entry/filter: + date_after: + date_before: + processed: + processed_at_after: + processed_at_before: meter_reading_period/filter: begins_at_after: Début de période du compteur à partir du begins_at_before: Début de période du compteur jusqu'au @@ -41,11 +47,12 @@ fr: ends_at_after: Fin de la location à partir du ends_at_before: Fin de la location jusqu'au occupiable_settings: + accounting_cost_center_nr: booking_margin: Espacement/période entre les réservations organisation_settings: awaiting_contract_deadline: Délai de réception du contrat - awaiting_tenant_deadline: - booking_window: + awaiting_tenant_deadline: Demande de location transmise (données du locataire manquantes) + booking_window: Plage de réservation closed_occupancy_color: Couleur pour les clôtures deadline_postponable_for: Prolongation du délai default_begins_at_time: Heure de début de l'occupation par défaut @@ -98,7 +105,7 @@ fr: booking_agent_ref: Votre référence home: Chalet tenant_email: Email du locataire - tenant_infos: + tenant_infos: Informations supplémentaires sur le locataire booking: approximate_headcount: Nombre de personnes attendues begins_at: Début de la location @@ -162,8 +169,9 @@ fr: tenant_mode: type: type booking_validation: + check_on: enabling_conditions: - error_message: + error_message: Message d'erreur à afficher ordinal_position: Position de l'affichage validating_conditions: contract: @@ -223,6 +231,8 @@ fr: text: Texte type: Type de facture invoice_part: + accounting_account_nr: zugewiesenes Buchhaltungskonto + accounting_cost_center_nr: amount: Montant apply: Employer breakdown: Ventilation @@ -230,6 +240,25 @@ fr: ordinal_position: Numéro d'ordre type: Type de poste de facturation vat: TVA incluse en pourcentage + vat_category_id: + journal_entry: + account_nr: + amount: Montant + book_type: Type + booking_id: Réservation + date: + haben_account: + haben_amount: + invoice_id: Facture + invoice_part_id: + payment_id: Paiement + processed_at: + ref: + side: + soll_account: + soll_amount: + text: Texte + vat_category_id: meter_reading_period: begins_at: Début end_value: Nouvelle valeur du compteur @@ -280,17 +309,19 @@ fr: remarks: Remarques responsibility: Responsabilité organisation: - account_address: + account_address: Adresse du compte pour les factures QR (si différente) address: Adresse bcc: BCC booking_ref_template: contract_signature: Image de signature de contrat + cors_origins: creditor_address: Adresse sur les factures currency: Währung default_payment_info_type: Partie paiement standard esr_beneficiary_account: Numéro de participant BVR / compte postal esr_ref_prefix: Numéro d'identification BVR / Préfixe iban: IBAN + invoice_payment_ref_template: invoice_ref_template: locale: Langue locales: Langues @@ -298,7 +329,7 @@ fr: logo: Logo mail_from: Expéditeur des e-mails name: Nom - nickname_label: + nickname_label: Désignation du surnom notifications_enabled: Messages activés privacy_statement_pdf: Déclaration de confidentialité representative_address: Adresse de la suppléance pour les contrats @@ -306,6 +337,7 @@ fr: slug: Espace de nommage smtp_settings: Paramètres du serveur de messagerie smtp_settings_json: Paramètres du serveur de messagerie + tenant_ref_template: terms_pdf: CGU organisation_user: email: Email @@ -330,13 +362,14 @@ fr: namespace: Domaine title: Titre tarif: - accounting_account_nr: Compte comptable + accounting_account_nr: zugewiesenes Buchhaltungskonto + accounting_cost_center_nr: associated_types: Affiché dans les documents enabling_conditions: Conditions de sélection autorisée label: Désignation du tarif meter: Nom du compteur (pour les tarifs par relevé de compteur) - minimum_price_per_night: - minimum_price_total: + minimum_price_per_night: Prix minimum (par nuit) + minimum_price_total: Prix minimum (total) minimum_usage_per_night: Consommation minimale (par nuit) minimum_usage_total: Consommation minimale (total) ordinal_position: Ordnungsnummer @@ -348,12 +381,13 @@ fr: tarif_group: groupe tarifaire type: type unit: unité + vat_category_id: tenant: address: Adresse address_addon: Complément d'adresse / école / entreprise birth_date: Date de naissance bookings_without_contract: - bookings_without_invoice: + bookings_without_invoice: Réservations sans facture city: Lieu country_code: Pays email: Email @@ -397,6 +431,10 @@ fr: reset_password_token: Clé de réinitialisation du mot de passe sign_in_count: Nombre des connexions updated_at: Date de mise à jour + vat_category: + accounting_vat_code: + label: Désignation + percentage: enums: booking: committed_request: @@ -427,6 +465,14 @@ fr: blank_editable: Visible par les locataires, modifiable si vide not_visible: Invisible pour les locataires provisional_editable: Visible pour les locataires, modifiable jusqu'à définitif + booking_validation: + check_on: + agent_create: + agent_update: + manage_create: + manage_update: + public_create: + public_update: data_digest: periods: ever: Aller Zeiten @@ -460,6 +506,21 @@ fr: paid: only_paid: Uniquement les factures réglées only_unpaid: Seulement ouvert + journal_entry: + book_types: + cost: + main: + vat: + side: + haben: + soll: + trigger: + invoice_created: + payment_created: + journal_entry/filter: + processed: + only_processed: + only_unprocessed: mail_template: attachable_booking_documents: contract: Contrat @@ -490,6 +551,7 @@ fr: organisation_user: role: admin: Admin + finance: Caissier manager: Administration none: "-" readonly: Lecture @@ -569,6 +631,8 @@ fr: one: Condition de l'objet de location booking_conditions/tarif: one: 'Condition du tarif sélectionné ' + booking_conditions/tenant_attribute: + one: booking_question: one: Infos supplémentaires other: Informations supplémentaires @@ -606,9 +670,12 @@ fr: data_digest_templates/invoice_part: one: Extrait des postes de la facture other: Extraits des postes de facturation - data_digest_templates/meter_reading_period: + data_digest_templates/journal_entry: one: other: + data_digest_templates/meter_reading_period: + one: Relevé du compteur (Début) + other: Relevés de compteurs (Fin) data_digest_templates/payment: one: Extrait de paiements other: Extrait de paiements @@ -633,6 +700,9 @@ fr: invoice_parts/add: one: Poste de facturation normal other: Postes de facturation normaux + invoice_parts/deposit: + one: + other: invoice_parts/percentage: one: Réduction other: Réduction @@ -648,6 +718,9 @@ fr: invoices/offer: one: Offre other: Offres + journal_entry: + one: + other: mail_template: one: Modèle de couriel other: Modèles de courriel @@ -711,6 +784,9 @@ fr: user: one: Compte other: Compte + vat_category: + one: + other: add_record: Ajouter %{model_name} back: Retour booking_actions: @@ -1205,6 +1281,7 @@ fr: flat: Forfait à %{price_per_unit} minimum: Montant minimum %{minimum} deposited_amount: Avoir à partir d'un acompte + unassigned_payments_amount: invoices: total: Total vat_title: TVA incluse dans les prix @@ -1226,6 +1303,7 @@ fr: github: GitHub title: Logiciel manage_navigation: + finances: help: Aide outbox: Courrier sortant settings: Réglages @@ -1233,14 +1311,14 @@ fr: bookings: booking_actions: invoke: - failure: - success: + failure: "Échec de l'importation :" + success: Importation réussie calendar: ics_link: comme ICAL private_ics_link: import: headers: En-têtes - import_error: 'Échec de l''importation : ' + import_error: "Échec de l'importation : " import_file: Fichier d'importation (CSV) import_success: Importation réussie index: @@ -1249,7 +1327,7 @@ fr: new: Nouveau new_closed_occupancy: Fermeture new_request: Demande de réservation - no_records_yet: + no_records_yet: "Aucune réservation n'a été trouvée. Ajouter une nouvelle réservation : %{options}." too_many_records: Trop de réservations ont été trouvées. Veuillez restreindre la recherche. navigation: costs: Tarifs & consommation @@ -1287,12 +1365,12 @@ fr: occupancies: span: nights: - one: - other: - zero: + one: 1 nuit + other: "%{count} Nuits" + zero: sans nuitée organisation_users: form: - regenerate_token: + regenerate_token: Générer à nouveau un Token index: invite_user: Inviter un nouvel utilisateur organisations: @@ -1313,21 +1391,21 @@ fr: form: help_html: help_title: Aide sur les variables - load_locale_defaults: + load_locale_defaults: Rétablir le texte par défaut pour cette langue index: booking_action: Action booking_state: Statut comptable - create_missing: - created_missing: + create_missing: Ajouter automatiquement les modèles de texte manquants + created_missing: "%{count} Les modèles de texte manquants ont été ajoutés : %{list}" document: Document - missing_templates: + missing_templates: "%{count} Les modèles de texte sont manquants." rich_text_templates_by_booking_action: Actions rich_text_templates_by_booking_state: Statut comptable rich_text_templates_by_document: Documents rich_text_templates_by_rest: Autres tarifs: form: - minimum: + minimum: Minimum nav: edit_account: Gérer le compte manage: Administration @@ -1387,7 +1465,7 @@ fr: pb: Po tb: To nth: - ordinalized: + ordinalized: "%{number}." ordinals: "." percentage: format: @@ -1431,7 +1509,7 @@ fr: title: Traiter une réservation form: tenant_email_help: Veuillez NE PAS utiliser l'adresse e-mail de l'intermédiaire. Nous n'avons besoin de l'adresse e-mail du locataire que lorsque la demande devient définitive, afin de pouvoir établir le contact. - tenant_infos_help: + tenant_infos_help: Nom de la personne responsable, autres informations sur la médiation new: title: Réservation bookings: @@ -1441,8 +1519,8 @@ fr: title: Traiter une réservation form: booking_details: Informations sur la réservation - committed_request_definitive_request_help: - committed_request_provisional_request_help: + committed_request_definitive_request_help: "Engagement ferme et définitif. Le contrat est préparé et envoyé si l'administration accepte la demande" + committed_request_provisional_request_help: Demande sans engagement. Acceptation ou refus dans le délai indiqué last_minute_warning: Attention ! La date de réservation est proche. Merci de t'annoncer en plus par téléphone ou par e-mail auprès de l'administration new: accept_conditions_html: J'accepte %{conditions_html} @@ -1539,8 +1617,8 @@ fr: description: manage_cancelled_request_notification: default_body: - default_title: - description: + default_title: Annulation Demande de réservation {{ booking.ref }} + description: Notification de demande de réservation annulée (administration) manage_definitive_request_notification: default_body: default_title: Demande de location {{ booking.ref }} définitive @@ -1567,12 +1645,12 @@ fr: description: Notification pour location annulée (propriétaire) operator_contract_sent_notification: default_body: - default_title: - description: + default_title: Contrat/acompte envoyé + description: Notification de contrat/paiement envoyé (fonctionnaire) operator_invoice_sent_notification: default_body: - default_title: - description: + default_title: Facture envoyée + description: Notification de facture envoyée (fonctionnaire) operator_upcoming_notification: default_body: default_title: Location diff --git a/config/locales/it.yml b/config/locales/it.yml index ec356336b..1ec16961b 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -26,6 +26,12 @@ it: homes: Oggetti principali di affitto issued_at_after: Data di fatturazione dopo issued_at_before: Data di fatturazione precedente + journal_entry/filter: + date_after: + date_before: + processed: + processed_at_after: + processed_at_before: meter_reading_period/filter: begins_at_after: begins_at_before: @@ -41,6 +47,7 @@ it: ends_at_after: Fine occupazione da ends_at_before: Fine dell'occupazione fino a occupiable_settings: + accounting_cost_center_nr: booking_margin: Durata tra le prenotazioni organisation_settings: awaiting_contract_deadline: Periodo per i contratti in entrata @@ -162,6 +169,7 @@ it: tenant_mode: type: Tipo booking_validation: + check_on: enabling_conditions: error_message: ordinal_position: Numero di serie @@ -223,6 +231,8 @@ it: text: Testo type: Tipo di fattura invoice_part: + accounting_account_nr: Conto contabile + accounting_cost_center_nr: amount: Importo apply: Applicare breakdown: Ripartizione @@ -230,6 +240,25 @@ it: ordinal_position: Numero di serie type: Tipo di voce della fattura vat: + vat_category_id: + journal_entry: + account_nr: + amount: Importo + book_type: Tipo + booking_id: Prenotazione + date: + haben_account: + haben_amount: + invoice_id: Fattura + invoice_part_id: + payment_id: Pagamento + processed_at: + ref: + side: + soll_account: + soll_amount: + text: Testo + vat_category_id: meter_reading_period: begins_at: Inizio end_value: Lettura del contatore nuovo @@ -285,12 +314,14 @@ it: bcc: BCC booking_ref_template: contract_signature: Immagine della firma del contratto + cors_origins: creditor_address: Indirizzo sulle fatture currency: Valuta default_payment_info_type: Parte di pagamento standard esr_beneficiary_account: esr_ref_prefix: Numero di identificazione del PVR / prefisso iban: IBAN + invoice_payment_ref_template: invoice_ref_template: locale: Lingua locales: @@ -306,6 +337,7 @@ it: slug: Namespace smtp_settings: Impostazioni del mailserver smtp_settings_json: Impostazioni del mailserver + tenant_ref_template: terms_pdf: Condizioni generali di contratto organisation_user: email: E-mail @@ -331,6 +363,7 @@ it: title: Titolo tarif: accounting_account_nr: Conto contabile + accounting_cost_center_nr: associated_types: Indicato nei documenti enabling_conditions: Condizioni per la selezione consentita label: Designazione tariffa @@ -348,6 +381,7 @@ it: tarif_group: Gruppo tariffario type: Tipo unit: Unità + vat_category_id: tenant: address: Indirizzo address_addon: Suffisso indirizzo / scuola / azienda @@ -397,6 +431,10 @@ it: reset_password_token: Token per la reimpostazione della password sign_in_count: Numero di accessi updated_at: Aggiornato il + vat_category: + accounting_vat_code: + label: Nome + percentage: enums: booking: committed_request: @@ -427,6 +465,14 @@ it: blank_editable: not_visible: provisional_editable: + booking_validation: + check_on: + agent_create: + agent_update: + manage_create: + manage_update: + public_create: + public_update: data_digest: periods: ever: Tutti tempi @@ -460,6 +506,21 @@ it: paid: only_paid: Solo pagate only_unpaid: Solo non pagate + journal_entry: + book_types: + cost: + main: + vat: + side: + haben: + soll: + trigger: + invoice_created: + payment_created: + journal_entry/filter: + processed: + only_processed: + only_unprocessed: mail_template: attachable_booking_documents: contract: Contratto @@ -490,6 +551,7 @@ it: organisation_user: role: admin: Administration + finance: manager: Verwaltung none: "-" readonly: Lesend @@ -569,6 +631,8 @@ it: one: Oggetto in affitto (condizione) booking_conditions/tarif: one: Tariffa selezionata (condizione) + booking_conditions/tenant_attribute: + one: booking_question: one: other: @@ -606,6 +670,9 @@ it: data_digest_templates/invoice_part: one: Voci di fattura (estratto) other: Estratti di voci di fattura + data_digest_templates/journal_entry: + one: + other: data_digest_templates/meter_reading_period: one: other: @@ -633,6 +700,9 @@ it: invoice_parts/add: one: Voce di fattura normale other: Voci di fattura normali + invoice_parts/deposit: + one: + other: invoice_parts/percentage: one: Sconto other: Sconto @@ -648,6 +718,9 @@ it: invoices/offer: one: Offerta other: Offerte + journal_entry: + one: + other: mail_template: one: other: @@ -711,6 +784,9 @@ it: user: one: Utente other: Utenti + vat_category: + one: + other: add_record: aggiungere %{model_name} back: Indietro booking_actions: @@ -1162,6 +1238,115 @@ it: create: Crea %{model} submit: Invia %{model} update: Aggiorna %{model} + ice_cube: + array: + last_word_connector: + two_words_connector: + words_connector: + at_hours_of_the_day: + one: + other: + at_seconds_of_minute: + one: + other: + date: + day_names: + - domenica + - lunedì + - martedì + - mercoledì + - giovedì + - venerdì + - sabato + formats: + default: + month_names: + - + - gennaio + - febbraio + - marzo + - aprile + - mag + - giugno + - luglio + - agosto + - settembre + - ottobre + - novembre + - dicembre + days_of_month: + one: + other: + days_of_week: + days_of_year: + one: + other: + days_on: + - + - + - + - + - + - + - + each_day: + one: + other: + each_hour: + one: + other: + each_minute: + one: + other: + each_month: + one: + other: + each_second: + one: + other: + each_week: + one: + other: + each_year: + one: + other: + in: + integer: + literal_ordinals: + "-1": + "-2": + negative: + ordinal: + ordinals: + default: + not: + not_on: + 'on': + on_days: + on_minutes_of_hour: + one: + other: + on_seconds_of_minute: + one: + other: + on_weekdays: + on_weekends: + pieces_connector: + string: + format: + count: + day: + day_of_month: + day_of_week: + day_of_year: + default: + hour_of_day: + minute_of_hour: + until: + times: + one: + other: + until: import: Importazione import_records: importa %{model_name}. invoice_parts: @@ -1170,6 +1355,7 @@ it: flat: Forfettario di %{price_per_unit} minimum: Importo minimo %{minimum} deposited_amount: + unassigned_payments_amount: invoices: total: Totale vat_title: @@ -1191,6 +1377,7 @@ it: github: title: manage_navigation: + finances: help: Aiuto outbox: settings: Impostazioni From 3dfe8b17d74d1c54688065692e04bad287e23b73 Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Sat, 4 Jan 2025 10:30:17 +0000 Subject: [PATCH 30/30] fix: translations --- config/locales/it.yml | 109 ------------------------------------------ 1 file changed, 109 deletions(-) diff --git a/config/locales/it.yml b/config/locales/it.yml index 1ec16961b..e194da17c 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -1238,115 +1238,6 @@ it: create: Crea %{model} submit: Invia %{model} update: Aggiorna %{model} - ice_cube: - array: - last_word_connector: - two_words_connector: - words_connector: - at_hours_of_the_day: - one: - other: - at_seconds_of_minute: - one: - other: - date: - day_names: - - domenica - - lunedì - - martedì - - mercoledì - - giovedì - - venerdì - - sabato - formats: - default: - month_names: - - - - gennaio - - febbraio - - marzo - - aprile - - mag - - giugno - - luglio - - agosto - - settembre - - ottobre - - novembre - - dicembre - days_of_month: - one: - other: - days_of_week: - days_of_year: - one: - other: - days_on: - - - - - - - - - - - - - - - each_day: - one: - other: - each_hour: - one: - other: - each_minute: - one: - other: - each_month: - one: - other: - each_second: - one: - other: - each_week: - one: - other: - each_year: - one: - other: - in: - integer: - literal_ordinals: - "-1": - "-2": - negative: - ordinal: - ordinals: - default: - not: - not_on: - 'on': - on_days: - on_minutes_of_hour: - one: - other: - on_seconds_of_minute: - one: - other: - on_weekdays: - on_weekends: - pieces_connector: - string: - format: - count: - day: - day_of_month: - day_of_week: - day_of_year: - default: - hour_of_day: - minute_of_hour: - until: - times: - one: - other: - until: import: Importazione import_records: importa %{model_name}. invoice_parts: