diff --git a/apps/eu_einvoice.nix b/apps/eu_einvoice.nix
new file mode 100644
index 0000000..202aae6
--- /dev/null
+++ b/apps/eu_einvoice.nix
@@ -0,0 +1,27 @@
+{ stdenv
+, lib
+, fetchFromGitHub
+}:
+
+stdenv.mkDerivation rec {
+  pname = "eu_einvoice";
+  version = "15.1.1";
+
+  src = fetchFromGitHub {
+    owner = "alyf-de";
+    repo = "eu_einvoice";
+    rev = "v${version}";
+    sha256 = "sha256-71XuVvmXr6T/OhAtSpV6YObx3PWW8rl4ZKQmujnlJXI=";
+  };
+
+  installPhase = ''
+    cp -r $src $out
+  '';
+
+  meta = with lib; {
+    homepage = "https://github.com/alyf-de/eu_einvoice";
+    description = "Create and import e-invoices with ERPNext. ";
+    license = licenses.gpl3;
+    maintainers = [ ];
+  };
+}
diff --git a/apps/overlay.nix b/apps/overlay.nix
new file mode 100644
index 0000000..70499ed
--- /dev/null
+++ b/apps/overlay.nix
@@ -0,0 +1,3 @@
+final: prev: {
+  eu_einvoice = final.callPackage ./eu_einvoice.nix {};
+}
diff --git a/flake.lock b/flake.lock
index 8a15ee8..8b6bf22 100644
--- a/flake.lock
+++ b/flake.lock
@@ -91,16 +91,16 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1704295289,
-        "narHash": "sha256-9WZDRfpMqCYL6g/HNWVvXF0hxdaAgwgIGeLYiOhmes8=",
+        "lastModified": 1736061677,
+        "narHash": "sha256-DjkQPnkAfd7eB522PwnkGhOMuT9QVCZspDpJJYyOj60=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "b0b2c5445c64191fd8d0b31f2b1a34e45a64547d",
+        "rev": "cbd8ec4de4469333c82ff40d057350c30e9f7d36",
         "type": "github"
       },
       "original": {
         "owner": "NixOS",
-        "ref": "nixos-23.11",
+        "ref": "nixos-24.11",
         "repo": "nixpkgs",
         "type": "github"
       }
diff --git a/flake.nix b/flake.nix
index aca39f7..072d31c 100644
--- a/flake.nix
+++ b/flake.nix
@@ -2,7 +2,7 @@
   description = "Dev Setup";
 
   inputs = {
-    nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
+    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
 
     systems.url = "github:nix-systems/default";
 
@@ -24,6 +24,7 @@
           overlays = [
             self.overlays.default
             self.overlays.pythonOverlay
+            self.overlays.apps
             agenix.overlays.default
             devshell.overlays.default
             # https://github.com/NixOS/nixpkgs/issues/265675#issuecomment-1846591842
@@ -38,6 +39,7 @@
       overlays = {
         default = (import ./overlay.nix);
         pythonOverlay = (import ./python-overlay.nix);
+        apps = (import ./apps/overlay.nix);
       };
       devShells = eachSystem (system:
       let
diff --git a/modules/erpnext.nix b/modules/erpnext.nix
index 1a347b1..4fee5f9 100644
--- a/modules/erpnext.nix
+++ b/modules/erpnext.nix
@@ -9,9 +9,9 @@ in
   # interface
   options.services.erpnext = {
     enable = mkOption {
-      type = lib.types.bool;
+      type = types.bool;
       default = false;
-      description = lib.mdDoc ''
+      description = mdDoc ''
         Enable ERPNext.
 
         When started, the ERPNext database is automatically created if it doesn't
@@ -22,7 +22,7 @@ in
     domain = mkOption {
       type = types.str;
       default = "localhost";
-      description = lib.mdDoc ''
+      description = mdDoc ''
         Domain name of your server.
       '';
     };
@@ -30,50 +30,57 @@ in
     workDir = mkOption {
       type = types.str;
       default = "/var/lib/erpnext";
-      description = lib.mdDoc "Working directory of ERPNext.";
+      description = mdDoc "Working directory of ERPNext.";
     };
 
     benchDir = mkOption {
       type = types.str;
       default = "${cfg.workDir}/bench";
-      description = lib.mdDoc "Bench directory for ERPNext.";
+      description = mdDoc "Bench directory for ERPNext.";
     };
 
     adminPasswordFile = mkOption {
       type = types.nullOr types.path;
       default = null;
       example = "/run/secrets/erpnext-admin-password";
-      description = lib.mdDoc ''
+      description = mdDoc ''
         A file containing the Administrator user password.
       '';
     };
 
+    apps = mkOption {
+      description = ''
+        Apps to install into ERPNext.
+      '';
+      type = types.listOf types.package;
+    };
+
     database = {
       host = mkOption {
         type = types.str;
         default = "localhost";
-        description = lib.mdDoc "Database host address.";
+        description = mdDoc "Database host address.";
       };
       port = mkOption {
         type = types.port;
         default = 3306;
-        description = lib.mdDoc "Database host port.";
+        description = mdDoc "Database host port.";
       };
       name = mkOption {
         type = types.str;
         default = "erpnext";
-        description = lib.mdDoc "Database name.";
+        description = mdDoc "Database name.";
       };
       user = mkOption {
         type = types.str;
         default = "erpnext";
-        description = lib.mdDoc "Database username.";
+        description = mdDoc "Database username.";
       };
       userPasswordFile = mkOption {
         type = types.nullOr types.path;
         default = null;
         example = "/run/secrets/erpnext-db-user-password";
-        description = lib.mdDoc ''
+        description = mdDoc ''
           A file containing the MariaDB erpnext user password.
         '';
       };
@@ -81,17 +88,17 @@ in
         type = types.nullOr types.path;
         default = null;
         example = "/run/secrets/erpnext-db-root-password";
-        description = lib.mdDoc ''
+        description = mdDoc ''
           A file containing the MariaDB root user password.
         '';
       };
       createLocally = mkOption {
         type = types.bool;
         default = true;
-        description = lib.mdDoc "Create the database and database user locally.";
+        description = mdDoc "Create the database and database user locally.";
       };
       automaticMigrations = mkEnableOption
-        (lib.mdDoc "automatic migrations for database schema and data") // {
+        (mdDoc "automatic migrations for database schema and data") // {
           default = true;
         };
     };
@@ -100,17 +107,17 @@ in
       host = mkOption {
         type = types.str;
         default = "localhost";
-        description = lib.mdDoc "Redis host address.";
+        description = mdDoc "Redis host address.";
       };
       port = mkOption {
         type = types.port;
         default = 6379;
-        description = lib.mdDoc "Redis host port.";
+        description = mdDoc "Redis host port.";
       };
       createLocally = mkOption {
         type = types.bool;
         default = true;
-        description = lib.mdDoc "Create the redis server locally.";
+        description = mdDoc "Create the redis server locally.";
       };
     };
 
@@ -118,19 +125,19 @@ in
       bindAddress = mkOption {
         type = types.str;
         default = "localhost";
-        description = lib.mdDoc "Web interface address.";
+        description = mdDoc "Web interface address.";
       };
       bindPort = mkOption {
         type = types.port;
         default = 9090;
-        description = lib.mdDoc "Web interface port.";
+        description = mdDoc "Web interface port.";
       };
     };
 
     caddy = mkOption {
       type = types.nullOr types.attrs;
       default = null;
-      example = lib.literalExpression ''
+      example = literalExpression ''
         {
           serverAliases = [
             "erpnext.your.domain"
@@ -142,7 +149,7 @@ in
           '';
         }
       '';
-      description = lib.mdDoc ''
+      description = mdDoc ''
           With this option, you can customize a caddy virtual host.
           Set to {} if you do not need any customization to the virtual host.
           If enabled, then by default, the {option}`hostName` is
@@ -160,14 +167,14 @@ in
     user = mkOption {
       type = types.str;
       default = defaultUser;
-      description = lib.mdDoc "User under which ERPNext runs.";
+      description = mdDoc "User under which ERPNext runs.";
     };
 
     package = mkOption {
       type = types.package;
       default = pkgs.python3.pkgs.erpnext;
       defaultText = literalExpression "pkgs.python3.pkgs.erpnext";
-      description = lib.mdDoc "The ERPNext package to use.";
+      description = mdDoc "The ERPNext package to use.";
     };
   };
 
@@ -184,7 +191,7 @@ in
     appsFile = pkgs.writeText "erpnext-apps.txt" ''
       frappe
       erpnext
-    '';
+    '' + lib.lists.fold (app: string: string + "\n${app.name}") "" cfg.apps;
     # In a module, this could be provided by a use as a file as it could
     # contain secrets and we don't want this in the nix-store. But here it
     # is OK.
@@ -215,7 +222,7 @@ in
         "${pkgs.frappe-erpnext-assets}/share/sites/assets:${cfg.benchDir}/sites/assets"
         "${appsFile}:${cfg.benchDir}/sites/apps.txt"
         "${penv}:${cfg.benchDir}/env"
-      ];
+      ] ++ lib.lists.map (app: "${app}:${cfg.benchDir}/apps/${app.name}") cfg.apps;
       WorkingDirectory = "${cfg.benchDir}";
       # Expands to /var/lib/erpnext, see: 'man 5 systemd.exec'
       StateDirectory = "erpnext";
@@ -324,7 +331,7 @@ in
       serviceConfig = defaultServiceConfig // {
         TimeoutStartSec = "300s";
         Restart = "on-failure";
-        ExecStartPre = assert cfg.adminPasswordFile != null && cfg.database.rootPasswordFile != null; pkgs.writeScript "erpnext-web-init" ''
+        ExecStartPre = assert cfg.adminPasswordFile != null && cfg.database.rootPasswordFile != null; pkgs.writeScript "erpnext-web-init" (''
           #!/bin/sh
           if ! test -e ${escapeShellArg "${cfg.workDir}/.db-created"}; then
             # Fail on error
@@ -352,7 +359,10 @@ in
             # Migrate the database
             ${penv}/bin/bench --site ${cfg.domain} migrate
         ''}
-        '';
+
+        '' + lib.lists.fold (app: str: str + ''
+            ${penv}/bin/bench --install-app ${app.name}
+        '') "" cfg.apps);
         ExecStart = ''
           ${penv}/bin/gunicorn \
             --chdir="${cfg.benchDir}/sites" \
diff --git a/node/mk-app.nix b/node/mk-app.nix
index 910ec95..d8d26d3 100644
--- a/node/mk-app.nix
+++ b/node/mk-app.nix
@@ -10,8 +10,8 @@ let
     mkdir -p $out/lib
     mkdir -p $out/bin
 
+    ls -la ${path}/pkgs/development/tools/yarn2nix-moretea/**/*
     cp ${path}/pkgs/development/tools/yarn2nix-moretea/yarn2nix/lib/urlToName.js $out/lib/urlToName.js
-    cp ${path}/pkgs/development/tools/yarn2nix-moretea/yarn2nix/internal/fixup_yarn_lock.js $out/bin/fixup_yarn_lock
 
     patchShebangs $out
   '';
diff --git a/python-overlay.nix b/python-overlay.nix
index f521d65..e6f3a8f 100644
--- a/python-overlay.nix
+++ b/python-overlay.nix
@@ -18,8 +18,12 @@ final: prev: {
       email-reply-parser = pyFinal.callPackage ./python/email-reply-parser.nix {};
       maxminddb-geolite2 = pyFinal.callPackage ./python/maxminddb-geolite2.nix {};
       psycopg2-binary = pyFinal.callPackage ./python/psycopg2-binary.nix {};
+      premailer = pyFinal.callPackage ./python/premailer.nix {};
+      rauth = pyFinal.callPackage ./python/rauth.nix {};
       traceback-with-variables = pyFinal.callPackage ./python/traceback-with-variables.nix {};
-      pydantic = pyFinal.callPackage ./python/pydantic.nix {};
+      # pydantic = pyFinal.callPackage ./python/pydantic.nix {};
+      hatchling = pyFinal.callPackage ./python/hatchling.nix {};
+      nose = pyFinal.callPackage ./python/nose.nix {};
 
       versioningit = pyPrev.versioningit.overridePythonAttrs (oldAttrs: (rec {
         version = "2.2.1";
@@ -31,7 +35,7 @@ final: prev: {
       }));
 
       fastapi = pyPrev.fastapi.overridePythonAttrs (oldAttrs: (rec {
-        propagatedBuildInputs = oldAttrs.propagatedBuildInputs ++ [
+        propagatedBuildInputs = [
           pyPrev.pydantic-settings
           pyPrev.pydantic-extra-types
         ];
diff --git a/python/0001-nose-python-3.12-fixes.patch b/python/0001-nose-python-3.12-fixes.patch
new file mode 100644
index 0000000..67a671a
--- /dev/null
+++ b/python/0001-nose-python-3.12-fixes.patch
@@ -0,0 +1,576 @@
+diff --git a/LICENSE.cpython b/LICENSE.cpython
+new file mode 100644
+index 0000000..14603b9
+--- /dev/null
++++ b/LICENSE.cpython
+@@ -0,0 +1,277 @@
++A. HISTORY OF THE SOFTWARE
++==========================
++
++Python was created in the early 1990s by Guido van Rossum at Stichting
++Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands
++as a successor of a language called ABC.  Guido remains Python's
++principal author, although it includes many contributions from others.
++
++In 1995, Guido continued his work on Python at the Corporation for
++National Research Initiatives (CNRI, see https://www.cnri.reston.va.us)
++in Reston, Virginia where he released several versions of the
++software.
++
++In May 2000, Guido and the Python core development team moved to
++BeOpen.com to form the BeOpen PythonLabs team.  In October of the same
++year, the PythonLabs team moved to Digital Creations, which became
++Zope Corporation.  In 2001, the Python Software Foundation (PSF, see
++https://www.python.org/psf/) was formed, a non-profit organization
++created specifically to own Python-related Intellectual Property.
++Zope Corporation was a sponsoring member of the PSF.
++
++All Python releases are Open Source (see https://opensource.org for
++the Open Source Definition).  Historically, most, but not all, Python
++releases have also been GPL-compatible; the table below summarizes
++the various releases.
++
++    Release         Derived     Year        Owner       GPL-
++                    from                                compatible? (1)
++
++    0.9.0 thru 1.2              1991-1995   CWI         yes
++    1.3 thru 1.5.2  1.2         1995-1999   CNRI        yes
++    1.6             1.5.2       2000        CNRI        no
++    2.0             1.6         2000        BeOpen.com  no
++    1.6.1           1.6         2001        CNRI        yes (2)
++    2.1             2.0+1.6.1   2001        PSF         no
++    2.0.1           2.0+1.6.1   2001        PSF         yes
++    2.1.1           2.1+2.0.1   2001        PSF         yes
++    2.1.2           2.1.1       2002        PSF         yes
++    2.1.3           2.1.2       2002        PSF         yes
++    2.2 and above   2.1.1       2001-now    PSF         yes
++
++Footnotes:
++
++(1) GPL-compatible doesn't mean that we're distributing Python under
++    the GPL.  All Python licenses, unlike the GPL, let you distribute
++    a modified version without making your changes open source.  The
++    GPL-compatible licenses make it possible to combine Python with
++    other software that is released under the GPL; the others don't.
++
++(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
++    because its license has a choice of law clause.  According to
++    CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
++    is "not incompatible" with the GPL.
++
++Thanks to the many outside volunteers who have worked under Guido's
++direction to make these releases possible.
++
++
++B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
++===============================================================
++
++Python software and documentation are licensed under the
++Python Software Foundation License Version 2.
++
++Starting with Python 3.8.6, examples, recipes, and other code in
++the documentation are dual licensed under the PSF License Version 2
++and the Zero-Clause BSD license.
++
++Some software incorporated into Python is under different licenses.
++The licenses are listed with code falling under that license.
++
++
++PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
++--------------------------------------------
++
++1. This LICENSE AGREEMENT is between the Python Software Foundation
++("PSF"), and the Individual or Organization ("Licensee") accessing and
++otherwise using this software ("Python") in source or binary form and
++its associated documentation.
++
++2. Subject to the terms and conditions of this License Agreement, PSF hereby
++grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
++analyze, test, perform and/or display publicly, prepare derivative works,
++distribute, and otherwise use Python alone or in any derivative version,
++provided, however, that PSF's License Agreement and PSF's notice of copyright,
++i.e., "Copyright (c) 2001-2024 Python Software Foundation; All Rights Reserved"
++are retained in Python alone or in any derivative version prepared by Licensee.
++
++3. In the event Licensee prepares a derivative work that is based on
++or incorporates Python or any part thereof, and wants to make
++the derivative work available to others as provided herein, then
++Licensee hereby agrees to include in any such work a brief summary of
++the changes made to Python.
++
++4. PSF is making Python available to Licensee on an "AS IS"
++basis.  PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
++IMPLIED.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
++DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
++FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
++INFRINGE ANY THIRD PARTY RIGHTS.
++
++5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
++FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
++A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
++OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
++
++6. This License Agreement will automatically terminate upon a material
++breach of its terms and conditions.
++
++7. Nothing in this License Agreement shall be deemed to create any
++relationship of agency, partnership, or joint venture between PSF and
++Licensee.  This License Agreement does not grant permission to use PSF
++trademarks or trade name in a trademark sense to endorse or promote
++products or services of Licensee, or any third party.
++
++8. By copying, installing or otherwise using Python, Licensee
++agrees to be bound by the terms and conditions of this License
++Agreement.
++
++
++BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
++-------------------------------------------
++
++BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
++
++1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
++office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
++Individual or Organization ("Licensee") accessing and otherwise using
++this software in source or binary form and its associated
++documentation ("the Software").
++
++2. Subject to the terms and conditions of this BeOpen Python License
++Agreement, BeOpen hereby grants Licensee a non-exclusive,
++royalty-free, world-wide license to reproduce, analyze, test, perform
++and/or display publicly, prepare derivative works, distribute, and
++otherwise use the Software alone or in any derivative version,
++provided, however, that the BeOpen Python License is retained in the
++Software, alone or in any derivative version prepared by Licensee.
++
++3. BeOpen is making the Software available to Licensee on an "AS IS"
++basis.  BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
++IMPLIED.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
++DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
++FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
++INFRINGE ANY THIRD PARTY RIGHTS.
++
++4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
++SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
++AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
++DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
++
++5. This License Agreement will automatically terminate upon a material
++breach of its terms and conditions.
++
++6. This License Agreement shall be governed by and interpreted in all
++respects by the law of the State of California, excluding conflict of
++law provisions.  Nothing in this License Agreement shall be deemed to
++create any relationship of agency, partnership, or joint venture
++between BeOpen and Licensee.  This License Agreement does not grant
++permission to use BeOpen trademarks or trade names in a trademark
++sense to endorse or promote products or services of Licensee, or any
++third party.  As an exception, the "BeOpen Python" logos available at
++http://www.pythonlabs.com/logos.html may be used according to the
++permissions granted on that web page.
++
++7. By copying, installing or otherwise using the software, Licensee
++agrees to be bound by the terms and conditions of this License
++Agreement.
++
++
++CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
++---------------------------------------
++
++1. This LICENSE AGREEMENT is between the Corporation for National
++Research Initiatives, having an office at 1895 Preston White Drive,
++Reston, VA 20191 ("CNRI"), and the Individual or Organization
++("Licensee") accessing and otherwise using Python 1.6.1 software in
++source or binary form and its associated documentation.
++
++2. Subject to the terms and conditions of this License Agreement, CNRI
++hereby grants Licensee a nonexclusive, royalty-free, world-wide
++license to reproduce, analyze, test, perform and/or display publicly,
++prepare derivative works, distribute, and otherwise use Python 1.6.1
++alone or in any derivative version, provided, however, that CNRI's
++License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
++1995-2001 Corporation for National Research Initiatives; All Rights
++Reserved" are retained in Python 1.6.1 alone or in any derivative
++version prepared by Licensee.  Alternately, in lieu of CNRI's License
++Agreement, Licensee may substitute the following text (omitting the
++quotes): "Python 1.6.1 is made available subject to the terms and
++conditions in CNRI's License Agreement.  This Agreement together with
++Python 1.6.1 may be located on the internet using the following
++unique, persistent identifier (known as a handle): 1895.22/1013.  This
++Agreement may also be obtained from a proxy server on the internet
++using the following URL: http://hdl.handle.net/1895.22/1013".
++
++3. In the event Licensee prepares a derivative work that is based on
++or incorporates Python 1.6.1 or any part thereof, and wants to make
++the derivative work available to others as provided herein, then
++Licensee hereby agrees to include in any such work a brief summary of
++the changes made to Python 1.6.1.
++
++4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
++basis.  CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
++IMPLIED.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
++DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
++FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
++INFRINGE ANY THIRD PARTY RIGHTS.
++
++5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
++1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
++A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
++OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
++
++6. This License Agreement will automatically terminate upon a material
++breach of its terms and conditions.
++
++7. This License Agreement shall be governed by the federal
++intellectual property law of the United States, including without
++limitation the federal copyright law, and, to the extent such
++U.S. federal law does not apply, by the law of the Commonwealth of
++Virginia, excluding Virginia's conflict of law provisions.
++Notwithstanding the foregoing, with regard to derivative works based
++on Python 1.6.1 that incorporate non-separable material that was
++previously distributed under the GNU General Public License (GPL), the
++law of the Commonwealth of Virginia shall govern this License
++Agreement only as to issues arising under or with respect to
++Paragraphs 4, 5, and 7 of this License Agreement.  Nothing in this
++License Agreement shall be deemed to create any relationship of
++agency, partnership, or joint venture between CNRI and Licensee.  This
++License Agreement does not grant permission to use CNRI trademarks or
++trade name in a trademark sense to endorse or promote products or
++services of Licensee, or any third party.
++
++8. By clicking on the "ACCEPT" button where indicated, or by copying,
++installing or otherwise using Python 1.6.1, Licensee agrees to be
++bound by the terms and conditions of this License Agreement.
++
++        ACCEPT
++
++
++CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
++--------------------------------------------------
++
++Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
++The Netherlands.  All rights reserved.
++
++Permission to use, copy, modify, and distribute this software and its
++documentation for any purpose and without fee is hereby granted,
++provided that the above copyright notice appear in all copies and that
++both that copyright notice and this permission notice appear in
++supporting documentation, and that the name of Stichting Mathematisch
++Centrum or CWI not be used in advertising or publicity pertaining to
++distribution of the software without specific, written prior
++permission.
++
++STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
++THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
++FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
++FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
++WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
++ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
++OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
++
++ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION
++----------------------------------------------------------------------
++
++Permission to use, copy, modify, and/or distribute this software for any
++purpose with or without fee is hereby granted.
++
++THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
++REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
++AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
++INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
++LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
++OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
++PERFORMANCE OF THIS SOFTWARE.
+diff --git a/nose/importer.py b/nose/importer.py
+index e677658..77099eb 100644
+--- a/nose/importer.py
++++ b/nose/importer.py
+@@ -7,9 +7,10 @@ the builtin importer.
+ import logging
+ import os
+ import sys
++import tokenize
+ from nose.config import Config
+-
+-from imp import find_module, load_module, acquire_lock, release_lock
++from importlib import _imp
++from importlib import machinery
+ 
+ log = logging.getLogger(__name__)
+ 
+@@ -20,6 +21,244 @@ except AttributeError:
+         return (os.path.normcase(os.path.realpath(src)) ==
+                 os.path.normcase(os.path.realpath(dst)))
+ 
++################################################################################
++# BEGIN IMPORTLIB SHIMS
++################################################################################
++
++# Adapted from the CPython 3.11 imp.py code.
++# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation; All Rights Reserved
++# Originally licensed under the PSLv2 (see LICENSE.cpython) and incorporated under the LGPL 2.1 (see lgpl.txt).
++
++try:
++    from _imp import create_dynamic
++except ImportError:
++    # Platform doesn't support dynamic loading.
++    create_dynamic = None
++
++from importlib._bootstrap import _ERR_MSG, _exec, _load, _builtin_from_name
++from importlib._bootstrap_external import SourcelessFileLoader
++
++from importlib import machinery
++from importlib import util
++import importlib
++import os
++import sys
++import tokenize
++import types
++
++
++SEARCH_ERROR = 0
++PY_SOURCE = 1
++PY_COMPILED = 2
++C_EXTENSION = 3
++PY_RESOURCE = 4
++PKG_DIRECTORY = 5
++C_BUILTIN = 6
++PY_FROZEN = 7
++PY_CODERESOURCE = 8
++IMP_HOOK = 9
++
++
++def get_suffixes():
++    extensions = [(s, 'rb', C_EXTENSION) for s in machinery.EXTENSION_SUFFIXES]
++    source = [(s, 'r', PY_SOURCE) for s in machinery.SOURCE_SUFFIXES]
++    bytecode = [(s, 'rb', PY_COMPILED) for s in machinery.BYTECODE_SUFFIXES]
++
++    return extensions + source + bytecode
++
++
++class _HackedGetData:
++
++    """Compatibility support for 'file' arguments of various load_*()
++    functions."""
++
++    def __init__(self, fullname, path, file=None):
++        super().__init__(fullname, path)
++        self.file = file
++
++    def get_data(self, path):
++        """Gross hack to contort loader to deal w/ load_*()'s bad API."""
++        if self.file and path == self.path:
++            # The contract of get_data() requires us to return bytes. Reopen the
++            # file in binary mode if needed.
++            if not self.file.closed:
++                file = self.file
++                if 'b' not in file.mode:
++                    file.close()
++            if self.file.closed:
++                self.file = file = open(self.path, 'rb')
++
++            with file:
++                return file.read()
++        else:
++            return super().get_data(path)
++
++
++class _LoadSourceCompatibility(_HackedGetData, machinery.SourceFileLoader):
++
++    """Compatibility support for implementing load_source()."""
++
++
++def load_source(name, pathname, file=None):
++    loader = _LoadSourceCompatibility(name, pathname, file)
++    spec = util.spec_from_file_location(name, pathname, loader=loader)
++    if name in sys.modules:
++        module = _exec(spec, sys.modules[name])
++    else:
++        module = _load(spec)
++    # To allow reloading to potentially work, use a non-hacked loader which
++    # won't rely on a now-closed file object.
++    module.__loader__ = machinery.SourceFileLoader(name, pathname)
++    module.__spec__.loader = module.__loader__
++    return module
++
++
++class _LoadCompiledCompatibility(_HackedGetData, SourcelessFileLoader):
++
++    """Compatibility support for implementing load_compiled()."""
++
++
++def load_compiled(name, pathname, file=None):
++    loader = _LoadCompiledCompatibility(name, pathname, file)
++    spec = util.spec_from_file_location(name, pathname, loader=loader)
++    if name in sys.modules:
++        module = _exec(spec, sys.modules[name])
++    else:
++        module = _load(spec)
++    # To allow reloading to potentially work, use a non-hacked loader which
++    # won't rely on a now-closed file object.
++    module.__loader__ = SourcelessFileLoader(name, pathname)
++    module.__spec__.loader = module.__loader__
++    return module
++
++
++def load_package(name, path):
++    if os.path.isdir(path):
++        extensions = (machinery.SOURCE_SUFFIXES[:] +
++                      machinery.BYTECODE_SUFFIXES[:])
++        for extension in extensions:
++            init_path = os.path.join(path, '__init__' + extension)
++            if os.path.exists(init_path):
++                path = init_path
++                break
++        else:
++            raise ValueError('{!r} is not a package'.format(path))
++    spec = util.spec_from_file_location(name, path,
++                                        submodule_search_locations=[])
++    if name in sys.modules:
++        return _exec(spec, sys.modules[name])
++    else:
++        return _load(spec)
++
++
++def load_module(name, file, filename, details):
++    """
++
++    Load a module, given information returned by find_module().
++
++    The module name must include the full package name, if any.
++
++    """
++    suffix, mode, type_ = details
++    if mode and (not mode.startswith('r') or '+' in mode):
++        raise ValueError('invalid file open mode {!r}'.format(mode))
++    elif file is None and type_ in {PY_SOURCE, PY_COMPILED}:
++        msg = 'file object required for import (type code {})'.format(type_)
++        raise ValueError(msg)
++    elif type_ == PY_SOURCE:
++        return load_source(name, filename, file)
++    elif type_ == PY_COMPILED:
++        return load_compiled(name, filename, file)
++    elif type_ == PKG_DIRECTORY:
++        return load_package(name, filename)
++    elif type_ == C_BUILTIN:
++        return init_builtin(name)
++    elif type_ == PY_FROZEN:
++        return _imp.init_frozen(name)
++    else:
++        msg =  "Don't know how to import {} (type code {})".format(name, type_)
++        raise ImportError(msg, name=name)
++
++
++def find_module(name, path=None):
++    """
++
++    Search for a module.
++
++    If path is omitted or None, search for a built-in, frozen or special
++    module and continue search in sys.path. The module name cannot
++    contain '.'; to search for a submodule of a package, pass the
++    submodule name and the package's __path__.
++
++    """
++    if not isinstance(name, str):
++        raise TypeError("'name' must be a str, not {}".format(type(name)))
++    elif not isinstance(path, (type(None), list)):
++        # Backwards-compatibility
++        raise RuntimeError("'path' must be None or a list, "
++                           "not {}".format(type(path)))
++
++    if path is None:
++        if _imp.is_builtin(name):
++            return None, None, ('', '', C_BUILTIN)
++        elif _imp.is_frozen(name):
++            return None, None, ('', '', PY_FROZEN)
++        else:
++            path = sys.path
++
++    for entry in path:
++        package_directory = os.path.join(entry, name)
++        for suffix in ['.py', machinery.BYTECODE_SUFFIXES[0]]:
++            package_file_name = '__init__' + suffix
++            file_path = os.path.join(package_directory, package_file_name)
++            if os.path.isfile(file_path):
++                return None, package_directory, ('', '', PKG_DIRECTORY)
++        for suffix, mode, type_ in get_suffixes():
++            file_name = name + suffix
++            file_path = os.path.join(entry, file_name)
++            if os.path.isfile(file_path):
++                break
++        else:
++            continue
++        break  # Break out of outer loop when breaking out of inner loop.
++    else:
++        raise ImportError(_ERR_MSG.format(name), name=name)
++
++    encoding = None
++    if 'b' not in mode:
++        with open(file_path, 'rb') as file:
++            encoding = tokenize.detect_encoding(file.readline)[0]
++    file = open(file_path, mode, encoding=encoding)
++    return file, file_path, (suffix, mode, type_)
++
++
++def reload(module):
++    """
++
++    Reload the module and return it.
++
++    The module must have been successfully imported before.
++
++    """
++    return importlib.reload(module)
++
++
++def init_builtin(name):
++    """
++
++    Load and return a built-in module by name, or None is such module doesn't
++    exist
++    """
++    try:
++        return _builtin_from_name(name)
++    except ImportError:
++        return None
++
++
++################################################################################
++# END IMPORTLIB SHIMS
++################################################################################
++
+ 
+ class Importer(object):
+     """An importer class that does only path-specific imports. That
+@@ -73,7 +312,7 @@ class Importer(object):
+             else:
+                 part_fqname = "%s.%s" % (part_fqname, part)
+             try:
+-                acquire_lock()
++                _imp.acquire_lock()
+                 log.debug("find module part %s (%s) in %s",
+                           part, part_fqname, path)
+                 fh, filename, desc = find_module(part, path)
+@@ -95,7 +334,7 @@ class Importer(object):
+             finally:
+                 if fh:
+                     fh.close()
+-                release_lock()
++                _imp.release_lock()
+             if parent:
+                 setattr(parent, part, mod)
+             if hasattr(mod, '__path__'):
+diff --git a/nose/result.py b/nose/result.py
+index f974a14..228a42c 100644
+--- a/nose/result.py
++++ b/nose/result.py
+@@ -13,7 +13,7 @@ try:
+     # 2.7+
+     from unittest.runner import _TextTestResult
+ except ImportError:
+-    from unittest import _TextTestResult
++    from unittest import TextTestResult as _TextTestResult
+ from nose.config import Config
+ from nose.util import isclass, ln as _ln # backwards compat
+ 
diff --git a/python/frappe.nix b/python/frappe.nix
index a36e516..7479ec3 100644
--- a/python/frappe.nix
+++ b/python/frappe.nix
@@ -44,7 +44,6 @@
 , passlib
 , pdfkit
 , phonenumbers
-, premailer
 , psutil
 , psycopg2-binary
 , pydantic
@@ -137,7 +136,6 @@ buildPythonPackage rec {
     passlib
     pdfkit
     phonenumbers
-    premailer
     psutil
     psycopg2-binary
     pydantic
diff --git a/python/hatchling.nix b/python/hatchling.nix
new file mode 100644
index 0000000..221c266
--- /dev/null
+++ b/python/hatchling.nix
@@ -0,0 +1,75 @@
+{ lib
+, buildPythonPackage
+, fetchPypi
+, pythonOlder
+
+# runtime
+, editables
+, packaging
+, pathspec
+, pluggy
+, tomli
+, trove-classifiers
+
+# tests
+, build
+, python
+, requests
+, virtualenv
+}:
+
+buildPythonPackage rec {
+  pname = "hatchling";
+  version = "1.21.0";
+  format = "pyproject";
+  disabled = pythonOlder "3.8";
+
+  src = fetchPypi {
+    inherit pname version;
+    hash = "sha256-XAhncjV6UHI7gl/V2lJ4rH42l833eX0HVBpskLb/dUw=";
+  };
+
+  # listed in backend/pyproject.toml
+  propagatedBuildInputs = [
+    editables
+    packaging
+    pathspec
+    pluggy
+    trove-classifiers
+  ] ++ lib.optionals (pythonOlder "3.11") [
+    tomli
+  ];
+
+  pythonImportsCheck = [
+    "hatchling"
+    "hatchling.build"
+  ];
+
+  # tries to fetch packages from the internet
+  doCheck = false;
+
+  # listed in /backend/tests/downstream/requirements.txt
+  nativeCheckInputs = [
+    build
+    requests
+    virtualenv
+  ];
+
+  preCheck = ''
+    export HOME=$TMPDIR
+  '';
+
+  checkPhase = ''
+    runHook preCheck
+    ${python.interpreter} tests/downstream/integrate.py
+    runHook postCheck
+  '';
+
+  meta = with lib; {
+    description = "Modern, extensible Python build backend";
+    homepage = "https://hatch.pypa.io/latest/";
+    changelog = "https://github.com/pypa/hatch/releases/tag/hatchling-v${version}";
+    license = licenses.mit;
+    maintainers = with maintainers; [ hexa ofek ];
+  };
+}
diff --git a/python/nose.nix b/python/nose.nix
new file mode 100644
index 0000000..8ad80e5
--- /dev/null
+++ b/python/nose.nix
@@ -0,0 +1,67 @@
+# Removed in nixpkgs, copied from there
+#
+# Ref: https://github.com/NixOS/nixpkgs/issues/326513
+# drop PR: https://github.com/NixOS/nixpkgs/pull/348699
+{
+  lib,
+  buildPythonPackage,
+  fetchPypi,
+  isPy3k,
+  isPyPy,
+  python,
+  python312,
+  coverage,
+  setuptools,
+}:
+
+buildPythonPackage rec {
+  version = "1.3.7";
+  pname = "nose";
+  pyproject = true;
+
+  src = fetchPypi {
+    inherit pname version;
+    sha256 = "f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98";
+  };
+
+  build-system = [ setuptools ];
+
+  patches = lib.optional isPy3k [ ./0001-nose-python-3.12-fixes.patch ];
+
+  postPatch = ''
+    substituteInPlace setup.py \
+      --replace "'use_2to3': True," ""
+
+    substituteInPlace setup3lib.py \
+      --replace "from setuptools.command.build_py import Mixin2to3" "from distutils.util import Mixin2to3"
+  '';
+
+  # 2to3 is removed from Python 3.13, so always use Python 3.12 2to3 for now.
+  preBuild = lib.optionalString isPy3k ''
+    ${python312.pythonOnBuildForHost}/bin/2to3 -wn nose functional_tests unit_tests
+  '';
+
+  propagatedBuildInputs = [ coverage ];
+
+  doCheck = false; # lot's of transient errors, too much hassle
+  checkPhase =
+    if isPy3k then
+      ''
+        ${python.pythonOnBuildForHost.interpreter} setup.py build_tests
+      ''
+    else
+      ""
+      + ''
+        rm functional_tests/test_multiprocessing/test_concurrent_shared.py* # see https://github.com/nose-devs/nose/commit/226bc671c73643887b36b8467b34ad485c2df062
+        ${python.pythonOnBuildForHost.interpreter} selftest.py
+      '';
+
+  meta = with lib; {
+    broken = isPyPy; # missing 2to3 conversion utility
+    description = "Unittest-based testing framework for python that makes writing and running tests easier";
+    mainProgram = "nosetests";
+    homepage = "https://nose.readthedocs.io/";
+    license = licenses.lgpl3;
+    maintainers = [ ];
+  };
+}
diff --git a/python/premailer.nix b/python/premailer.nix
new file mode 100644
index 0000000..62c9fda
--- /dev/null
+++ b/python/premailer.nix
@@ -0,0 +1,47 @@
+# Removed in nixpkgs, copied from there
+#
+# Ref: https://github.com/NixOS/nixpkgs/issues/326513
+# drop PR: https://github.com/NixOS/nixpkgs/pull/348580
+{
+  lib,
+  buildPythonPackage,
+  fetchPypi,
+  isPy27,
+  cssselect,
+  cssutils,
+  lxml,
+  mock,
+  nose,
+  requests,
+  cachetools,
+}:
+
+buildPythonPackage rec {
+  pname = "premailer";
+  version = "3.10.0";
+  format = "setuptools";
+  disabled = isPy27; # no longer compatible with urllib
+
+  src = fetchPypi {
+    inherit pname version;
+    sha256 = "d1875a8411f5dc92b53ef9f193db6c0f879dc378d618e0ad292723e388bfe4c2";
+  };
+
+  buildInputs = [
+    mock
+    nose
+  ];
+  propagatedBuildInputs = [
+    cachetools
+    cssselect
+    cssutils
+    lxml
+    requests
+  ];
+
+  meta = {
+    description = "Turns CSS blocks into style attributes";
+    homepage = "https://github.com/peterbe/premailer";
+    license = lib.licenses.bsd3;
+  };
+}
diff --git a/python/rauth.nix b/python/rauth.nix
new file mode 100644
index 0000000..5ddc6e6
--- /dev/null
+++ b/python/rauth.nix
@@ -0,0 +1,56 @@
+# Removed in nixpkgs, copied from there
+#
+# Ref: https://github.com/NixOS/nixpkgs/issues/326513
+# drop PR: https://github.com/NixOS/nixpkgs/pull/330417
+{
+  lib,
+  buildPythonPackage,
+  fetchFromGitHub,
+  fetchpatch,
+  requests,
+  pytestCheckHook,
+  mock,
+  nose,
+  pycrypto,
+}:
+
+buildPythonPackage rec {
+  pname = "rauth";
+  version = "0.7.2";
+  format = "setuptools";
+
+  src = fetchFromGitHub {
+    owner = "litl";
+    repo = "rauth";
+    rev = version;
+    hash = "sha256-wRKZbxZCEfihOaJM8sk8438LE++KJWxdOGImpL1gHa4=";
+  };
+
+  patches = [
+    (fetchpatch {
+      # https://github.com/litl/rauth/pull/211
+      name = "fix-pycrypdodome-replacement-for-pycrypto.patch";
+      url = "https://github.com/litl/rauth/commit/7fb3b7bf1a1869a52cf59ee3eb607d318e97265c.patch";
+      hash = "sha256-jiAIw+VQ2d/bkm2brqfY1RUrNGf+lsMPnoI91gGUS6o=";
+    })
+  ];
+
+  propagatedBuildInputs = [ requests ];
+
+  pythonImportsCheck = [ "rauth" ];
+
+  nativeCheckInputs = [
+    pytestCheckHook
+    mock
+    nose
+    pycrypto
+  ];
+
+  meta = with lib; {
+    description = "Python library for OAuth 1.0/a, 2.0, and Ofly";
+    homepage = "https://github.com/litl/rauth";
+    changelog = "https://github.com/litl/rauth/blob/${src.rev}/CHANGELOG";
+    license = licenses.mit;
+    maintainers = with maintainers; [ blaggacao ];
+  };
+}
diff --git a/srcs/pin.nix b/srcs/pin.nix
index dbb13ec..13ac3fa 100644
--- a/srcs/pin.nix
+++ b/srcs/pin.nix
@@ -1,12 +1,12 @@
 {
-  benchVersion = "5.19.0";
-  erpnextVersion = "15.9.1";
-  frappeVersion = "15.8.1";
+  benchVersion = "5.23.0";
+  erpnextVersion = "15.47.5";
+  frappeVersion = "15.40.4";
   hashes = {
-    "benchSrcHash" = "sha256-y8nx4vFVQggwGv2MWQ88WczgVbPxPybZV38FF5u5aWI=";
-    "erpnextSrcHash" = "sha256-nkXN0PTcWt1nSy3eRdBF2h0WMdAC79qWzaj9kXRsG2I=";
-    "erpnextYarnHash" = "1farnqrfnzshpbpx4nyarw13g8m3389ix3hrc4661xxm887lz5fv";
-    "frappeSrcHash" = "sha256-FDUUNbULPmMY6dDgbMHrxXD8pK1AP+T7kG7mY9MmMDg=";
-    "frappeYarnHash" = "0rj2v69siagwjz632hyaii5ni24fp434cznaxpi8978fq07qx6l9";
+    "benchSrcHash" = "sha256-HGlYLo62EZERM0I2usKpw3ACkPpLxxwwmNQ+Rd9dsN4=";
+    "erpnextSrcHash" = "sha256-1DJIXldd/2gqxtYRTc1qFVdnNVmIy3Z+TAj+LC5DSYk=";
+    "erpnextYarnHash" = "";
+    "frappeSrcHash" = "sha256-QbMX1hyHkzxcEdcIMKPbU9qYNbdtpwH5wjXtcrA9q1Y=";
+    "frappeYarnHash" = "";
   };
 }
diff --git a/test-vm/configuration.nix b/test-vm/configuration.nix
index a33e397..727f080 100644
--- a/test-vm/configuration.nix
+++ b/test-vm/configuration.nix
@@ -87,6 +87,9 @@
       database.rootPasswordFile = config.age.secrets.erpnext-db-root-password.path;
       database.userPasswordFile = config.age.secrets.erpnext-db-user-password.path;
       caddy = {};
+      apps = [
+        pkgs.eu_einvoice
+      ];
     };
     services.caddy = {
       email = "admins@pub.solar";