diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..69ce2ca1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3090 @@ +{ + "name": "propms", + "version": "16.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "propms", + "version": "16.0.0", + "devDependencies": { + "@vitest/coverage-v8": "^2.1.9", + "jsdom": "^25.0.1", + "vitest": "^2.1.9" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.0.tgz", + "integrity": "sha512-70TeIFezKKy65LgAVyQh+w94/gjWhvPWaLaGGeMEgVrPkQhuj/M5bAYYZzIFUj9Y69oHyTm5Um/R6gcLh4A8JA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.0.tgz", + "integrity": "sha512-YC86tYIHK6M1IV+wbzO+Bxk8RCBr6ZyWYgWxUCzaZD8mc8rrFoIJDNzDrkHBYRc/wKdrsIXmm6/F7NzrAO+OrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.0.tgz", + "integrity": "sha512-oI+ECtUcli0y0fi4xpW82GdPIXdTkI8G8DSjG2LRuw09fPAGykaWYH/hXxiKuTxiAjiPSTIIuYUqof5Z2hShWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.0.tgz", + "integrity": "sha512-NwV+1s7TiKrMe4owHyKB/dTLD7ZJD0YEBEhIz+hvav1Cu1GReJjF+rsdNwjzENQeIAbE/CoNiaAc5Vz2h5DPAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.0.tgz", + "integrity": "sha512-tWtHBTu5gOPK4u4Urtk4qAHW3zZ9rQAmbssO8gp7ELvGTGI3aCiq6NqyTQ0PCIg7KbHJF2UkGDDs77YZGxfjCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.0.tgz", + "integrity": "sha512-2qPoJiwTvtHQ27NnYvTnsgk8laXWYuVmNESG8WFZBcEPKLfZ3I27qBJarjVRQtwGeYyRfq5ZowHXih9lm2BItw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.0.tgz", + "integrity": "sha512-FQwsTRvLNuHoTdICABJQfbPUSEueISGmnpT06tXTMpfprf5NiKLSXKA0A+w45wJnCmZAnzgqBwbt6ARFuyOi5w==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.0.tgz", + "integrity": "sha512-BBVTXziw8mY1a4ZbWME9tZyfzqXCDPqaC7Z3heQ29p5dkvXzwL0NwelO8zLa8c3RBKvl3YTuSnBgsBhYBtwjIw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.0.tgz", + "integrity": "sha512-w2Iyy9+RqKwx3d9qWMKsJg0FfRBsY0/pXNv0mCQ3ueRvJI6+QAScfD4nrMlzFLs2HNVW6Ew+mtZfDl9b7Ew5/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.0.tgz", + "integrity": "sha512-YK++KtrFRHYE0P6/RtYEAy9t8F37znP+K03RrIuLPYOL6SVlObRumf/0OE4V/h63xL9DwkWbNssZfmA9hawuDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.0.tgz", + "integrity": "sha512-aBfOG6fP7YkkPmTqPwufRJeFyz7WPpECv9XNbnsk9+vg7rxdih0lbtEel7jcRng4LZrrmU3FfitCFyEj4BWDWg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.0.tgz", + "integrity": "sha512-LGaHEOeHNAag9VuS1Crs5DFg4RrU9MPi2nVnNJk9DTePx/B6RRYKVmrIXt2h7YOJlwjaFJ6lwtFDliZxScTLrQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.0.tgz", + "integrity": "sha512-jClvk+J0FC3b7Udvegiw5/4hErbHtmsNsQgENnKXDWtNCJXsJYZH5WURvu7imDOO38xYml24eeh5x3A04ppwCw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.0.tgz", + "integrity": "sha512-0OJlaGK+8+B777Ql5okIpD7ua5Ro9+VB9Ve0OKa28OQJZ1RbuUBVNHK/e3pr4BROqsyPl1JrPO1ZxJseCNffcA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.0.tgz", + "integrity": "sha512-Ygsx+HoNH7afwi1bTIXbnTvVnsO+zurPLSYxybV1hHFVU72OWOCl6v05ql/z0hkpAPx+DK7Kn9Bi7MayCcjLTA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.0.tgz", + "integrity": "sha512-pDQxtMGb+OvG3fLwR2OkZlSd47hW+kWg4BYMG/++sR6RqorQccwPTDsxda5hPwiIeIErAnCF9ma3SAU06bdQtQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.0.tgz", + "integrity": "sha512-0BnUG9mS8I4SSHr3XsxVhuCMEiu+rX61xxZF5vujso4LaiAGFZFxvDjg6Xn6tLPNTUAfuCvQYas4LMQMVsKRSQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.0.tgz", + "integrity": "sha512-Adu/VttB1dpPNW+FEacrZ+xVm9tFty84+RrFzsqlFaPxoJB+9XXyDGtp5dCOoBwGBIEVH0To7lExFXEx0BIF4A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.0.tgz", + "integrity": "sha512-NQ3bDvjUbFKmP23671xUlXtKmqVsUBd6M4PQCvbmNtOy06hnQIdKHy8oG/6S3R/S6He1JgPk6A5VT+prAJMYEw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.0.tgz", + "integrity": "sha512-u2eDAl4+0aFvA13GxlGBtTI3SS3sdgwgtV0HyjZ0QaQVCgNE+jqNGey+GtxWiq+wxr/UycAx/OnfJzApCFamvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.0.tgz", + "integrity": "sha512-XvRb5vfW3wAZQ+ZUG21AnHHDKtNcw99eigzEhjr//NZ3u7SoBaPP0seSc7FgP7p1epAEdAoZckMW9WY/+4w70w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.0.tgz", + "integrity": "sha512-iZPmniy4kNBf5yo2RezbkYNNK5HPbXE9+g+twnbqSng7dtLEJy1SKoxiE/ni4FDacjyuZpEeb9U054N4EoKHYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.0.tgz", + "integrity": "sha512-mFBBd+LF37fnE8JnYUOH+imj0aPFPK30vpar4ehJkgnLj9sZn8ZxiRENmLtgIwxK7TC8klF6N57fxdNBwQoqOA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.0.tgz", + "integrity": "sha512-ujeqEY3B+zbGn3Z4Q03cUBG/LGWnBJncVT36WER31LcOsQk9+1dmINKKtvmmfChUvRbK1G0R8OhMWFgHgaZtAw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.0.tgz", + "integrity": "sha512-hncn90N4sOky0L2LKE5oESKLbxCPeVo4eLA2LSMoDzM+879ml4WSr+Rr4DWknNIVVvS1Hirkc9hx02W6YxS8rQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/coverage-v8": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz", + "integrity": "sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.7", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.12", + "magicast": "^0.3.5", + "std-env": "^3.8.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "2.1.9", + "vitest": "2.1.9" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nwsapi": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", + "dev": true, + "license": "MIT" + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/rollup": { + "version": "4.63.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.0.tgz", + "integrity": "sha512-T5vnZ2y4QqC3/4P+w2+JO+Q/OVdnPsv4XcSYJYMEn0R9/jjl5AgLwO9LAZMzP2lN71O6pypn91rB7lDstUkfrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.0", + "@rollup/rollup-android-arm64": "4.63.0", + "@rollup/rollup-darwin-arm64": "4.63.0", + "@rollup/rollup-darwin-x64": "4.63.0", + "@rollup/rollup-freebsd-arm64": "4.63.0", + "@rollup/rollup-freebsd-x64": "4.63.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.0", + "@rollup/rollup-linux-arm-musleabihf": "4.63.0", + "@rollup/rollup-linux-arm64-gnu": "4.63.0", + "@rollup/rollup-linux-arm64-musl": "4.63.0", + "@rollup/rollup-linux-loong64-gnu": "4.63.0", + "@rollup/rollup-linux-loong64-musl": "4.63.0", + "@rollup/rollup-linux-ppc64-gnu": "4.63.0", + "@rollup/rollup-linux-ppc64-musl": "4.63.0", + "@rollup/rollup-linux-riscv64-gnu": "4.63.0", + "@rollup/rollup-linux-riscv64-musl": "4.63.0", + "@rollup/rollup-linux-s390x-gnu": "4.63.0", + "@rollup/rollup-linux-x64-gnu": "4.63.0", + "@rollup/rollup-linux-x64-musl": "4.63.0", + "@rollup/rollup-openbsd-x64": "4.63.0", + "@rollup/rollup-openharmony-arm64": "4.63.0", + "@rollup/rollup-win32-arm64-msvc": "4.63.0", + "@rollup/rollup-win32-ia32-msvc": "4.63.0", + "@rollup/rollup-win32-x64-gnu": "4.63.0", + "@rollup/rollup-win32-x64-msvc": "4.63.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..5fbbb78c --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "propms", + "private": true, + "version": "16.0.0", + "description": "Property Management Solution client-side test harness", + "scripts": { + "test": "vitest run", + "test:watch": "vitest" + }, + "devDependencies": { + "@vitest/coverage-v8": "^2.1.9", + "jsdom": "^25.0.1", + "vitest": "^2.1.9" + } +} diff --git a/propms/auto_custom.py b/propms/auto_custom.py index 045aa198..c6ee786a 100755 --- a/propms/auto_custom.py +++ b/propms/auto_custom.py @@ -428,7 +428,7 @@ def makeInvoiceSchedule( doctype="Lease Invoice Schedule", parent=name, parentfield="lease_invoice_schedule", - parenttype="lease", + parenttype="Lease", date_to_invoice=date_to_invoice, schedule_start_date=date, lease_item=item, diff --git a/propms/hooks.py b/propms/hooks.py index f868721c..ed1289f7 100755 --- a/propms/hooks.py +++ b/propms/hooks.py @@ -163,7 +163,7 @@ # Testing # ------- -# before_tests = "propms.install.before_tests" +before_tests = "propms.tests.utils.before_tests" # Overriding Whitelisted Methods # ------------------------------ diff --git a/propms/lease_invoice.py b/propms/lease_invoice.py index 25cddea0..4253ab46 100755 --- a/propms/lease_invoice.py +++ b/propms/lease_invoice.py @@ -100,6 +100,8 @@ def makeInvoice( doc.submit() return doc + except frappe.ValidationError: + raise except Exception as e: app_error_log(frappe.session.user, str(e)) @@ -147,7 +149,6 @@ def leaseInvoiceAutoCreate(): "invoice_number", "sales_order_number", "parent", - "parent", "invoice_item_group", "lease_item", "paid_by", @@ -260,6 +261,8 @@ def leaseInvoiceAutoCreate(): prev_currency = invoice_item.currency row_num += 1 # increment by 1 # Create the last invoice + if not invoice_item: + return res = makeInvoice( invoice_item.date_to_invoice, invoice_item.paid_by, @@ -285,6 +288,7 @@ def leaseInvoiceAutoCreate(): frappe.msgprint(_("Lease Invoice generated with number: {0}").format(res.name)) except Exception as e: + frappe.log_error(frappe.get_traceback(), "Lease Invoice Auto Create failed") app_error_log(frappe.session.user, str(e)) diff --git a/propms/lease_invoice_schedule.py b/propms/lease_invoice_schedule.py index ff353334..e22feadd 100644 --- a/propms/lease_invoice_schedule.py +++ b/propms/lease_invoice_schedule.py @@ -38,7 +38,7 @@ def make_lease_invoice_schedule(): # Only check start_date, ignore end_date for inclusion ) ) - lease_names = [row[0] for row in frappe.db.sql(query.get_sql())] + lease_names = [row[0] for row in query.run()] for lease_name in lease_names: try: @@ -145,8 +145,8 @@ def make_lease_invoice_schedule(): # Move to first day of next period invoice_date = add_days(invoice_period_end, 1) - frappe.msgprint(_(f"Completed invoice schedule for Lease: {lease.name}")) + frappe.msgprint(_("Completed invoice schedule for Lease: {0}").format(lease.name)) except Exception as e: - frappe.msgprint(_(f"Error in {lease_name}. Check app error log.")) + frappe.msgprint(_("Error in {0}. Check app error log.").format(lease_name)) app_error_log(frappe.session.user, f"{lease_name}: {str(e)}") diff --git a/propms/property_management_solution/doctype/apartment_status/test_apartment_status.js b/propms/property_management_solution/doctype/apartment_status/test_apartment_status.js deleted file mode 100755 index d8fd2278..00000000 --- a/propms/property_management_solution/doctype/apartment_status/test_apartment_status.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Apartment Status", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Apartment Status - () => frappe.tests.make('Apartment Status', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/propms/property_management_solution/doctype/checklist_checkup_area/test_checklist_checkup_area.js b/propms/property_management_solution/doctype/checklist_checkup_area/test_checklist_checkup_area.js deleted file mode 100755 index e08e9e8e..00000000 --- a/propms/property_management_solution/doctype/checklist_checkup_area/test_checklist_checkup_area.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Checklist Checkup Area", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Checklist Checkup Area - () => frappe.tests.make('Checklist Checkup Area', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/propms/property_management_solution/doctype/custom_error_log/test_custom_error_log.js b/propms/property_management_solution/doctype/custom_error_log/test_custom_error_log.js deleted file mode 100755 index 2dc0e0a5..00000000 --- a/propms/property_management_solution/doctype/custom_error_log/test_custom_error_log.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Custom Error Log", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Custom Error Log - () => frappe.tests.make('Custom Error Log', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/propms/property_management_solution/doctype/daily_checklist/daily_checklist.json b/propms/property_management_solution/doctype/daily_checklist/daily_checklist.json index 4589628b..ce2c479e 100755 --- a/propms/property_management_solution/doctype/daily_checklist/daily_checklist.json +++ b/propms/property_management_solution/doctype/daily_checklist/daily_checklist.json @@ -221,7 +221,7 @@ "collapsible": 0, "columns": 0, "fieldname": "naming_series", - "fieldtype": "Data", + "fieldtype": "Select", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -229,7 +229,8 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "naming_series", + "label": "Naming Series", + "options": "DC-.YYYY.-", "length": 0, "no_copy": 0, "permlevel": 0, @@ -239,9 +240,9 @@ "read_only": 0, "remember_last_selected_value": 0, "report_hide": 0, - "reqd": 0, + "reqd": 1, "search_index": 0, - "set_only_once": 0, + "set_only_once": 1, "translatable": 0, "unique": 0 } @@ -257,7 +258,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-12-24 19:42:26.306039", + "modified": "2026-08-27 12:00:00.000000", "modified_by": "Administrator", "module": "Property Management Solution", "name": "Daily Checklist", diff --git a/propms/property_management_solution/doctype/daily_checklist/test_daily_checklist.js b/propms/property_management_solution/doctype/daily_checklist/test_daily_checklist.js deleted file mode 100755 index 8659152c..00000000 --- a/propms/property_management_solution/doctype/daily_checklist/test_daily_checklist.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Daily Checklist", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Daily Checklist - () => frappe.tests.make('Daily Checklist', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/propms/property_management_solution/doctype/exit/test_exit.js b/propms/property_management_solution/doctype/exit/test_exit.js deleted file mode 100755 index 2d41cbd2..00000000 --- a/propms/property_management_solution/doctype/exit/test_exit.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Exit", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Exit - () => frappe.tests.make('Exit', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/propms/property_management_solution/doctype/guard_shift/test_guard_shift.js b/propms/property_management_solution/doctype/guard_shift/test_guard_shift.js deleted file mode 100755 index 72bb73a6..00000000 --- a/propms/property_management_solution/doctype/guard_shift/test_guard_shift.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Guard Shift", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Guard Shift - () => frappe.tests.make('Guard Shift', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/propms/property_management_solution/doctype/insurance/test_insurance.js b/propms/property_management_solution/doctype/insurance/test_insurance.js deleted file mode 100755 index 86d8c178..00000000 --- a/propms/property_management_solution/doctype/insurance/test_insurance.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Insurance", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Insurance - () => frappe.tests.make('Insurance', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/propms/property_management_solution/doctype/key_set/test_key_set.js b/propms/property_management_solution/doctype/key_set/test_key_set.js deleted file mode 100755 index 84130fd3..00000000 --- a/propms/property_management_solution/doctype/key_set/test_key_set.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Key Set", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Key Set - () => frappe.tests.make('Key Set', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/propms/property_management_solution/doctype/key_set_detail/test_key_set_detail.js b/propms/property_management_solution/doctype/key_set_detail/test_key_set_detail.js deleted file mode 100755 index cf5088a3..00000000 --- a/propms/property_management_solution/doctype/key_set_detail/test_key_set_detail.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Key Set Detail", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Key Set Detail - () => frappe.tests.make('Key Set Detail', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/propms/property_management_solution/doctype/lease/lease.py b/propms/property_management_solution/doctype/lease/lease.py index d30a777d..112b3327 100755 --- a/propms/property_management_solution/doctype/lease/lease.py +++ b/propms/property_management_solution/doctype/lease/lease.py @@ -45,6 +45,7 @@ def on_submit(self): app_error_log(frappe.session.user, str(e)) def validate(self): + self.set_lease_status() self.validate_days_to_invoice_in_advance() try: properties = self.get_all_properties() @@ -119,7 +120,28 @@ def validate(self): raise except Exception as e: app_error_log(frappe.session.user, str(e)) - self.set_lease_status() + + def validate_days_to_invoice_in_advance(self): + """Prevent changing 'Days to Invoice in Advance' once invoices have been generated.""" + if not self.is_new() and self.has_value_changed("days_to_invoice_in_advance"): + has_generated_invoices = any( + row.invoice_number or row.sales_order_number for row in (self.lease_invoice_schedule or []) + ) + if not has_generated_invoices: + has_generated_invoices = frappe.db.exists( + "Lease Invoice Schedule", + { + "parent": self.name, + "invoice_number": ["is", "set"], + }, + ) + if has_generated_invoices: + frappe.throw( + _( + "Cannot change 'Days to Invoice in Advance' after invoices have been generated for this Lease." + ), + title=_("Field Read Only"), + ) def validate_days_to_invoice_in_advance(self): """Prevent changing 'Days to Invoice in Advance' once invoices have been generated.""" @@ -153,7 +175,7 @@ def set_lease_status(self): All other statuses are considered manual and are not overwritten. """ - if self.lease_status not in get_system_controlled_statuses(): + if self.lease_status and self.lease_status not in get_system_controlled_statuses(): return status = get_status_for_lease(self) diff --git a/propms/property_management_solution/doctype/lease/test_lease.js b/propms/property_management_solution/doctype/lease/test_lease.js deleted file mode 100755 index 5662e6ef..00000000 --- a/propms/property_management_solution/doctype/lease/test_lease.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Lease", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Lease - () => frappe.tests.make('Lease', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/propms/property_management_solution/doctype/lease_invoice_schedule/test_lease_invoice_schedule.js b/propms/property_management_solution/doctype/lease_invoice_schedule/test_lease_invoice_schedule.js deleted file mode 100755 index ee17d654..00000000 --- a/propms/property_management_solution/doctype/lease_invoice_schedule/test_lease_invoice_schedule.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Lease Invoice Schedule", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Lease Invoice Schedule - () => frappe.tests.make('Lease Invoice Schedule', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/propms/property_management_solution/doctype/lease_item/test_lease_item.js b/propms/property_management_solution/doctype/lease_item/test_lease_item.js deleted file mode 100755 index a589009d..00000000 --- a/propms/property_management_solution/doctype/lease_item/test_lease_item.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Lease Item", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Lease Item - () => frappe.tests.make('Lease Item', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/propms/property_management_solution/doctype/meter/test_meter.js b/propms/property_management_solution/doctype/meter/test_meter.js deleted file mode 100644 index 95cb2ea1..00000000 --- a/propms/property_management_solution/doctype/meter/test_meter.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Meter", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Meter - () => frappe.tests.make('Meter', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/propms/property_management_solution/doctype/meter_reading/test_meter_reading.js b/propms/property_management_solution/doctype/meter_reading/test_meter_reading.js deleted file mode 100644 index be38ad8e..00000000 --- a/propms/property_management_solution/doctype/meter_reading/test_meter_reading.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Meter Reading", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Meter Reading - () => frappe.tests.make('Meter Reading', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/propms/property_management_solution/doctype/meter_reading_detail/test_meter_reading_detail.js b/propms/property_management_solution/doctype/meter_reading_detail/test_meter_reading_detail.js deleted file mode 100644 index 4b1eaca5..00000000 --- a/propms/property_management_solution/doctype/meter_reading_detail/test_meter_reading_detail.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Meter Reading Detail", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Meter Reading Detail - () => frappe.tests.make('Meter Reading Detail', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/propms/property_management_solution/doctype/outsource_contact/test_outsource_contact.js b/propms/property_management_solution/doctype/outsource_contact/test_outsource_contact.js deleted file mode 100755 index 7bb31cec..00000000 --- a/propms/property_management_solution/doctype/outsource_contact/test_outsource_contact.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Outsource Contact", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Outsource Contact - () => frappe.tests.make('Outsource Contact', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/propms/property_management_solution/doctype/outsourcing_attendance/outsourcing_attendance.json b/propms/property_management_solution/doctype/outsourcing_attendance/outsourcing_attendance.json index f287e327..1fe4a122 100755 --- a/propms/property_management_solution/doctype/outsourcing_attendance/outsourcing_attendance.json +++ b/propms/property_management_solution/doctype/outsourcing_attendance/outsourcing_attendance.json @@ -154,27 +154,28 @@ "collapsible": 0, "columns": 0, "fieldname": "naming_series", - "fieldtype": "Data", - "hidden": 1, + "fieldtype": "Select", + "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "naming_series", + "label": "Naming Series", + "options": "OA-.YYYY.-", "length": 0, "no_copy": 0, "permlevel": 0, "precision": "", "print_hide": 1, "print_hide_if_no_value": 0, - "read_only": 1, + "read_only": 0, "remember_last_selected_value": 0, "report_hide": 0, - "reqd": 0, + "reqd": 1, "search_index": 0, - "set_only_once": 0, + "set_only_once": 1, "translatable": 0, "unique": 0 }, @@ -222,7 +223,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2019-01-16 15:54:32.839686", + "modified": "2026-08-27 12:00:00.000000", "modified_by": "Administrator", "module": "Property Management Solution", "name": "Outsourcing Attendance", diff --git a/propms/property_management_solution/doctype/outsourcing_attendance/test_outsourcing_attendance.js b/propms/property_management_solution/doctype/outsourcing_attendance/test_outsourcing_attendance.js deleted file mode 100755 index db5e8f41..00000000 --- a/propms/property_management_solution/doctype/outsourcing_attendance/test_outsourcing_attendance.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Outsourcing Attendance", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Outsourcing Attendance - () => frappe.tests.make('Outsourcing Attendance', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/propms/property_management_solution/doctype/outsourcing_category/test_outsourcing_category.js b/propms/property_management_solution/doctype/outsourcing_category/test_outsourcing_category.js deleted file mode 100755 index 74f7a1b5..00000000 --- a/propms/property_management_solution/doctype/outsourcing_category/test_outsourcing_category.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Outsourcing Category", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Outsourcing Category - () => frappe.tests.make('Outsourcing Category', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/propms/property_management_solution/doctype/outsourcing_shift/test_outsourcing_shift.js b/propms/property_management_solution/doctype/outsourcing_shift/test_outsourcing_shift.js deleted file mode 100755 index 69a96a74..00000000 --- a/propms/property_management_solution/doctype/outsourcing_shift/test_outsourcing_shift.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Outsourcing Shift", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Outsourcing Shift - () => frappe.tests.make('Outsourcing Shift', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/propms/property_management_solution/doctype/property/property_tree.js b/propms/property_management_solution/doctype/property/property_tree.js index 4c9d00a7..3cca0ee2 100644 --- a/propms/property_management_solution/doctype/property/property_tree.js +++ b/propms/property_management_solution/doctype/property/property_tree.js @@ -14,7 +14,7 @@ frappe.treeview_settings["Property"] = { ], fields: [ - { fieldtype: "Data", fieldname: "property_name", label: __("New Property Name"), reqd: true }, + { fieldtype: "Data", fieldname: "name1", label: __("New Property Name"), reqd: true }, { fieldtype: "Check", fieldname: "is_group", diff --git a/propms/property_management_solution/doctype/property/test_property.js b/propms/property_management_solution/doctype/property/test_property.js deleted file mode 100755 index 75ea0aae..00000000 --- a/propms/property_management_solution/doctype/property/test_property.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Property", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Property - () => frappe.tests.make('Property', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/propms/property_management_solution/doctype/property_amenity/test_property_amenity.js b/propms/property_management_solution/doctype/property_amenity/test_property_amenity.js deleted file mode 100755 index 5204ed59..00000000 --- a/propms/property_management_solution/doctype/property_amenity/test_property_amenity.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Property Amenity", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Property Amenity - () => frappe.tests.make('Property Amenity', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/propms/property_management_solution/doctype/property_management_settings/test_property_management_settings.js b/propms/property_management_solution/doctype/property_management_settings/test_property_management_settings.js deleted file mode 100755 index 42074097..00000000 --- a/propms/property_management_solution/doctype/property_management_settings/test_property_management_settings.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Property Management Settings", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Property Management Settings - () => frappe.tests.make('Property Management Settings', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/propms/property_management_solution/doctype/property_meter_reading/test_property_meter_reading.js b/propms/property_management_solution/doctype/property_meter_reading/test_property_meter_reading.js deleted file mode 100644 index dc2bb7af..00000000 --- a/propms/property_management_solution/doctype/property_meter_reading/test_property_meter_reading.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Property Meter Reading", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Property Meter Reading - () => frappe.tests.make('Property Meter Reading', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/propms/property_management_solution/doctype/security_attendance/security_attendance.json b/propms/property_management_solution/doctype/security_attendance/security_attendance.json index 53824fae..5c6dc794 100755 --- a/propms/property_management_solution/doctype/security_attendance/security_attendance.json +++ b/propms/property_management_solution/doctype/security_attendance/security_attendance.json @@ -43,15 +43,18 @@ }, { "fieldname": "naming_series", - "fieldtype": "Data", - "label": "naming_series", + "fieldtype": "Select", + "hidden": 0, + "label": "Naming Series", + "options": "SA-.YYYY.-", "print_hide": 1, - "read_only": 1 + "reqd": 1, + "set_only_once": 1 } ], "icon": "octicon octicon-clippy", "is_submittable": 1, - "modified": "2019-12-05 16:49:29.347921", + "modified": "2026-08-27 12:00:00.000000", "modified_by": "Administrator", "module": "Property Management Solution", "name": "Security Attendance", @@ -99,4 +102,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/propms/property_management_solution/doctype/security_attendance/test_security_attendance.js b/propms/property_management_solution/doctype/security_attendance/test_security_attendance.js deleted file mode 100755 index 0b6478cd..00000000 --- a/propms/property_management_solution/doctype/security_attendance/test_security_attendance.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Security Attendance", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Security Attendance - () => frappe.tests.make('Security Attendance', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/propms/property_management_solution/doctype/security_attendance_details/test_security_attendance_details.js b/propms/property_management_solution/doctype/security_attendance_details/test_security_attendance_details.js deleted file mode 100755 index 1967ac7d..00000000 --- a/propms/property_management_solution/doctype/security_attendance_details/test_security_attendance_details.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Security Attendance Details", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Security Attendance Details - () => frappe.tests.make('Security Attendance Details', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/propms/property_management_solution/doctype/security_deposit_details/test_security_deposit_details.js b/propms/property_management_solution/doctype/security_deposit_details/test_security_deposit_details.js deleted file mode 100755 index 10b83c3a..00000000 --- a/propms/property_management_solution/doctype/security_deposit_details/test_security_deposit_details.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Security Deposit Details", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Security Deposit Details - () => frappe.tests.make('Security Deposit Details', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/propms/property_management_solution/doctype/unit_type/test_unit_type.js b/propms/property_management_solution/doctype/unit_type/test_unit_type.js deleted file mode 100755 index c4eaac01..00000000 --- a/propms/property_management_solution/doctype/unit_type/test_unit_type.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Unit Type", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Unit Type - () => frappe.tests.make('Unit Type', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/propms/property_management_solution/issue.js b/propms/property_management_solution/issue.js index ab9154e6..d8897489 100644 --- a/propms/property_management_solution/issue.js +++ b/propms/property_management_solution/issue.js @@ -1,246 +1,246 @@ -frappe.ui.form.on('Issue', { - validate: (frm) => { - // frm.trigger("make_row_readonly"); - if (!frm.doc.materials_required) { - return - } - let to_update = []; - frm.doc.materials_required.forEach((item, idx) => { - var sle_qty = 0 - frappe.call({ - method: "propms.issue_hook.get_stock_availability", - args: { - item_code: item.item, - company: frm.doc.company, - is_pos: item.is_pos - }, - async: false, - callback: function (r) { - if (r.message) { - sle_qty = r.message; - } - } - }); - if (item.material_status === "Bill" || item.material_status === "Self Consumption" && frm.doc.status === "Closed") { - if (sle_qty < item.quantity) { - frappe.throw(__(`Existing stock quantity of item ${item.item} is ${sle_qty} not enough`)) - return - } - let child = frm.add_child("materials_billed"); - child.item = item.item; - child.quantity = item.quantity; - child.uom = item.uom; - child.rate = item.rate; - child.amount = item.amount; - child.is_pos = item.is_pos; - child.material_status = item.material_status; - } - else { - to_update.push(item); - } - }); - frm.clear_table("materials_required"); - refresh_field("materials_required"); - to_update.forEach(item => { - let child = frm.add_child("materials_required"); - child.item = item.item; - child.quantity = item.quantity; - child.uom = item.uom; - child.rate = item.rate; - child.amount = item.amount; - child.is_pos = item.is_pos; - child.material_status = item.material_status; - refresh_field("materials_required"); - }); - refresh_field("materials_required"); - - if (!frm.doc.materials_billed) { - return - } - const sort_list = []; - frm.doc.materials_billed.forEach((item, idx) => { - let item_inv_no = Number.MAX_SAFE_INTEGER; - let item_inv_ser = ""; - if (item.sales_invoice) { - item_inv_no = +item.sales_invoice.slice(9).replace("-", ""); - item_inv_ser = item.sales_invoice.slice(0, 8); - } - sort_list.push({ - idx: idx, - no: item_inv_no, - ser: item_inv_ser, - pos: item.is_pos, - name: item.name - }); - }); - const sorted_list = sort_list.sort((a, b) => a.no - b.no); - const pos_list = []; - const not_list = []; - sorted_list.forEach(i => { - if (i.pos) { pos_list.push(i) } - else { not_list.push(i) } - }); - const new_sorted = [].concat(pos_list, not_list); - new_sorted.forEach((i, idx) => { - const row = locals["Issue Materials Billed"][i.name]; - row.idx = idx + 1; - }); - refresh_field("materials_billed"); - }, - - refresh: (frm) => { - frm.trigger("make_pos_readonly"); - }, - - onload: (frm) => { - frm.trigger("make_pos_readonly"); - }, - - make_pos_readonly: (frm) => { - if (!frm.doc.materials_required) { - return; - } - let child = frm.doc.materials_required; - child.forEach(function (e) { - $("[data-idx='" + e.idx + "']").find('.btn-open-row').css("pointer-events", "none"); - if (e.material_status === "Self Consumption") { - $("[data-idx='" + e.idx + "']").find('[data-fieldname = is_pos]').css("pointer-events", "none"); - } - else { - $("[data-idx='" + e.idx + "']").find('[data-fieldname = is_pos]').css("pointer-events", "auto"); - } - }); - refresh_field("materials_required"); - }, - - setup: function (frm) { - frm.set_query('person_in_charge', function () { - return { - filters: { - 'department': ['like', 'Maintenance - %'] - } - } - }); - frm.set_query('sub_contractor_contact', function () { - return { - filters: { - 'supplier_group': 'Sub-Contractor' - } - } - }); - frappe.call({ - method: "propms.issue_hook.get_items_group", - async: false, - callback: function (r) { - if (r.message) { - let maintenance_item_group = r.message; - frm.fields_dict["materials_required"].grid.get_field("item").get_query = function (doc, cdt, cdn) { - return { - filters: [ - ["Item", "item_group", "in", maintenance_item_group], - - ] - } - } - } - } - }); - }, - property_name: function (frm, cdt, cdn) { - // frappe.msgprint(__("Testing")) - frappe.model.set_value(cdt, cdn, 'customer', ''); - if (frm.doc.property_name) { - frappe.call({ - method: 'frappe.client.get_value', - args: { - doctype: 'Property', - fieldname: 'status', - filters: { - name: frm.doc.property_name - }, - }, - async: false, - callback: function (r, rt) { - if (r.message) { - if (r.message.status.toLowerCase() == 'on lease' || r.message.status.toLowerCase() == 'off lease in 3 months') { - frappe.call({ - method: 'frappe.client.get_value', - args: { - doctype: 'Lease', - fieldname: ['name', 'customer'], - filters: { - property: frm.doc.property_name, - start_date: ["<=", frappe.datetime.nowdate()], - end_date: [">=", frappe.datetime.nowdate()] - }, - }, - async: false, - callback: function (r, rt) { - if (r.message) { - frm.set_value("customer", r.message.customer); - refresh_field("customer") - } - } - }); - } else { - frappe.db.get_value("Property", frm.doc.property_name, "unit_owner", (r) => { - frm.set_value("customer", r.unit_owner); - }); - } - } - } - }); - } - }, -}); - -frappe.ui.form.on("Issue Materials Detail", "quantity", function (frm, cdt, cdn) { - var item_row = locals[cdt][cdn]; - item_row.amount = item_row.rate * item_row.quantity; - refresh_field("materials_required"); -}); - - -frappe.ui.form.on("Issue Materials Detail", "rate", function (frm, cdt, cdn) { - var item_row = locals[cdt][cdn]; - item_row.amount = item_row.rate * item_row.quantity; - refresh_field("materials_required"); -}); - - -frappe.ui.form.on("Issue Materials Detail", "material_status", function (frm, cdt, cdn) { - var item_row = locals[cdt][cdn]; - var is_pos = $("[data-idx='" + item_row.idx + "']").find('[data-fieldname = is_pos]') - - if (item_row.material_status === "Self Consumption") { - is_pos.css("pointer-events", "none"); - item_row.is_pos = 0; - - } else { - is_pos.css("pointer-events", "auto"); - } - refresh_field("materials_required"); -}); - - -frappe.ui.form.on("Issue Materials Detail", "item", function (frm, cdt, cdn) { - var item_row = locals[cdt][cdn]; - if (!item_row.item) { - return; - } - frappe.call({ - method: "propms.issue_hook.get_item_rate", - args: { - item: item_row.item, - customer: frm.doc.customer, - }, - async: false, - callback: function (r) { - if (r.message) { - item_row.rate = r.message; - item_row.amount = item_row.rate * item_row.quantity; - refresh_field("materials_required"); - } - } - }); -}); +frappe.ui.form.on('Issue', { + validate: (frm) => { + // frm.trigger("make_row_readonly"); + if (!frm.doc.materials_required) { + return + } + let to_update = []; + frm.doc.materials_required.forEach((item, idx) => { + var sle_qty = 0 + frappe.call({ + method: "propms.issue_hook.get_stock_availability", + args: { + item_code: item.item, + company: frm.doc.company, + is_pos: item.is_pos + }, + async: false, + callback: function (r) { + if (r.message) { + sle_qty = r.message; + } + } + }); + if (item.material_status === "Bill" || item.material_status === "Self Consumption" && frm.doc.status === "Closed") { + if (sle_qty < item.quantity) { + frappe.throw(__("Existing stock quantity of item {0} is {1}, which is not enough", [item.item, sle_qty])) + return + } + let child = frm.add_child("materials_billed"); + child.item = item.item; + child.quantity = item.quantity; + child.uom = item.uom; + child.rate = item.rate; + child.amount = item.amount; + child.is_pos = item.is_pos; + child.material_status = item.material_status; + } + else { + to_update.push(item); + } + }); + frm.clear_table("materials_required"); + refresh_field("materials_required"); + to_update.forEach(item => { + let child = frm.add_child("materials_required"); + child.item = item.item; + child.quantity = item.quantity; + child.uom = item.uom; + child.rate = item.rate; + child.amount = item.amount; + child.is_pos = item.is_pos; + child.material_status = item.material_status; + refresh_field("materials_required"); + }); + refresh_field("materials_required"); + + if (!frm.doc.materials_billed) { + return + } + const sort_list = []; + frm.doc.materials_billed.forEach((item, idx) => { + let item_inv_no = Number.MAX_SAFE_INTEGER; + let item_inv_ser = ""; + if (item.sales_invoice) { + item_inv_no = +item.sales_invoice.slice(9).replace("-", ""); + item_inv_ser = item.sales_invoice.slice(0, 8); + } + sort_list.push({ + idx: idx, + no: item_inv_no, + ser: item_inv_ser, + pos: item.is_pos, + name: item.name + }); + }); + const sorted_list = sort_list.sort((a, b) => a.no - b.no); + const pos_list = []; + const not_list = []; + sorted_list.forEach(i => { + if (i.pos) { pos_list.push(i) } + else { not_list.push(i) } + }); + const new_sorted = [].concat(pos_list, not_list); + new_sorted.forEach((i, idx) => { + const row = locals["Issue Materials Billed"][i.name]; + row.idx = idx + 1; + }); + refresh_field("materials_billed"); + }, + + refresh: (frm) => { + frm.trigger("make_pos_readonly"); + }, + + onload: (frm) => { + frm.trigger("make_pos_readonly"); + }, + + make_pos_readonly: (frm) => { + if (!frm.doc.materials_required) { + return; + } + let child = frm.doc.materials_required; + child.forEach(function (e) { + $("[data-idx='" + e.idx + "']").find('.btn-open-row').css("pointer-events", "none"); + if (e.material_status === "Self Consumption") { + $("[data-idx='" + e.idx + "']").find('[data-fieldname = is_pos]').css("pointer-events", "none"); + } + else { + $("[data-idx='" + e.idx + "']").find('[data-fieldname = is_pos]').css("pointer-events", "auto"); + } + }); + refresh_field("materials_required"); + }, + + setup: function (frm) { + frm.set_query('person_in_charge', function () { + return { + filters: { + 'department': ['like', 'Maintenance - %'] + } + } + }); + frm.set_query('sub_contractor_contact', function () { + return { + filters: { + 'supplier_group': 'Sub-Contractor' + } + } + }); + frappe.call({ + method: "propms.issue_hook.get_items_group", + async: false, + callback: function (r) { + if (r.message) { + let maintenance_item_group = r.message; + frm.fields_dict["materials_required"].grid.get_field("item").get_query = function (doc, cdt, cdn) { + return { + filters: [ + ["Item", "item_group", "in", maintenance_item_group], + + ] + } + } + } + } + }); + }, + property_name: function (frm, cdt, cdn) { + // frappe.msgprint(__("Testing")) + frappe.model.set_value(cdt, cdn, 'customer', ''); + if (frm.doc.property_name) { + frappe.call({ + method: 'frappe.client.get_value', + args: { + doctype: 'Property', + fieldname: 'status', + filters: { + name: frm.doc.property_name + }, + }, + async: false, + callback: function (r, rt) { + if (r.message) { + if (r.message.status.toLowerCase() == 'on lease' || r.message.status.toLowerCase() == 'off lease in 3 months') { + frappe.call({ + method: 'frappe.client.get_value', + args: { + doctype: 'Lease', + fieldname: ['name', 'customer'], + filters: { + property: frm.doc.property_name, + start_date: ["<=", frappe.datetime.nowdate()], + end_date: [">=", frappe.datetime.nowdate()] + }, + }, + async: false, + callback: function (r, rt) { + if (r.message) { + frm.set_value("customer", r.message.customer); + refresh_field("customer") + } + } + }); + } else { + frappe.db.get_value("Property", frm.doc.property_name, "unit_owner", (r) => { + frm.set_value("customer", r.unit_owner); + }); + } + } + } + }); + } + }, +}); + +frappe.ui.form.on("Issue Materials Detail", "quantity", function (frm, cdt, cdn) { + var item_row = locals[cdt][cdn]; + item_row.amount = item_row.rate * item_row.quantity; + refresh_field("materials_required"); +}); + + +frappe.ui.form.on("Issue Materials Detail", "rate", function (frm, cdt, cdn) { + var item_row = locals[cdt][cdn]; + item_row.amount = item_row.rate * item_row.quantity; + refresh_field("materials_required"); +}); + + +frappe.ui.form.on("Issue Materials Detail", "material_status", function (frm, cdt, cdn) { + var item_row = locals[cdt][cdn]; + var is_pos = $("[data-idx='" + item_row.idx + "']").find('[data-fieldname = is_pos]') + + if (item_row.material_status === "Self Consumption") { + is_pos.css("pointer-events", "none"); + item_row.is_pos = 0; + + } else { + is_pos.css("pointer-events", "auto"); + } + refresh_field("materials_required"); +}); + + +frappe.ui.form.on("Issue Materials Detail", "item", function (frm, cdt, cdn) { + var item_row = locals[cdt][cdn]; + if (!item_row.item) { + return; + } + frappe.call({ + method: "propms.issue_hook.get_item_rate", + args: { + item: item_row.item, + customer: frm.doc.customer, + }, + async: false, + callback: function (r) { + if (r.message) { + item_row.rate = r.message; + item_row.amount = item_row.rate * item_row.quantity; + refresh_field("materials_required"); + } + } + }); +}); diff --git a/propms/property_management_solution/report/invoice_details/other_methods.py b/propms/property_management_solution/report/invoice_details/other_methods.py index b5ab9611..76f181c7 100644 --- a/propms/property_management_solution/report/invoice_details/other_methods.py +++ b/propms/property_management_solution/report/invoice_details/other_methods.py @@ -183,11 +183,13 @@ def months_array(): def get_rate(invoice_name, filters): - filters_value = " and item_code= '" + filters.get("rental") + "' " + item_code = filters.get("rental") if filters.get("maintenance"): - filters_value = "and item_code = 'Service Charge - " + filters.get("rental").split()[0] + "'" - query = """ SELECT rate FROM `tabSales Invoice Item` WHERE {} {}""".format( - "parent = '" + invoice_name + "' ", filters_value - ) + item_code = "Service Charge - " + filters.get("rental").split()[0] - return frappe.db.sql(query, as_dict=True)[0].rate if len(frappe.db.sql(query, as_dict=True)) > 0 else "" + rows = frappe.db.sql( + """SELECT rate FROM `tabSales Invoice Item` WHERE parent = %(parent)s AND item_code = %(item_code)s""", + {"parent": invoice_name, "item_code": item_code}, + as_dict=True, + ) + return rows[0].rate if rows else "" diff --git a/propms/property_management_solution/report/mis_income_break_up/other_methods.py b/propms/property_management_solution/report/mis_income_break_up/other_methods.py index f45ced49..7b5e6309 100644 --- a/propms/property_management_solution/report/mis_income_break_up/other_methods.py +++ b/propms/property_management_solution/report/mis_income_break_up/other_methods.py @@ -34,6 +34,8 @@ def get_rentals(filters): return_data.append(tax) return_data.append(net_rent) for i in sum_monthly: + if i == "income": + continue tax[i] = float(sum_monthly[i]) * 0.10 net_rent[i] = sum_monthly[i] - tax[i] return return_data diff --git a/propms/property_management_solution/report/rent_invoices_details/rent_invoices_details.py b/propms/property_management_solution/report/rent_invoices_details/rent_invoices_details.py index 182a5fc7..7016d7f6 100644 --- a/propms/property_management_solution/report/rent_invoices_details/rent_invoices_details.py +++ b/propms/property_management_solution/report/rent_invoices_details/rent_invoices_details.py @@ -28,10 +28,12 @@ def execute(filters=None): def get_data(filters): rows = [] - _from_date = "'{from_date}'".format(from_date=filters["from_date"]) - _to_date = "'{to_date}'".format(to_date=filters["to_date"]) - _company = "'{company}'".format(company=filters["company"]) _items_grupe = filters.get("type_name") + values = { + "company": filters["company"], + "from_date": filters["from_date"], + "to_date": filters["to_date"], + } float_precision = cint(frappe.db.get_default("float_precision")) or 2 if filters.get("company"): default_currency = get_company_currency(filters["company"]) @@ -41,9 +43,10 @@ def get_data(filters): conditions = "" if not filters.get("extand"): - conditions = f"AND DATE(posting_date) BETWEEN {_from_date} AND {_to_date}" + conditions = "AND DATE(posting_date) BETWEEN %(from_date)s AND %(to_date)s" - query = f""" + query = ( + """ SELECT name as invoice_id, customer, @@ -57,16 +60,19 @@ def get_data(filters): `tabSales Invoice` WHERE docstatus = 1 - AND company = {_company} + AND company = %(company)s AND lease != "" AND from_date != "" AND to_date != "" AND is_return != 1 - {conditions} + """ + + conditions + + """ ORDER BY lease DESC, posting_date DESC """ + ) - sales_invoices = frappe.db.sql(query, as_dict=True) + sales_invoices = frappe.db.sql(query, values, as_dict=True) for invoice in sales_invoices: _items_rwos = [] @@ -83,9 +89,7 @@ def get_data(filters): # for key,value in months_obj.items(): # invoice[key] = value - invoice_id = "'{invoice_id}'".format(invoice_id=invoice["invoice_id"]) - - query_items = f""" + query_items = """ SELECT item_code, base_net_amount as item_total, @@ -97,10 +101,10 @@ def get_data(filters): FROM `tabSales Invoice Item` WHERE - parent = {invoice_id} + parent = %(invoice_id)s """ - items = frappe.db.sql(query_items, as_dict=True) + items = frappe.db.sql(query_items, {"invoice_id": invoice["invoice_id"]}, as_dict=True) for item in items: item_group = frappe.db.get_value("Item", item["item_code"], "item_group") item["item_group"] = item_group diff --git a/propms/property_management_solution/report/rent_invoices_details_usd/rent_invoices_details_usd.py b/propms/property_management_solution/report/rent_invoices_details_usd/rent_invoices_details_usd.py index d3fef271..b3ae0c4f 100644 --- a/propms/property_management_solution/report/rent_invoices_details_usd/rent_invoices_details_usd.py +++ b/propms/property_management_solution/report/rent_invoices_details_usd/rent_invoices_details_usd.py @@ -30,10 +30,12 @@ def execute(filters=None): def get_data(filters): rows = [] - _from_date = "'{from_date}'".format(from_date=filters["from_date"]) - _to_date = "'{to_date}'".format(to_date=filters["to_date"]) - _company = "'{company}'".format(company=filters["company"]) _items_grupe = filters.get("type_name") + values = { + "company": filters["company"], + "from_date": filters["from_date"], + "to_date": filters["to_date"], + } float_precision = cint(frappe.db.get_default("float_precision")) or 2 if filters.get("company"): default_currency = get_company_currency(filters["company"]) @@ -43,9 +45,10 @@ def get_data(filters): conditions = "" if not filters.get("extand"): - conditions = f"AND DATE(posting_date) BETWEEN {_from_date} AND {_to_date}" + conditions = "AND DATE(posting_date) BETWEEN %(from_date)s AND %(to_date)s" - query = f""" + query = ( + """ SELECT name as invoice_id, customer, @@ -59,15 +62,18 @@ def get_data(filters): `tabSales Invoice` WHERE docstatus = 1 - AND company = {_company} + AND company = %(company)s AND lease != "" AND from_date != "" AND to_date != "" - {conditions} + """ + + conditions + + """ ORDER BY lease DESC, posting_date ASC """ + ) - sales_invoices = frappe.db.sql(query, as_dict=True) + sales_invoices = frappe.db.sql(query, values, as_dict=True) for invoice in sales_invoices: _items_rwos = [] @@ -78,9 +84,7 @@ def get_data(filters): invoice.exchange_rate = get_exchange_rate("USD", default_currency, invoice.posting_date) invoice.foreign_total = invoice.total / invoice.exchange_rate - invoice_id = "'{invoice_id}'".format(invoice_id=invoice["invoice_id"]) - - query_items = f""" + query_items = """ SELECT item_code, base_net_amount as item_total, @@ -92,10 +96,10 @@ def get_data(filters): FROM `tabSales Invoice Item` WHERE - parent = {invoice_id} + parent = %(invoice_id)s """ - items = frappe.db.sql(query_items, as_dict=True) + items = frappe.db.sql(query_items, {"invoice_id": invoice["invoice_id"]}, as_dict=True) for item in items: item_group = frappe.db.get_value("Item", item["item_code"], "item_group") item["item_group"] = item_group diff --git a/propms/property_management_solution/report/subscription_service_report/subscription_service_report.js b/propms/property_management_solution/report/subscription_service_report/subscription_service_report.js index fe7e5e6b..a4acbe02 100644 --- a/propms/property_management_solution/report/subscription_service_report/subscription_service_report.js +++ b/propms/property_management_solution/report/subscription_service_report/subscription_service_report.js @@ -1,30 +1,30 @@ -// Copyright (c) 2016, Aakvatech and contributors -// For license information, please see license.txt -/* eslint-disable */ - -frappe.query_reports["Subscription Service Report"] = { - "filters": [ - { - "fieldname":"service_type", - "label": __("Service Type"), - "fieldtype": "Link", - "options": "Item", - "default": "Gym services", - "reqd": 1 - get_query: () => { - return { - filters: { - 'item_group': "Services" - } - } - } - }, - { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - "default": frappe.datetime.get_today(), - "reqd": 1 - } - ] -} +// Copyright (c) 2016, Aakvatech and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Subscription Service Report"] = { + "filters": [ + { + "fieldname":"service_type", + "label": __("Service Type"), + "fieldtype": "Link", + "options": "Item", + "default": "Gym services", + "reqd": 1, + "get_query": () => { + return { + filters: { + 'item_group': "Services" + } + } + } + }, + { + "fieldname":"to_date", + "label": __("To Date"), + "fieldtype": "Date", + "default": frappe.datetime.get_today(), + "reqd": 1 + } + ] +} diff --git a/propms/property_management_solution/report/subscription_service_report/subscription_service_report.json b/propms/property_management_solution/report/subscription_service_report/subscription_service_report.json index 18725825..a56c8ced 100644 --- a/propms/property_management_solution/report/subscription_service_report/subscription_service_report.json +++ b/propms/property_management_solution/report/subscription_service_report/subscription_service_report.json @@ -7,13 +7,13 @@ "doctype": "Report", "idx": 0, "is_standard": "Yes", - "modified": "2020-04-27 13:53:51.035254", + "modified": "2026-08-27 12:00:00.000000", "modified_by": "Administrator", "module": "Property Management Solution", "name": "Subscription Service Report", "owner": "Administrator", "prepared_report": 0, - "query": "SELECT si.name, \r\n si.customer,\r\n\t from_date,\r\n\t to_date\r\nFROM `tabSales Invoice` si LEFT JOIN `tabSales Invoice Item` sii on si.name = sii.parent\r\nWHERE si.from_date IS NOT NULL \r\nAND si.to_date IS NOT NULL \r\nAND sii.item_group = 'Services'\r\nAND si.item_name = %(service_type)s;", + "query": "SELECT si.name, \r\n si.customer,\r\n\t from_date,\r\n\t to_date\r\nFROM `tabSales Invoice` si LEFT JOIN `tabSales Invoice Item` sii on si.name = sii.parent\r\nWHERE si.from_date IS NOT NULL \r\nAND si.to_date IS NOT NULL \r\nAND sii.item_group = 'Services'\r\nAND sii.item_name = %(service_type)s;", "ref_doctype": "Sales Invoice", "report_name": "Subscription Service Report", "report_type": "Query Report", @@ -25,4 +25,4 @@ "role": "Accounts User" } ] -} \ No newline at end of file +} diff --git a/propms/property_management_solution/report/utility_invoices/other_methods.py b/propms/property_management_solution/report/utility_invoices/other_methods.py index 8b3fd06e..0f4bc26c 100644 --- a/propms/property_management_solution/report/utility_invoices/other_methods.py +++ b/propms/property_management_solution/report/utility_invoices/other_methods.py @@ -177,10 +177,9 @@ def months_array(): def get_rate(invoice_name): - filters_value = "and item_code = 'Utility Charges'" - query = """ SELECT rate FROM `tabSales Invoice Item` WHERE {} {}""".format( - "parent = '" + invoice_name + "' ", filters_value + rows = frappe.db.sql( + """SELECT rate FROM `tabSales Invoice Item` WHERE parent = %(parent)s AND item_code = 'Utility Charges'""", + {"parent": invoice_name}, + as_dict=True, ) - print(query) - - return frappe.db.sql(query, as_dict=True)[0].rate if len(frappe.db.sql(query, as_dict=True)) > 0 else "" + return rows[0].rate if rows else "" diff --git a/propms/tests/__init__.py b/propms/tests/__init__.py index 9990ee00..d5b70ae1 100644 --- a/propms/tests/__init__.py +++ b/propms/tests/__init__.py @@ -1,9 +1,11 @@ import frappe -# Skip Frappe's global test_records dependency resolution. ERPNext's -# ItemTaxTemplate test_records reference an Account whose Company -# ("_Test Company") doesn't reliably insert in this app's test bench, -# breaking run-parallel-tests before any real assertion runs. Tests in -# this module set up their own data; declare per-class -# `test_dependencies` if a specific DocType needs seeding. -frappe.flags.skip_test_records = True +TEST_COMPANY = "_Test Property Company" +TEST_ABBR = "_TPC" + + +def get_test_company() -> str: + """Company every propms test writes against.""" + if frappe.db.exists("Company", TEST_COMPANY): + return TEST_COMPANY + return frappe.defaults.get_defaults().get("company") or frappe.get_all("Company", pluck="name")[0] diff --git a/propms/tests/e2e/__init__.py b/propms/tests/e2e/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/propms/tests/e2e/test_desk_ui.py b/propms/tests/e2e/test_desk_ui.py new file mode 100644 index 00000000..01ef7b17 --- /dev/null +++ b/propms/tests/e2e/test_desk_ui.py @@ -0,0 +1,105 @@ +"""Browser checks over every propms desk surface. + +Skipped unless PROPMS_E2E_BASE_URL is set and playwright is importable:: + + PROPMS_E2E_BASE_URL=http://propms-test.localhost:8003 \ + PROPMS_E2E_PASSWORD=admin \ + python -m unittest propms.tests.e2e.test_desk_ui +""" + +import os +import unittest + +BASE_URL = os.environ.get("PROPMS_E2E_BASE_URL") +USER = os.environ.get("PROPMS_E2E_USER", "Administrator") +PASSWORD = os.environ.get("PROPMS_E2E_PASSWORD", "admin") + +try: + from playwright.sync_api import sync_playwright +except ImportError: # pragma: no cover - playwright is an optional dev dependency + sync_playwright = None + +# Console noise that says nothing about the app: the dev server has no socket.io. +IGNORED_CONSOLE = ( + "socket.io", + "Failed to load resource: the server responded with a status of 404", + # Chrome refuses the unsaved-form prompt when navigating without a gesture. + "beforeunload", +) + +ROUTES = [ + ("Lease list", "/desk/lease"), + ("Lease form", "/desk/lease/new"), + ("Property list", "/desk/property"), + ("Property tree", "/desk/property/view/tree"), + ("Property form", "/desk/property/new"), + ("Daily Checklist form", "/desk/daily-checklist/new"), + ("Key Set Detail form", "/desk/key-set-detail/new"), + ("Meter Reading form", "/desk/meter-reading/new"), + ("Outsourcing Attendance form", "/desk/outsourcing-attendance/new"), + ("Security Attendance form", "/desk/security-attendance/new"), + ("Security Attendance Details form", "/desk/security-attendance-details/new"), + ("Tool Item Record form", "/desk/tool-item-record/new"), + ("Property Management Settings", "/desk/property-management-settings"), + ("Maintenance Job Card form", "/desk/issue/new"), + ("Sales Invoice form", "/desk/sales-invoice/new"), + ("Company form", "/desk/company"), + ("Point of Sale", "/desk/point-of-sale"), + ("Workspace", "/desk/property-management-solution"), + ("Report: Lease Information", "/desk/query-report/Lease Information"), + ("Report: Property Status", "/desk/query-report/Property Status"), + ("Report: Rent Invoices Details", "/desk/query-report/Rent Invoices Details"), + ("Report: Subscription Service Report", "/desk/query-report/Subscription Service Report"), + ("Report: Security Deposit", "/desk/query-report/Security Deposit"), + ("Report: Mis-Income Break Up", "/desk/query-report/Mis-Income Break Up"), + ("Report: Debtors Report", "/desk/query-report/Debtors Report"), + ("Report: Invoice Details", "/desk/query-report/Invoice Details"), + ("Report: Utility Invoices", "/desk/query-report/Utility Invoices"), +] + + +@unittest.skipUnless(BASE_URL and sync_playwright, "PROPMS_E2E_BASE_URL or playwright is missing") +class TestDeskSurfaces(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls._playwright = sync_playwright().start() + cls.browser = cls._playwright.chromium.launch(channel="chrome", headless=True) + cls.page = cls.browser.new_context(viewport={"width": 1440, "height": 900}).new_page() + cls.page.goto(f"{BASE_URL}/login", wait_until="domcontentloaded") + cls.page.fill("#login_email", USER) + cls.page.fill("#login_password", PASSWORD) + cls.page.click("button.btn-login") + cls.page.wait_for_url("**/desk**", timeout=60000) + + @classmethod + def tearDownClass(cls): + cls.browser.close() + cls._playwright.stop() + + def visit(self, route: str) -> list: + errors = [] + + def on_console(message): + if message.type == "error" and not any(noise in message.text for noise in IGNORED_CONSOLE): + errors.append(message.text[:300]) + + def on_page_error(exception): + errors.append(f"PAGEERROR: {exception}"[:300]) + + self.page.on("console", on_console) + self.page.on("pageerror", on_page_error) + try: + self.page.goto(BASE_URL + route, wait_until="domcontentloaded", timeout=60000) + self.page.wait_for_timeout(3000) + finally: + self.page.remove_listener("console", on_console) + self.page.remove_listener("pageerror", on_page_error) + return errors + + def test_every_desk_surface_loads_without_console_errors(self): + failures = [] + for label, route in ROUTES: + errors = self.visit(route) + if errors: + failures.append(f"{label} ({route}): {errors}") + self.assertEqual(failures, [], "desk surfaces reported console errors:\n" + "\n".join(failures)) diff --git a/propms/tests/e2e/test_rest_api.py b/propms/tests/e2e/test_rest_api.py new file mode 100644 index 00000000..c9c20468 --- /dev/null +++ b/propms/tests/e2e/test_rest_api.py @@ -0,0 +1,109 @@ +"""End-to-end checks over HTTP against a running propms site. + +Skipped unless PROPMS_E2E_BASE_URL is set, for example:: + + PROPMS_E2E_BASE_URL=http://propms-test.localhost:8003 \ + PROPMS_E2E_PASSWORD=admin \ + env/bin/python -m unittest discover -s propms/tests/e2e -t . +""" + +import json +import os +import unittest + +import requests + +BASE_URL = os.environ.get("PROPMS_E2E_BASE_URL") +USER = os.environ.get("PROPMS_E2E_USER", "Administrator") +PASSWORD = os.environ.get("PROPMS_E2E_PASSWORD", "admin") + + +@unittest.skipUnless(BASE_URL, "PROPMS_E2E_BASE_URL is not set") +class TestPropmsRestApi(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.session = requests.Session() + response = cls.session.post( + f"{BASE_URL}/api/method/login", data={"usr": USER, "pwd": PASSWORD}, timeout=30 + ) + response.raise_for_status() + + def call(self, method: str, **params): + response = self.session.get(f"{BASE_URL}/api/method/{method}", params=params, timeout=60) + self.assertEqual(response.status_code, 200, response.text[:400]) + return response.json().get("message") + + def resource(self, doctype: str, **params): + response = self.session.get(f"{BASE_URL}/api/resource/{doctype}", params=params, timeout=60) + self.assertEqual(response.status_code, 200, response.text[:400]) + return response.json()["data"] + + def test_lease_resource_lists(self): + self.assertIsInstance(self.resource("Lease", limit_page_length=5), list) + + def test_property_resource_lists(self): + self.assertIsInstance(self.resource("Property", limit_page_length=5), list) + + def test_property_management_settings_is_readable(self): + response = self.session.get( + f"{BASE_URL}/api/resource/Property Management Settings/Property Management Settings", timeout=30 + ) + self.assertEqual(response.status_code, 200, response.text[:400]) + + def test_maintenance_item_groups_endpoint(self): + self.assertIsInstance(self.call("propms.issue_hook.get_items_group"), list) + + def test_stock_availability_endpoint(self): + value = self.call( + "propms.issue_hook.get_stock_availability", + item_code="_Test Maintenance Item", + company="_Test Property Company", + is_pos=0, + ) + self.assertIsNotNone(value) + + def test_date_helper_endpoints(self): + self.assertEqual( + self.call("propms.auto_custom.getMonthADD", date="2026-01-31", month=1), "2026-02-28" + ) + self.assertEqual( + self.call("propms.auto_custom.getDateDiff", date1="2026-01-10", date2="2026-01-01"), 9 + ) + self.assertEqual(self.call("propms.auto_custom.getNumberOfDays", date="2026-02-05"), 28) + + def test_latest_active_lease_endpoint(self): + self.assertIsNotNone( + self.call("propms.auto_custom.get_latest_active_lease", property_id="_Test POS Property") + ) + + def test_property_tree_children_endpoint(self): + rows = self.call( + "propms.property_management_solution.doctype.property.property.get_children", + doctype="Property", + parent="", + company="_Test Property Company", + is_root=1, + ) + self.assertIsInstance(rows, list) + + def test_query_report_runs_over_http(self): + response = self.session.get( + f"{BASE_URL}/api/method/frappe.desk.query_report.run", + params={ + "report_name": "Property Status", + "filters": json.dumps({"property_type": "%", "owner_type": "%"}), + "ignore_prepared_report": "1", + }, + timeout=120, + ) + self.assertEqual(response.status_code, 200, response.text[:400]) + self.assertIn("columns", response.json()["message"]) + + def test_desk_workspace_is_served(self): + response = self.session.get(f"{BASE_URL}/desk/property-management-solution", timeout=60) + self.assertEqual(response.status_code, 200) + + def test_logout_requires_post(self): + """v16 made logout POST-only.""" + response = self.session.get(f"{BASE_URL}/api/method/logout", timeout=30) + self.assertIn(response.status_code, (403, 404, 405), response.text[:200]) diff --git a/propms/tests/js/form_scripts.test.js b/propms/tests/js/form_scripts.test.js new file mode 100644 index 00000000..d9aa138e --- /dev/null +++ b/propms/tests/js/form_scripts.test.js @@ -0,0 +1,84 @@ +import { beforeEach, describe, expect, it } from "vitest"; + +import { loadClientScript, readClientScript } from "./load.js"; +import { installGlobals, makeForm, makeFrappe } from "./setup.js"; + +describe("Company form script", () => { + let frappe; + + beforeEach(() => { + frappe = installGlobals(makeFrappe()); + globalThis.cur_frm = makeForm(); + loadClientScript("propms/property_management_solution/company.js"); + }); + + it("scopes every propms account picker to the company", () => { + const frm = makeForm({ doctype: "Company", name: "_Test Property Company" }); + frappe._handlers.Company.setup(frm); + const fields = Object.keys(frm._queries); + expect(fields).toEqual([ + "security_account_code", + "default_tax_account_head", + "default_tax_template", + "default_maintenance_tax_template", + ]); + for (const field of fields) { + expect(frm._queries[field]().filters).toEqual([["company", "=", "_Test Property Company"]]); + } + }); +}); + +describe("Sales Invoice form script", () => { + let frappe; + + beforeEach(() => { + frappe = installGlobals(makeFrappe()); + globalThis.cur_frm = makeForm(); + loadClientScript("propms/property_management_solution/sales_invoice.js"); + }); + + it("clears the customer when no cost centre is set", () => { + const frm = makeForm({ doctype: "Sales Invoice", cost_center: "" }); + frappe._handlers["Sales Invoice"].property_name(frm, "Sales Invoice", "SI-1"); + expect(frappe.model.set_value).toHaveBeenLastCalledWith("Sales Invoice", "SI-1", "customer", ""); + }); + + it("looks up the property status before the lease customer", () => { + const frm = makeForm({ doctype: "Sales Invoice", cost_center: "Main - _TPC" }); + frappe._handlers["Sales Invoice"].property_name(frm, "Sales Invoice", "SI-1"); + expect(frappe._calls[0].args.doctype).toBe("Property"); + expect(frappe._calls[0].args.fieldname).toBe("status"); + }); +}); + +describe("Property tree view settings", () => { + let frappe; + + beforeEach(() => { + frappe = installGlobals(makeFrappe()); + loadClientScript("propms/property_management_solution/doctype/property/property_tree.js"); + }); + + it("points at the app's own tree endpoints", () => { + const settings = frappe.treeview_settings.Property; + expect(settings.get_tree_nodes).toBe( + "propms.property_management_solution.doctype.property.property.get_children" + ); + expect(settings.add_tree_node).toBe( + "propms.property_management_solution.doctype.property.property.add_node" + ); + }); + + it("names the new-node field after the DocType's autoname field", () => { + const settings = frappe.treeview_settings.Property; + const nameField = settings.fields.find((field) => field.fieldtype === "Data"); + expect(nameField.fieldname).toBe("name1"); + }); +}); + +describe("Issue form script", () => { + it("formats the stock shortage message with placeholders, not interpolation", () => { + const source = readClientScript("propms/property_management_solution/issue.js"); + expect(source).toMatch(/__\("Existing stock quantity of item \{0\} is \{1\}/); + }); +}); diff --git a/propms/tests/js/lease_form.test.js b/propms/tests/js/lease_form.test.js new file mode 100644 index 00000000..65cfc3c4 --- /dev/null +++ b/propms/tests/js/lease_form.test.js @@ -0,0 +1,95 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { loadClientScript } from "./load.js"; +import { installGlobals, makeForm, makeFrappe } from "./setup.js"; + +const LEASE_JS = "propms/property_management_solution/doctype/lease/lease.js"; + +describe("Lease form script", () => { + let frappe; + + beforeEach(() => { + frappe = installGlobals(makeFrappe()); + globalThis.cur_frm = makeForm(); + loadClientScript(LEASE_JS); + }); + + it("registers handlers on the Lease form", () => { + expect(Object.keys(frappe._handlers.Lease)).toEqual( + expect.arrayContaining(["setup", "refresh", "custom_set_intro"]) + ); + }); + + it("restricts lease items to the Lease Items group", () => { + const frm = makeForm(); + frappe._handlers.Lease.setup(frm); + expect(frm._queries.lease_item()).toEqual({ + filters: [["item_group", "=", "Lease Items"]], + }); + }); + + it("scopes the property picker to the lease company", () => { + const frm = makeForm({ company: "_Test Property Company" }); + frappe._handlers.Lease.setup(frm); + expect(frm._queries.property().filters.company).toBe("_Test Property Company"); + }); + + it("adds the three Actions buttons on refresh", () => { + const frm = makeForm(); + globalThis.cur_frm = frm; + frappe._handlers.Lease.refresh(frm); + const actions = frm._buttons.filter((button) => button.group === "Actions").map((b) => b.label); + expect(actions).toEqual([ + "Make Invoice Schedule", + "Generate Pending Invoice", + "Make Invoice Schedule for all Lease", + ]); + }); + + it("adds the report buttons only for a saved lease", () => { + const saved = makeForm({ __islocal: 0, lease_customer: "_Test Tenant" }); + globalThis.cur_frm = saved; + frappe._handlers.Lease.refresh(saved); + const views = saved._buttons.filter((button) => button.group === "View").map((b) => b.label); + expect(views).toEqual(["Accounts Receivable", "Accounting Ledger"]); + + const unsaved = makeForm({ __islocal: 1 }); + globalThis.cur_frm = unsaved; + frappe._handlers.Lease.refresh(unsaved); + expect(unsaved._buttons.filter((button) => button.group === "View")).toHaveLength(0); + }); + + it("routes the Accounts Receivable button at the lease customer", () => { + const frm = makeForm({ lease_customer: "_Test Tenant" }); + globalThis.cur_frm = frm; + frappe._handlers.Lease.refresh(frm); + frm._buttons.find((button) => button.label === "Accounts Receivable").action(); + expect(frappe.set_route).toHaveBeenCalledWith("query-report", "Accounts Receivable", { + party_type: "Customer", + party: "_Test Tenant", + }); + }); + + it("calls the schedule builder with the lease name", () => { + const frm = makeForm({ name: "_Test Property-00001" }); + globalThis.cur_frm = frm; + frappe._handlers.Lease.refresh(frm); + frm._buttons.find((button) => button.label === "Make Invoice Schedule").action(); + expect(frappe._calls[0].method).toBe( + "propms.property_management_solution.doctype.lease.lease.make_lease_invoice_schedule" + ); + expect(frappe._calls[0].args).toEqual({ leasedoc: "_Test Property-00001" }); + }); + + it("links the renewal banner at a /desk route", async () => { + frappe.db.get_value = vi.fn(() => + Promise.resolve({ message: { renewal_initiated_by: "a@b.c", creation: "2026-01-01" } }) + ); + const frm = makeForm({ name: "L-2", renewed_from: "L-1" }); + globalThis.cur_frm = frm; + frappe._handlers.Lease.custom_set_intro(frm); + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(frm._intro.message).toContain("/desk/lease/L-1"); + expect(frm._intro.message).not.toContain("/app/"); + }); +}); diff --git a/propms/tests/js/load.js b/propms/tests/js/load.js new file mode 100644 index 00000000..951cc888 --- /dev/null +++ b/propms/tests/js/load.js @@ -0,0 +1,35 @@ +// Executes a desk client script against the mocked globals in setup.js. +import fs from "node:fs"; +import path from "node:path"; +import vm from "node:vm"; + +const APP_ROOT = path.resolve(import.meta.dirname, "../../.."); + +export function loadClientScript(relativePath) { + const source = fs.readFileSync(path.join(APP_ROOT, relativePath), "utf8"); + // v16 evaluates page, report and chart scripts as IIFEs, so nothing a file + // declares at the top level may leak into the global scope. + const filename = path.join(APP_ROOT, relativePath); + vm.runInThisContext(`(function () {\n${source}\n})();`, { filename }); +} + +export function readClientScript(relativePath) { + return fs.readFileSync(path.join(APP_ROOT, relativePath), "utf8"); +} + +export function listClientScripts() { + const found = []; + const walk = (dir) => { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + if (entry.name === "node_modules" || entry.name === "js") continue; + walk(full); + } else if (entry.name.endsWith(".js")) { + found.push(path.relative(APP_ROOT, full)); + } + } + }; + walk(path.join(APP_ROOT, "propms")); + return found.sort(); +} diff --git a/propms/tests/js/point_of_sale.test.js b/propms/tests/js/point_of_sale.test.js new file mode 100644 index 00000000..158c266e --- /dev/null +++ b/propms/tests/js/point_of_sale.test.js @@ -0,0 +1,106 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { loadClientScript } from "./load.js"; +import { installGlobals, makeFrappe } from "./setup.js"; + +const POS_JS = "propms/property_management_solution/point_of_sale.js"; + +function makePosClasses() { + class ItemCart { + constructor() { + this.$customer_section = { append: vi.fn(), find: vi.fn(() => ({})) }; + this.customer_field = { set_value: vi.fn() }; + this.frm = { doc: { items: [{}, {}] }, set_value: vi.fn() }; + this.events = { get_frm: () => this.frm }; + } + + make_customer_selector() { + this.customer_selector_called = true; + } + } + class Controller { + constructor() { + this.frm = { doc: { items: [{}, {}] }, is_dirty: () => false }; + this.checked_out = false; + } + + async save_and_checkout() { + this.checked_out = true; + } + } + return { ItemCart, Controller }; +} + +describe("Point of Sale cost centre patch", () => { + let frappe; + let ItemCart; + let Controller; + + beforeEach(() => { + frappe = installGlobals(makeFrappe()); + ({ ItemCart, Controller } = makePosClasses()); + globalThis.erpnext.PointOfSale = { ItemCart, Controller }; + loadClientScript(POS_JS); + }); + + it("waits for the point-of-sale bundle before patching", () => { + expect(frappe._required).toContain("point-of-sale.bundle.js"); + }); + + it("keeps the stock customer selector working", () => { + const cart = new ItemCart(); + cart.make_customer_selector(); + expect(cart.customer_selector_called).toBe(true); + }); + + it("adds a non-group Cost Center control to the cart", () => { + const cart = new ItemCart(); + cart.make_customer_selector(); + const control = frappe.ui.form.make_control.mock.calls[0][0]; + expect(control.df.fieldtype).toBe("Link"); + expect(control.df.options).toBe("Cost Center"); + expect(control.df.get_query()).toEqual({ filters: { is_group: 0 } }); + }); + + it("stamps the chosen cost centre on the invoice and every item", () => { + const cart = new ItemCart(); + cart.make_customer_selector(); + cart.propms_apply_cost_center("Main - _TPC"); + expect(cart.frm.doc.cost_center).toBe("Main - _TPC"); + expect(cart.frm.doc.items.every((item) => item.cost_center === "Main - _TPC")).toBe(true); + }); + + it("looks the customer up from the property behind the cost centre", async () => { + frappe.call = vi.fn(() => Promise.resolve({ message: { customer: "_Test Tenant" } })); + const cart = new ItemCart(); + cart.make_customer_selector(); + cart.propms_apply_cost_center("Main - _TPC"); + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(frappe.call.mock.calls[0][0].method).toBe("propms.pos.get_pos_data"); + expect(cart.frm.set_value).toHaveBeenCalledWith("customer", "_Test Tenant"); + }); + + it("falls back to the cash customer when no lease is found", async () => { + frappe.call = vi.fn(() => Promise.resolve({ message: null })); + const cart = new ItemCart(); + cart.make_customer_selector(); + cart.propms_apply_cost_center("Main - _TPC"); + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(cart.frm.set_value).toHaveBeenCalledWith("customer", "Cash Customer"); + }); + + it("re-stamps the cost centre at checkout and still checks out", async () => { + const controller = new Controller(); + controller.cart = { cost_center_field: { get_value: () => "Main - _TPC" } }; + await controller.save_and_checkout(); + expect(controller.frm.doc.cost_center).toBe("Main - _TPC"); + expect(controller.checked_out).toBe(true); + }); + + it("is applied only once", () => { + loadClientScript(POS_JS); + const cart = new ItemCart(); + cart.make_customer_selector(); + expect(frappe.ui.form.make_control).toHaveBeenCalledTimes(1); + }); +}); diff --git a/propms/tests/js/scripts.test.js b/propms/tests/js/scripts.test.js new file mode 100644 index 00000000..aa596974 --- /dev/null +++ b/propms/tests/js/scripts.test.js @@ -0,0 +1,71 @@ +import { beforeEach, describe, expect, it } from "vitest"; + +import { listClientScripts, loadClientScript, readClientScript } from "./load.js"; +import { installGlobals, makeForm, makeFrappe } from "./setup.js"; + +const APP_SCRIPTS = listClientScripts(); +// v16 evaluates report, page and chart scripts as IIFEs, so anything these +// files declare at the top level no longer reaches the global scope. +const IIFE_SCRIPTS = APP_SCRIPTS.filter( + (file) => file.includes("/report/") || file.endsWith("point_of_sale.js") +); + +describe("client script inventory", () => { + it("finds every shipped script", () => { + expect(APP_SCRIPTS.length).toBeGreaterThan(0); + }); +}); + +describe("every client script", () => { + beforeEach(() => { + installGlobals(makeFrappe()); + }); + + it.each(APP_SCRIPTS)("%s is syntactically valid", (file) => { + expect(() => new Function(readClientScript(file))).not.toThrow(); + }); + + it.each(APP_SCRIPTS)("%s uses /desk routes, not the removed /app prefix", (file) => { + expect(readClientScript(file)).not.toMatch(/["'`]\/app\//); + }); + + it.each(APP_SCRIPTS)("%s does not translate an interpolated template literal", (file) => { + expect(readClientScript(file)).not.toMatch(/__\(`[^`]*\$\{/); + }); + + it.each(APP_SCRIPTS)("%s does not use the removed offline POS API", (file) => { + const source = readClientScript(file); + expect(source).not.toMatch(/erpnext\.pos\.PointOfSale/); + expect(source).not.toMatch(/\bPOSCart\b/); + expect(source).not.toMatch(/\.extend\(\{/); + }); +}); + +describe("scripts evaluated as IIFEs", () => { + beforeEach(() => { + installGlobals(makeFrappe()); + }); + + it.each(IIFE_SCRIPTS)("%s runs wrapped in an IIFE without a global", (file) => { + expect(() => loadClientScript(file)).not.toThrow(); + }); +}); + +describe("every script executes against the desk globals", () => { + beforeEach(() => { + installGlobals(makeFrappe()); + // Desk form scripts run with a current form in scope; the browser sweep + // confirmed frappe sets cur_frm before it evaluates them. + globalThis.cur_frm = makeForm(); + globalThis.cur_frm.fields_dict = new Proxy( + {}, + { + get: () => ({ grid: { get_field: () => ({}) } }), + } + ); + }); + + it.each(APP_SCRIPTS)("%s runs without throwing", (file) => { + expect(() => loadClientScript(file)).not.toThrow(); + }); +}); diff --git a/propms/tests/js/setup.js b/propms/tests/js/setup.js new file mode 100644 index 00000000..215411a3 --- /dev/null +++ b/propms/tests/js/setup.js @@ -0,0 +1,123 @@ +// Minimal stand-in for the desk globals every propms client script expects. +import { vi } from "vitest"; + +export function makeFrappe() { + const frappe = { + _handlers: {}, + _calls: [], + _required: [], + provide(path) { + path.split(".").reduce((parent, key) => (parent[key] = parent[key] || {}), globalThis); + }, + ui: { + form: { + on(doctype, handlers) { + frappe._handlers[doctype] = Object.assign(frappe._handlers[doctype] || {}, handlers); + }, + make_control: vi.fn((options) => ({ + df: options.df, + value: "", + get_value() { + return this.value; + }, + set_value(value) { + this.value = value; + return Promise.resolve(); + }, + set_focus: vi.fn(), + toggle_label: vi.fn(), + })), + }, + }, + call: vi.fn((options) => { + frappe._calls.push(options); + if (options.callback) options.callback({ message: undefined }); + return Promise.resolve({ message: undefined }); + }), + require: vi.fn((asset, callback) => { + frappe._required.push(asset); + callback && callback(); + }), + db: { + get_value: vi.fn(() => Promise.resolve({ message: {} })), + }, + model: { set_value: vi.fn() }, + msgprint: vi.fn(), + throw: vi.fn((message) => { + throw new Error(message); + }), + confirm: vi.fn(), + set_route: vi.fn(), + show_alert: vi.fn(), + user_defaults: {}, + utils: { escape_html: (value) => value }, + treeview_settings: {}, + query_reports: {}, + pages: {}, + dom: { freeze: vi.fn(), unfreeze: vi.fn() }, + datetime: { + get_today: () => "2026-01-01", + now_datetime: () => "2026-01-01 00:00:00", + month_start: () => "2026-01-01", + month_end: () => "2026-01-31", + year_start: () => "2026-01-01", + add_months: (date) => date, + }, + defaults: { + get_user_default: () => "", + }, + session: { user: "Administrator" }, + }; + return frappe; +} + +export function makeForm(doc = {}) { + return { + doc: Object.assign({ doctype: "Lease", __islocal: 0 }, doc), + _buttons: [], + _queries: {}, + _intro: null, + add_custom_button(label, action, group) { + this._buttons.push({ label, action, group }); + }, + set_query(fieldname, arg1, arg2) { + this._queries[fieldname] = arg2 || arg1; + }, + set_value: vi.fn(), + set_intro(message, colour) { + this._intro = { message, colour }; + }, + trigger(event) { + const handler = globalThis.frappe._handlers[this.doc.doctype][event]; + return handler && handler(this); + }, + add_child(fieldname) { + this.doc[fieldname] = this.doc[fieldname] || []; + const row = {}; + this.doc[fieldname].push(row); + return row; + }, + reload_doc: vi.fn(), + add_fetch: vi.fn(), + set_df_property: vi.fn(), + fields_dict: {}, + }; +} + +export function installGlobals(frappe) { + globalThis.frappe = frappe; + globalThis.__ = (text, args) => + args ? args.reduce((out, value, index) => out.split(`{${index}}`).join(value), text) : text; + globalThis.cur_frm = undefined; + globalThis.erpnext = { + PointOfSale: {}, + get_presentation_currency_list: () => ["TZS", "USD"], + utils: {}, + }; + globalThis.moment = (value) => ({ format: () => String(value) }); + globalThis.locals = {}; + globalThis.refresh_field = () => {}; + globalThis.cint = (value) => parseInt(value, 10) || 0; + globalThis.flt = (value) => parseFloat(value) || 0; + return frappe; +} diff --git a/propms/tests/test_auto_custom.py b/propms/tests/test_auto_custom.py new file mode 100644 index 00000000..306f4a01 --- /dev/null +++ b/propms/tests/test_auto_custom.py @@ -0,0 +1,169 @@ +import frappe +from frappe.tests import IntegrationTestCase, UnitTestCase +from frappe.utils import add_days, nowdate + +from propms.auto_custom import ( + changeStatusKeyset, + diff_month, + get_active_meter_from_property, + get_cost_center, + get_item_details, + get_latest_active_lease, + get_previous_meter_reading, + getDateDiff, + getMonthADD, + getNumberOfDays, + statusChangeAfterLeaseExpire, + statusChangeBeforeLeaseExpire, + update_property_status, + validateSalesInvoiceItemDuplication, +) +from propms.tests.utils import create_customer, create_item, create_lease, create_property + + +class TestDateHelpers(UnitTestCase): + def test_get_month_add(self): + self.assertEqual(str(getMonthADD("2026-01-31", 1)), "2026-02-28") + + def test_get_date_diff(self): + self.assertEqual(getDateDiff("2026-01-10", "2026-01-01"), 9) + + def test_get_number_of_days(self): + self.assertEqual(getNumberOfDays("2026-02-05"), 28) + + def test_diff_month(self): + from datetime import datetime + + self.assertEqual(diff_month(datetime(2026, 4, 10), datetime(2026, 1, 10)), 3) + + def test_get_item_details_shape(self): + rows = get_item_details("Water", 12, "2026-01-01", "2026-01-31") + self.assertEqual(rows[0]["item_code"], "Water") + self.assertEqual(rows[0]["qty"], 12) + + +class TestSalesInvoiceDuplication(IntegrationTestCase): + def test_duplicate_item_rejected(self): + doc = frappe.get_doc( + { + "doctype": "Sales Order", + "items": [{"item_code": "A", "name": "row1"}, {"item_code": "A", "name": "row2"}], + } + ) + with self.assertRaises(frappe.ValidationError): + validateSalesInvoiceItemDuplication(doc, "validate") + + def test_distinct_items_pass(self): + doc = frappe.get_doc( + { + "doctype": "Sales Order", + "items": [{"item_code": "A", "name": "row1"}, {"item_code": "B", "name": "row2"}], + } + ) + validateSalesInvoiceItemDuplication(doc, "validate") + + +class TestLeaseLookups(IntegrationTestCase): + def test_latest_active_lease(self): + name = create_property("_Test Lookup Property") + lease = create_lease(name) + self.assertEqual(get_latest_active_lease(name), lease.name) + + def test_latest_active_lease_without_lease(self): + name = create_property("_Test Empty Lookup Property") + self.assertEqual(get_latest_active_lease(name), "") + + def test_get_cost_center(self): + name = create_property("_Test Cost Centre Lookup Property") + self.assertEqual(get_cost_center(name), frappe.db.get_value("Property", name, "cost_center")) + + +class TestPropertyStatusSchedulers(IntegrationTestCase): + def test_status_change_before_lease_expire(self): + name = create_property("_Test Expiry Soon Property") + create_lease(name, end_date=add_days(nowdate(), 30)) + frappe.db.set_value("Property", name, "status", "On Lease") + statusChangeBeforeLeaseExpire() + self.assertEqual(frappe.db.get_value("Property", name, "status"), "Off Lease in 3 Months") + + def test_status_change_after_lease_expire(self): + name = create_property("_Test Vacated Property") + create_lease(name, start_date=add_days(nowdate(), -60), end_date=add_days(nowdate(), -1)) + frappe.db.set_value("Property", name, "status", "On Lease") + statusChangeAfterLeaseExpire() + self.assertEqual(frappe.db.get_value("Property", name, "status"), "Available") + + def test_update_property_status_marks_leased_property(self): + name = create_property("_Test Occupied Property") + lease = create_lease(name, start_date=add_days(nowdate(), -1)) + frappe.db.set_value("Lease", lease.name, "lease_status", "Active") + frappe.db.set_value("Property", name, "status", "Available") + update_property_status() + self.assertEqual(frappe.db.get_value("Property", name, "status"), "On Lease") + + +class TestKeySet(IntegrationTestCase): + def make_key_set(self): + doc = frappe.get_doc( + {"doctype": "Key Set", "set_name": "_Test Keys", "shelf_no": "S1", "location_no": "L1"} + ) + doc.insert(ignore_permissions=True) + return doc + + def test_keyset_status_follows_detail(self): + key_set = self.make_key_set() + detail = frappe.get_doc( + { + "doctype": "Key Set Detail", + "key_set": key_set.name, + "staff_type": "Others", + "taken_by": "_Test Guard", + "reason_for_key_taken": "Inspection", + "returned": 0, + } + ) + detail.insert(ignore_permissions=True) + changeStatusKeyset(detail, "on_change") + self.assertEqual(frappe.db.get_value("Key Set", key_set.name, "status"), "Out") + + detail.returned = 1 + changeStatusKeyset(detail, "on_change") + self.assertEqual(frappe.db.get_value("Key Set", key_set.name, "status"), "In") + + +class TestMeterReading(IntegrationTestCase): + def make_meter(self, property_name, meter_number="_Test Meter 1"): + item = create_item("_Test Water", is_stock_item=0) + meter = frappe.get_doc( + {"doctype": "Meter", "meter_number": meter_number, "meter_type": item, "status": "Active"} + ) + meter.insert(ignore_permissions=True) + prop = frappe.get_doc("Property", property_name) + prop.append( + "property_meter_reading", + { + "meter_number": meter.name, + "meter_type": item, + "installation_date": add_days(nowdate(), -30), + "initial_meter_reading": 100, + "invoice_customer": create_customer(), + "status": "Active", + }, + ) + prop.save(ignore_permissions=True) + return meter, item + + def test_active_meter_lookup(self): + name = create_property("_Test Meter Property") + meter, item = self.make_meter(name) + self.assertEqual(get_active_meter_from_property(name, item), meter.name) + + def test_previous_reading_falls_back_to_initial(self): + name = create_property("_Test Initial Meter Property") + meter, item = self.make_meter(name, "_Test Meter 2") + reading = get_previous_meter_reading(meter.name, name, item) + self.assertEqual(reading["previous_reading"], 100) + + def test_previous_reading_without_meter_returns_zero(self): + name = create_property("_Test No Meter Property") + self.assertEqual(get_previous_meter_reading("_Nope", name, "_Nope"), 0) diff --git a/propms/tests/test_doctype_controllers.py b/propms/tests/test_doctype_controllers.py new file mode 100644 index 00000000..d27144e3 --- /dev/null +++ b/propms/tests/test_doctype_controllers.py @@ -0,0 +1,66 @@ +import frappe +from frappe.tests import IntegrationTestCase + +from propms.tests.test_doctype_schema import load_doctypes + + +class TestDocTypeControllers(IntegrationTestCase): + """Every shipped controller class must import and construct on version 16.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.doctypes = load_doctypes() + + def test_controller_modules_import(self): + broken = [] + for doctype in self.doctypes: + try: + frappe.get_module( + f"propms.property_management_solution.doctype." + f"{frappe.scrub(doctype['name'])}.{frappe.scrub(doctype['name'])}" + ) + except Exception as exc: # noqa: BLE001 + broken.append(f"{doctype['name']}: {type(exc).__name__}: {exc}") + self.assertEqual(broken, [], f"controller modules failed to import: {broken}") + + def test_controller_classes_resolve(self): + from frappe.model.base_document import get_controller + + broken = [] + for doctype in self.doctypes: + try: + self.assertTrue(callable(get_controller(doctype["name"]))) + except Exception as exc: # noqa: BLE001 + broken.append(f"{doctype['name']}: {type(exc).__name__}: {exc}") + self.assertEqual(broken, [], f"controllers failed to resolve: {broken}") + + def test_new_documents_can_be_constructed(self): + broken = [] + for doctype in self.doctypes: + if doctype.get("issingle"): + continue + try: + doc = frappe.new_doc(doctype["name"]) + self.assertEqual(doc.doctype, doctype["name"]) + except Exception as exc: # noqa: BLE001 + broken.append(f"{doctype['name']}: {type(exc).__name__}: {exc}") + self.assertEqual(broken, [], f"new_doc failed: {broken}") + + def test_singles_load(self): + for doctype in self.doctypes: + if not doctype.get("issingle"): + continue + self.assertEqual(frappe.get_single(doctype["name"]).doctype, doctype["name"]) + + def test_list_views_load_for_every_doctype(self): + """v16 moved get_list onto the query builder; every DocType must still list.""" + broken = [] + for doctype in self.doctypes: + if doctype.get("issingle") or doctype.get("istable"): + continue + try: + frappe.get_list(doctype["name"], limit=1, ignore_permissions=True) + except Exception as exc: # noqa: BLE001 + broken.append(f"{doctype['name']}: {type(exc).__name__}: {exc}") + self.assertEqual(broken, [], f"list view queries failed: {broken}") diff --git a/propms/tests/test_doctype_schema.py b/propms/tests/test_doctype_schema.py new file mode 100644 index 00000000..bb1ad0a7 --- /dev/null +++ b/propms/tests/test_doctype_schema.py @@ -0,0 +1,70 @@ +import json +import os + +import frappe +from frappe.tests import IntegrationTestCase + +LINK_FIELDTYPES = {"Link", "Table", "Table MultiSelect", "Tree Select"} + + +def app_doctype_files() -> list: + root = frappe.get_app_path("propms", "property_management_solution", "doctype") + paths = [] + for folder in sorted(os.listdir(root)): + path = os.path.join(root, folder, f"{folder}.json") + if os.path.isfile(path): + paths.append(path) + return paths + + +def load_doctypes() -> list: + doctypes = [] + for path in app_doctype_files(): + with open(path) as handle: + doctypes.append(json.load(handle)) + return doctypes + + +class TestDocTypeSchema(IntegrationTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.doctypes = load_doctypes() + + def test_every_doctype_is_installed(self): + missing = [d["name"] for d in self.doctypes if not frappe.db.exists("DocType", d["name"])] + self.assertEqual(missing, [], f"DocTypes not installed on the site: {missing}") + + def test_link_targets_exist(self): + """A Link or Table field pointing at a missing DocType aborts the whole test run.""" + broken = [] + for doctype in self.doctypes: + for field in doctype.get("fields", []): + options = (field.get("options") or "").strip() + if field.get("fieldtype") in LINK_FIELDTYPES and options: + if not frappe.db.exists("DocType", options): + broken.append(f"{doctype['name']}.{field.get('fieldname')} -> {options}") + self.assertEqual(broken, [], f"link fields target missing DocTypes: {broken}") + + def test_no_myisam_tables(self): + """MyISAM has no transactions, so its rows survive every test rollback.""" + myisam = [d["name"] for d in self.doctypes if d.get("engine") == "MyISAM"] + self.assertEqual(myisam, [], f"DocTypes still on MyISAM: {myisam}") + + def test_naming_series_fields_have_options(self): + broken = [] + for doctype in self.doctypes: + if not str(doctype.get("autoname") or "").startswith("naming_series:"): + continue + field = next( + (f for f in doctype.get("fields", []) if f.get("fieldname") == "naming_series"), None + ) + if not field or not (field.get("options") or "").strip(): + broken.append(doctype["name"]) + self.assertEqual(broken, [], f"autoname=naming_series: without a series option: {broken}") + + def test_every_doctype_can_be_described(self): + """frappe.get_meta() resolves fetch_from, links and permissions.""" + for doctype in self.doctypes: + meta = frappe.get_meta(doctype["name"]) + self.assertEqual(meta.name, doctype["name"]) diff --git a/propms/tests/test_hooks.py b/propms/tests/test_hooks.py new file mode 100644 index 00000000..fb8d2331 --- /dev/null +++ b/propms/tests/test_hooks.py @@ -0,0 +1,67 @@ +import os + +import frappe +from frappe.tests import IntegrationTestCase + +from propms import hooks + + +def as_list(value): + return value if isinstance(value, list) else [value] + + +class TestHooks(IntegrationTestCase): + """hooks.py must point at code and files that exist on version 16.""" + + def test_doc_event_targets_resolve(self): + for doctype, events in hooks.doc_events.items(): + self.assertTrue(frappe.db.exists("DocType", doctype), f"unknown DocType in doc_events: {doctype}") + for event, handlers in events.items(): + for handler in as_list(handlers): + self.assertTrue( + callable(frappe.get_attr(handler)), + f"{doctype}.{event} -> {handler} is not callable", + ) + + def test_scheduler_event_targets_resolve(self): + for key, value in hooks.scheduler_events.items(): + handlers = [] + if isinstance(value, dict): + for cron_handlers in value.values(): + handlers.extend(cron_handlers) + else: + handlers.extend(value) + for handler in handlers: + self.assertTrue(callable(frappe.get_attr(handler)), f"{key} -> {handler} is not callable") + + def test_scheduler_cron_key_is_lowercase(self): + self.assertIn("cron", hooks.scheduler_events) + self.assertNotIn("Cron", hooks.scheduler_events) + + def test_install_and_migrate_hooks_resolve(self): + for handler in list(hooks.after_install) + list(hooks.after_migrate): + self.assertTrue(callable(frappe.get_attr(handler)), f"{handler} is not callable") + + def test_before_tests_hook_resolves(self): + self.assertTrue(callable(frappe.get_attr(hooks.before_tests))) + + def test_doctype_js_files_exist(self): + for doctype, path in hooks.doctype_js.items(): + self.assertTrue(frappe.db.exists("DocType", doctype), f"unknown DocType in doctype_js: {doctype}") + self.assertTrue( + os.path.isfile(frappe.get_app_path("propms", path)), f"missing doctype_js asset: {path}" + ) + + def test_page_js_files_exist(self): + for page, path in hooks.page_js.items(): + self.assertTrue( + os.path.isfile(frappe.get_app_path("propms", path)), + f"missing page_js asset for {page}: {path}", + ) + + def test_page_js_targets_existing_pages(self): + """The legacy offline `pos` page was removed from ERPNext before version 16.""" + for page in hooks.page_js: + self.assertTrue( + frappe.db.exists("Page", page), f"page_js targets a page that does not exist: {page}" + ) diff --git a/propms/tests/test_install.py b/propms/tests/test_install.py new file mode 100644 index 00000000..62b4e2fd --- /dev/null +++ b/propms/tests/test_install.py @@ -0,0 +1,46 @@ +import frappe +from frappe.tests import IntegrationTestCase + +from propms.utils.create_custom_fields import execute as create_custom_fields +from propms.utils.create_custom_fields import export_custom_fields +from propms.utils.create_property_setter import execute as create_property_setters + +APP_CUSTOM_FIELDS = [ + ("Issue", "property_name"), + ("Issue", "materials_billed"), + ("Issue", "materials_required"), + ("Sales Invoice", "lease"), + ("Sales Invoice", "lease_item"), + ("Sales Invoice", "job_card"), + ("Company", "default_maintenance_tax_template"), + ("Company", "security_account_code"), + ("Material Request", "sales_invoice"), + ("Property", "territory"), +] + + +class TestInstallHooks(IntegrationTestCase): + """after_install and after_migrate both run these two builders.""" + + def test_every_declared_custom_field_exists(self): + missing = [ + f"{doctype}.{fieldname}" + for doctype, fieldname in APP_CUSTOM_FIELDS + if not frappe.db.exists("Custom Field", {"dt": doctype, "fieldname": fieldname}) + ] + self.assertEqual(missing, [], f"custom fields missing after install: {missing}") + + def test_custom_field_builder_is_idempotent(self): + before = frappe.db.count("Custom Field") + create_custom_fields() + self.assertEqual(before, frappe.db.count("Custom Field")) + + def test_property_setter_builder_is_idempotent(self): + before = frappe.db.count("Property Setter") + create_property_setters() + self.assertEqual(before, frappe.db.count("Property Setter")) + + def test_export_custom_fields_round_trips(self): + name = frappe.db.get_value("Custom Field", {"dt": "Issue", "fieldname": "property_name"}, "name") + exported = export_custom_fields(frappe.as_json([name])) + self.assertIn("property_name", exported) diff --git a/propms/tests/test_invoice_schedule.py b/propms/tests/test_invoice_schedule.py new file mode 100644 index 00000000..6dd4a62c --- /dev/null +++ b/propms/tests/test_invoice_schedule.py @@ -0,0 +1,100 @@ +import frappe +from frappe.tests import IntegrationTestCase, UnitTestCase +from frappe.utils import add_months, get_first_day, nowdate + +from propms.auto_custom import getDateMonthDiff, makeInvoiceSchedule +from propms.lease_invoice_schedule import get_aligned_invoice_date, make_lease_invoice_schedule +from propms.property_management_solution.doctype.lease.lease import ( + make_lease_invoice_schedule as build_schedule_for_lease, +) +from propms.tests.utils import create_customer, create_item, create_lease, set_settings + + +class TestScheduleMath(UnitTestCase): + def test_whole_month_span(self): + self.assertEqual(getDateMonthDiff("2026-01-01", "2026-03-31", 1), 3) + + def test_partial_month_span_is_fractional(self): + value = getDateMonthDiff("2026-01-01", "2026-02-14", 1) + self.assertGreater(value, 1) + self.assertLess(value, 2) + + def test_single_day_span(self): + self.assertGreater(getDateMonthDiff("2026-01-01", "2026-01-01", 1), 0) + + def test_aligned_invoice_date_is_month_start(self): + self.assertEqual(str(get_aligned_invoice_date("2026-05-17")), "2026-05-01") + + +class TestLeaseInvoiceSchedule(IntegrationTestCase): + def setUp(self): + set_settings(invoice_start_date=add_months(nowdate(), -1)) + + def test_build_schedule_creates_rows(self): + lease = create_lease("_Test Schedule Property", end_date=add_months(nowdate(), 6)) + build_schedule_for_lease(lease.name) + rows = frappe.get_all("Lease Invoice Schedule", filters={"parent": lease.name}, pluck="name") + self.assertGreater(len(rows), 0) + + def test_schedule_rows_link_back_to_the_lease(self): + lease = create_lease("_Test Schedule Parent Property", end_date=add_months(nowdate(), 3)) + build_schedule_for_lease(lease.name) + rows = frappe.get_all( + "Lease Invoice Schedule", + filters={"parent": lease.name}, + fields=["parenttype", "parentfield"], + ) + self.assertTrue(rows) + for row in rows: + self.assertEqual(row.parenttype, "Lease") + self.assertEqual(row.parentfield, "lease_invoice_schedule") + + def test_schedule_rows_appear_on_the_lease_document(self): + lease = create_lease("_Test Schedule Child Property", end_date=add_months(nowdate(), 3)) + build_schedule_for_lease(lease.name) + lease.reload() + self.assertGreater(len(lease.lease_invoice_schedule), 0) + + def test_make_invoice_schedule_honours_advance_days(self): + lease = create_lease("_Test Advance Property", days_to_invoice_in_advance=5) + makeInvoiceSchedule( + get_first_day(nowdate()), + create_item(), + create_customer(), + create_item(), + lease.name, + 1.0, + 1000, + 1, + "TZS", + 0, + 5, + None, + "Sales Invoice", + ) + row = frappe.get_all( + "Lease Invoice Schedule", + filters={"parent": lease.name}, + fields=["date_to_invoice", "schedule_start_date"], + limit=1, + )[0] + self.assertEqual( + frappe.utils.date_diff(row.schedule_start_date, row.date_to_invoice), + 5, + ) + + def test_monthly_scheduler_is_a_no_op_when_disabled(self): + set_settings(make_invoice_schedule_up_to_tomorrow_only=0) + before = frappe.db.count("Lease Invoice Schedule") + make_lease_invoice_schedule() + self.assertEqual(before, frappe.db.count("Lease Invoice Schedule")) + + def test_monthly_scheduler_creates_rows_for_active_leases(self): + set_settings( + make_invoice_schedule_up_to_tomorrow_only=1, + invoice_start_date=get_first_day(nowdate()), + ) + lease = create_lease("_Test Monthly Scheduler Property", end_date=add_months(nowdate(), 6)) + make_lease_invoice_schedule() + rows = frappe.get_all("Lease Invoice Schedule", filters={"parent": lease.name}, pluck="name") + self.assertGreater(len(rows), 0) diff --git a/propms/tests/test_issue_hook.py b/propms/tests/test_issue_hook.py new file mode 100644 index 00000000..61ea2a1b --- /dev/null +++ b/propms/tests/test_issue_hook.py @@ -0,0 +1,156 @@ +import frappe +from frappe.tests import IntegrationTestCase + +from propms.issue_hook import ( + get_item_rate, + get_items_group, + get_stock_availability, + get_taxes_template, + validate, + validate_materials_required, +) +from propms.tests import TEST_COMPANY +from propms.tests.utils import create_customer, create_item, create_lease, create_property, set_settings + + +def get_tax_template() -> str: + name = frappe.db.get_value("Sales Taxes and Charges Template", {"company": TEST_COMPANY}, "name") + if name: + return name + account = frappe.db.get_value( + "Account", {"company": TEST_COMPANY, "account_type": "Tax", "is_group": 0}, "name" + ) or frappe.db.get_value("Account", {"company": TEST_COMPANY, "is_group": 0}, "name") + doc = frappe.get_doc( + { + "doctype": "Sales Taxes and Charges Template", + "title": "_Test Maintenance Tax", + "company": TEST_COMPANY, + "taxes": [ + { + "charge_type": "On Net Total", + "account_head": account, + "description": "VAT", + "rate": 18, + } + ], + } + ) + doc.insert(ignore_permissions=True) + return doc.name + + +class TestMaintenanceJobCard(IntegrationTestCase): + """Issue.validate is the propms doc hook that bills maintenance work.""" + + def setUp(self): + # Job card billing commits through ERPNext, so leases from an earlier + # test in this module survive into the next one. + self.property_name = create_property("_Test Job Card Property", max_active_leases=99) + self.customer = create_customer() + self.item = create_item("_Test Maintenance Item", is_stock_item=0) + create_lease(self.property_name, customer=self.customer) + frappe.db.set_value("Company", TEST_COMPANY, "default_maintenance_tax_template", get_tax_template()) + set_settings(group_maintenance_job_items=0, submit_maintenance_invoice=0) + + def make_issue(self, **kwargs): + doc = frappe.get_doc( + { + "doctype": "Issue", + "subject": "_Test Job Card", + "company": TEST_COMPANY, + "property_name": self.property_name, + "customer": self.customer, + **kwargs, + } + ) + return doc + + def test_billing_creates_a_draft_sales_invoice(self): + doc = self.make_issue( + materials_billed=[ + { + "item": self.item, + "quantity": 2, + "rate": 500, + "amount": 1000, + "material_status": "Bill", + } + ] + ) + before = frappe.db.count("Sales Invoice") + validate(doc, "validate") + self.assertEqual( + frappe.db.count("Sales Invoice") - before, + 1, + "no Sales Invoice was created for the billed material", + ) + self.assertEqual(doc.materials_billed[0].invoiced, 1) + + def test_billing_runs_without_committing(self): + """v16 aborts any frappe.db.commit() reached from a document hook.""" + doc = self.make_issue( + materials_billed=[{"item": self.item, "quantity": 1, "rate": 100, "material_status": "Bill"}] + ) + validate(doc, "validate") + + def test_grouped_billing_makes_one_invoice(self): + set_settings(group_maintenance_job_items=1) + doc = self.make_issue( + materials_billed=[ + {"item": self.item, "quantity": 1, "rate": 100, "material_status": "Bill"}, + {"item": self.item, "quantity": 2, "rate": 100, "material_status": "Bill"}, + ] + ) + before = frappe.db.count("Sales Invoice") + validate(doc, "validate") + self.assertEqual(frappe.db.count("Sales Invoice") - before, 1) + + def test_no_invoice_without_a_customer(self): + doc = self.make_issue( + customer=None, + materials_billed=[{"item": self.item, "quantity": 1, "rate": 100, "material_status": "Bill"}], + ) + before = frappe.db.count("Sales Invoice") + validate(doc, "validate") + self.assertEqual(frappe.db.count("Sales Invoice"), before) + + def test_open_materials_block_closing_the_job_card(self): + doc = self.make_issue( + status="Closed", + materials_required=[{"item": self.item, "quantity": 1, "material_status": "Bill"}], + ) + with self.assertRaises(frappe.ValidationError): + validate_materials_required(doc) + + def test_self_consumption_materials_do_not_block_closing(self): + doc = self.make_issue( + status="Closed", + materials_required=[{"item": self.item, "quantity": 1, "material_status": "Self Consumption"}], + ) + validate_materials_required(doc) + + def test_missing_tax_template_is_reported(self): + frappe.db.set_value("Company", TEST_COMPANY, "default_maintenance_tax_template", None) + doc = self.make_issue( + materials_billed=[{"item": self.item, "quantity": 1, "rate": 100, "material_status": "Bill"}] + ) + with self.assertRaises(frappe.ValidationError): + validate(doc, "validate") + + +class TestIssueHelpers(IntegrationTestCase): + def test_items_group_reads_the_settings_table(self): + settings = frappe.get_single("Property Management Settings") + settings.set("maintenance_item_group", []) + settings.append("maintenance_item_group", {"item_group": "All Item Groups"}) + settings.save(ignore_permissions=True) + self.assertEqual(get_items_group(), ["All Item Groups"]) + + def test_taxes_template_for_an_item_without_one(self): + self.assertEqual(get_taxes_template(create_item("_Test Untaxed Item")), "") + + def test_stock_availability_of_an_unstocked_item(self): + self.assertEqual(get_stock_availability(create_item("_Test No Stock Item"), TEST_COMPANY, 0), 0) + + def test_item_rate_without_a_price_list_entry(self): + self.assertIsNone(get_item_rate(create_item("_Test Unpriced Item"), create_customer())) diff --git a/propms/tests/test_lease.py b/propms/tests/test_lease.py new file mode 100644 index 00000000..cb1144a4 --- /dev/null +++ b/propms/tests/test_lease.py @@ -0,0 +1,136 @@ +from unittest.mock import patch + +import frappe +from frappe.tests import IntegrationTestCase, UnitTestCase +from frappe.utils import add_days, add_months, nowdate + +from propms.property_management_solution.doctype.lease.lease import ( + get_status_for_lease, + get_system_controlled_statuses, + initiate_lease_renewal, + update_lease_statuses, +) +from propms.tests.utils import create_lease, create_property, set_settings + + +class TestLeaseStatusRules(UnitTestCase): + """get_status_for_lease() is pure date arithmetic.""" + + def status(self, **lease): + return get_status_for_lease(frappe._dict(lease), getdate_today()) + + def test_future_start_date_is_upcoming(self): + self.assertEqual( + self.status(start_date=add_days(nowdate(), 5), end_date=add_months(nowdate(), 6)), "Upcoming" + ) + + def test_current_lease_is_active(self): + self.assertEqual( + self.status(start_date=add_days(nowdate(), -5), end_date=add_months(nowdate(), 6)), "Active" + ) + + def test_past_end_date_is_expired(self): + self.assertEqual( + self.status(start_date=add_months(nowdate(), -6), end_date=add_days(nowdate(), -1)), "Expired" + ) + + def test_end_date_today_stays_active(self): + self.assertEqual(self.status(start_date=add_months(nowdate(), -6), end_date=nowdate()), "Active") + + def test_skip_end_date_never_expires(self): + self.assertEqual( + self.status( + start_date=add_months(nowdate(), -6), end_date=add_days(nowdate(), -1), skip_end_date=1 + ), + "Active", + ) + + def test_missing_start_date_returns_none(self): + self.assertIsNone(self.status(start_date=None, end_date=None)) + + def test_system_controlled_statuses(self): + self.assertEqual(get_system_controlled_statuses(), {"Upcoming", "Active", "Expired"}) + + +def getdate_today(): + from frappe.utils import getdate + + return getdate(nowdate()) + + +class TestLease(IntegrationTestCase): + def test_lease_insert_sets_status(self): + lease = create_lease("_Test Lease Property") + self.assertEqual(lease.lease_status, "Active") + + def test_upcoming_lease_status(self): + lease = create_lease( + "_Test Upcoming Property", + start_date=add_days(nowdate(), 10), + end_date=add_months(nowdate(), 12), + ) + self.assertEqual(lease.lease_status, "Upcoming") + + def test_manual_status_is_not_overwritten(self): + lease = create_lease("_Test Manual Property") + lease.lease_status = "Terminated" + lease.save() + self.assertEqual(lease.lease_status, "Terminated") + + def test_max_active_leases_blocks_second_active_lease(self): + property_name = create_property("_Test Capped Property", max_active_leases=1) + create_lease(property_name) + with self.assertRaises(frappe.ValidationError): + create_lease(property_name, customer="_Test Tenant") + + def test_property_level_limit_overrides_settings(self): + set_settings(max_active_leases=1) + property_name = create_property("_Test Two Lease Property", max_active_leases=2) + create_lease(property_name) + second = create_lease(property_name) + self.assertTrue(second.name) + + def test_update_lease_statuses_expires_finished_lease(self): + lease = create_lease( + "_Test Expiring Property", + start_date=add_months(nowdate(), -12), + end_date=add_months(nowdate(), 6), + ) + frappe.db.set_value("Lease", lease.name, "end_date", add_days(nowdate(), -1)) + # The scheduler commits; that would leak this test's rows into the site. + with patch.object(frappe.db, "commit"): + update_lease_statuses() + self.assertEqual(frappe.db.get_value("Lease", lease.name, "lease_status"), "Expired") + + def test_renewal_creates_draft_with_shifted_dates(self): + lease = create_lease("_Test Renewal Property") + new_name = initiate_lease_renewal(lease.name) + new_lease = frappe.get_doc("Lease", new_name) + self.assertEqual(new_lease.renewed_from, lease.name) + self.assertEqual(new_lease.lease_status, "Renewal to Previous Lease") + self.assertEqual(str(new_lease.start_date), add_days(lease.end_date, 1)) + + def test_renewal_rejects_duplicate(self): + lease = create_lease("_Test Single Renewal Property") + initiate_lease_renewal(lease.name) + with self.assertRaises(frappe.ValidationError): + initiate_lease_renewal(lease.name) + + def test_renewal_rejects_ineligible_status(self): + lease = create_lease("_Test Draft Renewal Property") + frappe.db.set_value("Lease", lease.name, "lease_status", "Terminated") + with self.assertRaises(frappe.ValidationError): + initiate_lease_renewal(lease.name) + + def test_renewal_comment_uses_desk_route(self): + lease = create_lease("_Test Route Property") + initiate_lease_renewal(lease.name) + comment = frappe.get_all( + "Comment", + filters={"reference_doctype": "Lease", "reference_name": lease.name}, + pluck="content", + order_by="creation desc", + limit=1, + )[0] + self.assertNotIn("/app/", comment) + self.assertIn("/desk/", comment) diff --git a/propms/tests/test_lease_invoice.py b/propms/tests/test_lease_invoice.py new file mode 100644 index 00000000..15ad44cc --- /dev/null +++ b/propms/tests/test_lease_invoice.py @@ -0,0 +1,82 @@ +import json + +import frappe +from frappe.tests import IntegrationTestCase +from frappe.utils import add_months, get_first_day, nowdate + +from propms.lease_invoice import ( + getCostCenter, + getDueDate, + leaseInvoiceAutoCreate, + makeInvoice, +) +from propms.property_management_solution.doctype.lease.lease import ( + make_lease_invoice_schedule as build_schedule_for_lease, +) +from propms.tests.utils import create_customer, create_item, create_lease, set_settings + + +class TestLeaseInvoice(IntegrationTestCase): + def setUp(self): + set_settings( + invoice_start_date=add_months(nowdate(), -1), + auto_submit_sales_invoice=0, + auto_submit_sales_order=0, + ) + + def test_get_cost_center_from_lease(self): + lease = create_lease("_Test Cost Centre Property") + self.assertEqual( + getCostCenter(lease.name), + frappe.db.get_value("Property", lease.property, "cost_center"), + ) + + def test_get_due_date(self): + self.assertTrue(getDueDate(nowdate(), create_customer())) + + def test_make_invoice_creates_draft_sales_invoice(self): + lease = create_lease("_Test Invoice Property") + items = json.dumps([{"item_code": create_item(), "qty": 1, "rate": 1000}]) + invoice = makeInvoice( + nowdate(), + lease.customer, + items, + frappe.db.get_value("Lease", lease.name, "security_deposit_currency"), + lease.name, + create_item(), + 1.0, + get_first_day(nowdate()), + ) + self.assertIsNotNone(invoice, "makeInvoice swallowed an exception and returned None") + self.assertEqual(invoice.doctype, "Sales Invoice") + self.assertEqual(invoice.docstatus, 0) + + def test_make_invoice_without_customer_is_reported(self): + lease = create_lease("_Test No Customer Property") + items = json.dumps([{"item_code": create_item(), "qty": 1, "rate": 1000}]) + with self.assertRaises(frappe.ValidationError): + makeInvoice(nowdate(), None, items, "TZS", lease.name, create_item(), 1.0, nowdate()) + + def test_auto_create_handles_empty_schedule(self): + """An empty schedule must not raise; the last-group flush has nothing to flush.""" + frappe.db.delete("Lease Invoice Schedule") + leaseInvoiceAutoCreate() + + def test_auto_create_stamps_invoice_number_on_schedule(self): + frappe.db.delete("Lease Invoice Schedule") + lease = create_lease("_Test Auto Invoice Property", end_date=add_months(nowdate(), 3)) + build_schedule_for_lease(lease.name) + frappe.db.set_value( + "Lease Invoice Schedule", + {"parent": lease.name}, + "date_to_invoice", + nowdate(), + update_modified=False, + ) + leaseInvoiceAutoCreate() + stamped = frappe.get_all( + "Lease Invoice Schedule", + filters={"parent": lease.name, "invoice_number": ("is", "set")}, + pluck="invoice_number", + ) + self.assertTrue(stamped, "no Sales Invoice was linked back to the schedule") diff --git a/propms/tests/test_pos.py b/propms/tests/test_pos.py new file mode 100644 index 00000000..124c1290 --- /dev/null +++ b/propms/tests/test_pos.py @@ -0,0 +1,23 @@ +import frappe +from frappe.tests import IntegrationTestCase + +from propms.pos import get_pos_data +from propms.tests.utils import create_lease, create_property, get_cost_center + + +class TestPointOfSaleData(IntegrationTestCase): + """The POS cost centre control resolves its customer through this method.""" + + def test_returns_the_lease_behind_a_cost_centre(self): + name = create_property("_Test POS Property") + lease = create_lease(name) + result = get_pos_data(get_cost_center()) + self.assertIsNotNone(result) + self.assertEqual(result.customer, lease.customer) + + def test_returns_none_for_an_unknown_cost_centre(self): + self.assertIsNone(get_pos_data("_Test Missing Cost Center")) + + def test_returns_none_when_the_property_has_no_lease(self): + frappe.db.delete("Lease") + self.assertIsNone(get_pos_data(get_cost_center())) diff --git a/propms/tests/test_property.py b/propms/tests/test_property.py new file mode 100644 index 00000000..bcdbc849 --- /dev/null +++ b/propms/tests/test_property.py @@ -0,0 +1,71 @@ +import re + +import frappe +from frappe.tests import IntegrationTestCase + +from propms.property_management_solution.doctype.property.property import add_node, get_children +from propms.tests import TEST_COMPANY +from propms.tests.utils import create_lease, create_property, get_cost_center + + +class TestProperty(IntegrationTestCase): + def test_property_insert(self): + name = create_property("_Test Basic Property") + self.assertEqual(frappe.db.get_value("Property", name, "status"), "Available") + + def test_nested_set_parent_child(self): + parent = create_property("_Test Parent Property", is_group=1) + child = create_property("_Test Child Property", parent_property=parent) + lft, rgt = frappe.db.get_value("Property", parent, ["lft", "rgt"]) + child_lft = frappe.db.get_value("Property", child, "lft") + self.assertTrue(lft < child_lft < rgt) + + def test_get_children_root(self): + create_property("_Test Tree Root") + rows = get_children("Property", parent="", company=TEST_COMPANY, is_root=True) + self.assertTrue(any(row["value"] == "_Test Tree Root" for row in rows)) + + def test_get_children_of_parent(self): + parent = create_property("_Test Tree Parent", is_group=1) + create_property("_Test Tree Child", parent_property=parent) + rows = get_children("Property", parent=parent, company=TEST_COMPANY) + self.assertTrue(any(row["value"] == "_Test Tree Child" for row in rows)) + + def test_add_node_uses_the_field_the_tree_view_sends(self): + """The tree dialog field name must match the DocType's autoname field.""" + tree_js = frappe.get_app_path( + "propms", "property_management_solution/doctype/property/property_tree.js" + ) + with open(tree_js) as handle: + source = handle.read() + name_field = re.search(r'fieldname:\s*"([^"]+)",\s*label:\s*__\("New Property Name"\)', source) + self.assertIsNotNone(name_field, "the tree view no longer defines a new-property name field") + + frappe.form_dict.update( + { + "doctype": "Property", + name_field.group(1): "_Test Node Property", + "is_root": "true", + "company": TEST_COMPANY, + "cost_center": get_cost_center(), + } + ) + try: + add_node() + finally: + frappe.form_dict.clear() + self.assertTrue(frappe.db.exists("Property", "_Test Node Property")) + + def test_status_change_blocked_while_lease_is_active(self): + name = create_property("_Test Locked Property") + create_lease(name) + doc = frappe.get_doc("Property", name) + doc.status = "Available" + with self.assertRaises(frappe.ValidationError): + doc.save() + + def test_get_active_leases_returns_current_lease(self): + name = create_property("_Test Active Lease Property") + lease = create_lease(name) + doc = frappe.get_doc("Property", name) + self.assertIn(lease.name, [row.name for row in doc.get_active_leases()]) diff --git a/propms/tests/test_property_increment.py b/propms/tests/test_property_increment.py new file mode 100644 index 00000000..6204e1cb --- /dev/null +++ b/propms/tests/test_property_increment.py @@ -0,0 +1,139 @@ +import frappe +from frappe.tests import IntegrationTestCase, UnitTestCase +from frappe.utils import add_months, nowdate + +from propms.property_increment import ( + _apply_increment, + _round_amount, + run_property_increment_engine, + validate_property_increment_settings, +) +from propms.tests.utils import create_item, create_lease, create_property + + +class TestIncrementMath(UnitTestCase): + def test_percent_increment(self): + self.assertEqual(_apply_increment(1000, "Percent", 10, "Round", 2), 1100.0) + + def test_amount_increment(self): + self.assertEqual(_apply_increment(1000, "Amount", 250, "Round", 2), 1250.0) + + def test_round_mode(self): + self.assertEqual(_round_amount(10.567, "Round", 1), 10.6) + + def test_ceil_mode(self): + self.assertEqual(_round_amount(10.51, "Ceil", 1), 10.6) + + def test_floor_mode(self): + self.assertEqual(_round_amount(10.59, "Floor", 1), 10.5) + + def test_none_mode_keeps_precision(self): + self.assertEqual(_round_amount(10.5678, "None", 2), 10.5678) + + def test_unknown_mode_falls_back_to_round(self): + self.assertEqual(_round_amount(10.567, "Bogus", 1), 10.6) + + +class TestIncrementValidation(IntegrationTestCase): + def make_property(self, **rule): + item = create_item("_Test Increment Item") + doc = frappe.get_doc("Property", create_property("_Test Increment Property")) + doc.enable_auto_increment = 1 + doc.auto_create_lease_items_for_months = 24 + doc.increment_effective_from = nowdate() + doc.set("lease_increment_rules", []) + doc.append( + "lease_increment_rules", + { + "lease_item": item, + "increment_every": 1, + "increment_uom": "Year", + "increment_type": "Percent", + "increment_value": 10, + "rule_effective_from": nowdate(), + "is_active": 1, + **rule, + }, + ) + return doc + + def test_valid_settings_pass(self): + doc = self.make_property() + validate_property_increment_settings(doc) + + def test_zero_horizon_rejected(self): + doc = self.make_property() + doc.auto_create_lease_items_for_months = 0 + with self.assertRaises(frappe.ValidationError): + validate_property_increment_settings(doc) + + def test_missing_rules_rejected(self): + doc = self.make_property() + doc.set("lease_increment_rules", []) + with self.assertRaises(frappe.ValidationError): + validate_property_increment_settings(doc) + + def test_bad_uom_rejected(self): + doc = self.make_property(increment_uom="Week") + with self.assertRaises(frappe.ValidationError): + validate_property_increment_settings(doc) + + def test_bad_increment_type_rejected(self): + doc = self.make_property(increment_type="Ratio") + with self.assertRaises(frappe.ValidationError): + validate_property_increment_settings(doc) + + def test_zero_increment_value_rejected(self): + doc = self.make_property(increment_value=0) + with self.assertRaises(frappe.ValidationError): + validate_property_increment_settings(doc) + + def test_disabled_property_skips_validation(self): + doc = self.make_property() + doc.enable_auto_increment = 0 + doc.auto_create_lease_items_for_months = 0 + validate_property_increment_settings(doc) + + +class TestIncrementEngine(IntegrationTestCase): + def test_engine_appends_future_lease_item_version(self): + item = create_item("_Test Engine Item") + name = create_property("_Test Engine Property") + prop = frappe.get_doc("Property", name) + prop.enable_auto_increment = 1 + prop.auto_create_lease_items_for_months = 24 + prop.increment_effective_from = nowdate() + prop.increment_rounding_mode = "Round" + prop.increment_rounding_precision = 2 + prop.set("lease_increment_rules", []) + prop.append( + "lease_increment_rules", + { + "lease_item": item, + "increment_every": 1, + "increment_uom": "Year", + "increment_type": "Percent", + "increment_value": 10, + "rule_effective_from": add_months(nowdate(), 12), + "is_active": 1, + }, + ) + prop.save() + + lease = create_lease( + name, + item=item, + start_date=nowdate(), + end_date=add_months(nowdate(), 36), + ) + run_property_increment_engine() + lease.reload() + amounts = sorted(row.amount for row in lease.lease_item) + self.assertGreater(len(lease.lease_item), 1) + self.assertIn(1100.0, amounts) + + def test_engine_is_idempotent(self): + run_property_increment_engine() + before = frappe.db.count("Lease Item") + run_property_increment_engine() + self.assertEqual(before, frappe.db.count("Lease Item")) diff --git a/propms/tests/test_reports.py b/propms/tests/test_reports.py new file mode 100644 index 00000000..5cdc56ae --- /dev/null +++ b/propms/tests/test_reports.py @@ -0,0 +1,90 @@ +import os + +import frappe +from frappe.desk.query_report import run as run_query_report +from frappe.tests import IntegrationTestCase +from frappe.utils import add_months, nowdate + +from propms.tests import TEST_COMPANY +from propms.tests.utils import create_lease + +REPORT_ROOT = "property_management_solution/report" + +DEFAULT_FILTERS = { + "company": TEST_COMPANY, + "from_date": add_months(nowdate(), -1), + "to_date": nowdate(), + "start_date": add_months(nowdate(), -1), + "end_date": nowdate(), + "date": nowdate(), + "year": str(frappe.utils.now_datetime().year), + "month": "January", + "from": "January", + "to": "December", + "property_type": "%", + "account": "%", + "owner_type": "%", + "service_type": "%", + "status": "Available", +} + +# These reports read Sales Invoice fields owned by csf_tz (TRA control number, +# withholding tax rate). They cannot run on a bench without that app. +CSF_TZ_DEPENDENT = { + "Withholding Tax Summary on Sales (Properties)", + "Withholding Tax Summary on Sales for Properties", +} + + +def app_reports() -> list: + """Every Report record shipped by propms, read from its module folder.""" + root = frappe.get_app_path("propms", REPORT_ROOT) + names = [] + for folder in sorted(os.listdir(root)): + json_path = os.path.join(root, folder, f"{folder}.json") + if os.path.isfile(json_path): + with open(json_path) as handle: + names.append(frappe.parse_json(handle.read())["name"]) + return names + + +class TestReports(IntegrationTestCase): + """Every shipped report must load and execute on version 16.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.report_names = app_reports() + + def test_reports_are_installed(self): + missing = [name for name in self.report_names if not frappe.db.exists("Report", name)] + self.assertEqual(missing, [], f"reports missing from the site: {missing}") + + def test_report_modules_import(self): + from frappe.core.doctype.report.report import get_report_module_dotted_path + + broken = [] + for name in self.report_names: + report = frappe.get_doc("Report", name) + if report.report_type != "Script Report": + continue + try: + frappe.get_attr(get_report_module_dotted_path(report.module, name) + ".execute") + except Exception as exc: # noqa: BLE001 + broken.append(f"{name}: {type(exc).__name__}: {exc}") + self.assertEqual(broken, [], f"script report modules failed to import: {broken}") + + def test_reports_execute(self): + create_lease("_Test Report Property") + failures = [] + skipped = [] + for name in self.report_names: + if name in CSF_TZ_DEPENDENT and "csf_tz" not in frappe.get_installed_apps(): + skipped.append(name) + continue + try: + run_query_report(name, filters=dict(DEFAULT_FILTERS), ignore_prepared_report=True) + except Exception as exc: # noqa: BLE001 + failures.append(f"{name}: {type(exc).__name__}: {exc}") + self.assertEqual(failures, [], "reports failed to execute:\n" + "\n".join(failures)) + self.assertLessEqual(len(skipped), len(CSF_TZ_DEPENDENT)) diff --git a/propms/tests/test_v16_compat.py b/propms/tests/test_v16_compat.py new file mode 100644 index 00000000..4138575b --- /dev/null +++ b/propms/tests/test_v16_compat.py @@ -0,0 +1,86 @@ +import inspect +import re + +import frappe +from frappe.tests import IntegrationTestCase, UnitTestCase + +from propms import hooks +from propms.tests.utils import create_lease, create_property + +COMMIT_PATTERN = re.compile(r"^\s*frappe\.db\.commit\(", re.MULTILINE) +LEGACY_DESK_ROUTE = re.compile(r"[\"'`]/app/") + + +def hook_handler_modules() -> set: + modules = set() + for events in hooks.doc_events.values(): + for handlers in events.values(): + for handler in handlers if isinstance(handlers, list) else [handlers]: + modules.add(handler.rsplit(".", 1)[0]) + return modules + + +class TestVersion16Compatibility(UnitTestCase): + """Source-level guards for the version-16 breaking changes.""" + + def test_no_commit_inside_document_hooks(self): + """v16 raises `Cannot commit database transaction from document hooks`.""" + offenders = [] + for module_name in hook_handler_modules(): + source = inspect.getsource(frappe.get_module(module_name)) + if COMMIT_PATTERN.search(source): + offenders.append(module_name) + self.assertEqual(offenders, [], f"frappe.db.commit() reached from a doc_event handler: {offenders}") + + def test_no_legacy_app_routes_in_python(self): + """/app was rerouted to /desk in version 16.""" + offenders = [] + for module_name in ("propms.property_management_solution.doctype.lease.lease",): + source = inspect.getsource(frappe.get_module(module_name)) + for match in LEGACY_DESK_ROUTE.finditer(source): + line = source.count("\n", 0, match.start()) + 1 + offenders.append(f"{module_name}:{line}") + self.assertEqual(offenders, [], f"hard-coded /app desk route: {offenders}") + + def test_no_deprecated_test_flags(self): + source = inspect.getsource(frappe.get_module("propms.tests")) + for deprecated in ("skip_test_records", "flags.in_test", "test_dependencies", "test_ignore"): + self.assertNotIn(deprecated, source, f"deprecated v15 test API still referenced: {deprecated}") + + def test_app_version_targets_v16(self): + from propms import __version__ + + self.assertTrue( + __version__.startswith("16."), + f"propms __version__ is {__version__}; the version-16 branch must report 16.x", + ) + + +class TestVersion16Runtime(IntegrationTestCase): + """Runtime behaviour that version 16 changed.""" + + def test_get_all_string_expression_filters_still_work(self): + """Property treeview relies on an ifnull() expression filter.""" + from propms.property_management_solution.doctype.property.property import get_children + + create_property("_Test Root Property") + children = get_children("Property", parent="", is_root=True) + self.assertIsInstance(children, list) + + def test_get_all_default_order_is_creation(self): + """v16 changed the implicit order_by from `modified desc` to `creation desc`.""" + create_property("_Test Order Property A") + create_property("_Test Order Property B") + rows = frappe.get_all("Property", pluck="name", limit=2) + ordered = frappe.get_all("Property", pluck="name", order_by="creation desc", limit=2) + self.assertEqual(rows, ordered) + + def test_custom_error_log_uses_innodb(self): + """MyISAM tables cannot roll back, so they leak rows out of every test.""" + meta = frappe.get_meta("Custom Error Log") + self.assertEqual((meta.engine or "InnoDB"), "InnoDB") + + def test_lease_document_saves_without_manual_commit(self): + lease = create_lease("_Test Commit Property") + lease.reload() + self.assertTrue(lease.name) diff --git a/propms/tests/utils.py b/propms/tests/utils.py new file mode 100644 index 00000000..22c93909 --- /dev/null +++ b/propms/tests/utils.py @@ -0,0 +1,160 @@ +"""Bootstrap and factories shared by the propms test suite.""" + +import frappe +from frappe.utils import add_months, now_datetime, nowdate + +from propms.tests import TEST_ABBR, TEST_COMPANY + + +def before_tests(): + """Run once by the test runner before the propms suite.""" + frappe.clear_cache() + setup_company() + enable_all_roles() + frappe.db.commit() # nosemgrep + + +def setup_company(): + from frappe.desk.page.setup_wizard.setup_wizard import setup_complete + + if frappe.db.exists("Company", TEST_COMPANY): + return + + year = now_datetime().year + setup_complete( + { + "currency": "TZS", + "full_name": "Test User", + "company_name": TEST_COMPANY, + "company_abbr": TEST_ABBR, + "timezone": "Africa/Dar_es_Salaam", + "industry": "Service", + "country": "Tanzania", + "fy_start_date": f"{year}-01-01", + "fy_end_date": f"{year}-12-31", + "language": "english", + "company_tagline": "Testing", + "email": "test@propms.local", + "password": "test", + "chart_of_accounts": "Standard", + } + ) + + +def enable_all_roles(): + from erpnext.setup.utils import enable_all_roles_and_domains + + enable_all_roles_and_domains() + + +def get_or_create(doctype: str, name: str, values: dict) -> str: + if frappe.db.exists(doctype, name): + return name + doc = frappe.get_doc({"doctype": doctype, **values}) + doc.insert(ignore_permissions=True, ignore_if_duplicate=True) + return doc.name + + +def get_customer_group() -> str: + return frappe.db.get_value("Customer Group", {"is_group": 0}, "name") + + +def get_territory() -> str: + return frappe.db.get_value("Territory", {"is_group": 0}, "name") + + +def create_customer(name: str = "_Test Tenant") -> str: + return get_or_create( + "Customer", + name, + { + "customer_name": name, + "customer_type": "Company", + "customer_group": get_customer_group(), + "territory": get_territory(), + }, + ) + + +def create_uom(name: str = "_Test Month") -> str: + """Lease quantities are fractional months, so the UOM must allow fractions.""" + return get_or_create("UOM", name, {"uom_name": name, "must_be_whole_number": 0}) + + +def create_item(code: str = "_Test Rent Item", is_stock_item: int = 0) -> str: + return get_or_create( + "Item", + code, + { + "item_code": code, + "item_name": code, + "item_group": "All Item Groups", + "stock_uom": create_uom(), + "is_stock_item": is_stock_item, + }, + ) + + +def get_cost_center(company: str | None = None) -> str: + company = company or TEST_COMPANY + return frappe.db.get_value("Cost Center", {"company": company, "is_group": 0}, "name") + + +def create_property(name: str = "_Test Property", **kwargs) -> str: + company = kwargs.pop("company", TEST_COMPANY) + values = { + "name1": name, + "company": company, + "cost_center": get_cost_center(company), + "status": "Available", + } + values.update(kwargs) + return get_or_create("Property", name, values) + + +def create_lease(property_name: str | None = None, **kwargs): + """Insert a Lease with one monthly item. Returns the document.""" + property_name = create_property(property_name) if property_name else create_property() + customer = kwargs.pop("customer", None) or create_customer() + item = kwargs.pop("item", None) or create_item() + start_date = kwargs.pop("start_date", nowdate()) + end_date = kwargs.pop("end_date", add_months(nowdate(), 12)) + currency = frappe.db.get_value("Company", TEST_COMPANY, "default_currency") + lease_items = kwargs.pop( + "lease_item", + [ + { + "lease_item": item, + "frequency": "Monthly", + "amount": 1000, + "currency_code": currency, + "paid_by": customer, + "document_type": "Sales Invoice", + } + ], + ) + doc = frappe.get_doc( + { + "doctype": "Lease", + "property": property_name, + "lease_date": start_date, + "lease_customer": customer, + "customer": customer, + "start_date": start_date, + "end_date": end_date, + "notice_period": 30, + "security_deposit_currency": currency, + "lease_item": lease_items, + **kwargs, + } + ) + doc.insert(ignore_permissions=True) + return doc + + +def set_settings(**values): + settings = frappe.get_single("Property Management Settings") + for key, value in values.items(): + settings.set(key, value) + settings.save(ignore_permissions=True) + return settings diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 00000000..811e353e --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "jsdom", + include: ["propms/tests/js/**/*.test.js"], + setupFiles: ["propms/tests/js/setup.js"], + coverage: { + provider: "v8", + include: ["propms/**/*.js"], + exclude: ["propms/tests/js/**"], + reporter: ["text", "json-summary"], + }, + }, +});