diff --git a/nixos/doc/manual/release-notes/rl-2205.section.md b/nixos/doc/manual/release-notes/rl-2205.section.md index f45258d0038b..38f503570105 100644 --- a/nixos/doc/manual/release-notes/rl-2205.section.md +++ b/nixos/doc/manual/release-notes/rl-2205.section.md @@ -99,7 +99,7 @@ In addition to numerous new and upgraded packages, this release has the followin - [input-remapper](https://github.com/sezanzeb/input-remapper), an easy to use tool to change the mapping of your input device buttons. Available at [services.input-remapper](#opt-services.input-remapper.enable). -- [InvoicePlane](https://invoiceplane.com), web application for managing and creating invoices. Available at `services.invoiceplane`. +- [InvoicePlane](https://invoiceplane.com), web application for managing and creating invoices. Available at [services.invoiceplane](#opt-services.invoiceplane.sites._name_.enable). - [k3b](https://userbase.kde.org/K3b), the KDE disk burning application. Available as [programs.k3b](#opt-programs.k3b.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index cb003fc2709e..9d81f8a6b674 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1658,6 +1658,7 @@ ./services/web-apps/immich.nix ./services/web-apps/immichframe.nix ./services/web-apps/invidious.nix + ./services/web-apps/invoiceplane.nix ./services/web-apps/isso.nix ./services/web-apps/jirafeau.nix ./services/web-apps/jitsi-meet.nix diff --git a/nixos/modules/rename.nix b/nixos/modules/rename.nix index f3fcf6055946..0e4afc8935cd 100644 --- a/nixos/modules/rename.nix +++ b/nixos/modules/rename.nix @@ -442,10 +442,6 @@ in Consider migrating or switching to Incus, or remove from your configuration. https://linuxcontainers.org/incus/docs/main/howto/server_migrate_lxd/ '') - (mkRemovedOptionModule [ "services" "invoiceplane" ] '' - services.invoiceplane has been removed since the service only supported PHP 8.1 which is EOL - and removed from nixpkgs. - '') (mkRemovedOptionModule [ "services" "filesender" ] '' services.filesender has been removed since it depends on simplesamlphp which was severely unmaintained. '') diff --git a/nixos/modules/services/web-apps/invoiceplane.nix b/nixos/modules/services/web-apps/invoiceplane.nix new file mode 100644 index 000000000000..bd261ea9ec23 --- /dev/null +++ b/nixos/modules/services/web-apps/invoiceplane.nix @@ -0,0 +1,491 @@ +{ + config, + pkgs, + lib, + ... +}: + +with lib; + +let + cfg = config.services.invoiceplane; + eachSite = cfg.sites; + user = "invoiceplane"; + webserver = config.services.${cfg.webserver}; + + invoiceplane-config = + hostName: cfg: + pkgs.writeText "ipconfig.php" '' + # + + IP_URL=http://${hostName} + ENABLE_DEBUG=false + DISABLE_SETUP=false + REMOVE_INDEXPHP=false + DB_HOSTNAME=${cfg.database.host} + DB_USERNAME=${cfg.database.user} + # NOTE: file_get_contents adds newline at the end of returned string + DB_PASSWORD=${ + optionalString ( + cfg.database.passwordFile != null + ) "trim(file_get_contents('${cfg.database.passwordFile}'), \"\\r\\n\")" + } + DB_DATABASE=${cfg.database.name} + DB_PORT=${toString cfg.database.port} + SESS_EXPIRATION=864000 + ENABLE_INVOICE_DELETION=false + DISABLE_READ_ONLY=false + ENCRYPTION_KEY= + ENCRYPTION_CIPHER=AES-256 + SETUP_COMPLETED=false + REMOVE_INDEXPHP=true + ''; + + mkPhpValue = + v: + if isString v then + escapeShellArg v + # NOTE: If any value contains a , (comma) this will not get escaped + else if isList v && strings.isConvertibleWithToString v then + escapeShellArg (concatMapStringsSep "," toString v) + else if isInt v then + toString v + else if isBool v then + boolToString v + else + abort "The Invoiceplane config value ${lib.generators.toPretty { } v} can not be encoded."; + + extraConfig = + hostName: cfg: + let + settings = mapAttrsToList (k: v: "${k}=${mkPhpValue v}") cfg.settings; + in + pkgs.writeText "extraConfig.php" (concatStringsSep "\n" settings); + + pkg = + hostName: cfg: + pkgs.stdenv.mkDerivation rec { + pname = "invoiceplane-${hostName}"; + version = src.version; + src = pkgs.invoiceplane; + + postPatch = '' + # Patch index.php file to load additional config file + substituteInPlace index.php \ + --replace-fail "require __DIR__ . '/vendor/autoload.php';" "require('vendor/autoload.php'); \$dotenv = Dotenv\Dotenv::createImmutable(__DIR__, 'extraConfig.php'); \$dotenv->load();"; + ''; + + installPhase = '' + mkdir -p $out + cp -r * $out/ + + # symlink uploads and log directories + rm -r $out/uploads $out/application/logs $out/vendor/mpdf/mpdf/tmp + ln -sf ${cfg.stateDir}/uploads $out/ + ln -sf ${cfg.stateDir}/logs $out/application/ + ln -sf ${cfg.stateDir}/tmp $out/vendor/mpdf/mpdf/ + + # symlink the InvoicePlane config + ln -s ${cfg.stateDir}/ipconfig.php $out/ipconfig.php + + # symlink the extraConfig file + ln -s ${extraConfig hostName cfg} $out/extraConfig.php + + # symlink additional templates + ${concatMapStringsSep "\n" ( + template: "cp -r ${template}/. $out/application/views/invoice_templates/pdf/" + ) cfg.invoiceTemplates} + ${concatMapStringsSep "\n" ( + template: "cp -r ${template}/. $out/application/views/quote_templates/pdf/" + ) cfg.quoteTemplates} + ''; + }; + + siteOpts = + { lib, name, ... }: + { + options = { + + enable = mkEnableOption "InvoicePlane web application"; + + stateDir = mkOption { + type = types.path; + default = "/var/lib/invoiceplane/${name}"; + description = '' + This directory is used for uploads of attachments and cache. + The directory passed here is automatically created and permissions + adjusted as required. + ''; + }; + + database = { + host = mkOption { + type = types.str; + default = "localhost"; + description = "Database host address."; + }; + + port = mkOption { + type = types.port; + default = 3306; + description = "Database host port."; + }; + + name = mkOption { + type = types.str; + default = "invoiceplane"; + description = "Database name."; + }; + + user = mkOption { + type = types.str; + default = "invoiceplane"; + description = "Database user."; + }; + + passwordFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/run/keys/invoiceplane-dbpassword"; + description = '' + A file containing the password corresponding to + {option}`database.user`. + ''; + }; + + createLocally = mkOption { + type = types.bool; + default = true; + description = "Create the database and database user locally."; + }; + }; + + invoiceTemplates = mkOption { + type = types.listOf types.path; + default = [ ]; + description = '' + List of path(s) to respective template(s) which are copied from the 'invoice_templates/pdf' directory. + + ::: {.note} + These templates need to be packaged before use, see example. + ::: + ''; + example = literalExpression '' + let + # Let's package an example template + template-vtdirektmarketing = pkgs.stdenv.mkDerivation { + name = "vtdirektmarketing"; + # Download the template from a public repository + src = pkgs.fetchgit { + url = "https://git.project-insanity.org/onny/invoiceplane-vtdirektmarketing.git"; + sha256 = "1hh0q7wzsh8v8x03i82p6qrgbxr4v5fb05xylyrpp975l8axyg2z"; + }; + sourceRoot = "."; + # Installing simply means copying template php file to the output directory + installPhase = "" + mkdir -p $out + cp invoiceplane-vtdirektmarketing/vtdirektmarketing.php $out/ + ""; + }; + # And then pass this package to the template list like this: + in [ template-vtdirektmarketing ] + ''; + }; + + quoteTemplates = mkOption { + type = types.listOf types.path; + default = [ ]; + description = '' + List of path(s) to respective template(s) which are copied from the 'quote_templates/pdf' directory. + + ::: {.note} + These templates need to be packaged before use, see example. + ::: + ''; + example = literalExpression '' + let + # Let's package an example template + template-vtdirektmarketing = pkgs.stdenv.mkDerivation { + name = "vtdirektmarketing"; + # Download the template from a public repository + src = pkgs.fetchgit { + url = "https://git.project-insanity.org/onny/invoiceplane-vtdirektmarketing.git"; + sha256 = "1hh0q7wzsh8v8x03i82p6qrgbxr4v5fb05xylyrpp975l8axyg2z"; + }; + sourceRoot = "."; + # Installing simply means copying template php file to the output directory + installPhase = "" + mkdir -p $out + cp invoiceplane-vtdirektmarketing/vtdirektmarketing.php $out/ + ""; + }; + # And then pass this package to the template list like this: + in [ template-vtdirektmarketing ] + ''; + }; + + poolConfig = mkOption { + type = + with types; + attrsOf (oneOf [ + str + int + bool + ]); + default = { + "pm" = "dynamic"; + "pm.max_children" = 32; + "pm.start_servers" = 2; + "pm.min_spare_servers" = 2; + "pm.max_spare_servers" = 4; + "pm.max_requests" = 500; + }; + description = '' + Options for the InvoicePlane PHP pool. See the documentation on `php-fpm.conf` + for details on configuration directives. + ''; + }; + + settings = mkOption { + type = types.attrsOf types.anything; + default = { }; + description = '' + Structural InvoicePlane configuration. Refer to + + for details and supported values. + ''; + example = literalExpression '' + { + SETUP_COMPLETED = true; + DISABLE_SETUP = true; + IP_URL = "https://invoice.example.com"; + } + ''; + }; + + cron = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Enable cron service which periodically runs Invoiceplane tasks. + Requires key taken from the administration page. Refer to + + on how to configure it. + ''; + }; + key = mkOption { + type = types.str; + description = "Cron key taken from the administration page."; + }; + }; + + }; + + }; +in +{ + # interface + options = { + services.invoiceplane = mkOption { + type = types.submodule { + + options.sites = mkOption { + type = types.attrsOf (types.submodule siteOpts); + default = { }; + description = "Specification of one or more InvoicePlane sites to serve"; + }; + + options.webserver = mkOption { + type = types.enum [ + "caddy" + "nginx" + ]; + default = "caddy"; + example = "nginx"; + description = '' + Which webserver to use for virtual host management. + ''; + }; + }; + default = { }; + description = "InvoicePlane configuration."; + }; + + }; + + # implementation + config = mkIf (eachSite != { }) (mkMerge [ + { + + assertions = flatten ( + mapAttrsToList (hostName: cfg: [ + { + assertion = cfg.database.createLocally -> cfg.database.user == user; + message = ''services.invoiceplane.sites."${hostName}".database.user must be ${user} if the database is to be automatically provisioned''; + } + { + assertion = cfg.database.createLocally -> cfg.database.passwordFile == null; + message = ''services.invoiceplane.sites."${hostName}".database.passwordFile cannot be specified if services.invoiceplane.sites."${hostName}".database.createLocally is set to true.''; + } + { + assertion = cfg.cron.enable -> cfg.cron.key != null; + message = ''services.invoiceplane.sites."${hostName}".cron.key must be set in order to use cron service.''; + } + ]) eachSite + ); + + services.mysql = mkIf (any (v: v.database.createLocally) (attrValues eachSite)) { + enable = true; + package = mkDefault pkgs.mariadb; + ensureDatabases = mapAttrsToList (hostName: cfg: cfg.database.name) eachSite; + ensureUsers = mapAttrsToList (hostName: cfg: { + name = cfg.database.user; + ensurePermissions = { + "${cfg.database.name}.*" = "ALL PRIVILEGES"; + }; + }) eachSite; + }; + + services.phpfpm = { + pools = mapAttrs' ( + hostName: cfg: + (nameValuePair "invoiceplane-${hostName}" { + inherit user; + group = webserver.group; + settings = { + "listen.owner" = webserver.user; + "listen.group" = webserver.group; + } + // cfg.poolConfig; + }) + ) eachSite; + }; + + } + + { + + systemd.tmpfiles.rules = flatten ( + mapAttrsToList (hostName: cfg: [ + "d ${cfg.stateDir} 0750 ${user} ${webserver.group} - -" + "f ${cfg.stateDir}/ipconfig.php 0750 ${user} ${webserver.group} - -" + "d ${cfg.stateDir}/logs 0750 ${user} ${webserver.group} - -" + "d ${cfg.stateDir}/uploads 0750 ${user} ${webserver.group} - -" + "d ${cfg.stateDir}/uploads/archive 0750 ${user} ${webserver.group} - -" + "d ${cfg.stateDir}/uploads/customer_files 0750 ${user} ${webserver.group} - -" + "d ${cfg.stateDir}/uploads/temp 0750 ${user} ${webserver.group} - -" + "d ${cfg.stateDir}/uploads/temp/mpdf 0750 ${user} ${webserver.group} - -" + "d ${cfg.stateDir}/tmp 0750 ${user} ${webserver.group} - -" + ]) eachSite + ); + + systemd.services.invoiceplane-config = { + serviceConfig.Type = "oneshot"; + script = concatStrings ( + mapAttrsToList (hostName: cfg: '' + mkdir -p ${cfg.stateDir}/logs \ + ${cfg.stateDir}/uploads + if ! grep -q IP_URL "${cfg.stateDir}/ipconfig.php"; then + cp "${invoiceplane-config hostName cfg}" "${cfg.stateDir}/ipconfig.php" + fi + if ! grep -q 'php exit' "${cfg.stateDir}/ipconfig.php"; then + sed -i "1i # " "${cfg.stateDir}/ipconfig.php" + fi + '') eachSite + ); + wantedBy = [ "multi-user.target" ]; + }; + + users.users.${user} = { + group = webserver.group; + isSystemUser = true; + }; + + } + { + + # Cron service implementation + + systemd.timers = mapAttrs' ( + hostName: cfg: + (nameValuePair "invoiceplane-cron-${hostName}" ( + mkIf cfg.cron.enable { + wantedBy = [ "timers.target" ]; + timerConfig = { + OnBootSec = "5m"; + OnUnitActiveSec = "5m"; + Unit = "invoiceplane-cron-${hostName}.service"; + }; + } + )) + ) eachSite; + + systemd.services = mapAttrs' ( + hostName: cfg: + (nameValuePair "invoiceplane-cron-${hostName}" ( + mkIf cfg.cron.enable { + serviceConfig = { + Type = "oneshot"; + User = user; + ExecStart = "${pkgs.curl}/bin/curl --header 'Host: ${hostName}' http://localhost/invoices/cron/recur/${cfg.cron.key}"; + }; + } + )) + ) eachSite; + + } + + (mkIf (cfg.webserver == "caddy") { + services.caddy = { + enable = true; + virtualHosts = mapAttrs' ( + hostName: cfg: + (nameValuePair "http://${hostName}" { + extraConfig = '' + root * ${pkg hostName cfg} + file_server + php_fastcgi unix/${config.services.phpfpm.pools."invoiceplane-${hostName}".socket} + ''; + }) + ) eachSite; + }; + }) + + (mkIf (cfg.webserver == "nginx") { + services.nginx = { + enable = true; + virtualHosts = mapAttrs' ( + hostName: cfg: + (nameValuePair hostName { + root = pkg hostName cfg; + extraConfig = '' + index index.php index.html index.htm; + + if (!-e $request_filename){ + rewrite ^(.*)$ /index.php break; + } + ''; + + locations = { + "/setup".extraConfig = '' + rewrite ^(.*)$ http://${hostName}/ redirect; + ''; + + "~ .php$" = { + extraConfig = '' + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_pass unix:${config.services.phpfpm.pools."invoiceplane-${hostName}".socket}; + include ${config.services.nginx.package}/conf/fastcgi_params; + include ${config.services.nginx.package}/conf/fastcgi.conf; + ''; + }; + }; + }) + ) eachSite; + }; + }) + + ]); +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 38a30c38368f..0ad508dd87ec 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -788,6 +788,7 @@ in installer-systemd-stage-1 = handleTest ./installer-systemd-stage-1.nix { }; intune = runTest ./intune.nix; invidious = runTest ./invidious.nix; + invoiceplane = runTest ./invoiceplane.nix; iodine = runTest ./iodine.nix; iosched = runTest ./iosched.nix; ipget = runTest ./ipget.nix; diff --git a/nixos/tests/invoiceplane.nix b/nixos/tests/invoiceplane.nix new file mode 100644 index 000000000000..4ead3ef94ee7 --- /dev/null +++ b/nixos/tests/invoiceplane.nix @@ -0,0 +1,116 @@ +{ pkgs, ... }: + +{ + name = "invoiceplane"; + meta = with pkgs.lib.maintainers; { + maintainers = [ + onny + ]; + }; + + nodes = { + invoiceplane_caddy = + { ... }: + { + services.invoiceplane.webserver = "caddy"; + services.invoiceplane.sites = { + "site1.local" = { + database.name = "invoiceplane1"; + database.createLocally = true; + enable = true; + }; + "site2.local" = { + database.name = "invoiceplane2"; + database.createLocally = true; + enable = true; + }; + }; + + networking.firewall.allowedTCPPorts = [ 80 ]; + networking.hosts."127.0.0.1" = [ + "site1.local" + "site2.local" + ]; + }; + + invoiceplane_nginx = + { ... }: + { + services.invoiceplane.webserver = "nginx"; + services.invoiceplane.sites = { + "site1.local" = { + database.name = "invoiceplane1"; + database.createLocally = true; + enable = true; + }; + "site2.local" = { + database.name = "invoiceplane2"; + database.createLocally = true; + enable = true; + }; + }; + + networking.firewall.allowedTCPPorts = [ 80 ]; + networking.hosts."127.0.0.1" = [ + "site1.local" + "site2.local" + ]; + }; + }; + + testScript = '' + start_all() + + invoiceplane_caddy.wait_for_unit("caddy") + invoiceplane_nginx.wait_for_unit("nginx") + + site_names = ["site1.local", "site2.local"] + + machines = [invoiceplane_caddy, invoiceplane_nginx] + + for machine in machines: + machine.wait_for_open_port(80) + machine.wait_for_open_port(3306) + + for site_name in site_names: + machine.wait_for_unit(f"phpfpm-invoiceplane-{site_name}") + + with subtest("Website returns welcome screen"): + assert "Please install InvoicePlane" in machine.succeed(f"curl -L {site_name}") + + with subtest("Finish InvoicePlane setup"): + machine.succeed( + f"curl -sSfL --cookie-jar cjar {site_name}/setup/language" + ) + csrf_token = machine.succeed( + "grep ip_csrf_cookie cjar | cut -f 7 | tr -d '\n'" + ) + machine.succeed( + f"curl -sSfL --cookie cjar --cookie-jar cjar -d '_ip_csrf={csrf_token}&ip_lang=english&btn_continue=Continue' {site_name}/setup/language" + ) + csrf_token = machine.succeed( + "grep ip_csrf_cookie cjar | cut -f 7 | tr -d '\n'" + ) + machine.succeed( + f"curl -sSfL --cookie cjar --cookie-jar cjar -d '_ip_csrf={csrf_token}&btn_continue=Continue' {site_name}/setup/prerequisites" + ) + csrf_token = machine.succeed( + "grep ip_csrf_cookie cjar | cut -f 7 | tr -d '\n'" + ) + machine.succeed( + f"curl -sSfL --cookie cjar --cookie-jar cjar -d '_ip_csrf={csrf_token}&btn_continue=Continue' {site_name}/setup/configure_database" + ) + csrf_token = machine.succeed( + "grep ip_csrf_cookie cjar | cut -f 7 | tr -d '\n'" + ) + machine.succeed( + f"curl -sSfl --cookie cjar --cookie-jar cjar -d '_ip_csrf={csrf_token}&btn_continue=Continue' {site_name}/setup/install_tables" + ) + csrf_token = machine.succeed( + "grep ip_csrf_cookie cjar | cut -f 7 | tr -d '\n'" + ) + machine.succeed( + f"curl -sSfl --cookie cjar --cookie-jar cjar -d '_ip_csrf={csrf_token}&btn_continue=Continue' {site_name}/setup/upgrade_tables" + ) + ''; +} diff --git a/pkgs/by-name/in/invoiceplane/fix-yarn-lockfile.patch b/pkgs/by-name/in/invoiceplane/fix-yarn-lockfile.patch new file mode 100644 index 000000000000..dc25725398f5 --- /dev/null +++ b/pkgs/by-name/in/invoiceplane/fix-yarn-lockfile.patch @@ -0,0 +1,19 @@ +diff --git a/yarn.lock b/yarn.lock +index 227d51e2..11c80f04 100644 +--- a/yarn.lock ++++ b/yarn.lock +@@ -1397,10 +1397,10 @@ safe-json-parse@~1.0.1: + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +-sass@^1.89.2: +- version "1.97.2" +- resolved "https://registry.yarnpkg.com/sass/-/sass-1.97.2.tgz#e515a319092fd2c3b015228e3094b40198bff0da" +- integrity sha512-y5LWb0IlbO4e97Zr7c3mlpabcbBtS+ieiZ9iwDooShpFKWXf62zz5pEPdwrLYm+Bxn1fnbwFGzHuCLSA9tBmrw== ++sass@^1.97: ++ version "1.97.3" ++ resolved "https://registry.yarnpkg.com/sass/-/sass-1.97.3.tgz#9cb59339514fa7e2aec592b9700953ac6e331ab2" ++ integrity sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg== + dependencies: + chokidar "^4.0.0" + immutable "^5.0.2" diff --git a/pkgs/by-name/in/invoiceplane/package.nix b/pkgs/by-name/in/invoiceplane/package.nix new file mode 100644 index 000000000000..a0b8b6faf144 --- /dev/null +++ b/pkgs/by-name/in/invoiceplane/package.nix @@ -0,0 +1,80 @@ +{ + lib, + fetchFromGitHub, + nixosTests, + fetchYarnDeps, + applyPatches, + php, + yarnConfigHook, + yarnBuildHook, + yarnInstallHook, + grunt-cli, + fetchzip, +}: +let + version = "1.7.0"; + # Fetch release tarball which contains language files + # https://github.com/InvoicePlane/InvoicePlane/issues/1170 + languages = fetchzip { + #url = "https://github.com/InvoicePlane/InvoicePlane/releases/download/v${version}/v${version}.zip"; + url = "https://github.com/InvoicePlane/InvoicePlane/releases/download/v1.7.0/v1.7.0.zip"; + hash = "sha256-D5wZg745xjbBsEPUbvle8ynErFB4xn9zdxOGh0xKCCU="; + }; +in +php.buildComposerProject2 (finalAttrs: { + pname = "invoiceplane"; + inherit version; + + src = fetchFromGitHub { + owner = "InvoicePlane"; + repo = "InvoicePlane"; + tag = "v${version}"; + hash = "sha256-6fuUmXe8mFSnLYwQCwBzxmSQxM06rQXe00IKUZvWnpM="; + }; + + # Fixes error: Couldn't find any versions for "sass" that matches "^1.97" in our cache + patches = [ ./fix-yarn-lockfile.patch ]; + + # Composer.lock validation currently fails for unknown reason + composerStrictValidation = false; + + vendorHash = "sha256-/fNVq3WJCr9f/NE0s1x8N48W3ZMRUxdh1Qf3pLl0Lpg="; + + nativeBuildInputs = [ + yarnConfigHook + yarnBuildHook + yarnInstallHook + # Needed for executing package.json scripts + grunt-cli + ]; + + offlineCache = fetchYarnDeps { + inherit (finalAttrs) src patches; + hash = "sha256-YDknkQzdRKRRMXS6/cPRSrfhhIyTIDRnFPNGQueu74A="; + }; + + postBuild = '' + grunt build + ''; + + # Cleanup and language files + postInstall = '' + chmod -R u+w $out/share + mv $out/share/php/invoiceplane/* $out/ + cp -r ${languages}/application/language $out/application/ + rm -r $out/{composer.json,composer.lock,CONTRIBUTING.md,docker-compose.yml,Gruntfile.js,package.json,node_modules,yarn.lock,share} + ''; + + passthru.tests = { + inherit (nixosTests) invoiceplane; + }; + + meta = { + description = "Self-hosted open source application for managing your invoices, clients and payments"; + changelog = "https://github.com/InvoicePlane/InvoicePlane/releases/tag/v${version}"; + homepage = "https://www.invoiceplane.com"; + license = lib.licenses.mit; + platforms = lib.platforms.all; + maintainers = with lib.maintainers; [ onny ]; + }; +}) diff --git a/pkgs/top-level/aliases.nix b/pkgs/top-level/aliases.nix index 007093461426..c8733d1c47b9 100644 --- a/pkgs/top-level/aliases.nix +++ b/pkgs/top-level/aliases.nix @@ -901,7 +901,6 @@ mapAliases { inotifyTools = throw "'inotifyTools' has been renamed to/replaced by 'inotify-tools'"; # Converted to throw 2025-10-27 insync-emblem-icons = throw "'insync-emblem-icons' has been removed, use 'insync-nautilus' instead"; # Added 2025-05-14 invalidateFetcherByDrvHash = throw "'invalidateFetcherByDrvHash' has been renamed to/replaced by 'testers.invalidateFetcherByDrvHash'"; # Converted to throw 2025-10-27 - invoiceplane = throw "'invoiceplane' doesn't support non-EOL PHP versions"; # Added 2025-10-03 ioccheck = throw "ioccheck was dropped since it was unmaintained."; # Added 2025-07-06 ipfs = throw "'ipfs' has been renamed to/replaced by 'kubo'"; # Converted to throw 2025-10-27 ipfs-migrator = throw "'ipfs-migrator' has been renamed to/replaced by 'kubo-migrator'"; # Converted to throw 2025-10-27