diff --git a/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
index ce45b0d7977..2875ea683f8 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
@@ -108,6 +108,14 @@
services.powerdns-admin.
+
+
+ InvoicePlane,
+ web application for managing and creating invoices. Available
+ at
+ services.invoiceplane.
+
+
maddy, a
diff --git a/nixos/doc/manual/release-notes/rl-2205.section.md b/nixos/doc/manual/release-notes/rl-2205.section.md
index 25b3ada2c56..a59513adfc9 100644
--- a/nixos/doc/manual/release-notes/rl-2205.section.md
+++ b/nixos/doc/manual/release-notes/rl-2205.section.md
@@ -35,6 +35,8 @@ In addition to numerous new and upgraded packages, this release has the followin
- [PowerDNS-Admin](https://github.com/ngoduykhanh/PowerDNS-Admin), a web interface for the PowerDNS server. Available at [services.powerdns-admin](options.html#opt-services.powerdns-admin.enable).
+- [InvoicePlane](https://invoiceplane.com), web application for managing and creating invoices. Available at [services.invoiceplane](options.html#opt-services.invoiceplane.enable).
+
- [maddy](https://maddy.email), a composable all-in-one mail server. Available as [services.maddy](options.html#opt-services.maddy.enable).
- [mtr-exporter](https://github.com/mgumz/mtr-exporter), a Prometheus exporter for mtr metrics. Available as [services.mtr-exporter](options.html#opt-services.mtr-exporter.enable).
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index fdf93f2e17c..4b2cb803e20 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -1022,6 +1022,7 @@
./services/web-apps/keycloak.nix
./services/web-apps/lemmy.nix
./services/web-apps/invidious.nix
+ ./services/web-apps/invoiceplane.nix
./services/web-apps/limesurvey.nix
./services/web-apps/mastodon.nix
./services/web-apps/mattermost.nix
diff --git a/nixos/modules/services/web-apps/invoiceplane.nix b/nixos/modules/services/web-apps/invoiceplane.nix
new file mode 100644
index 00000000000..095eec36dec
--- /dev/null
+++ b/nixos/modules/services/web-apps/invoiceplane.nix
@@ -0,0 +1,305 @@
+{ 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=${if cfg.database.passwordFile == null then "" else "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
+ '';
+
+ extraConfig = hostName: cfg: pkgs.writeText "extraConfig.php" ''
+ ${toString cfg.extraConfig}
+ '';
+
+ pkg = hostName: cfg: pkgs.stdenv.mkDerivation rec {
+ pname = "invoiceplane-${hostName}";
+ version = src.version;
+ src = pkgs.invoiceplane;
+
+ patchPhase = ''
+ # Patch index.php file to load additional config file
+ substituteInPlace index.php \
+ --replace "require('vendor/autoload.php');" "require('vendor/autoload.php'); \$dotenv = new \Dotenv\Dotenv(__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}
+ '';
+ };
+
+ 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 attachements 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
+ .
+ '';
+ };
+
+ 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.
+ 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.
+ '';
+ };
+
+ extraConfig = mkOption {
+ type = types.nullOr types.lines;
+ default = null;
+ example = ''
+ SETUP_COMPLETED=true
+ DISABLE_SETUP=true
+ IP_URL=https://invoice.example.com
+ '';
+ description = ''
+ InvoicePlane configuration. Refer to
+
+ for details on supported values.
+ '';
+ };
+
+ };
+
+ };
+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 WordPress sites to serve";
+ };
+
+ options.webserver = mkOption {
+ type = types.enum [ "caddy" ];
+ default = "caddy";
+ description = ''
+ Which webserver to use for virtual host management. Currently only
+ caddy is supported.
+ '';
+ };
+ };
+ 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.'';
+ }]
+ ) 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 = {
+ phpPackage = pkgs.php74;
+ 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
+ '') eachSite);
+ wantedBy = [ "multi-user.target" ];
+ };
+
+ users.users.${user} = {
+ group = webserver.group;
+ isSystemUser = true;
+ };
+ }
+
+ (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;
+ };
+ })
+
+
+ ]);
+}
+
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 6e66e9cbe96..940ae11ddd1 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -215,6 +215,7 @@ in
initrd-secrets = handleTest ./initrd-secrets.nix {};
inspircd = handleTest ./inspircd.nix {};
installer = handleTest ./installer.nix {};
+ invoiceplane = handleTest ./invoiceplane.nix {};
iodine = handleTest ./iodine.nix {};
ipfs = handleTest ./ipfs.nix {};
ipv6 = handleTest ./ipv6.nix {};
diff --git a/nixos/tests/invoiceplane.nix b/nixos/tests/invoiceplane.nix
new file mode 100644
index 00000000000..4e63f8ac21c
--- /dev/null
+++ b/nixos/tests/invoiceplane.nix
@@ -0,0 +1,82 @@
+import ./make-test-python.nix ({ 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" ];
+ };
+ };
+
+ testScript = ''
+ start_all()
+
+ invoiceplane_caddy.wait_for_unit("caddy")
+ invoiceplane_caddy.wait_for_open_port(80)
+ invoiceplane_caddy.wait_for_open_port(3306)
+
+ site_names = ["site1.local", "site2.local"]
+
+ 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}/index.php/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}/index.php/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}/index.php/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}/index.php/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}/index.php/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}/index.php/setup/upgrade_tables"
+ )
+ '';
+})
diff --git a/pkgs/servers/web-apps/invoiceplane/default.nix b/pkgs/servers/web-apps/invoiceplane/default.nix
new file mode 100644
index 00000000000..6c9ffd44b9d
--- /dev/null
+++ b/pkgs/servers/web-apps/invoiceplane/default.nix
@@ -0,0 +1,32 @@
+{ lib, stdenv, fetchurl, writeText, unzip, nixosTests }:
+
+stdenv.mkDerivation rec {
+ pname = "invoiceplane";
+ version = "1.5.11";
+
+ src = fetchurl {
+ url = "https://github.com/InvoicePlane/InvoicePlane/releases/download/v${version}/v${version}.zip";
+ sha256 = "137g0xps4kb3j7f5gz84ql18iggbya6d9dnrfp05g2qcbbp8kqad";
+ };
+
+ nativeBuildInputs = [ unzip ];
+
+ sourceRoot = ".";
+
+ installPhase = ''
+ mkdir -p $out/
+ cp -r . $out/
+ '';
+
+ passthru.tests = {
+ inherit (nixosTests) invoiceplane;
+ };
+
+ meta = with lib; {
+ description = "Self-hosted open source application for managing your invoices, clients and payments";
+ license = licenses.mit;
+ homepage = "https://www.invoiceplane.com";
+ platforms = platforms.all;
+ maintainers = with maintainers; [ onny ];
+ };
+}
diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix
index de442328506..0244986ffb5 100644
--- a/pkgs/top-level/all-packages.nix
+++ b/pkgs/top-level/all-packages.nix
@@ -3240,6 +3240,8 @@ with pkgs;
interlock = callPackage ../servers/interlock {};
+ invoiceplane = callPackage ../servers/web-apps/invoiceplane { };
+
iotools = callPackage ../tools/misc/iotools { };
jellyfin = callPackage ../servers/jellyfin { };