mirror of
https://github.com/grocy/grocy.git
synced 2025-09-16 17:56:51 +00:00
Compare commits
271 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
756ec319cc | ||
|
ba2d32be60 | ||
|
7cc09cec67 | ||
|
8b815fce93 | ||
|
f1c78659be | ||
|
5c79a80f7a | ||
|
f451e65278 | ||
|
176333df5b | ||
|
d4227d2e41 | ||
|
0bbd2d9880 | ||
|
b81316bd60 | ||
|
d11dcb38fe | ||
|
77d82f22dc | ||
|
be326a5211 | ||
|
83624eaf27 | ||
|
055619d275 | ||
|
cda3dde120 | ||
|
5a0b862d22 | ||
|
bb5fd8360b | ||
|
d7180bd7b2 | ||
|
8c9b0dedb2 | ||
|
9c2c2c1fa2 | ||
|
596dc9e36d | ||
|
b2019ba42d | ||
|
003d4a567a | ||
|
5112e0f551 | ||
|
8008fcdc65 | ||
|
8d41dcc650 | ||
|
037d024862 | ||
|
03ca5cd45b | ||
|
60d47bef84 | ||
|
98a7bcb044 | ||
|
7401971884 | ||
|
067a10e1b2 | ||
|
ddfe33fab6 | ||
|
2a0ec30bb0 | ||
|
8540fc44f3 | ||
|
66095738e3 | ||
|
e472711d23 | ||
|
8e054a4981 | ||
|
feb28211d8 | ||
|
06f25b7006 | ||
|
f85a67a1ff | ||
|
6fe0100927 | ||
|
bcb359e317 | ||
|
4075067a10 | ||
|
bd3c63218b | ||
|
27daf384da | ||
|
905fc0f357 | ||
|
9cd0e4ab2d | ||
|
6b38cd450f | ||
|
bb60f5f043 | ||
|
e777be4d3b | ||
|
8a71d55f0f | ||
|
b01b49d10c | ||
|
496594d898 | ||
|
1d5e82c341 | ||
|
a9b696f41c | ||
|
e50b1eb359 | ||
|
92e0245387 | ||
|
67d0d3c3d6 | ||
|
23bcbc23e9 | ||
|
085d9a0bc7 | ||
|
368df142cf | ||
|
d38edabb14 | ||
|
4426a10e2e | ||
|
931dc9d243 | ||
|
c5b8893008 | ||
|
c27f41aee4 | ||
|
ef043b38ce | ||
|
bb261f99c4 | ||
|
48ca0f2ac7 | ||
|
b7f0b06684 | ||
|
324487d395 | ||
|
9a8c61497b | ||
|
bc7afe4bdd | ||
|
bb5dcb2434 | ||
|
71b9d11ff5 | ||
|
3e73a44576 | ||
|
dedfe3a854 | ||
|
c4b0ef4d49 | ||
|
339d81318f | ||
|
282ee0885b | ||
|
5833364e51 | ||
|
525f1705d1 | ||
|
5a13cb5ffe | ||
|
e830805443 | ||
|
ca3f28b615 | ||
|
6081b8ee67 | ||
|
7eef4acd81 | ||
|
678579e933 | ||
|
4cc2d39063 | ||
|
14cc153422 | ||
|
f5b5c4c7e1 | ||
|
88b76a52a5 | ||
|
a4a25af460 | ||
|
41a72d11da | ||
|
c8236b101b | ||
|
ef1df0a446 | ||
|
5c4953b9b2 | ||
|
ccaf2411fe | ||
|
bce8bd6b35 | ||
|
66c07887cb | ||
|
be99880ce4 | ||
|
e026609972 | ||
|
3474f55866 | ||
|
f583810d5c | ||
|
419445f5ae | ||
|
c64eb27ca1 | ||
|
f4eb5196f7 | ||
|
9e493430d8 | ||
|
7690eedd70 | ||
|
aaa270a52f | ||
|
6f47a5415c | ||
|
42c1709633 | ||
|
4685ff4145 | ||
|
249b01d7a8 | ||
|
bcbdf58376 | ||
|
7f8540ff4e | ||
|
b52ab91606 | ||
|
7246ac55b6 | ||
|
848931da21 | ||
|
bf4092e746 | ||
|
7cee18c926 | ||
|
e9a4b43268 | ||
|
b1522742cc | ||
|
ecdaaab789 | ||
|
3379942086 | ||
|
12eaa8c074 | ||
|
c6310d636d | ||
|
9bedc6a138 | ||
|
70dbc6018f | ||
|
3afeb44b1d | ||
|
3131b8965e | ||
|
bbc2fc9e42 | ||
|
3b4141eb4d | ||
|
5f826be82c | ||
|
db9ee93d2b | ||
|
1eabd29105 | ||
|
dc05c56440 | ||
|
cb88ab2080 | ||
|
254e1a9bc1 | ||
|
0fc7c297bf | ||
|
82bfb6a3c3 | ||
|
277c622475 | ||
|
091a0f3efe | ||
|
823c76aa08 | ||
|
37dee2a50b | ||
|
ea0f5101ec | ||
|
be650d093d | ||
|
734814d96b | ||
|
d9246b9b42 | ||
|
70e7e630c3 | ||
|
71fc49252f | ||
|
aa0771877f | ||
|
e5a4d11c0b | ||
|
909949a9e1 | ||
|
f018696219 | ||
|
347a47d0d2 | ||
|
c3de4b86b0 | ||
|
594e77ca41 | ||
|
31ce7a13ea | ||
|
5d762001c8 | ||
|
bc3d339d9c | ||
|
33e5ed9ddc | ||
|
2d712b0ef7 | ||
|
13d81a4e4b | ||
|
8d917aee12 | ||
|
09b2cfc46a | ||
|
789e475207 | ||
|
eec5105e5b | ||
|
82f7b2109c | ||
|
840dd58c03 | ||
|
37d1377f99 | ||
|
882a3545e5 | ||
|
778191fd11 | ||
|
71701804ea | ||
|
306c404362 | ||
|
4fab4f87d3 | ||
|
54717a81b1 | ||
|
eca299454b | ||
|
c58083f84a | ||
|
ecf96252b9 | ||
|
92e648490a | ||
|
6dd3c26ddd | ||
|
02ea26b090 | ||
|
0954b5a741 | ||
|
02b6c3b721 | ||
|
6fa4e13ba2 | ||
|
9837f79f9c | ||
|
6e4cd22118 | ||
|
ca00dd8e2d | ||
|
5455ec7bde | ||
|
2e7af1b050 | ||
|
89bae8d25e | ||
|
5b5c272909 | ||
|
3e394a3840 | ||
|
ab8094e1c0 | ||
|
bbb5f1c7c7 | ||
|
b607f188af | ||
|
9ab1a674fe | ||
|
2f0a1391b7 | ||
|
a9a1358b08 | ||
|
4853174d03 | ||
|
538d789366 | ||
|
0751919b82 | ||
|
99b2a84667 | ||
|
9bd6aac09c | ||
|
7be35a90c1 | ||
|
eae5b8bad9 | ||
|
0c85342404 | ||
|
9ddcdb3ab2 | ||
|
1c537cf5da | ||
|
607a90cccc | ||
|
3d1c6fc5f0 | ||
|
df1d3677e8 | ||
|
4da2ac9b35 | ||
|
b4ae7d8538 | ||
|
870b679e0e | ||
|
4656a85732 | ||
|
4949913ccb | ||
|
580bd5ac0c | ||
|
2bf3448d18 | ||
|
e9bc51ca3d | ||
|
5ddae116e0 | ||
|
13566bc6fd | ||
|
642f95a3f8 | ||
|
5a1d21ef31 | ||
|
7dcd39f82f | ||
|
655aa89bd6 | ||
|
feb88ab685 | ||
|
79b4bad014 | ||
|
bcd5092427 | ||
|
554a83fa01 | ||
|
57acb62520 | ||
|
52ed5f2285 | ||
|
dd1d253ea5 | ||
|
e40979a874 | ||
|
2ddbc2656b | ||
|
7351fce395 | ||
|
e4b0bbf7f7 | ||
|
f71121e6b2 | ||
|
9114dec695 | ||
|
2d5ed67ae1 | ||
|
dad6322bac | ||
|
05a9406a32 | ||
|
2a3822e781 | ||
|
c69b3a2936 | ||
|
6d4b86a730 | ||
|
9c2ee58433 | ||
|
f84593882d | ||
|
1241261ca4 | ||
|
ebe92335a6 | ||
|
35f2f33ae3 | ||
|
f0f84b304b | ||
|
23146417e6 | ||
|
bd3155d39b | ||
|
b5fe0a642b | ||
|
b4b29878db | ||
|
9e68d38df8 | ||
|
574d363d7c | ||
|
69a011bc86 | ||
|
fe969c57c4 | ||
|
88d8b72c57 | ||
|
5639797c8d | ||
|
049a9cee06 | ||
|
14faf57a9e | ||
|
e19b548eff | ||
|
e3d84c40f7 | ||
|
50d49219a5 | ||
|
96209c852c |
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
.git
|
||||
.vscode
|
||||
.gitignore
|
||||
build.bat
|
||||
Dockerfile
|
||||
.DS_store
|
202
.gitignore
vendored
202
.gitignore
vendored
@@ -1,202 +1,4 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
build/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
|
||||
# Visual Studo 2015 cache/options directory
|
||||
.vs/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_i.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding addin-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/packages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/packages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/packages/repositories.config
|
||||
|
||||
# Windows Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Windows Store app package directory
|
||||
AppPackages/
|
||||
|
||||
# Others
|
||||
*.[Cc]ache
|
||||
ClientBin/
|
||||
[Ss]tyle[Cc]op.*
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
node_modules/
|
||||
bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
/bower_components
|
||||
/public/node_modules
|
||||
/vendor
|
||||
/.release
|
||||
/composer.phar
|
||||
/composer.lock
|
||||
embedded.txt
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"phpserver.relativePath": "public"
|
||||
}
|
4
.yarnrc
Normal file
4
.yarnrc
Normal file
@@ -0,0 +1,4 @@
|
||||
--modules-folder public/node_modules
|
||||
--install.production true
|
||||
--install.ignore-scripts true
|
||||
--install.ignore-optional true
|
58
Dockerfile-grocy
Normal file
58
Dockerfile-grocy
Normal file
@@ -0,0 +1,58 @@
|
||||
FROM php:7.2-fpm-alpine
|
||||
MAINTAINER Talmai Oliveira <to@talm.ai>
|
||||
|
||||
RUN apk update && \
|
||||
apk upgrade && \
|
||||
apk add --update yarn git &&\
|
||||
mkdir -p /www && \
|
||||
# Set environments
|
||||
sed -i "s|;*daemonize\s*=\s*yes|daemonize = no|g" /usr/local/etc/php-fpm.conf && \
|
||||
sed -i "s|;*listen\s*=\s*127.0.0.1:9000|listen = 9000|g" /usr/local/etc/php-fpm.conf && \
|
||||
sed -i "s|;*listen\s*=\s*/||g" /usr/local/etc/php-fpm.conf && \
|
||||
# sed -i "s|;*log_level\s*=\s*notice|log_level = debug|g" /usr/local/etc/php-fpm.conf && \
|
||||
sed -i "s|;*chdir\s*=\s*/var/www|chdir = /www|g" /usr/local/etc/php-fpm.d/www.conf && \
|
||||
# sed -i "s|;*access.log\s*=\s*log/\$pool.access.log|access.log = \$pool.access.log|g" /usr/local/etc/php-fpm.d/www.conf && \
|
||||
# sed -i "s|;*pm.status_path\s*=\s*/status|pm.status_path = /status|g" /usr/local/etc/php-fpm.d/www.conf && \
|
||||
# sed -i "s|;*memory_limit =.*|memory_limit = ${PHP_MEMORY_LIMIT}|i" /usr/local/etc/php.ini && \
|
||||
# sed -i "s|;*upload_max_filesize =.*|upload_max_filesize = ${MAX_UPLOAD}|i" /usr/local/etc/php.ini && \
|
||||
# sed -i "s|;*max_file_uploads =.*|max_file_uploads = ${PHP_MAX_FILE_UPLOAD}|i" /usr/local/etc/php.ini && \
|
||||
# sed -i "s|;*post_max_size =.*|post_max_size = ${PHP_MAX_POST}|i" /usr/local/etc/php.ini && \
|
||||
# sed -i "s|;*cgi.fix_pathinfo=.*|cgi.fix_pathinfo= 0|i" /usr/local/etc/php.ini && \
|
||||
wget https://raw.githubusercontent.com/composer/getcomposer.org/1b137f8bf6db3e79a38a5bc45324414a6b1f9df2/web/installer -O - -q | php -- --quiet && \
|
||||
# Cleaning up
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
COPY public /www/public
|
||||
COPY info.php /www/public
|
||||
COPY controllers /www/controllers
|
||||
COPY data /www/data
|
||||
COPY helpers /www/helpers
|
||||
COPY localization/ /www/localization
|
||||
COPY middleware/ /www/middleware
|
||||
COPY migrations/ /www/migrations
|
||||
COPY publication_assets/ /www/publication_assets
|
||||
COPY services/ /www/services
|
||||
COPY views/ /www/views
|
||||
COPY .yarnrc /www/
|
||||
COPY *.php /www/
|
||||
COPY *.json /www/
|
||||
COPY composer.* /root/.composer/
|
||||
COPY *yarn* /www/
|
||||
COPY *.sh /www/
|
||||
|
||||
# run php composer.phar with -vvv for extra debug information
|
||||
RUN cd /var/www/html && \
|
||||
php composer.phar --working-dir=/www/ -n install && \
|
||||
cp /www/config-dist.php /www/data/config.php && \
|
||||
cd /www && \
|
||||
yarn install && \
|
||||
chown www-data:www-data -R /www/
|
||||
|
||||
# Set Workdir
|
||||
WORKDIR /www/public
|
||||
|
||||
# Expose volumes
|
||||
VOLUME ["/www"]
|
||||
|
||||
# Expose ports
|
||||
EXPOSE 9000
|
32
Dockerfile-grocy-nginx
Normal file
32
Dockerfile-grocy-nginx
Normal file
@@ -0,0 +1,32 @@
|
||||
FROM alpine:latest
|
||||
MAINTAINER Talmai Oliveira <to@talm.ai>
|
||||
|
||||
RUN apk update && \
|
||||
apk upgrade && \
|
||||
apk add --update openssl nginx && \
|
||||
mkdir -p /etc/nginx/certificates && \
|
||||
mkdir -p /var/run/nginx && \
|
||||
mkdir -p /usr/share/nginx/html && \
|
||||
openssl req \
|
||||
-x509 \
|
||||
-newkey rsa:2048 \
|
||||
-keyout /etc/nginx/certificates/key.pem \
|
||||
-out /etc/nginx/certificates/cert.pem \
|
||||
-days 365 \
|
||||
-nodes \
|
||||
-subj /CN=localhost && \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
COPY docker_nginx/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY docker_nginx/common.conf /etc/nginx/common.conf
|
||||
COPY docker_nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY docker_nginx/conf.d/ssl.conf /etc/nginx/conf.d/ssl.conf
|
||||
|
||||
# Expose volumes
|
||||
VOLUME ["/etc/nginx/conf.d", "/var/log/nginx"]
|
||||
|
||||
# Expose ports
|
||||
EXPOSE 80 443
|
||||
|
||||
# Entry point
|
||||
ENTRYPOINT ["/usr/sbin/nginx", "-g", "daemon off;"]
|
104
Grocy.php
104
Grocy.php
@@ -1,104 +0,0 @@
|
||||
<?php
|
||||
|
||||
class Grocy
|
||||
{
|
||||
private static $DbConnectionRaw;
|
||||
/**
|
||||
* @return PDO
|
||||
*/
|
||||
public static function GetDbConnectionRaw($doMigrations = false)
|
||||
{
|
||||
if ($doMigrations === true)
|
||||
{
|
||||
self::$DbConnectionRaw = null;
|
||||
}
|
||||
|
||||
if (self::$DbConnectionRaw == null)
|
||||
{
|
||||
$pdo = new PDO('sqlite:' . __DIR__ . '/data/grocy.db');
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
if ($doMigrations === true)
|
||||
{
|
||||
Grocy::ExecuteDbStatement($pdo, "CREATE TABLE IF NOT EXISTS migrations (migration INTEGER NOT NULL UNIQUE, execution_time_timestamp DATETIME DEFAULT (datetime('now', 'localtime')), PRIMARY KEY(migration)) WITHOUT ROWID");
|
||||
GrocyDbMigrator::MigrateDb($pdo);
|
||||
|
||||
if (self::IsDemoInstallation())
|
||||
{
|
||||
GrocyDemoDataGenerator::PopulateDemoData($pdo);
|
||||
}
|
||||
}
|
||||
|
||||
self::$DbConnectionRaw = $pdo;
|
||||
}
|
||||
|
||||
return self::$DbConnectionRaw;
|
||||
}
|
||||
|
||||
private static $DbConnection;
|
||||
/**
|
||||
* @return LessQL\Database
|
||||
*/
|
||||
public static function GetDbConnection($doMigrations = false)
|
||||
{
|
||||
if ($doMigrations === true)
|
||||
{
|
||||
self::$DbConnection = null;
|
||||
}
|
||||
|
||||
if (self::$DbConnection == null)
|
||||
{
|
||||
self::$DbConnection = new LessQL\Database(self::GetDbConnectionRaw($doMigrations));
|
||||
}
|
||||
|
||||
return self::$DbConnection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
*/
|
||||
public static function ExecuteDbStatement(PDO $pdo, string $sql)
|
||||
{
|
||||
if ($pdo->exec(utf8_encode($sql)) === false)
|
||||
{
|
||||
throw new Exception($pdo->errorInfo());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean|PDOStatement
|
||||
*/
|
||||
public static function ExecuteDbQuery(PDO $pdo, string $sql)
|
||||
{
|
||||
if (self::ExecuteDbStatement($pdo, $sql) === true)
|
||||
{
|
||||
return $pdo->query(utf8_encode($sql));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
*/
|
||||
public static function IsDemoInstallation()
|
||||
{
|
||||
return file_exists(__DIR__ . '/data/demo.txt');
|
||||
}
|
||||
|
||||
private static $InstalledVersion;
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public static function GetInstalledVersion()
|
||||
{
|
||||
if (self::$InstalledVersion == null)
|
||||
{
|
||||
self::$InstalledVersion = file_get_contents(__DIR__ . '/version.txt');
|
||||
}
|
||||
|
||||
return self::$InstalledVersion;
|
||||
}
|
||||
}
|
@@ -1,116 +0,0 @@
|
||||
<?php
|
||||
|
||||
class GrocyDbMigrator
|
||||
{
|
||||
public static function MigrateDb(PDO $pdo)
|
||||
{
|
||||
self::ExecuteMigrationWhenNeeded($pdo, 1, "
|
||||
CREATE TABLE products (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
location_id INTEGER NOT NULL,
|
||||
qu_id_purchase INTEGER NOT NULL,
|
||||
qu_id_stock INTEGER NOT NULL,
|
||||
qu_factor_purchase_to_stock REAL NOT NULL,
|
||||
barcode TEXT,
|
||||
min_stock_amount INTEGER NOT NULL DEFAULT 0,
|
||||
default_best_before_days INTEGER NOT NULL DEFAULT 0,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
)"
|
||||
);
|
||||
|
||||
self::ExecuteMigrationWhenNeeded($pdo, 2, "
|
||||
CREATE TABLE locations (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
)"
|
||||
);
|
||||
|
||||
self::ExecuteMigrationWhenNeeded($pdo, 3, "
|
||||
CREATE TABLE quantity_units (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
)"
|
||||
);
|
||||
|
||||
self::ExecuteMigrationWhenNeeded($pdo, 4, "
|
||||
CREATE TABLE stock (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
product_id INTEGER NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
best_before_date DATE,
|
||||
purchased_date DATE DEFAULT (datetime('now', 'localtime')),
|
||||
stock_id TEXT NOT NULL,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
)"
|
||||
);
|
||||
|
||||
self::ExecuteMigrationWhenNeeded($pdo, 5, "
|
||||
CREATE TABLE stock_log (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
product_id INTEGER NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
best_before_date DATE,
|
||||
purchased_date DATE,
|
||||
used_date DATE,
|
||||
spoiled INTEGER NOT NULL DEFAULT 0,
|
||||
stock_id TEXT NOT NULL,
|
||||
transaction_type TEXT NOT NULL,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
)"
|
||||
);
|
||||
|
||||
self::ExecuteMigrationWhenNeeded($pdo, 6, "
|
||||
INSERT INTO locations (name, description) VALUES ('DefaultLocation', 'This is the first default location, edit or delete it');
|
||||
INSERT INTO quantity_units (name, description) VALUES ('DefaultQuantityUnit', 'This is the first default quantity unit, edit or delete it');
|
||||
INSERT INTO products (name, description, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('DefaultProduct1', 'This is the first default product, edit or delete it', 1, 1, 1, 1);
|
||||
INSERT INTO products (name, description, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('DefaultProduct2', 'This is the second default product, edit or delete it', 1, 1, 1, 1);"
|
||||
);
|
||||
|
||||
self::ExecuteMigrationWhenNeeded($pdo, 7, "
|
||||
CREATE VIEW stock_missing_products
|
||||
AS
|
||||
SELECT p.id, MAX(p.name) AS name, p.min_stock_amount - IFNULL(SUM(s.amount), 0) AS amount_missing
|
||||
FROM products p
|
||||
LEFT JOIN stock s
|
||||
ON p.id = s.product_id
|
||||
WHERE p.min_stock_amount != 0
|
||||
GROUP BY p.id
|
||||
HAVING IFNULL(SUM(s.amount), 0) < p.min_stock_amount;"
|
||||
);
|
||||
|
||||
self::ExecuteMigrationWhenNeeded($pdo, 8, "
|
||||
CREATE VIEW stock_current
|
||||
AS
|
||||
SELECT product_id, SUM(amount) AS amount, MIN(best_before_date) AS best_before_date
|
||||
from stock
|
||||
GROUP BY product_id
|
||||
ORDER BY MIN(best_before_date) ASC;"
|
||||
);
|
||||
|
||||
self::ExecuteMigrationWhenNeeded($pdo, 9, "
|
||||
CREATE TABLE shopping_list (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
product_id INTEGER NOT NULL UNIQUE,
|
||||
amount INTEGER NOT NULL DEFAULT 0,
|
||||
amount_autoadded INTEGER NOT NULL DEFAULT 0,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
)"
|
||||
);
|
||||
}
|
||||
|
||||
private static function ExecuteMigrationWhenNeeded(PDO $pdo, int $migrationId, string $sql)
|
||||
{
|
||||
$rowCount = Grocy::ExecuteDbQuery($pdo, 'SELECT COUNT(*) FROM migrations WHERE migration = ' . $migrationId)->fetchColumn();
|
||||
if (intval($rowCount) === 0)
|
||||
{
|
||||
Grocy::ExecuteDbStatement($pdo, $sql);
|
||||
Grocy::ExecuteDbStatement($pdo, 'INSERT INTO migrations (migration) VALUES (' . $migrationId . ')');
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,59 +0,0 @@
|
||||
<?php
|
||||
|
||||
class GrocyDemoDataGenerator
|
||||
{
|
||||
public static function PopulateDemoData(PDO $pdo)
|
||||
{
|
||||
$rowCount = Grocy::ExecuteDbQuery($pdo, 'SELECT COUNT(*) FROM migrations WHERE migration = -1')->fetchColumn();
|
||||
if (intval($rowCount) === 0)
|
||||
{
|
||||
$sql = "
|
||||
UPDATE locations SET name = 'Vorratskammer', description = '' WHERE id = 1;
|
||||
INSERT INTO locations (name) VALUES ('S<><53>igkeitenschrank'); --2
|
||||
INSERT INTO locations (name) VALUES ('Konservenschrank'); --3
|
||||
INSERT INTO locations (name) VALUES ('K<>hlschrank'); --4
|
||||
|
||||
UPDATE quantity_units SET name = 'St<53>ck' WHERE id = 1;
|
||||
INSERT INTO quantity_units (name) VALUES ('Packung'); --2
|
||||
INSERT INTO quantity_units (name) VALUES ('Glas'); --3
|
||||
INSERT INTO quantity_units (name) VALUES ('Dose'); --4
|
||||
INSERT INTO quantity_units (name) VALUES ('Becher'); --5
|
||||
INSERT INTO quantity_units (name) VALUES ('Bund'); --6
|
||||
|
||||
DELETE FROM products WHERE id IN (1, 2);
|
||||
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount) VALUES ('Gummib<69>rchen', 2, 2, 2, 1, 8); --3
|
||||
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount) VALUES ('Chips', 2, 2, 2, 1, 10); --4
|
||||
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Eier', 1, 2, 1, 10); --5
|
||||
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Nudeln', 1, 2, 2, 1); --6
|
||||
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Essiggurken', 3, 3, 3, 1); --7
|
||||
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Gulaschsuppe', 3, 4, 4, 1); --8
|
||||
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Joghurt', 4, 5, 5, 1); --9
|
||||
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('K<>se', 4, 2, 2, 1); --10
|
||||
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Aufschnitt', 4, 2, 2, 1); --11
|
||||
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Paprika', 4, 1, 1, 1); --12
|
||||
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Gurke', 4, 1, 1, 1); --13
|
||||
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Radieschen', 4, 6, 6, 1); --14
|
||||
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Tomate', 4, 1, 1, 1); --15
|
||||
|
||||
INSERT INTO migrations (migration) VALUES (-1);
|
||||
";
|
||||
|
||||
Grocy::ExecuteDbStatement($pdo, $sql);
|
||||
|
||||
GrocyLogicStock::AddProduct(3, 5, date('Y-m-d', strtotime('+180 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
|
||||
GrocyLogicStock::AddProduct(4, 5, date('Y-m-d', strtotime('+180 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
|
||||
GrocyLogicStock::AddProduct(5, 5, date('Y-m-d', strtotime('+20 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
|
||||
GrocyLogicStock::AddProduct(6, 5, date('Y-m-d', strtotime('+600 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
|
||||
GrocyLogicStock::AddProduct(7, 5, date('Y-m-d', strtotime('+800 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
|
||||
GrocyLogicStock::AddProduct(8, 5, date('Y-m-d', strtotime('+900 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
|
||||
GrocyLogicStock::AddProduct(9, 5, date('Y-m-d', strtotime('+14 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
|
||||
GrocyLogicStock::AddProduct(10, 5, date('Y-m-d', strtotime('+21 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
|
||||
GrocyLogicStock::AddProduct(11, 5, date('Y-m-d', strtotime('+10 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
|
||||
GrocyLogicStock::AddProduct(12, 5, date('Y-m-d', strtotime('+2 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
|
||||
GrocyLogicStock::AddProduct(13, 5, date('Y-m-d', strtotime('-2 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
|
||||
GrocyLogicStock::AddProduct(14, 5, date('Y-m-d', strtotime('+2 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
|
||||
GrocyLogicStock::AddProduct(15, 5, date('Y-m-d', strtotime('-2 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
|
||||
GrocyLogicStock::AddMissingProductsToShoppingList();
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,191 +0,0 @@
|
||||
<?php
|
||||
|
||||
class GrocyLogicStock
|
||||
{
|
||||
const TRANSACTION_TYPE_PURCHASE = 'purchase';
|
||||
const TRANSACTION_TYPE_CONSUME = 'consume';
|
||||
const TRANSACTION_TYPE_INVENTORY_CORRECTION = 'inventory-correction';
|
||||
|
||||
public static function GetCurrentStock()
|
||||
{
|
||||
$sql = 'SELECT * from stock_current';
|
||||
return Grocy::ExecuteDbQuery(Grocy::GetDbConnectionRaw(), $sql)->fetchAll(PDO::FETCH_OBJ);
|
||||
}
|
||||
|
||||
public static function GetMissingProducts()
|
||||
{
|
||||
$sql = 'SELECT * from stock_missing_products';
|
||||
return Grocy::ExecuteDbQuery(Grocy::GetDbConnectionRaw(), $sql)->fetchAll(PDO::FETCH_OBJ);
|
||||
}
|
||||
|
||||
public static function GetProductDetails(int $productId)
|
||||
{
|
||||
$db = Grocy::GetDbConnection();
|
||||
|
||||
$product = $db->products($productId);
|
||||
$productStockAmount = $db->stock()->where('product_id', $productId)->sum('amount');
|
||||
$productLastPurchased = $db->stock()->where('product_id', $productId)->max('purchased_date');
|
||||
$productLastUsed = $db->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_CONSUME)->max('used_date');
|
||||
$quPurchase = $db->quantity_units($product->qu_id_purchase);
|
||||
$quStock = $db->quantity_units($product->qu_id_stock);
|
||||
|
||||
return array(
|
||||
'product' => $product,
|
||||
'last_purchased' => $productLastPurchased,
|
||||
'last_used' => $productLastUsed,
|
||||
'stock_amount' => $productStockAmount,
|
||||
'quantity_unit_purchase' => $quPurchase,
|
||||
'quantity_unit_stock' => $quStock
|
||||
);
|
||||
}
|
||||
|
||||
public static function AddProduct(int $productId, int $amount, string $bestBeforeDate, $transactionType)
|
||||
{
|
||||
if ($transactionType === self::TRANSACTION_TYPE_CONSUME || $transactionType === self::TRANSACTION_TYPE_PURCHASE || $transactionType === self::TRANSACTION_TYPE_INVENTORY_CORRECTION)
|
||||
{
|
||||
$db = Grocy::GetDbConnection();
|
||||
$stockId = uniqid();
|
||||
|
||||
$logRow = $db->stock_log()->createRow(array(
|
||||
'product_id' => $productId,
|
||||
'amount' => $amount,
|
||||
'best_before_date' => $bestBeforeDate,
|
||||
'purchased_date' => date('Y-m-d'),
|
||||
'stock_id' => $stockId,
|
||||
'transaction_type' => $transactionType
|
||||
));
|
||||
$logRow->save();
|
||||
|
||||
$stockRow = $db->stock()->createRow(array(
|
||||
'product_id' => $productId,
|
||||
'amount' => $amount,
|
||||
'best_before_date' => $bestBeforeDate,
|
||||
'purchased_date' => date('Y-m-d'),
|
||||
'stock_id' => $stockId,
|
||||
));
|
||||
$stockRow->save();
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Transaction type $transactionType is not valid (GrocyLogicStock.AddProduct)");
|
||||
}
|
||||
}
|
||||
|
||||
public static function ConsumeProduct(int $productId, int $amount, bool $spoiled, $transactionType)
|
||||
{
|
||||
if ($transactionType === self::TRANSACTION_TYPE_CONSUME || $transactionType === self::TRANSACTION_TYPE_PURCHASE || $transactionType === self::TRANSACTION_TYPE_INVENTORY_CORRECTION)
|
||||
{
|
||||
$db = Grocy::GetDbConnection();
|
||||
|
||||
$productStockAmount = $db->stock()->where('product_id', $productId)->sum('amount');
|
||||
$potentialStockEntries = $db->stock()->where('product_id', $productId)->orderBy('purchased_date', 'ASC')->fetchAll(); //FIFO
|
||||
|
||||
if ($amount > $productStockAmount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($potentialStockEntries as $stockEntry)
|
||||
{
|
||||
if ($amount == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if ($amount >= $stockEntry->amount) //Take the whole stock entry
|
||||
{
|
||||
$logRow = $db->stock_log()->createRow(array(
|
||||
'product_id' => $stockEntry->product_id,
|
||||
'amount' => $stockEntry->amount * -1,
|
||||
'best_before_date' => $stockEntry->best_before_date,
|
||||
'purchased_date' => $stockEntry->purchased_date,
|
||||
'used_date' => date('Y-m-d'),
|
||||
'spoiled' => $spoiled,
|
||||
'stock_id' => $stockEntry->stock_id,
|
||||
'transaction_type' => $transactionType
|
||||
));
|
||||
$logRow->save();
|
||||
|
||||
$amount -= $stockEntry->amount;
|
||||
$stockEntry->delete();
|
||||
}
|
||||
else //Stock entry amount is > than needed amount -> split the stock entry resp. update the amount
|
||||
{
|
||||
$logRow = $db->stock_log()->createRow(array(
|
||||
'product_id' => $stockEntry->product_id,
|
||||
'amount' => $amount * -1,
|
||||
'best_before_date' => $stockEntry->best_before_date,
|
||||
'purchased_date' => $stockEntry->purchased_date,
|
||||
'used_date' => date('Y-m-d'),
|
||||
'spoiled' => $spoiled,
|
||||
'stock_id' => $stockEntry->stock_id,
|
||||
'transaction_type' => $transactionType
|
||||
));
|
||||
$logRow->save();
|
||||
|
||||
$restStockAmount = $stockEntry->amount - $amount;
|
||||
$amount = 0;
|
||||
|
||||
$stockEntry->update(array(
|
||||
'amount' => $restStockAmount
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Transaction type $transactionType is not valid (GrocyLogicStock.ConsumeProduct)");
|
||||
}
|
||||
}
|
||||
|
||||
public static function InventoryProduct(int $productId, int $newAmount, string $bestBeforeDate)
|
||||
{
|
||||
$db = Grocy::GetDbConnection();
|
||||
$productStockAmount = $db->stock()->where('product_id', $productId)->sum('amount');
|
||||
|
||||
if ($newAmount > $productStockAmount)
|
||||
{
|
||||
$amountToAdd = $newAmount - $productStockAmount;
|
||||
self::AddProduct($productId, $amountToAdd, $bestBeforeDate, self::TRANSACTION_TYPE_INVENTORY_CORRECTION);
|
||||
}
|
||||
else if ($newAmount < $productStockAmount)
|
||||
{
|
||||
$amountToRemove = $productStockAmount - $newAmount;
|
||||
self::ConsumeProduct($productId, $amountToRemove, false, self::TRANSACTION_TYPE_INVENTORY_CORRECTION);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function AddMissingProductsToShoppingList()
|
||||
{
|
||||
$db = Grocy::GetDbConnection();
|
||||
|
||||
$missingProducts = self::GetMissingProducts();
|
||||
foreach ($missingProducts as $missingProduct)
|
||||
{
|
||||
$product = $db->products()->where('id', $missingProduct->id)->fetch();
|
||||
$amount = ceil($missingProduct->amount_missing / $product->qu_factor_purchase_to_stock);
|
||||
|
||||
$alreadyExistingEntry = $db->shopping_list()->where('product_id', $missingProduct->id)->fetch();
|
||||
if ($alreadyExistingEntry) //Update
|
||||
{
|
||||
$alreadyExistingEntry->update(array(
|
||||
'amount_autoadded' => $amount
|
||||
));
|
||||
}
|
||||
else //Insert
|
||||
{
|
||||
$shoppinglistRow = $db->shopping_list()->createRow(array(
|
||||
'product_id' => $missingProduct->id,
|
||||
'amount_autoadded' => $amount
|
||||
));
|
||||
$shoppinglistRow->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,60 +0,0 @@
|
||||
<?php
|
||||
|
||||
class GrocyPhpHelper
|
||||
{
|
||||
public static function FindObjectInArrayByPropertyValue($array, $propertyName, $propertyValue)
|
||||
{
|
||||
foreach($array as $object)
|
||||
{
|
||||
if($object->{$propertyName} == $propertyValue)
|
||||
{
|
||||
return $object;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function FindAllObjectsInArrayByPropertyValue($array, $propertyName, $propertyValue, $operator = '==')
|
||||
{
|
||||
$returnArray = array();
|
||||
|
||||
foreach($array as $object)
|
||||
{
|
||||
switch($operator)
|
||||
{
|
||||
case '==':
|
||||
if($object->{$propertyName} == $propertyValue)
|
||||
{
|
||||
$returnArray[] = $object;
|
||||
}
|
||||
break;
|
||||
case '>':
|
||||
if($object->{$propertyName} > $propertyValue)
|
||||
{
|
||||
$returnArray[] = $object;
|
||||
}
|
||||
break;
|
||||
case '<':
|
||||
if($object->{$propertyName} < $propertyValue)
|
||||
{
|
||||
$returnArray[] = $object;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $returnArray;
|
||||
}
|
||||
|
||||
public static function SumArrayValue($array, $propertyName)
|
||||
{
|
||||
$sum = 0;
|
||||
foreach($array as $object)
|
||||
{
|
||||
$sum += $object->{$propertyName};
|
||||
}
|
||||
|
||||
return $sum;
|
||||
}
|
||||
}
|
102
README.md
102
README.md
@@ -1,17 +1,103 @@
|
||||
# grocy
|
||||
ERP beyond your fridge
|
||||
|
||||
## Give it a try
|
||||
- Public demo of the latest stable version → [https://demo.grocy.info](https://demo.grocy.info)
|
||||
- Public demo of the latest pre-release version (current master branch) → [https://demo-prerelease.grocy.info](https://demo-prerelease.grocy.info)
|
||||
|
||||
## Motivation
|
||||
A household needs to be managed. I did this so far (almost 10 years) with my first self written software (a C# windows forms application) and with a bunch of Excel sheets. The software is a pain to use and Excel is Excel. So I searched for and tried different things for a (very) long time, nothing 100 % fitted, so this is my aim for a "complete houshold management"-thing.
|
||||
|
||||
## What it is about
|
||||
For now my main focus is on stock management, ERP your fridge!
|
||||
|
||||
# Give it a try
|
||||
Public demo of the latest version → [https://grocy.projectdemos.berrnd.org](https://grocy.projectdemos.berrnd.org)
|
||||
A household needs to be managed. I did this so far (almost 10 years) with my first self written software (a C# windows forms application) and with a bunch of Excel sheets. The software is a pain to use and Excel is Excel. So I searched for and tried different things for a (very) long time, nothing 100 % fitted, so this is my aim for a "complete household management"-thing. ERP your fridge!
|
||||
|
||||
## How to install
|
||||
Just unpack the [latest release](https://github.com/berrnd/grocy/releases/latest) on your PHP enabled webserver, copy `config-dist.php` to `data/config.php`, edit it to your needs, ensure that the `data` directory is writable and you're ready to go. Alternatively clone this repository and install Composer and Bower dependencies manually.
|
||||
> **NEW**
|
||||
>
|
||||
> There is now grocy-desktop if you want to run grocy without a webserver just like a normal (windows) desktop application.
|
||||
>
|
||||
> See https://github.com/berrnd/grocy-desktop or directly download the [latest release](https://releases.grocy.info/latest-desktop) - the installation is nothing more than just clicking 2 times "next"...
|
||||
|
||||
Just unpack the [latest release](https://releases.grocy.info/latest) on your PHP (SQLite extension required, currently only tested with PHP 7.2) enabled webserver (webservers root should point to the `public` directory), copy `config-dist.php` to `data/config.php`, edit it to your needs, ensure that the `data` directory is writable and you're ready to go, (to make writable `chown -R www-data:www-data data/`). Default login is user `admin` with password `admin`, please change the password immediately (see user menu).
|
||||
|
||||
Alternatively clone this repository and install Composer and Yarn dependencies manually.
|
||||
|
||||
If you use nginx as your webserver, please include `try_files $uri /index.php;` in your location block.
|
||||
|
||||
If, however, your webserver does not support URL rewriting, set `DISABLE_URL_REWRITING` in `data/config.php` (`Setting('DISABLE_URL_REWRITING', true);`).
|
||||
|
||||
## How to run using Docker
|
||||
|
||||
The docker images build are based on [Alpine](https://hub.docker.com/_/alpine/), with an extremelly low footprint (less than 10 MB for nginx, and less than 70MB for grocy with php-fm. That number is eventually bumped up to 353MB after all the dependencies are downloaded, however). Anyhow, to run using docker just do the following:
|
||||
|
||||
```
|
||||
> docker-compose up
|
||||
```
|
||||
|
||||
And grocy should be accessible via `http(s)://localhost/`. The https option will work. However, since the certificate is self-signed, most browsers will complain.
|
||||
|
||||
## How to update
|
||||
Just overwrite everything with the latest release while keeping the `data` directory, check `config-dist.php` for new configuration options and add them to your `data/config.php` (the default from values `config-dist.php` will be used for not in `data/config.php` defined settings). Just to be sure, please empty `data/viewcache`.
|
||||
|
||||
If you run grocy on Linux, there is also `update.sh` (remember to make the script executable, `chmod +x update.sh` and ensure that you have `unzip` installed) which does exactly this and additionally creates a backup (`.tgz` archive) of the current installation in `data/backups` (backups older than 60 days will be deleted during the update).
|
||||
|
||||
## Localization
|
||||
grocy is fully localizable - the default language is English (integrated into code), a German localization is always maintained by me. There is one file per language in the `localization` directory, if you want to create a translation, it's best to copy `localization/de.php` to a new one (e. g. `localization/it.php`) and translating all strings there. (Language can be changed in `data/config.php`, e. g. `Setting('CULTURE', 'it');`)
|
||||
|
||||
### Maintaining your own localization
|
||||
As the German translation will always be the most complete one, for maintaining your localization it would be easiest when you compare your localization with the German one with a diff tool of your choice.
|
||||
|
||||
## Things worth to know
|
||||
|
||||
### REST API & data model documentation
|
||||
See the integrated Swagger UI instance on [/api](https://demo-en.grocy.info/api).
|
||||
|
||||
### Barcode readers
|
||||
Some fields also allow to select a value by scanning a barcode. It works best when your barcode reader prefixes every barcode with a letter which is normally not part of a item name (I use a `$`) and sends a `TAB` after a scan.
|
||||
|
||||
### Input shorthands for date fields
|
||||
For (productivity) reasons all date (and time) input fields use the ISO-8601 format regardless of localization.
|
||||
The following shorthands are available:
|
||||
- `MMDD` gets expanded to the given day on the current year, if > today, or to the given day next year, if < today, in proper notation
|
||||
- Example: `0517` will be converted to `2018-05-17`
|
||||
- `YYYYMMDD` gets expanded to the proper ISO-8601 notation
|
||||
- Example: `20190417` will be converted to `2019-04-17`
|
||||
- `YYYYMMe` or `YYYYMM+` gets expanded to the end of the given month in the given year in proper notation
|
||||
- Example: `201807e` will be converted to `2018-07-31`
|
||||
- `x` gets expanded to `2999-12-31` (which I use for products which never expire)
|
||||
- Down/up arrow keys will increase/decrease the date by one day
|
||||
- Right/left arrow keys will increase/decrease the date by 1 week
|
||||
|
||||
### Keyboard shorthands for buttons
|
||||
Wherever a button contains a bold highlighted letter, this is a shortcut key.
|
||||
Example: Button "Add as new **p**roduct" can be "pressed" by using the `P` key on your keyboard.
|
||||
|
||||
### Barcode lookup via external services
|
||||
Products can be directly added to the database via looking them up against external services by a barcode.
|
||||
This is currently only possible through the REST API.
|
||||
There is no plugin included for any service, see the reference implementation in `data/plugins/DemoBarcodeLookupPlugin.php`.
|
||||
|
||||
### Database migrations
|
||||
Database schema migration is automatically done when visiting the root (`/`) route (click on the logo in the left upper edge).
|
||||
|
||||
### Adding your own CSS or JS without to have to modify the application itself
|
||||
- When the file `data/custom_js.html` exists, the contents of the file will be added just before `</body>` (end of body) on every page
|
||||
- When the file `data/custom_css.html` exists, the contents of the file will be added just before `</head>` (end of head) on every page
|
||||
|
||||
### Demo mode
|
||||
When the file `data/demo.txt` exists, the application will work in a demo mode which means authentication is disabled and some demo data will be generated during the database schema migration.
|
||||
|
||||
### Embedded mode
|
||||
When the file `embedded.txt` exists, it must contain a valid and writable path which will be used as the data directory instead of `data` and authentication will be disabled (used in [grocy-desktop](https://github.com/berrnd/grocy-desktop)).
|
||||
|
||||
In embedded mode, settings can be overridden by text files in `data/settingoverrides`, the file name must be `<SettingName>.txt` (e. g. `BASE_URL.txt`) and the content must be the setting value (normally one single line).
|
||||
|
||||
## Screenshots
|
||||
#### Dashboard
|
||||

|
||||
|
||||
#### Purchase - with barcode scan
|
||||

|
||||
|
||||
#### Consume - with manual search
|
||||

|
||||
|
||||
## License
|
||||
The MIT License (MIT)
|
||||
|
71
app.php
Normal file
71
app.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
use \Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use \Psr\Http\Message\ResponseInterface as Response;
|
||||
|
||||
use \Grocy\Helpers\UrlManager;
|
||||
use \Grocy\Controllers\LoginController;
|
||||
|
||||
// Definitions for embedded mode
|
||||
if (file_exists(__DIR__ . '/embedded.txt'))
|
||||
{
|
||||
define('GROCY_IS_EMBEDDED_INSTALL', true);
|
||||
define('GROCY_DATAPATH', file_get_contents(__DIR__ . '/embedded.txt'));
|
||||
define('GROCY_USER_ID', 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
define('GROCY_IS_EMBEDDED_INSTALL', false);
|
||||
define('GROCY_DATAPATH', __DIR__ . '/data');
|
||||
}
|
||||
|
||||
// Definitions for demo mode
|
||||
if (file_exists(GROCY_DATAPATH . '/demo.txt'))
|
||||
{
|
||||
define('GROCY_IS_DEMO_INSTALL', true);
|
||||
if (!defined('GROCY_USER_ID'))
|
||||
{
|
||||
define('GROCY_USER_ID', 1);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
define('GROCY_IS_DEMO_INSTALL', false);
|
||||
}
|
||||
|
||||
// Load composer dependencies
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
// Load config files
|
||||
require_once GROCY_DATAPATH . '/config.php';
|
||||
require_once __DIR__ . '/config-dist.php'; //For not in own config defined values we use the default ones
|
||||
|
||||
// Setup base application
|
||||
$appContainer = new \Slim\Container([
|
||||
'settings' => [
|
||||
'displayErrorDetails' => true,
|
||||
'determineRouteBeforeAppMiddleware' => true
|
||||
],
|
||||
'view' => function($container)
|
||||
{
|
||||
return new \Slim\Views\Blade(__DIR__ . '/views', GROCY_DATAPATH . '/viewcache');
|
||||
},
|
||||
'LoginControllerInstance' => function($container)
|
||||
{
|
||||
return new LoginController($container, 'grocy_session');
|
||||
},
|
||||
'UrlManager' => function($container)
|
||||
{
|
||||
return new UrlManager(GROCY_BASE_URL);
|
||||
},
|
||||
'ApiKeyHeaderName' => function($container)
|
||||
{
|
||||
return 'GROCY-API-KEY';
|
||||
}
|
||||
]);
|
||||
$app = new \Slim\App($appContainer);
|
||||
|
||||
// Load routes from separate file
|
||||
require_once __DIR__ . '/routes.php';
|
||||
|
||||
$app->run();
|
21
bower.json
21
bower.json
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "asp.net",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"bootstrap": "3.3.7",
|
||||
"font-awesome": "4.7.0",
|
||||
"bootbox": "4.4.0",
|
||||
"jquery.serializeJSON": "2.7.2",
|
||||
"bootstrap-validator": "0.11.9",
|
||||
"bootstrap-datepicker": "1.6.4",
|
||||
"moment": "2.18.1",
|
||||
"bootstrap-combobox": "1.1.8",
|
||||
"datatables.net": "1.10.13",
|
||||
"datatables.net-bs": "2.1.1",
|
||||
"datatables.net-responsive": "2.1.1",
|
||||
"datatables.net-responsive-bs": "2.1.1",
|
||||
"jquery-timeago": "1.5.4",
|
||||
"toastr": "2.1.3",
|
||||
"tagmanager": "3.0.2"
|
||||
}
|
||||
}
|
@@ -4,8 +4,10 @@ if %projectPath:~-1%==\ set projectPath=%projectPath:~0,-1%
|
||||
set releasePath=%projectPath%\.release
|
||||
mkdir "%releasePath%"
|
||||
|
||||
for /f "tokens=*" %%a in ('type version.txt') do set version=%%a
|
||||
for /f "tokens=*" %%a in ('build_tools\jq.exe .Version version.json --raw-output') do set version=%%a
|
||||
|
||||
del "%releasePath%\grocy_%version%.zip"
|
||||
"build_tools\7za.exe" a -r "%releasePath%\grocy_%version%.zip" "%projectPath%\*" -xr!.* -xr!build_tools -xr!build.bat -xr!composer.json -xr!composer.lock -xr!composer.phar -xr!grocy.phpproj -xr!grocy.phpproj.user -xr!grocy.sln -xr!bower.json
|
||||
"build_tools\7za.exe" d "%releasePath%\grocy_%version%.zip" data\*.*
|
||||
"build_tools\7za.exe" a -r "%releasePath%\grocy_%version%.zip" "%projectPath%\*" -xr!.* -xr!build_tools -xr!build.bat -xr!composer.json -xr!composer.lock -xr!package.json -xr!yarn.lock -xr!publication_assets
|
||||
"build_tools\7za.exe" a "%releasePath%\grocy_%version%.zip" "%projectPath%\public\.htaccess"
|
||||
"build_tools\7za.exe" rn "%releasePath%\grocy_%version%.zip" .htaccess public\.htaccess
|
||||
"build_tools\7za.exe" d "%releasePath%\grocy_%version%.zip" data\*.* data\storage data\viewcache\*
|
||||
|
BIN
build_tools/jq.exe
Normal file
BIN
build_tools/jq.exe
Normal file
Binary file not shown.
@@ -1,8 +1,20 @@
|
||||
{
|
||||
"require": {
|
||||
"php": ">=7.2",
|
||||
"slim/slim": "^3.8",
|
||||
"slim/php-view": "^2.2",
|
||||
"morris/lessql": "^0.3.4",
|
||||
"tuupola/slim-basic-auth": "^2.2"
|
||||
"rubellum/slim-blade-view": "^0.1.1",
|
||||
"tuupola/cors-middleware": "^0.7.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Grocy\\Services\\": "services/",
|
||||
"Grocy\\Controllers\\": "controllers/",
|
||||
"Grocy\\Middleware\\": "middleware/",
|
||||
"Grocy\\Helpers\\": "helpers/"
|
||||
},
|
||||
"files": [
|
||||
"helpers/extensions.php"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
1572
composer.lock
generated
Normal file
1572
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,44 @@
|
||||
<?php
|
||||
|
||||
define('HTTP_USER', 'admin');
|
||||
define('HTTP_PASSWORD', 'admin');
|
||||
# Either "production", "dev" or "prerelease"
|
||||
Setting('MODE', 'production');
|
||||
|
||||
# Either "en" or "de" or the filename (without extension) of
|
||||
# one of the other available localization files in the "/localization" directory
|
||||
Setting('CULTURE', 'en');
|
||||
|
||||
# To keep it simple: grocy does not handle any currency conversions,
|
||||
# this here is used to format all money values,
|
||||
# so can be anything (e. g. "USD" OR "$", doesn't matter...)
|
||||
Setting('CURRENCY', '$');
|
||||
|
||||
# The base url of your installation,
|
||||
# should be just "/" when running directly under the root of a (sub)domain
|
||||
# or for example "https:/example.com/grocy" when using a subdirectory
|
||||
Setting('BASE_URL', '/');
|
||||
|
||||
# The plugin to use for external barcode lookups,
|
||||
# must be the filename without .php extension and must be located in /data/plugins,
|
||||
# see /data/plugins/DemoBarcodeLookupPlugin.php for an example implementation
|
||||
Setting('STOCK_BARCODE_LOOKUP_PLUGIN', 'DemoBarcodeLookupPlugin');
|
||||
|
||||
# If, however, your webserver does not support URL rewriting,
|
||||
# set this to true
|
||||
Setting('DISABLE_URL_REWRITING', false);
|
||||
|
||||
|
||||
# Default user settings
|
||||
# These settings can be changed per user, here the defaults
|
||||
# are defined which are used when the user has not changed the setting so far
|
||||
|
||||
# Night mode related
|
||||
DefaultUserSetting('night_mode_enabled', false); // If night mode is enabled always
|
||||
DefaultUserSetting('auto_night_mode_enabled', false); // If night mode is enabled automatically when inside a given time range (see the two settings below)
|
||||
DefaultUserSetting('auto_night_mode_time_range_from', "20:00"); // Format HH:mm
|
||||
DefaultUserSetting('auto_night_mode_time_range_to', "07:00"); // Format HH:mm
|
||||
DefaultUserSetting('auto_night_mode_time_range_goes_over_midnight', true); // If the time range above goes over midnight
|
||||
DefaultUserSetting('currently_inside_night_mode_range', false); // If we're currently inside of night mode time range (this is not user configurable, but stored as a user setting because it's evaluated client side to be able to use the client time instead of the maybe different server time)
|
||||
|
||||
# If the page should be automatically reloaded when there was
|
||||
# an external change
|
||||
DefaultUserSetting('auto_reload_on_db_change', true);
|
||||
|
28
controllers/BaseApiController.php
Normal file
28
controllers/BaseApiController.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Controllers;
|
||||
|
||||
class BaseApiController extends BaseController
|
||||
{
|
||||
|
||||
public function __construct(\Slim\Container $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->OpenApiSpec = json_decode(file_get_contents(__DIR__ . '/../grocy.openapi.json'));
|
||||
}
|
||||
|
||||
protected $OpenApiSpec;
|
||||
|
||||
protected function ApiResponse($data)
|
||||
{
|
||||
return json_encode($data);
|
||||
}
|
||||
|
||||
protected function VoidApiActionResponse($response, $success = true, $status = 200, $errorMessage = '')
|
||||
{
|
||||
return $response->withStatus($status)->withJson(array(
|
||||
'success' => $success,
|
||||
'error_message' => $errorMessage
|
||||
));
|
||||
}
|
||||
}
|
60
controllers/BaseController.php
Normal file
60
controllers/BaseController.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Controllers;
|
||||
|
||||
use \Grocy\Services\DatabaseService;
|
||||
use \Grocy\Services\ApplicationService;
|
||||
use \Grocy\Services\LocalizationService;
|
||||
use \Grocy\Services\UsersService;
|
||||
|
||||
class BaseController
|
||||
{
|
||||
public function __construct(\Slim\Container $container) {
|
||||
$databaseService = new DatabaseService();
|
||||
$this->Database = $databaseService->GetDbConnection();
|
||||
|
||||
$localizationService = new LocalizationService(GROCY_CULTURE);
|
||||
$this->LocalizationService = $localizationService;
|
||||
|
||||
if (GROCY_MODE === 'prerelease')
|
||||
{
|
||||
$commitHash = trim(exec('git log --pretty="%h" -n1 HEAD'));
|
||||
$commitDate = trim(exec('git log --date=iso --pretty="%cd" -n1 HEAD'));
|
||||
|
||||
$container->view->set('version', "pre-release-$commitHash");
|
||||
$container->view->set('releaseDate', \substr($commitDate, 0, 19));
|
||||
}
|
||||
else
|
||||
{
|
||||
$applicationService = new ApplicationService();
|
||||
$versionInfo = $applicationService->GetInstalledVersion();
|
||||
$container->view->set('version', $versionInfo->Version);
|
||||
$container->view->set('releaseDate', $versionInfo->ReleaseDate);
|
||||
}
|
||||
|
||||
$container->view->set('localizationStrings', $localizationService->GetCurrentCultureLocalizations());
|
||||
$container->view->set('L', function($text, ...$placeholderValues) use($localizationService)
|
||||
{
|
||||
return $localizationService->Localize($text, ...$placeholderValues);
|
||||
});
|
||||
$container->view->set('U', function($relativePath, $isResource = false) use($container)
|
||||
{
|
||||
return $container->UrlManager->ConstructUrl($relativePath, $isResource);
|
||||
});
|
||||
|
||||
try {
|
||||
$usersService = new UsersService();
|
||||
$container->view->set('userSettings', $usersService->GetUserSettings(GROCY_USER_ID));
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
// Happens when database is not initialised or migrated...
|
||||
}
|
||||
|
||||
$this->AppContainer = $container;
|
||||
}
|
||||
|
||||
protected $AppContainer;
|
||||
protected $Database;
|
||||
protected $LocalizationService;
|
||||
}
|
52
controllers/BatteriesApiController.php
Normal file
52
controllers/BatteriesApiController.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Controllers;
|
||||
|
||||
use \Grocy\Services\BatteriesService;
|
||||
|
||||
class BatteriesApiController extends BaseApiController
|
||||
{
|
||||
public function __construct(\Slim\Container $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->BatteriesService = new BatteriesService();
|
||||
}
|
||||
|
||||
protected $BatteriesService;
|
||||
|
||||
public function TrackChargeCycle(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
$trackedTime = date('Y-m-d H:i:s');
|
||||
if (isset($request->getQueryParams()['tracked_time']) && !empty($request->getQueryParams()['tracked_time']) && IsIsoDateTime($request->getQueryParams()['tracked_time']))
|
||||
{
|
||||
$trackedTime = $request->getQueryParams()['tracked_time'];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$this->BatteriesService->TrackChargeCycle($args['batteryId'], $trackedTime);
|
||||
return $this->VoidApiActionResponse($response);
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function BatteryDetails(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
try
|
||||
{
|
||||
return $this->ApiResponse($this->BatteriesService->GetBatteryDetails($args['batteryId']));
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function Current(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->ApiResponse($this->BatteriesService->GetCurrent());
|
||||
}
|
||||
}
|
56
controllers/BatteriesController.php
Normal file
56
controllers/BatteriesController.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Controllers;
|
||||
|
||||
use \Grocy\Services\BatteriesService;
|
||||
|
||||
class BatteriesController extends BaseController
|
||||
{
|
||||
public function __construct(\Slim\Container $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->BatteriesService = new BatteriesService();
|
||||
}
|
||||
|
||||
protected $BatteriesService;
|
||||
|
||||
public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'batteriesoverview', [
|
||||
'batteries' => $this->Database->batteries()->orderBy('name'),
|
||||
'current' => $this->BatteriesService->GetCurrent(),
|
||||
'nextXDays' => 5
|
||||
]);
|
||||
}
|
||||
|
||||
public function TrackChargeCycle(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'batterytracking', [
|
||||
'batteries' => $this->Database->batteries()->orderBy('name')
|
||||
]);
|
||||
}
|
||||
|
||||
public function BatteriesList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'batteries', [
|
||||
'batteries' => $this->Database->batteries()->orderBy('name')
|
||||
]);
|
||||
}
|
||||
|
||||
public function BatteryEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
if ($args['batteryId'] == 'new')
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'batteryform', [
|
||||
'mode' => 'create'
|
||||
]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'batteryform', [
|
||||
'battery' => $this->Database->batteries($args['batteryId']),
|
||||
'mode' => 'edit'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
58
controllers/ChoresApiController.php
Normal file
58
controllers/ChoresApiController.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Controllers;
|
||||
|
||||
use \Grocy\Services\ChoresService;
|
||||
|
||||
class ChoresApiController extends BaseApiController
|
||||
{
|
||||
public function __construct(\Slim\Container $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->ChoresService = new ChoresService();
|
||||
}
|
||||
|
||||
protected $ChoresService;
|
||||
|
||||
public function TrackChoreExecution(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
$trackedTime = date('Y-m-d H:i:s');
|
||||
if (isset($request->getQueryParams()['tracked_time']) && !empty($request->getQueryParams()['tracked_time']) && IsIsoDateTime($request->getQueryParams()['tracked_time']))
|
||||
{
|
||||
$trackedTime = $request->getQueryParams()['tracked_time'];
|
||||
}
|
||||
|
||||
$doneBy = GROCY_USER_ID;
|
||||
if (isset($request->getQueryParams()['done_by']) && !empty($request->getQueryParams()['done_by']))
|
||||
{
|
||||
$doneBy = $request->getQueryParams()['done_by'];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$this->ChoresService->TrackChore($args['choreId'], $trackedTime, $doneBy);
|
||||
return $this->VoidApiActionResponse($response);
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function ChoreDetails(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
try
|
||||
{
|
||||
return $this->ApiResponse($this->ChoresService->GetChoreDetails($args['choreId']));
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function Current(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->ApiResponse($this->ChoresService->GetCurrent());
|
||||
}
|
||||
}
|
68
controllers/ChoresController.php
Normal file
68
controllers/ChoresController.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Controllers;
|
||||
|
||||
use \Grocy\Services\ChoresService;
|
||||
|
||||
class ChoresController extends BaseController
|
||||
{
|
||||
public function __construct(\Slim\Container $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->ChoresService = new ChoresService();
|
||||
}
|
||||
|
||||
protected $ChoresService;
|
||||
|
||||
public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'choresoverview', [
|
||||
'chores' => $this->Database->chores()->orderBy('name'),
|
||||
'currentChores' => $this->ChoresService->GetCurrent(),
|
||||
'nextXDays' => 5
|
||||
]);
|
||||
}
|
||||
|
||||
public function TrackChoreExecution(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'choretracking', [
|
||||
'chores' => $this->Database->chores()->orderBy('name'),
|
||||
'users' => $this->Database->users()->orderBy('username')
|
||||
]);
|
||||
}
|
||||
|
||||
public function ChoresList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'chores', [
|
||||
'chores' => $this->Database->chores()->orderBy('name')
|
||||
]);
|
||||
}
|
||||
|
||||
public function Analysis(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'choresanalysis', [
|
||||
'choresLog' => $this->Database->chores_log()->orderBy('tracked_time', 'DESC'),
|
||||
'chores' => $this->Database->chores()->orderBy('name'),
|
||||
'users' => $this->Database->users()->orderBy('username')
|
||||
]);
|
||||
}
|
||||
|
||||
public function ChoreEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
if ($args['choreId'] == 'new')
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'choreform', [
|
||||
'periodTypes' => GetClassConstants('\Grocy\Services\ChoresService'),
|
||||
'mode' => 'create'
|
||||
]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'choreform', [
|
||||
'chore' => $this->Database->chores($args['choreId']),
|
||||
'periodTypes' => GetClassConstants('\Grocy\Services\ChoresService'),
|
||||
'mode' => 'edit'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
38
controllers/FilesApiController.php
Normal file
38
controllers/FilesApiController.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Controllers;
|
||||
|
||||
use \Grocy\Services\FilesService;
|
||||
|
||||
class FilesApiController extends BaseApiController
|
||||
{
|
||||
public function __construct(\Slim\Container $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->FilesService = new FilesService();
|
||||
}
|
||||
|
||||
protected $FilesService;
|
||||
|
||||
public function Upload(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (isset($request->getQueryParams()['file_name']) && !empty($request->getQueryParams()['file_name']) && IsValidFileName($request->getQueryParams()['file_name']))
|
||||
{
|
||||
$fileName = $request->getQueryParams()['file_name'];
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \Exception('file_name query parameter missing or contains an invalid filename');
|
||||
}
|
||||
|
||||
$data = $request->getBody()->getContents();
|
||||
file_put_contents($this->FilesService->GetFilePath($args['group'], $fileName), $data);
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
80
controllers/GenericEntityApiController.php
Normal file
80
controllers/GenericEntityApiController.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Controllers;
|
||||
|
||||
class GenericEntityApiController extends BaseApiController
|
||||
{
|
||||
public function GetObjects(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
if ($this->IsValidEntity($args['entity']))
|
||||
{
|
||||
return $this->ApiResponse($this->Database->{$args['entity']}());
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, 'Entity does not exist or is not exposed');
|
||||
}
|
||||
}
|
||||
|
||||
public function GetObject(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
if ($this->IsValidEntity($args['entity']))
|
||||
{
|
||||
return $this->ApiResponse($this->Database->{$args['entity']}($args['objectId']));
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, 'Entity does not exist or is not exposed');
|
||||
}
|
||||
}
|
||||
|
||||
public function AddObject(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
if ($this->IsValidEntity($args['entity']))
|
||||
{
|
||||
$newRow = $this->Database->{$args['entity']}()->createRow($request->getParsedBody());
|
||||
$newRow->save();
|
||||
$success = $newRow->isClean();
|
||||
return $this->ApiResponse(array('success' => $success));
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, 'Entity does not exist or is not exposed');
|
||||
}
|
||||
}
|
||||
|
||||
public function EditObject(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
if ($this->IsValidEntity($args['entity']))
|
||||
{
|
||||
$row = $this->Database->{$args['entity']}($args['objectId']);
|
||||
$row->update($request->getParsedBody());
|
||||
$success = $row->isClean();
|
||||
return $this->ApiResponse(array('success' => $success));
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, 'Entity does not exist or is not exposed');
|
||||
}
|
||||
}
|
||||
|
||||
public function DeleteObject(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
if ($this->IsValidEntity($args['entity']))
|
||||
{
|
||||
$row = $this->Database->{$args['entity']}($args['objectId']);
|
||||
$row->delete();
|
||||
$success = $row->isClean();
|
||||
return $this->ApiResponse(array('success' => $success));
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, 'Entity does not exist or is not exposed');
|
||||
}
|
||||
}
|
||||
|
||||
private function IsValidEntity($entity)
|
||||
{
|
||||
return in_array($entity, $this->OpenApiSpec->components->internalSchemas->ExposedEntity->enum);
|
||||
}
|
||||
}
|
85
controllers/LoginController.php
Normal file
85
controllers/LoginController.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Controllers;
|
||||
|
||||
use \Grocy\Services\SessionService;
|
||||
use \Grocy\Services\DatabaseMigrationService;
|
||||
use \Grocy\Services\DemoDataGeneratorService;
|
||||
|
||||
class LoginController extends BaseController
|
||||
{
|
||||
public function __construct(\Slim\Container $container, string $sessionCookieName)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->SessionService = new SessionService();
|
||||
$this->SessionCookieName = $sessionCookieName;
|
||||
}
|
||||
|
||||
protected $SessionService;
|
||||
protected $SessionCookieName;
|
||||
|
||||
public function ProcessLogin(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
$postParams = $request->getParsedBody();
|
||||
if (isset($postParams['username']) && isset($postParams['password']))
|
||||
{
|
||||
$user = $this->Database->users()->where('username', $postParams['username'])->fetch();
|
||||
$inputPassword = $postParams['password'];
|
||||
$stayLoggedInPermanently = $postParams['stay_logged_in'] == 'on';
|
||||
|
||||
if ($user !== null && password_verify($inputPassword, $user->password))
|
||||
{
|
||||
$sessionKey = $this->SessionService->CreateSession($user->id, $stayLoggedInPermanently);
|
||||
setcookie($this->SessionCookieName, $sessionKey, time() + 31220640000); // Cookie expires in 999 years, but session validity is up to SessionService
|
||||
|
||||
if (password_needs_rehash($user->password, PASSWORD_DEFAULT))
|
||||
{
|
||||
$user->update(array(
|
||||
'password' => password_hash($inputPassword, PASSWORD_DEFAULT)
|
||||
));
|
||||
}
|
||||
|
||||
return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/'));
|
||||
}
|
||||
else
|
||||
{
|
||||
return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/login?invalid=true'));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/login?invalid=true'));
|
||||
}
|
||||
}
|
||||
|
||||
public function LoginPage(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'login');
|
||||
}
|
||||
|
||||
public function Logout(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
$this->SessionService->RemoveSession($_COOKIE[$this->SessionCookieName]);
|
||||
return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/'));
|
||||
}
|
||||
|
||||
public function Root(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
// Schema migration is done here
|
||||
$databaseMigrationService = new DatabaseMigrationService();
|
||||
$databaseMigrationService->MigrateDatabase();
|
||||
|
||||
if (GROCY_IS_DEMO_INSTALL)
|
||||
{
|
||||
$demoDataGeneratorService = new DemoDataGeneratorService();
|
||||
$demoDataGeneratorService->PopulateDemoData();
|
||||
}
|
||||
|
||||
return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/stockoverview'));
|
||||
}
|
||||
|
||||
public function GetSessionCookieName()
|
||||
{
|
||||
return $this->SessionCookieName;
|
||||
}
|
||||
}
|
49
controllers/OpenApiController.php
Normal file
49
controllers/OpenApiController.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Controllers;
|
||||
|
||||
use \Grocy\Services\ApplicationService;
|
||||
use \Grocy\Services\ApiKeyService;
|
||||
|
||||
class OpenApiController extends BaseApiController
|
||||
{
|
||||
public function __construct(\Slim\Container $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->ApiKeyService = new ApiKeyService();
|
||||
}
|
||||
|
||||
protected $ApiKeyService;
|
||||
|
||||
public function DocumentationUi(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'openapiui');
|
||||
}
|
||||
|
||||
public function DocumentationSpec(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
$applicationService = new ApplicationService();
|
||||
|
||||
$versionInfo = $applicationService->GetInstalledVersion();
|
||||
$this->OpenApiSpec->info->version = $versionInfo->Version;
|
||||
$this->OpenApiSpec->info->description = str_replace('PlaceHolderManageApiKeysUrl', $this->AppContainer->UrlManager->ConstructUrl('/manageapikeys'), $this->OpenApiSpec->info->description);
|
||||
$this->OpenApiSpec->servers[0]->url = $this->AppContainer->UrlManager->ConstructUrl('/api');
|
||||
|
||||
return $this->ApiResponse($this->OpenApiSpec);
|
||||
}
|
||||
|
||||
public function ApiKeysList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'manageapikeys', [
|
||||
'apiKeys' => $this->Database->api_keys(),
|
||||
'users' => $this->Database->users()
|
||||
]);
|
||||
}
|
||||
|
||||
public function CreateNewApiKey(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
$newApiKey = $this->ApiKeyService->CreateApiKey();
|
||||
$newApiKeyId = $this->ApiKeyService->GetApiKeyId($newApiKey);
|
||||
return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl("/manageapikeys?CreatedApiKeyId=$newApiKeyId"));
|
||||
}
|
||||
}
|
35
controllers/RecipesApiController.php
Normal file
35
controllers/RecipesApiController.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Controllers;
|
||||
|
||||
use \Grocy\Services\RecipesService;
|
||||
|
||||
class RecipesApiController extends BaseApiController
|
||||
{
|
||||
public function __construct(\Slim\Container $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->RecipesService = new RecipesService();
|
||||
}
|
||||
|
||||
protected $RecipesService;
|
||||
|
||||
public function AddNotFulfilledProductsToShoppingList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
$this->RecipesService->AddNotFulfilledProductsToShoppingList($args['recipeId']);
|
||||
return $this->VoidApiActionResponse($response);
|
||||
}
|
||||
|
||||
public function ConsumeRecipe(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
try
|
||||
{
|
||||
$this->RecipesService->ConsumeRecipe($args['recipeId']);
|
||||
return $this->VoidApiActionResponse($response);
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
95
controllers/RecipesController.php
Normal file
95
controllers/RecipesController.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Controllers;
|
||||
|
||||
use \Grocy\Services\RecipesService;
|
||||
|
||||
class RecipesController extends BaseController
|
||||
{
|
||||
public function __construct(\Slim\Container $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->RecipesService = new RecipesService();
|
||||
}
|
||||
|
||||
protected $RecipesService;
|
||||
|
||||
public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
$recipes = $this->Database->recipes()->orderBy('name');
|
||||
|
||||
$selectedRecipe = null;
|
||||
$selectedRecipePositions = null;
|
||||
if (isset($request->getQueryParams()['recipe']))
|
||||
{
|
||||
$selectedRecipe = $this->Database->recipes($request->getQueryParams()['recipe']);
|
||||
$selectedRecipePositions = $this->Database->recipes_pos()->where('recipe_id', $request->getQueryParams()['recipe']);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach ($recipes as $recipe)
|
||||
{
|
||||
$selectedRecipe = $recipe;
|
||||
$selectedRecipePositions = $this->Database->recipes_pos()->where('recipe_id', $recipe->id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->AppContainer->view->render($response, 'recipes', [
|
||||
'recipes' => $recipes,
|
||||
'recipesFulfillment' => $this->RecipesService->GetRecipesFulfillment(),
|
||||
'recipesSumFulfillment' => $this->RecipesService->GetRecipesSumFulfillment(),
|
||||
'selectedRecipe' => $selectedRecipe,
|
||||
'selectedRecipePositions' => $selectedRecipePositions,
|
||||
'products' => $this->Database->products(),
|
||||
'quantityunits' => $this->Database->quantity_units()
|
||||
]);
|
||||
}
|
||||
|
||||
public function RecipeEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
$recipeId = $args['recipeId'];
|
||||
if ($recipeId == 'new')
|
||||
{
|
||||
$newRecipe = $this->Database->recipes()->createRow(array(
|
||||
'name' => $this->LocalizationService->Localize('New recipe')
|
||||
));
|
||||
$newRecipe->save();
|
||||
|
||||
$recipeId = $this->Database->lastInsertId();
|
||||
}
|
||||
|
||||
return $this->AppContainer->view->render($response, 'recipeform', [
|
||||
'recipe' => $this->Database->recipes($recipeId),
|
||||
'recipePositions' => $this->Database->recipes_pos()->where('recipe_id', $recipeId),
|
||||
'mode' => 'edit',
|
||||
'products' => $this->Database->products(),
|
||||
'quantityunits' => $this->Database->quantity_units(),
|
||||
'recipesFulfillment' => $this->RecipesService->GetRecipesFulfillment(),
|
||||
'recipesSumFulfillment' => $this->RecipesService->GetRecipesSumFulfillment()
|
||||
]);
|
||||
}
|
||||
|
||||
public function RecipePosEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
if ($args['recipePosId'] == 'new')
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'recipeposform', [
|
||||
'mode' => 'create',
|
||||
'recipe' => $this->Database->recipes($args['recipeId']),
|
||||
'products' => $this->Database->products()->orderBy('name'),
|
||||
'quantityUnits' => $this->Database->quantity_units()->orderBy('name')
|
||||
]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'recipeposform', [
|
||||
'mode' => 'edit',
|
||||
'recipe' => $this->Database->recipes($args['recipeId']),
|
||||
'recipePos' => $this->Database->recipes_pos($args['recipePosId']),
|
||||
'products' => $this->Database->products()->orderBy('name'),
|
||||
'quantityUnits' => $this->Database->quantity_units()->orderBy('name')
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
168
controllers/StockApiController.php
Normal file
168
controllers/StockApiController.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Controllers;
|
||||
|
||||
use \Grocy\Services\StockService;
|
||||
|
||||
class StockApiController extends BaseApiController
|
||||
{
|
||||
public function __construct(\Slim\Container $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->StockService = new StockService();
|
||||
}
|
||||
|
||||
protected $StockService;
|
||||
|
||||
public function ProductDetails(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
try
|
||||
{
|
||||
return $this->ApiResponse($this->StockService->GetProductDetails($args['productId']));
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function ProductPriceHistory(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
try
|
||||
{
|
||||
return $this->ApiResponse($this->StockService->GetProductPriceHistory($args['productId']));
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function AddProduct(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
$bestBeforeDate = date('Y-m-d');
|
||||
if (isset($request->getQueryParams()['bestbeforedate']) && !empty($request->getQueryParams()['bestbeforedate']) && IsIsoDate($request->getQueryParams()['bestbeforedate']))
|
||||
{
|
||||
$bestBeforeDate = $request->getQueryParams()['bestbeforedate'];
|
||||
}
|
||||
|
||||
$price = null;
|
||||
if (isset($request->getQueryParams()['price']) && !empty($request->getQueryParams()['price']) && is_numeric($request->getQueryParams()['price']))
|
||||
{
|
||||
$price = $request->getQueryParams()['price'];
|
||||
}
|
||||
|
||||
$transactionType = StockService::TRANSACTION_TYPE_PURCHASE;
|
||||
if (isset($request->getQueryParams()['transactiontype']) && !empty($request->getQueryParams()['transactiontype']))
|
||||
{
|
||||
$transactionType = $request->getQueryParams()['transactiontype'];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$this->StockService->AddProduct($args['productId'], $args['amount'], $bestBeforeDate, $transactionType, date('Y-m-d'), $price);
|
||||
return $this->VoidApiActionResponse($response);
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function ConsumeProduct(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
$spoiled = false;
|
||||
if (isset($request->getQueryParams()['spoiled']) && !empty($request->getQueryParams()['spoiled']) && $request->getQueryParams()['spoiled'] == '1')
|
||||
{
|
||||
$spoiled = true;
|
||||
}
|
||||
|
||||
$transactionType = StockService::TRANSACTION_TYPE_CONSUME;
|
||||
if (isset($request->getQueryParams()['transactiontype']) && !empty($request->getQueryParams()['transactiontype']))
|
||||
{
|
||||
$transactionType = $request->getQueryParams()['transactiontype'];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$this->StockService->ConsumeProduct($args['productId'], $args['amount'], $spoiled, $transactionType);
|
||||
return $this->VoidApiActionResponse($response);
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function InventoryProduct(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
$bestBeforeDate = date('Y-m-d');
|
||||
if (isset($request->getQueryParams()['bestbeforedate']) && !empty($request->getQueryParams()['bestbeforedate']) && IsIsoDate($request->getQueryParams()['bestbeforedate']))
|
||||
{
|
||||
$bestBeforeDate = $request->getQueryParams()['bestbeforedate'];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$this->StockService->InventoryProduct($args['productId'], $args['newAmount'], $bestBeforeDate);
|
||||
return $this->VoidApiActionResponse($response);
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function CurrentStock(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->ApiResponse($this->StockService->GetCurrentStock());
|
||||
}
|
||||
|
||||
public function CurrentVolatilStock(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
$nextXDays = 5;
|
||||
if (isset($request->getQueryParams()['expiring_days']) && !empty($request->getQueryParams()['expiring_days']) && is_numeric($request->getQueryParams()['expiring_days']))
|
||||
{
|
||||
$nextXDays = $request->getQueryParams()['expiring_days'];
|
||||
}
|
||||
|
||||
$expiringProducts = $this->StockService->GetExpiringProducts($nextXDays);
|
||||
$expiredProducts = $this->StockService->GetExpiringProducts(-1);
|
||||
$missingProducts = $this->StockService->GetMissingProducts();
|
||||
return $this->ApiResponse(array(
|
||||
'expiring_products' => $expiringProducts,
|
||||
'expired_products' => $expiredProducts,
|
||||
'missing_products' => $missingProducts
|
||||
));
|
||||
}
|
||||
|
||||
public function AddMissingProductsToShoppingList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
$this->StockService->AddMissingProductsToShoppingList();
|
||||
return $this->VoidApiActionResponse($response);
|
||||
}
|
||||
|
||||
public function ClearShoppingList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
$this->StockService->ClearShoppingList();
|
||||
return $this->VoidApiActionResponse($response);
|
||||
}
|
||||
|
||||
public function ExternalBarcodeLookup(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
try
|
||||
{
|
||||
$addFoundProduct = false;
|
||||
if (isset($request->getQueryParams()['add']) && ($request->getQueryParams()['add'] === 'true' || $request->getQueryParams()['add'] === 1))
|
||||
{
|
||||
$addFoundProduct = true;
|
||||
}
|
||||
|
||||
return $this->ApiResponse($this->StockService->ExternalBarcodeLookup($args['barcode'], $addFoundProduct));
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
185
controllers/StockController.php
Normal file
185
controllers/StockController.php
Normal file
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Controllers;
|
||||
|
||||
use \Grocy\Services\StockService;
|
||||
|
||||
class StockController extends BaseController
|
||||
{
|
||||
|
||||
public function __construct(\Slim\Container $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->StockService = new StockService();
|
||||
}
|
||||
|
||||
protected $StockService;
|
||||
|
||||
public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'stockoverview', [
|
||||
'products' => $this->Database->products()->orderBy('name'),
|
||||
'quantityunits' => $this->Database->quantity_units()->orderBy('name'),
|
||||
'locations' => $this->Database->locations()->orderBy('name'),
|
||||
'currentStock' => $this->StockService->GetCurrentStock(),
|
||||
'missingProducts' => $this->StockService->GetMissingProducts(),
|
||||
'nextXDays' => 5
|
||||
]);
|
||||
}
|
||||
|
||||
public function Purchase(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'purchase', [
|
||||
'products' => $this->Database->products()->orderBy('name')
|
||||
]);
|
||||
}
|
||||
|
||||
public function Consume(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'consume', [
|
||||
'products' => $this->Database->products()->orderBy('name')
|
||||
]);
|
||||
}
|
||||
|
||||
public function Inventory(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'inventory', [
|
||||
'products' => $this->Database->products()->orderBy('name')
|
||||
]);
|
||||
}
|
||||
|
||||
public function ShoppingList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'shoppinglist', [
|
||||
'listItems' => $this->Database->shopping_list(),
|
||||
'products' => $this->Database->products()->orderBy('name'),
|
||||
'quantityunits' => $this->Database->quantity_units()->orderBy('name'),
|
||||
'missingProducts' => $this->StockService->GetMissingProducts(),
|
||||
'productGroups' => $this->Database->product_groups()->orderBy('name')
|
||||
]);
|
||||
}
|
||||
|
||||
public function ProductsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'products', [
|
||||
'products' => $this->Database->products()->orderBy('name'),
|
||||
'locations' => $this->Database->locations()->orderBy('name'),
|
||||
'quantityunits' => $this->Database->quantity_units()->orderBy('name'),
|
||||
'productGroups' => $this->Database->product_groups()->orderBy('name')
|
||||
]);
|
||||
}
|
||||
|
||||
public function LocationsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'locations', [
|
||||
'locations' => $this->Database->locations()->orderBy('name')
|
||||
]);
|
||||
}
|
||||
|
||||
public function ProductGroupsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'productgroups', [
|
||||
'productGroups' => $this->Database->product_groups()->orderBy('name')
|
||||
]);
|
||||
}
|
||||
|
||||
public function QuantityUnitsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'quantityunits', [
|
||||
'quantityunits' => $this->Database->quantity_units()->orderBy('name')
|
||||
]);
|
||||
}
|
||||
|
||||
public function ProductEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
if ($args['productId'] == 'new')
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'productform', [
|
||||
'locations' => $this->Database->locations()->orderBy('name'),
|
||||
'quantityunits' => $this->Database->quantity_units()->orderBy('name'),
|
||||
'productgroups' => $this->Database->product_groups()->orderBy('name'),
|
||||
'mode' => 'create'
|
||||
]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'productform', [
|
||||
'product' => $this->Database->products($args['productId']),
|
||||
'locations' => $this->Database->locations()->orderBy('name'),
|
||||
'quantityunits' => $this->Database->quantity_units()->orderBy('name'),
|
||||
'productgroups' => $this->Database->product_groups()->orderBy('name'),
|
||||
'mode' => 'edit'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function LocationEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
if ($args['locationId'] == 'new')
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'locationform', [
|
||||
'mode' => 'create'
|
||||
]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'locationform', [
|
||||
'location' => $this->Database->locations($args['locationId']),
|
||||
'mode' => 'edit'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function ProductGroupEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
if ($args['productGroupId'] == 'new')
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'productgroupform', [
|
||||
'mode' => 'create'
|
||||
]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'productgroupform', [
|
||||
'group' => $this->Database->product_groups($args['productGroupId']),
|
||||
'mode' => 'edit'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function QuantityUnitEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
if ($args['quantityunitId'] == 'new')
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'quantityunitform', [
|
||||
'mode' => 'create'
|
||||
]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'quantityunitform', [
|
||||
'quantityunit' => $this->Database->quantity_units($args['quantityunitId']),
|
||||
'mode' => 'edit'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function ShoppingListItemEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
if ($args['itemId'] == 'new')
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'shoppinglistform', [
|
||||
'products' => $this->Database->products()->orderBy('name'),
|
||||
'mode' => 'create'
|
||||
]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'shoppinglistform', [
|
||||
'listItem' => $this->Database->shopping_list($args['itemId']),
|
||||
'products' => $this->Database->products()->orderBy('name'),
|
||||
'mode' => 'edit'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
41
controllers/SystemApiController.php
Normal file
41
controllers/SystemApiController.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Controllers;
|
||||
|
||||
use \Grocy\Services\DatabaseService;
|
||||
|
||||
class SystemApiController extends BaseApiController
|
||||
{
|
||||
public function __construct(\Slim\Container $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->DatabaseService = new DatabaseService();
|
||||
}
|
||||
|
||||
protected $DatabaseService;
|
||||
|
||||
public function GetDbChangedTime(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->ApiResponse(array(
|
||||
'changed_time' => $this->DatabaseService->GetDbChangedTime()
|
||||
));
|
||||
}
|
||||
|
||||
public function LogMissingLocalization(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
if (GROCY_MODE === 'dev')
|
||||
{
|
||||
try
|
||||
{
|
||||
$requestBody = $request->getParsedBody();
|
||||
|
||||
$this->LocalizationService->LogMissingLocalization(GROCY_CULTURE, $requestBody['text']);
|
||||
return $this->ApiResponse(array('success' => true));
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
40
controllers/TasksApiController.php
Normal file
40
controllers/TasksApiController.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Controllers;
|
||||
|
||||
use \Grocy\Services\TasksService;
|
||||
|
||||
class TasksApiController extends BaseApiController
|
||||
{
|
||||
public function __construct(\Slim\Container $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->TasksService = new TasksService();
|
||||
}
|
||||
|
||||
protected $TasksService;
|
||||
|
||||
public function Current(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->ApiResponse($this->TasksService->GetCurrent());
|
||||
}
|
||||
|
||||
public function MarkTaskAsCompleted(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
$doneTime = date('Y-m-d H:i:s');
|
||||
if (isset($request->getQueryParams()['done_time']) && !empty($request->getQueryParams()['done_time']) && IsIsoDateTime($request->getQueryParams()['done_time']))
|
||||
{
|
||||
$doneTime = $request->getQueryParams()['done_time'];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$this->TasksService->MarkTaskAsCompleted($args['taskId'], $doneTime);
|
||||
return $this->VoidApiActionResponse($response);
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
80
controllers/TasksController.php
Normal file
80
controllers/TasksController.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Controllers;
|
||||
|
||||
use \Grocy\Services\TasksService;
|
||||
|
||||
class TasksController extends BaseController
|
||||
{
|
||||
public function __construct(\Slim\Container $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->TasksService = new TasksService();
|
||||
}
|
||||
|
||||
protected $TasksService;
|
||||
|
||||
public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
if (isset($request->getQueryParams()['include_done']))
|
||||
{
|
||||
$tasks = $this->Database->tasks()->orderBy('name');
|
||||
}
|
||||
else
|
||||
{
|
||||
$tasks = $this->TasksService->GetCurrent();
|
||||
}
|
||||
|
||||
return $this->AppContainer->view->render($response, 'tasks', [
|
||||
'tasks' => $tasks,
|
||||
'nextXDays' => 5,
|
||||
'taskCategories' => $this->Database->task_categories()->orderBy('name'),
|
||||
'users' => $this->Database->users()
|
||||
]);
|
||||
}
|
||||
|
||||
public function TaskEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
if ($args['taskId'] == 'new')
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'taskform', [
|
||||
'mode' => 'create',
|
||||
'taskCategories' => $this->Database->task_categories()->orderBy('name'),
|
||||
'users' => $this->Database->users()->orderBy('username')
|
||||
]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'taskform', [
|
||||
'task' => $this->Database->tasks($args['taskId']),
|
||||
'mode' => 'edit',
|
||||
'taskCategories' => $this->Database->task_categories()->orderBy('name'),
|
||||
'users' => $this->Database->users()->orderBy('username')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function TaskCategoriesList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'taskcategories', [
|
||||
'taskCategories' => $this->Database->task_categories()->orderBy('name')
|
||||
]);
|
||||
}
|
||||
|
||||
public function TaskCategoryEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
if ($args['categoryId'] == 'new')
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'taskcategoryform', [
|
||||
'mode' => 'create'
|
||||
]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'taskcategoryform', [
|
||||
'category' => $this->Database->task_categories($args['categoryId']),
|
||||
'mode' => 'edit'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
99
controllers/UsersApiController.php
Normal file
99
controllers/UsersApiController.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Controllers;
|
||||
|
||||
use \Grocy\Services\UsersService;
|
||||
|
||||
class UsersApiController extends BaseApiController
|
||||
{
|
||||
public function __construct(\Slim\Container $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->UsersService = new UsersService();
|
||||
}
|
||||
|
||||
protected $UsersService;
|
||||
|
||||
public function GetUsers(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
try
|
||||
{
|
||||
return $this->ApiResponse($this->UsersService->GetUsersAsDto());
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function CreateUser(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
$requestBody = $request->getParsedBody();
|
||||
|
||||
try
|
||||
{
|
||||
$this->UsersService->CreateUser($requestBody['username'], $requestBody['first_name'], $requestBody['last_name'], $requestBody['password']);
|
||||
return $this->ApiResponse(array('success' => true));
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function DeleteUser(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
try
|
||||
{
|
||||
$this->UsersService->DeleteUser($args['userId']);
|
||||
return $this->ApiResponse(array('success' => true));
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function EditUser(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
$requestBody = $request->getParsedBody();
|
||||
|
||||
try
|
||||
{
|
||||
$this->UsersService->EditUser($args['userId'], $requestBody['username'], $requestBody['first_name'], $requestBody['last_name'], $requestBody['password']);
|
||||
return $this->ApiResponse(array('success' => true));
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function GetUserSetting(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
try
|
||||
{
|
||||
$value = $this->UsersService->GetUserSetting(GROCY_USER_ID, $args['settingKey']);
|
||||
return $this->ApiResponse(array('value' => $value));
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function SetUserSetting(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
try
|
||||
{
|
||||
$requestBody = $request->getParsedBody();
|
||||
|
||||
$value = $this->UsersService->SetUserSetting(GROCY_USER_ID, $args['settingKey'], $requestBody['value']);
|
||||
return $this->ApiResponse(array('success' => true));
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
30
controllers/UsersController.php
Normal file
30
controllers/UsersController.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Controllers;
|
||||
|
||||
class UsersController extends BaseController
|
||||
{
|
||||
public function UsersList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'users', [
|
||||
'users' => $this->Database->users()->orderBy('username')
|
||||
]);
|
||||
}
|
||||
|
||||
public function UserEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
if ($args['userId'] == 'new')
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'userform', [
|
||||
'mode' => 'create'
|
||||
]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'userform', [
|
||||
'user' => $this->Database->users($args['userId']),
|
||||
'mode' => 'edit'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
2
data/.gitignore
vendored
2
data/.gitignore
vendored
@@ -1,2 +1,4 @@
|
||||
*
|
||||
!.gitignore
|
||||
!viewcache
|
||||
!plugins
|
||||
|
3
data/plugins/.gitignore
vendored
Normal file
3
data/plugins/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*
|
||||
!.gitignore
|
||||
!DemoBarcodeLookupPlugin.php
|
78
data/plugins/DemoBarcodeLookupPlugin.php
Normal file
78
data/plugins/DemoBarcodeLookupPlugin.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
use \Grocy\Helpers\BaseBarcodeLookupPlugin;
|
||||
|
||||
/*
|
||||
This class must extend BaseBarcodeLookupPlugin (in namespace \Grocy\Helpers)
|
||||
*/
|
||||
class DemoBarcodeLookupPlugin extends BaseBarcodeLookupPlugin
|
||||
{
|
||||
/*
|
||||
To use this plugin, configure it in data/config.php like this:
|
||||
Setting('STOCK_BARCODE_LOOKUP_PLUGIN', 'DemoBarcodeLookupPlugin');
|
||||
*/
|
||||
|
||||
/*
|
||||
To try it:
|
||||
Call the API function at /api/stock/external-barcode-lookup/{barcode}
|
||||
|
||||
When you also add ?add=true as a query parameter to the API call,
|
||||
on a successful lookup the product is added to the database and in the output
|
||||
the new product id is included (automatically, nothing to do here in the plugin)
|
||||
*/
|
||||
|
||||
/*
|
||||
Provided references:
|
||||
|
||||
$this->Locations contains all locations
|
||||
$this->QuantityUnits contains all quantity units
|
||||
*/
|
||||
|
||||
/*
|
||||
Useful hints:
|
||||
|
||||
Get a quantity unit by name:
|
||||
$quantityUnit = FindObjectInArrayByPropertyValue($this->QuantityUnits, 'name', 'Piece');
|
||||
|
||||
Get a location by name:
|
||||
$location = FindObjectInArrayByPropertyValue($this->Locations, 'name', 'Fridge');
|
||||
*/
|
||||
|
||||
/*
|
||||
This class must implement the protected abstract function ExecuteLookup($barcode),
|
||||
which is called with the barcode that needs to be looked up and must return an
|
||||
associative array of the product model or null, when nothing was found for the barcode.
|
||||
|
||||
The returned array must contain at least these properties:
|
||||
array(
|
||||
'name' => '',
|
||||
'location_id' => 1, // A valid id of a location object, check against $this->Locations
|
||||
'qu_id_purchase' => 1, // A valid id of quantity unit object, check against $this->QuantityUnits
|
||||
'qu_id_stock' => 1, // A valid id of quantity unit object, check against $this->QuantityUnits
|
||||
'qu_factor_purchase_to_stock' => 1, // Normally 1 when quantity unit stock and purchase is the same
|
||||
'barcode' => $barcode // The barcode of the product, maybe just pass through $barcode or manipulate it if necessary
|
||||
)
|
||||
*/
|
||||
protected function ExecuteLookup($barcode)
|
||||
{
|
||||
if ($barcode === 'x') // Demonstration when nothing is found
|
||||
{
|
||||
return null;
|
||||
}
|
||||
elseif ($barcode === 'e') // Demonstration when an error occurred
|
||||
{
|
||||
throw new \Exception('This is the error message from the plugin...');
|
||||
}
|
||||
else
|
||||
{
|
||||
return array(
|
||||
'name' => 'LookedUpProduct_' . RandomString(5),
|
||||
'location_id' => $this->Locations[0]->id,
|
||||
'qu_id_purchase' => $this->QuantityUnits[0]->id,
|
||||
'qu_id_stock' => $this->QuantityUnits[0]->id,
|
||||
'qu_factor_purchase_to_stock' => 1,
|
||||
'barcode' => $barcode
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
2
data/viewcache/.gitignore
vendored
Normal file
2
data/viewcache/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
30
docker-compose.yml
Normal file
30
docker-compose.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
# Usage:
|
||||
# docker-compose build && docker-compose up
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
grocy-nginx:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile-grocy-nginx
|
||||
depends_on:
|
||||
- grocy
|
||||
ports:
|
||||
- '80:80'
|
||||
- '443:443'
|
||||
volumes_from:
|
||||
- grocy
|
||||
container_name: grocy-nginx
|
||||
|
||||
grocy:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile-grocy
|
||||
expose:
|
||||
- 9000
|
||||
environment:
|
||||
PHP_MEMORY_LIMIT: 512M
|
||||
MAX_UPLOAD: 50M
|
||||
PHP_MAX_FILE_UPLOAD: 200
|
||||
PHP_MAX_POST: 100M
|
||||
container_name: grocy
|
28
docker_nginx/common.conf
Normal file
28
docker_nginx/common.conf
Normal file
@@ -0,0 +1,28 @@
|
||||
index index.php index.html index.htm;
|
||||
|
||||
charset utf-8;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location ~* .(jpg|jpeg|png|gif|ico|css|js)$ {
|
||||
expires 365d;
|
||||
}
|
||||
|
||||
error_page 404 /404.html;
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass grocy:9000;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
}
|
||||
|
||||
location ~ /\.ht {
|
||||
deny all;
|
||||
}
|
8
docker_nginx/conf.d/default.conf
Normal file
8
docker_nginx/conf.d/default.conf
Normal file
@@ -0,0 +1,8 @@
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name _;
|
||||
|
||||
root /www/public; # see: volumes_from
|
||||
|
||||
include /etc/nginx/common.conf;
|
||||
}
|
20
docker_nginx/conf.d/ssl.conf
Normal file
20
docker_nginx/conf.d/ssl.conf
Normal file
@@ -0,0 +1,20 @@
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name _;
|
||||
|
||||
root /www/public; # see: volumes_from
|
||||
|
||||
ssl_certificate /etc/nginx/certificates/cert.pem;
|
||||
ssl_certificate_key /etc/nginx/certificates/key.pem;
|
||||
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
# ssl_session_cache shared:SSL:1m;
|
||||
# ssl_session_timeout 5m;
|
||||
|
||||
# ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
# ssl_prefer_server_ciphers on;
|
||||
|
||||
include /etc/nginx/common.conf;
|
||||
|
||||
}
|
42
docker_nginx/nginx.conf
Normal file
42
docker_nginx/nginx.conf
Normal file
@@ -0,0 +1,42 @@
|
||||
user nobody;
|
||||
worker_processes 1;
|
||||
|
||||
pid /var/run/nginx/nginx.pid;
|
||||
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
sendfile on;
|
||||
#tcp_nopush on;
|
||||
|
||||
client_body_timeout 12;
|
||||
client_header_timeout 12;
|
||||
keepalive_timeout 15;
|
||||
send_timeout 10;
|
||||
|
||||
client_body_buffer_size 10K;
|
||||
client_header_buffer_size 1k;
|
||||
client_max_body_size 50M;
|
||||
large_client_header_buffers 2 1k;
|
||||
|
||||
gzip on;
|
||||
gzip_comp_level 2;
|
||||
gzip_min_length 1000;
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types text/plain application/x-javascript text/xml text/css application/xml;
|
||||
|
||||
access_log on;
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
103
grocy.js
103
grocy.js
@@ -1,103 +0,0 @@
|
||||
var Grocy = {};
|
||||
|
||||
$(function()
|
||||
{
|
||||
var menuItem = $('.nav').find("[data-nav-for-page='" + Grocy.ContentPage + "']");
|
||||
menuItem.addClass('active');
|
||||
|
||||
$.timeago.settings.allowFuture = true;
|
||||
$('time.timeago').timeago();
|
||||
});
|
||||
|
||||
Grocy.FetchJson = function(url, success, error)
|
||||
{
|
||||
var xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.onreadystatechange = function()
|
||||
{
|
||||
if (xhr.readyState === XMLHttpRequest.DONE)
|
||||
{
|
||||
if (xhr.status === 200)
|
||||
{
|
||||
if (success)
|
||||
{
|
||||
success(JSON.parse(xhr.responseText));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (error)
|
||||
{
|
||||
error(xhr);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
xhr.open('GET', url, true);
|
||||
xhr.send();
|
||||
};
|
||||
|
||||
Grocy.PostJson = function(url, jsonData, success, error)
|
||||
{
|
||||
var xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.onreadystatechange = function()
|
||||
{
|
||||
if (xhr.readyState === XMLHttpRequest.DONE)
|
||||
{
|
||||
if (xhr.status === 200)
|
||||
{
|
||||
if (success)
|
||||
{
|
||||
success(JSON.parse(xhr.responseText));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (error)
|
||||
{
|
||||
error(xhr);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
xhr.open('POST', url, true);
|
||||
xhr.setRequestHeader('Content-type', 'application/json');
|
||||
xhr.send(JSON.stringify(jsonData));
|
||||
};
|
||||
|
||||
Grocy.EmptyElementWhenMatches = function(selector, text)
|
||||
{
|
||||
if ($(selector).text() === text)
|
||||
{
|
||||
$(selector).text('');
|
||||
}
|
||||
};
|
||||
|
||||
String.prototype.contains = function(search)
|
||||
{
|
||||
return this.toLowerCase().indexOf(search.toLowerCase()) !== -1;
|
||||
};
|
||||
|
||||
Grocy.GetUriParam = function(key)
|
||||
{
|
||||
var currentUri = decodeURIComponent(window.location.search.substring(1));
|
||||
var vars = currentUri.split('&');
|
||||
|
||||
for (i = 0; i < vars.length; i++)
|
||||
{
|
||||
var currentParam = vars[i].split('=');
|
||||
|
||||
if (currentParam[0] === key)
|
||||
{
|
||||
return currentParam[1] === undefined ? true : currentParam[1];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Grocy.Wait = function(ms)
|
||||
{
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
2272
grocy.openapi.json
Normal file
2272
grocy.openapi.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,69 +0,0 @@
|
||||
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Name>grocy</Name>
|
||||
<ProjectGuid>edb77631-5196-4860-baeb-bca8900a4b6d</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<RootNamespace>
|
||||
</RootNamespace>
|
||||
<ProjectTypeGuids>{A0786B88-2ADB-4C21-ABE8-AA2D79766269}</ProjectTypeGuids>
|
||||
<AssemblyName>grocy</AssemblyName>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<IncludeDebugInformation>true</IncludeDebugInformation>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<IncludeDebugInformation>false</IncludeDebugInformation>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="config-dist.php" />
|
||||
<Compile Include="Grocy.php" />
|
||||
<Compile Include="GrocyLogicStock.php" />
|
||||
<Compile Include="GrocyDemoDataGenerator.php" />
|
||||
<Compile Include="GrocyPhpHelper.php" />
|
||||
<Compile Include="GrocyDbMigrator.php" />
|
||||
<Compile Include="index.php" />
|
||||
<Compile Include="views\consumption.php" />
|
||||
<Compile Include="views\inventory.php" />
|
||||
<Compile Include="views\shoppinglistform.php" />
|
||||
<Compile Include="views\shoppinglist.php" />
|
||||
<Compile Include="views\purchase.php" />
|
||||
<Compile Include="views\quantityunitform.php" />
|
||||
<Compile Include="views\locationform.php" />
|
||||
<Compile Include="views\productform.php" />
|
||||
<Compile Include="views\locations.php" />
|
||||
<Compile Include="views\quantityunits.php" />
|
||||
<Compile Include="views\products.php" />
|
||||
<Compile Include="views\dashboard.php" />
|
||||
<Compile Include="views\layout.php" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="bower.json" />
|
||||
<None Include="build.bat" />
|
||||
<Content Include="composer.json" />
|
||||
<Content Include="grocy.js" />
|
||||
<None Include="README.md" />
|
||||
<Content Include="README.html">
|
||||
<SubType>Content</SubType>
|
||||
<DependentUpon>README.md</DependentUpon>
|
||||
</Content>
|
||||
<Content Include="robots.txt" />
|
||||
<Content Include="style.css" />
|
||||
<Content Include="version.txt" />
|
||||
<Content Include="views\consumption.js" />
|
||||
<Content Include="views\dashboard.js" />
|
||||
<Content Include="views\inventory.js" />
|
||||
<Content Include="views\shoppinglistform.js" />
|
||||
<Content Include="views\shoppinglist.js" />
|
||||
<Content Include="views\purchase.js" />
|
||||
<Content Include="views\quantityunitform.js" />
|
||||
<Content Include="views\locationform.js" />
|
||||
<Content Include="views\productform.js" />
|
||||
<Content Include="views\locations.js" />
|
||||
<Content Include="views\quantityunits.js" />
|
||||
<Content Include="views\products.js" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="views\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
20
grocy.sln
20
grocy.sln
@@ -1,20 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.26403.3
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{A0786B88-2ADB-4C21-ABE8-AA2D79766269}") = "grocy", "grocy.phpproj", "{EDB77631-5196-4860-BAEB-BCA8900A4B6D}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{EDB77631-5196-4860-BAEB-BCA8900A4B6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EDB77631-5196-4860-BAEB-BCA8900A4B6D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
80
helpers/BaseBarcodeLookupPlugin.php
Normal file
80
helpers/BaseBarcodeLookupPlugin.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Helpers;
|
||||
|
||||
abstract class BaseBarcodeLookupPlugin
|
||||
{
|
||||
final public function __construct($locations, $quantityUnits)
|
||||
{
|
||||
$this->Locations = $locations;
|
||||
$this->QuantityUnits = $quantityUnits;
|
||||
}
|
||||
|
||||
protected $Locations;
|
||||
protected $QuantityUnits;
|
||||
|
||||
abstract protected function ExecuteLookup($barcode);
|
||||
|
||||
final public function Lookup($barcode)
|
||||
{
|
||||
$pluginOutput = $this->ExecuteLookup($barcode);
|
||||
|
||||
if ($pluginOutput === null)
|
||||
{
|
||||
return $pluginOutput;
|
||||
}
|
||||
|
||||
// Plugin must return an associative array
|
||||
if (!is_array($pluginOutput))
|
||||
{
|
||||
throw new \Exception('Plugin output must be an associative array');
|
||||
}
|
||||
if (!IsAssociativeArray($pluginOutput)) // $pluginOutput is at least an indexed array here
|
||||
{
|
||||
throw new \Exception('Plugin output must be an associative array');
|
||||
}
|
||||
|
||||
// Check for minimum needed properties
|
||||
$minimunNeededProperties = array(
|
||||
'name',
|
||||
'location_id',
|
||||
'qu_id_purchase',
|
||||
'qu_id_stock',
|
||||
'qu_factor_purchase_to_stock',
|
||||
'barcode'
|
||||
);
|
||||
foreach ($minimunNeededProperties as $prop)
|
||||
{
|
||||
if (!array_key_exists($prop, $pluginOutput))
|
||||
{
|
||||
throw new \Exception("Plugin output does not provide needed property $prop");
|
||||
}
|
||||
}
|
||||
|
||||
// $pluginOutput contains all needed properties here
|
||||
|
||||
// Check referenced entity ids are valid
|
||||
$locationId = $pluginOutput['location_id'];
|
||||
if (FindObjectInArrayByPropertyValue($this->Locations, 'id', $locationId) === null)
|
||||
{
|
||||
throw new \Exception("Location $locationId is not a valid location id");
|
||||
}
|
||||
$quIdPurchase = $pluginOutput['qu_id_purchase'];
|
||||
if (FindObjectInArrayByPropertyValue($this->QuantityUnits, 'id', $quIdPurchase) === null)
|
||||
{
|
||||
throw new \Exception("Location $quIdPurchase is not a valid quantity unit id");
|
||||
}
|
||||
$quIdStock = $pluginOutput['qu_id_stock'];
|
||||
if (FindObjectInArrayByPropertyValue($this->QuantityUnits, 'id', $quIdStock) === null)
|
||||
{
|
||||
throw new \Exception("Location $quIdStock is not a valid quantity unit id");
|
||||
}
|
||||
$quFactor = $pluginOutput['qu_factor_purchase_to_stock'];
|
||||
if (empty($quFactor) || !is_numeric($quFactor))
|
||||
{
|
||||
throw new \Exception('Quantity unit factor is empty or not a number');
|
||||
}
|
||||
|
||||
return $pluginOutput;
|
||||
}
|
||||
}
|
42
helpers/UrlManager.php
Normal file
42
helpers/UrlManager.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Helpers;
|
||||
|
||||
class UrlManager
|
||||
{
|
||||
public function __construct(string $basePath)
|
||||
{
|
||||
if ($basePath === '/')
|
||||
{
|
||||
$this->BasePath = $this->GetBaseUrl();
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->BasePath = $basePath;
|
||||
}
|
||||
}
|
||||
|
||||
protected $BasePath;
|
||||
|
||||
public function ConstructUrl($relativePath, $isResource = false)
|
||||
{
|
||||
if (GROCY_DISABLE_URL_REWRITING === false || $isResource === true)
|
||||
{
|
||||
return rtrim($this->BasePath, '/') . $relativePath;
|
||||
}
|
||||
else // Is not a resource and URL rewriting is disabled
|
||||
{
|
||||
return rtrim($this->BasePath, '/') . '/index.php' . $relativePath;
|
||||
}
|
||||
}
|
||||
|
||||
private function GetBaseUrl()
|
||||
{
|
||||
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strpos($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') !== false)
|
||||
{
|
||||
$_SERVER['HTTPS'] = 'on';
|
||||
}
|
||||
|
||||
return (isset($_SERVER['HTTPS']) ? "https" : "http") . "://$_SERVER[HTTP_HOST]";
|
||||
}
|
||||
}
|
201
helpers/extensions.php
Normal file
201
helpers/extensions.php
Normal file
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
function FindObjectInArrayByPropertyValue($array, $propertyName, $propertyValue)
|
||||
{
|
||||
foreach($array as $object)
|
||||
{
|
||||
if($object->{$propertyName} == $propertyValue)
|
||||
{
|
||||
return $object;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function FindAllObjectsInArrayByPropertyValue($array, $propertyName, $propertyValue, $operator = '==')
|
||||
{
|
||||
$returnArray = array();
|
||||
|
||||
foreach($array as $object)
|
||||
{
|
||||
switch($operator)
|
||||
{
|
||||
case '==':
|
||||
if($object->{$propertyName} == $propertyValue)
|
||||
{
|
||||
$returnArray[] = $object;
|
||||
}
|
||||
break;
|
||||
case '>':
|
||||
if($object->{$propertyName} > $propertyValue)
|
||||
{
|
||||
$returnArray[] = $object;
|
||||
}
|
||||
break;
|
||||
case '<':
|
||||
if($object->{$propertyName} < $propertyValue)
|
||||
{
|
||||
$returnArray[] = $object;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $returnArray;
|
||||
}
|
||||
|
||||
function FindAllItemsInArrayByValue($array, $value, $operator = '==')
|
||||
{
|
||||
$returnArray = array();
|
||||
|
||||
foreach($array as $item)
|
||||
{
|
||||
switch($operator)
|
||||
{
|
||||
case '==':
|
||||
if($item == $value)
|
||||
{
|
||||
$returnArray[] = $item;
|
||||
}
|
||||
break;
|
||||
case '>':
|
||||
if($item > $value)
|
||||
{
|
||||
$returnArray[] = $item;
|
||||
}
|
||||
break;
|
||||
case '<':
|
||||
if($item < $value)
|
||||
{
|
||||
$returnArray[] = $item;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $returnArray;
|
||||
}
|
||||
|
||||
function SumArrayValue($array, $propertyName)
|
||||
{
|
||||
$sum = 0;
|
||||
foreach($array as $object)
|
||||
{
|
||||
$sum += $object->{$propertyName};
|
||||
}
|
||||
|
||||
return $sum;
|
||||
}
|
||||
|
||||
function GetClassConstants($className)
|
||||
{
|
||||
$r = new ReflectionClass($className);
|
||||
return $r->getConstants();
|
||||
}
|
||||
|
||||
function RandomString($length, $allowedChars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
|
||||
{
|
||||
$randomString = '';
|
||||
for ($i = 0; $i < $length; $i++)
|
||||
{
|
||||
$randomString .= $allowedChars[rand(0, strlen($allowedChars) - 1)];
|
||||
}
|
||||
|
||||
return $randomString;
|
||||
}
|
||||
|
||||
function IsAssociativeArray(array $array)
|
||||
{
|
||||
$keys = array_keys($array);
|
||||
return array_keys($keys) !== $keys;
|
||||
}
|
||||
|
||||
function IsIsoDate($dateString)
|
||||
{
|
||||
$d = DateTime::createFromFormat('Y-m-d', $dateString);
|
||||
return $d && $d->format('Y-m-d') === $dateString;
|
||||
}
|
||||
|
||||
function IsIsoDateTime($dateTimeString)
|
||||
{
|
||||
$d = DateTime::createFromFormat('Y-m-d H:i:s', $dateTimeString);
|
||||
return $d && $d->format('Y-m-d H:i:s') === $dateTimeString;
|
||||
}
|
||||
|
||||
function BoolToString(bool $bool)
|
||||
{
|
||||
return $bool ? 'true' : 'false';
|
||||
}
|
||||
|
||||
function Setting(string $name, $value)
|
||||
{
|
||||
if (!defined('GROCY_' . $name))
|
||||
{
|
||||
// The content of a $name.txt file in /data/settingoverrides can overwrite the given setting (for embedded mode)
|
||||
$settingOverrideFile = GROCY_DATAPATH . '/settingoverrides/' . $name . '.txt';
|
||||
if (file_exists($settingOverrideFile))
|
||||
{
|
||||
define('GROCY_' . $name, file_get_contents($settingOverrideFile));
|
||||
}
|
||||
else
|
||||
{
|
||||
define('GROCY_' . $name, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
global $GROCY_DEFAULT_USER_SETTINGS;
|
||||
$GROCY_DEFAULT_USER_SETTINGS = array();
|
||||
function DefaultUserSetting(string $name, $value)
|
||||
{
|
||||
global $GROCY_DEFAULT_USER_SETTINGS;
|
||||
if (!array_key_exists($name, $GROCY_DEFAULT_USER_SETTINGS))
|
||||
{
|
||||
$GROCY_DEFAULT_USER_SETTINGS[$name] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
function GetUserDisplayName($user)
|
||||
{
|
||||
$displayName = '';
|
||||
|
||||
if (empty($user->first_name) && !empty($user->last_name))
|
||||
{
|
||||
$displayName = $user->last_name;
|
||||
}
|
||||
elseif (empty($user->last_name) && !empty($user->first_name))
|
||||
{
|
||||
$displayName = $user->first_name;
|
||||
}
|
||||
elseif (!empty($user->last_name) && !empty($user->first_name))
|
||||
{
|
||||
$displayName = $user->first_name . ' ' . $user->last_name;
|
||||
}
|
||||
else
|
||||
{
|
||||
$displayName = $user->username;
|
||||
}
|
||||
|
||||
return $displayName;
|
||||
}
|
||||
|
||||
function Pluralize($number, $singularForm, $pluralForm)
|
||||
{
|
||||
$text = $singularForm;
|
||||
if ($number != 1 && $pluralForm !== null && !empty($pluralForm))
|
||||
{
|
||||
$text = $pluralForm;
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
function IsValidFileName($fileName)
|
||||
{
|
||||
if(preg_match('#^[a-z0-9]+\.[a-z]+?$#i', $fileName))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
310
index.php
310
index.php
@@ -1,310 +0,0 @@
|
||||
<?php
|
||||
|
||||
use \Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use \Psr\Http\Message\ResponseInterface as Response;
|
||||
use Slim\Views\PhpRenderer;
|
||||
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/data/config.php';
|
||||
require_once __DIR__ . '/Grocy.php';
|
||||
require_once __DIR__ . '/GrocyDbMigrator.php';
|
||||
require_once __DIR__ . '/GrocyDemoDataGenerator.php';
|
||||
require_once __DIR__ . '/GrocyLogicStock.php';
|
||||
require_once __DIR__ . '/GrocyPhpHelper.php';
|
||||
|
||||
$app = new \Slim\App(new \Slim\Container([
|
||||
'settings' => [
|
||||
'displayErrorDetails' => true,
|
||||
],
|
||||
]));
|
||||
$container = $app->getContainer();
|
||||
$container['renderer'] = new PhpRenderer('./views');
|
||||
|
||||
if (!Grocy::IsDemoInstallation())
|
||||
{
|
||||
$isHttpsReverseProxied = !empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https';
|
||||
$app->add(new \Slim\Middleware\HttpBasicAuthentication([
|
||||
'realm' => 'grocy',
|
||||
'secure' => !$isHttpsReverseProxied,
|
||||
'users' => [
|
||||
HTTP_USER => HTTP_PASSWORD
|
||||
]
|
||||
]));
|
||||
}
|
||||
|
||||
$db = Grocy::GetDbConnection();
|
||||
|
||||
$app->get('/', function(Request $request, Response $response) use($db)
|
||||
{
|
||||
$db = Grocy::GetDbConnection(true); //For database schema migration
|
||||
|
||||
return $this->renderer->render($response, '/layout.php', [
|
||||
'title' => 'Dashboard',
|
||||
'contentPage' => 'dashboard.php',
|
||||
'products' => $db->products(),
|
||||
'currentStock' => GrocyLogicStock::GetCurrentStock(),
|
||||
'missingProducts' => GrocyLogicStock::GetMissingProducts()
|
||||
]);
|
||||
});
|
||||
|
||||
$app->get('/purchase', function(Request $request, Response $response) use($db)
|
||||
{
|
||||
return $this->renderer->render($response, '/layout.php', [
|
||||
'title' => 'Purchase',
|
||||
'contentPage' => 'purchase.php',
|
||||
'products' => $db->products()
|
||||
]);
|
||||
});
|
||||
|
||||
$app->get('/consumption', function(Request $request, Response $response) use($db)
|
||||
{
|
||||
return $this->renderer->render($response, '/layout.php', [
|
||||
'title' => 'Consumption',
|
||||
'contentPage' => 'consumption.php',
|
||||
'products' => $db->products()
|
||||
]);
|
||||
});
|
||||
|
||||
$app->get('/inventory', function(Request $request, Response $response) use($db)
|
||||
{
|
||||
return $this->renderer->render($response, '/layout.php', [
|
||||
'title' => 'Inventory',
|
||||
'contentPage' => 'inventory.php',
|
||||
'products' => $db->products()
|
||||
]);
|
||||
});
|
||||
|
||||
$app->get('/shoppinglist', function(Request $request, Response $response) use($db)
|
||||
{
|
||||
return $this->renderer->render($response, '/layout.php', [
|
||||
'title' => 'Shopping list',
|
||||
'contentPage' => 'shoppinglist.php',
|
||||
'listItems' => $db->shopping_list(),
|
||||
'products' => $db->products(),
|
||||
'quantityunits' => $db->quantity_units(),
|
||||
'missingProducts' => GrocyLogicStock::GetMissingProducts()
|
||||
]);
|
||||
});
|
||||
|
||||
$app->get('/products', function(Request $request, Response $response) use($db)
|
||||
{
|
||||
return $this->renderer->render($response, '/layout.php', [
|
||||
'title' => 'Products',
|
||||
'contentPage' => 'products.php',
|
||||
'products' => $db->products(),
|
||||
'locations' => $db->locations(),
|
||||
'quantityunits' => $db->quantity_units()
|
||||
]);
|
||||
});
|
||||
|
||||
$app->get('/locations', function(Request $request, Response $response) use($db)
|
||||
{
|
||||
return $this->renderer->render($response, '/layout.php', [
|
||||
'title' => 'Locations',
|
||||
'contentPage' => 'locations.php',
|
||||
'locations' => $db->locations()
|
||||
]);
|
||||
});
|
||||
|
||||
$app->get('/quantityunits', function(Request $request, Response $response) use($db)
|
||||
{
|
||||
return $this->renderer->render($response, '/layout.php', [
|
||||
'title' => 'Quantity units',
|
||||
'contentPage' => 'quantityunits.php',
|
||||
'quantityunits' => $db->quantity_units()
|
||||
]);
|
||||
});
|
||||
|
||||
$app->get('/product/{productId}', function(Request $request, Response $response, $args) use($db)
|
||||
{
|
||||
if ($args['productId'] == 'new')
|
||||
{
|
||||
return $this->renderer->render($response, '/layout.php', [
|
||||
'title' => 'Create product',
|
||||
'contentPage' => 'productform.php',
|
||||
'locations' => $db->locations(),
|
||||
'quantityunits' => $db->quantity_units(),
|
||||
'mode' => 'create'
|
||||
]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->renderer->render($response, '/layout.php', [
|
||||
'title' => 'Edit product',
|
||||
'contentPage' => 'productform.php',
|
||||
'product' => $db->products($args['productId']),
|
||||
'locations' => $db->locations(),
|
||||
'quantityunits' => $db->quantity_units(),
|
||||
'mode' => 'edit'
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
$app->get('/location/{locationId}', function(Request $request, Response $response, $args) use($db)
|
||||
{
|
||||
if ($args['locationId'] == 'new')
|
||||
{
|
||||
return $this->renderer->render($response, '/layout.php', [
|
||||
'title' => 'Create location',
|
||||
'contentPage' => 'locationform.php',
|
||||
'mode' => 'create'
|
||||
]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->renderer->render($response, '/layout.php', [
|
||||
'title' => 'Edit location',
|
||||
'contentPage' => 'locationform.php',
|
||||
'location' => $db->locations($args['locationId']),
|
||||
'mode' => 'edit'
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
$app->get('/quantityunit/{quantityunitId}', function(Request $request, Response $response, $args) use($db)
|
||||
{
|
||||
if ($args['quantityunitId'] == 'new')
|
||||
{
|
||||
return $this->renderer->render($response, '/layout.php', [
|
||||
'title' => 'Create quantity unit',
|
||||
'contentPage' => 'quantityunitform.php',
|
||||
'mode' => 'create'
|
||||
]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->renderer->render($response, '/layout.php', [
|
||||
'title' => 'Edit quantity unit',
|
||||
'contentPage' => 'quantityunitform.php',
|
||||
'quantityunit' => $db->quantity_units($args['quantityunitId']),
|
||||
'mode' => 'edit'
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
$app->get('/shoppinglist/{itemId}', function(Request $request, Response $response, $args) use($db)
|
||||
{
|
||||
if ($args['itemId'] == 'new')
|
||||
{
|
||||
return $this->renderer->render($response, '/layout.php', [
|
||||
'title' => 'Add shopping list item',
|
||||
'contentPage' => 'shoppinglistform.php',
|
||||
'products' => $db->products(),
|
||||
'mode' => 'create'
|
||||
]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->renderer->render($response, '/layout.php', [
|
||||
'title' => 'Edit shopping list item',
|
||||
'contentPage' => 'shoppinglistform.php',
|
||||
'listItem' => $db->shopping_list($args['itemId']),
|
||||
'products' => $db->products(),
|
||||
'mode' => 'edit'
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
$app->group('/api', function() use($db)
|
||||
{
|
||||
$this->get('/get-objects/{entity}', function(Request $request, Response $response, $args) use($db)
|
||||
{
|
||||
echo json_encode($db->{$args['entity']}());
|
||||
});
|
||||
|
||||
$this->get('/get-object/{entity}/{objectId}', function(Request $request, Response $response, $args) use($db)
|
||||
{
|
||||
echo json_encode($db->{$args['entity']}($args['objectId']));
|
||||
});
|
||||
|
||||
$this->post('/add-object/{entity}', function(Request $request, Response $response, $args) use($db)
|
||||
{
|
||||
$newRow = $db->{$args['entity']}()->createRow($request->getParsedBody());
|
||||
$newRow->save();
|
||||
$success = $newRow->isClean();
|
||||
echo json_encode(array('success' => $success));
|
||||
});
|
||||
|
||||
$this->post('/edit-object/{entity}/{objectId}', function(Request $request, Response $response, $args) use($db)
|
||||
{
|
||||
$row = $db->{$args['entity']}($args['objectId']);
|
||||
$row->update($request->getParsedBody());
|
||||
$success = $row->isClean();
|
||||
echo json_encode(array('success' => $success));
|
||||
});
|
||||
|
||||
$this->get('/delete-object/{entity}/{objectId}', function(Request $request, Response $response, $args) use($db)
|
||||
{
|
||||
$row = $db->{$args['entity']}($args['objectId']);
|
||||
$row->delete();
|
||||
$success = $row->isClean();
|
||||
echo json_encode(array('success' => $success));
|
||||
});
|
||||
|
||||
$this->get('/stock/add-product/{productId}/{amount}', function(Request $request, Response $response, $args)
|
||||
{
|
||||
$bestBeforeDate = date('Y-m-d');
|
||||
if (isset($request->getQueryParams()['bestbeforedate']) && !empty($request->getQueryParams()['bestbeforedate']))
|
||||
{
|
||||
$bestBeforeDate = $request->getQueryParams()['bestbeforedate'];
|
||||
}
|
||||
|
||||
$transactionType = GrocyLogicStock::TRANSACTION_TYPE_PURCHASE;
|
||||
if (isset($request->getQueryParams()['transactiontype']) && !empty($request->getQueryParams()['transactiontype']))
|
||||
{
|
||||
$transactionType = $request->getQueryParams()['transactiontype'];
|
||||
}
|
||||
|
||||
echo json_encode(array('success' => GrocyLogicStock::AddProduct($args['productId'], $args['amount'], $bestBeforeDate, $transactionType)));
|
||||
});
|
||||
|
||||
$this->get('/stock/consume-product/{productId}/{amount}', function(Request $request, Response $response, $args)
|
||||
{
|
||||
$spoiled = false;
|
||||
if (isset($request->getQueryParams()['spoiled']) && !empty($request->getQueryParams()['spoiled']) && $request->getQueryParams()['spoiled'] == '1')
|
||||
{
|
||||
$spoiled = true;
|
||||
}
|
||||
|
||||
$transactionType = GrocyLogicStock::TRANSACTION_TYPE_CONSUME;
|
||||
if (isset($request->getQueryParams()['transactiontype']) && !empty($request->getQueryParams()['transactiontype']))
|
||||
{
|
||||
$transactionType = $request->getQueryParams()['transactiontype'];
|
||||
}
|
||||
|
||||
echo json_encode(array('success' => GrocyLogicStock::ConsumeProduct($args['productId'], $args['amount'], $spoiled, $transactionType)));
|
||||
});
|
||||
|
||||
$this->get('/stock/inventory-product/{productId}/{newAmount}', function(Request $request, Response $response, $args)
|
||||
{
|
||||
$bestBeforeDate = date('Y-m-d');
|
||||
if (isset($request->getQueryParams()['bestbeforedate']) && !empty($request->getQueryParams()['bestbeforedate']))
|
||||
{
|
||||
$bestBeforeDate = $request->getQueryParams()['bestbeforedate'];
|
||||
}
|
||||
|
||||
echo json_encode(array('success' => GrocyLogicStock::InventoryProduct($args['productId'], $args['newAmount'], $bestBeforeDate)));
|
||||
});
|
||||
|
||||
$this->get('/stock/get-product-details/{productId}', function(Request $request, Response $response, $args)
|
||||
{
|
||||
echo json_encode(GrocyLogicStock::GetProductDetails($args['productId']));
|
||||
});
|
||||
|
||||
$this->get('/stock/get-current-stock', function(Request $request, Response $response)
|
||||
{
|
||||
echo json_encode(GrocyLogicStock::GetCurrentStock());
|
||||
});
|
||||
|
||||
$this->get('/stock/add-missing-products-to-shoppinglist', function(Request $request, Response $response)
|
||||
{
|
||||
GrocyLogicStock::AddMissingProductsToShoppingList();
|
||||
echo json_encode(array('success' => true));
|
||||
});
|
||||
})->add(function($request, $response, $next)
|
||||
{
|
||||
$response = $next($request, $response);
|
||||
return $response->withHeader('Content-Type', 'application/json');
|
||||
});
|
||||
|
||||
$app->run();
|
6
info.php
Normal file
6
info.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
// Show all information, defaults to INFO_ALL
|
||||
phpinfo();
|
||||
|
||||
?>
|
342
localization/de.php
Normal file
342
localization/de.php
Normal file
@@ -0,0 +1,342 @@
|
||||
<?php
|
||||
|
||||
return array(
|
||||
'Stock overview' => 'Bestand',
|
||||
'#1 products expiring within the next #2 days' => '#1 Produkte laufen innerhalb der nächsten #2 Tage ab',
|
||||
'#1 products are already expired' => '#1 Produkte sind bereits abgelaufen',
|
||||
'#1 products are below defined min. stock amount' => '#1 Produkte sind unter Mindestbestand',
|
||||
'Product' => 'Produkt',
|
||||
'Amount' => 'Menge',
|
||||
'Next best before date' => 'Nächstes MHD',
|
||||
'Logout' => 'Abmelden',
|
||||
'Chores overview' => 'Hausarbeiten',
|
||||
'Batteries overview' => 'Batterien',
|
||||
'Purchase' => 'Einkauf',
|
||||
'Consume' => 'Verbrauch',
|
||||
'Inventory' => 'Inventur',
|
||||
'Shopping list' => 'Einkaufszettel',
|
||||
'Chore tracking' => 'Hausarbeiten-Ausführung',
|
||||
'Battery tracking' => 'Batterie-Ladzyklus',
|
||||
'Products' => 'Produkte',
|
||||
'Locations' => 'Standorte',
|
||||
'Quantity units' => 'Mengeneinheiten',
|
||||
'Chores' => 'Hausarbeiten',
|
||||
'Batteries' => 'Batterien',
|
||||
'Chore' => 'Hausarbeit',
|
||||
'Next estimated tracking' => 'Nächste geplante Ausführung',
|
||||
'Last tracked' => 'Zuletzt ausgeführt',
|
||||
'Battery' => 'Batterie',
|
||||
'Last charged' => 'Zuletzt geladen',
|
||||
'Next planned charge cycle' => 'Nächster geplanter Ladezyklus',
|
||||
'Best before' => 'MHD',
|
||||
'OK' => 'OK',
|
||||
'Product overview' => 'Produktübersicht',
|
||||
'Stock quantity unit' => 'Mengeneinheit Bestand',
|
||||
'Stock amount' => 'Bestand',
|
||||
'Last purchased' => 'Zuletzt gekauft',
|
||||
'Last used' => 'Zuletzt benutzt',
|
||||
'Spoiled' => 'Verdorben',
|
||||
'Barcode lookup is disabled' => 'Barcode-Suche ist deaktiviert',
|
||||
'will be added to the list of barcodes for the selected product on submit' => 'wird der Liste der Barcodes für das ausgewählte Produkt beim Speichern hinzugefügt',
|
||||
'New amount' => 'Neue Menge',
|
||||
'Note' => 'Notiz',
|
||||
'Tracked time' => 'Ausführungszeit',
|
||||
'Chore overview' => 'Hausarbeit Übersicht',
|
||||
'Tracked count' => 'Ausführungsanzahl',
|
||||
'Battery overview' => 'Batterie Übersicht',
|
||||
'Charge cycles count' => 'Ladezyklen',
|
||||
'Create shopping list item' => 'Einkaufszettel Eintrag erstellen',
|
||||
'Edit shopping list item' => 'Einkaufszettel Eintrag bearbeiten',
|
||||
'#1 units were automatically added and will apply in addition to the amount entered here' => '#1 Einheiten wurden automatisch hinzugefügt und gelten zusätzlich der hier eingegebenen Menge',
|
||||
'Save' => 'Speichern',
|
||||
'Add' => 'Hinzufügen',
|
||||
'Name' => 'Name',
|
||||
'Location' => 'Standort',
|
||||
'Min. stock amount' => 'Mindestbestand',
|
||||
'QU purchase' => 'ME Einkauf',
|
||||
'QU stock' => 'ME Bestand',
|
||||
'QU factor' => 'ME-Faktor',
|
||||
'Description' => 'Beschreibung',
|
||||
'Create product' => 'Produkt erstellen',
|
||||
'Barcode(s)' => 'Barcode(s)',
|
||||
'Minimum stock amount' => 'Mindestbestand',
|
||||
'Default best before days' => 'Standard Haltbarkeit in Tagen',
|
||||
'Quantity unit purchase' => 'Mengeneinheit Einkauf',
|
||||
'Quantity unit stock' => 'Mengeneinheit Bestand',
|
||||
'Factor purchase to stock quantity unit' => 'Faktor Mengeneinheit Einkauf zu Mengeneinheit Bestand',
|
||||
'Create location' => 'Standort erstellen',
|
||||
'Create quantity unit' => 'Mengeneinheit erstellen',
|
||||
'Period type' => 'Periodentyp',
|
||||
'Period days' => 'Tage/Periode',
|
||||
'Create chore' => 'Hausarbeit erstellen',
|
||||
'Used in' => 'Benutzt in',
|
||||
'Create battery' => 'Batterie erstellen',
|
||||
'Edit battery' => 'Batterie bearbeiten',
|
||||
'Edit chore' => 'Hausarbeit bearbeiten',
|
||||
'Edit quantity unit' => 'Mengeneinheit bearbeiten',
|
||||
'Edit product' => 'Produkt bearbeiten',
|
||||
'Edit location' => 'Standort bearbeiten',
|
||||
'Record data' => 'Daten erfassen',
|
||||
'Manage master data' => 'Stammdaten verwalten',
|
||||
'This will apply to added products' => 'Dies gilt für hinzugefügte Produkte',
|
||||
'never' => 'nie',
|
||||
'Add products that are below defined min. stock amount' => 'Produkte unter Mindestbestand hinzufügen',
|
||||
'For purchases this amount of days will be added to today for the best before date suggestion' => 'Bei Einkäufen wird hierauf basierend das MHD vorausgefüllt',
|
||||
'This means 1 #1 purchased will be converted into #2 #3 in stock' => 'Das bedeutet 1 #1 im Einkauf entsprechen #2 #3 im Bestand',
|
||||
'Login' => 'Anmelden',
|
||||
'Username' => 'Benutzername',
|
||||
'Password' => 'Passwort',
|
||||
'Invalid credentials, please try again' => 'Ungültige Zugangsdaten, bitte versuche es erneut',
|
||||
'Are you sure to delete battery "#1"?' => 'Battery "#1" wirklich löschen?',
|
||||
'Yes' => 'Ja',
|
||||
'No' => 'Nein',
|
||||
'Are you sure to delete chore "#1"?' => 'Hausarbeit "#1" wirklich löschen?',
|
||||
'"#1" could not be resolved to a product, how do you want to proceed?' => '"#1" konnte nicht zu einem Produkt aufgelöst werden, wie möchtest du weiter machen?',
|
||||
'Create or assign product' => 'Produkt erstellen oder verknüpfen',
|
||||
'Cancel' => 'Abbrechen',
|
||||
'Add as new product' => 'Als neues Produkt hinzufügen',
|
||||
'Add as barcode to existing product' => 'Barcode vorhandenem Produkt zuweisen',
|
||||
'Add as new product and prefill barcode' => 'Neues Produkt erstellen und Barcode vorbelegen',
|
||||
'Are you sure to delete quantity unit "#1"?' => 'Mengeneinheit "#1" wirklich löschen?',
|
||||
'Are you sure to delete product "#1"?' => 'Produkt "#1" wirklich löschen?',
|
||||
'Are you sure to delete location "#1"?' => 'Standort "#1" wirklich löschen?',
|
||||
'Manage API keys' => 'API-Keys verwalten',
|
||||
'REST API & data model documentation' => 'REST-API & Datenmodell Dokumentation',
|
||||
'API keys' => 'API-Keys',
|
||||
'Create new API key' => 'Neuen API-Key erstellen',
|
||||
'API key' => 'API-Key',
|
||||
'Expires' => 'Läuft ab',
|
||||
'Created' => 'Erstellt',
|
||||
'This product is not in stock' => 'Dieses Produkt ist nicht vorrätig',
|
||||
'This means #1 will be added to stock' => 'Das bedeutet #1 wird dem Bestand hinzugefügt',
|
||||
'This means #1 will be removed from stock' => 'Das bedeutet #1 wird aus dem Bestand entfernt',
|
||||
'This means it is estimated that a new execution of this chore is tracked #1 days after the last was tracked' => 'Das bedeutet, dass eine erneute Ausführung der Hausarbeit #1 Tage nach der letzten Ausführung geplant wird',
|
||||
'Removed #1 #2 of #3 from stock' => '#1 #2 #3 aus dem Bestand entfernt',
|
||||
'About grocy' => 'Über grocy',
|
||||
'Close' => 'Schließen',
|
||||
'#1 batteries are due to be charged within the next #2 days' => '#1 Batterien müssen in den nächsten #2 Tagen geladen werden',
|
||||
'#1 batteries are overdue to be charged' => '#1 Batterien sind überfällig',
|
||||
'#1 chores are due to be done within the next #2 days' => '#1 Hausarbeiten stehen in den nächsten #2 Tagen an',
|
||||
'#1 chores are overdue to be done' => '#1 Hausarbeiten sind überfällig',
|
||||
'Released on' => 'Veröffentlicht am',
|
||||
'Consume #3 #1 of #2' => 'Verbrauche #3 #1 #2',
|
||||
'Added #1 #2 of #3 to stock' => '#1 #2 #3 dem Bestand hinzugefügt',
|
||||
'Stock amount of #1 is now #2 #3' => 'Es sind nun #2 #3 #1 im Bestand',
|
||||
'Tracked execution of chore #1 on #2' => 'Ausführung von #1 am #2 erfasst',
|
||||
'Tracked charge cycle of battery #1 on #2' => 'Ladezyklus für Batterie #1 am #2 erfasst',
|
||||
'Consume all #1 which are currently in stock' => 'Verbrauche den kompletten Bestand von #1',
|
||||
'All' => 'Alle',
|
||||
'Track charge cycle of battery #1' => 'Erfasse einen Ladezyklus für Batterie #1',
|
||||
'Track execution of chore #1' => 'Erfasse eine Ausführung von #1',
|
||||
'Filter by location' => 'Nach Standort filtern',
|
||||
'Search' => 'Suche',
|
||||
'Not logged in' => 'Nicht angemeldet',
|
||||
'You have to select a product' => 'Ein Produkt muss ausgewählt werden',
|
||||
'You have to select a chore' => 'Eine Hausarbeit muss ausgewählt werden',
|
||||
'You have to select a battery' => 'Eine Batterie muss ausgewählt werden',
|
||||
'A name is required' => 'Ein Name ist erforderlich',
|
||||
'A location is required' => 'Ein Standort ist erforderlich',
|
||||
'The amount cannot be lower than #1' => 'Die Menge darf nicht kleiner als #1 sein',
|
||||
'This cannot be negative' => 'Dies darf nicht negativ sein',
|
||||
'A quantity unit is required' => 'Eine Mengeneinheit muss ausgewählt werden',
|
||||
'A period type is required' => 'Eine Periodentyp muss ausgewählt werden',
|
||||
'A best before date is required and must be later than today' => 'Ein Mindesthaltbarkeitsdatum ist erforderlich und muss später als heute sein',
|
||||
'Settings' => 'Einstellungen',
|
||||
'This can only be before now' => 'Dies kann nur vor jetzt sein',
|
||||
'Calendar' => 'Kalender',
|
||||
'Recipes' => 'Rezepte',
|
||||
'Edit recipe' => 'Rezept bearbeiten',
|
||||
'New recipe' => 'Neues Rezept',
|
||||
'Ingredients list' => 'Zutatenliste',
|
||||
'Add recipe ingredient' => 'Rezeptzutat hinzufügen',
|
||||
'Edit recipe ingredient' => 'Rezeptzutat bearbeiten',
|
||||
'Are you sure to delete recipe "#1"?' => 'Rezept "#1" wirklich löschen?',
|
||||
'Are you sure to delete recipe ingredient "#1"?' => 'Rezeptzutat "#1" wirklich löschen?',
|
||||
'Are you sure to empty the shopping list?' => 'Sicher, dass den Einkaufszettel geleert werden soll?',
|
||||
'Clear list' => 'Liste leeren',
|
||||
'Requirements fulfilled' => 'Bedarf im Bestand',
|
||||
'Put missing products on shopping list' => 'Fehlende Produkte auf den Einkaufszettel setzen',
|
||||
'Not enough in stock, #1 ingredients missing' => 'Nicht ausreichend im Bestand, #1 Zutaten fehlen',
|
||||
'Enough in stock' => 'Bestand reicht aus',
|
||||
'Not enough in stock, #1 ingredients missing but already on the shopping list' => 'Bestand nicht ausreichend, #1 Zutaten fehlen, stehen aber bereits auf dem Einkaufszettel',
|
||||
'Expand to fullscreen' => 'Auf ganzen Bildschirm vergrößern',
|
||||
'Ingredients' => 'Zutaten',
|
||||
'Preparation' => 'Zubereitung',
|
||||
'Recipe' => 'Rezept',
|
||||
'Not enough in stock, #1 missing, #2 already on shopping list' => 'Nicht ausreichend im Bestand, #1 fehlen, #2 stehen bereits auf dem Einkaufszettel',
|
||||
'Show notes' => 'Notizen anzeigen',
|
||||
'Put missing amount on shopping list' => 'Fehlende Menge auf den Einkaufszettel setzen',
|
||||
'Are you sure to put all missing ingredients for recipe "#1" on the shopping list?' => 'Sicher alle fehlenden Zutaten für Rezept "#1" auf die Einkaufsliste zu setzen?',
|
||||
'Added for recipe #1' => 'Hinzugefügt für Rezept #1',
|
||||
'Manage users' => 'Benutzer verwalten',
|
||||
'User' => 'Benutzer',
|
||||
'Users' => 'Benutzer',
|
||||
'Are you sure to delete user "#1"?' => 'Benutzer "#1" wirklich löschen?',
|
||||
'Create user' => 'Benutzer erstellen',
|
||||
'Edit user' => 'Benutzer bearbeiten',
|
||||
'First name' => 'Vorname',
|
||||
'Last name' => 'Nachname',
|
||||
'A username is required' => 'Ein Benutzername ist erforderlich',
|
||||
'Confirm password' => 'Passwort bestätigen',
|
||||
'Passwords do not match' => 'Passwörter stimmen nicht überein',
|
||||
'Change password' => 'Passwort ändern',
|
||||
'Done by' => 'Ausgeführt von',
|
||||
'Last done by' => 'Zuletzt ausgeführt von',
|
||||
'Unknown' => 'Unbekannt',
|
||||
'Filter by chore' => 'Nach Hausarbeit filtern',
|
||||
'Chores analysis' => 'Hausarbeiten Analyse',
|
||||
'0 means suggestions for the next charge cycle are disabled' => '0 bedeutet dass Vorschläge für den nächsten Ladezyklus deaktiviert sind',
|
||||
'Charge cycle interval (days)' => 'Ladezyklusintervall (Tage)',
|
||||
'Last price' => 'Letzter Preis',
|
||||
'Price history' => 'Preisentwicklung',
|
||||
'No price history available' => 'Keine Preisdaten verfügbar',
|
||||
'Price' => 'Preis',
|
||||
'in #1 per purchase quantity unit' => 'in #1 pro Einkaufsmengeneinheit',
|
||||
'The price cannot be lower than #1' => 'Der Preis darf nicht niedriger als #1 sein',
|
||||
'#1 product expires within the next #2 days' => '#1 Produkt läuft innerhalb der nächsten #2 Tage ab',
|
||||
'#1 product is already expired' => '#1 Produkt ist bereits abgelaufen',
|
||||
'#1 product is below defined min. stock amount' => '#1 Produkt ist unter Mindestbestand',
|
||||
'Unit' => 'Einheit',
|
||||
'Units' => 'Einheiten',
|
||||
'#1 chore is due to be done within the next #2 days' => '#1 Hausarbeit steht in den nächsten #2 Tagen an',
|
||||
'#1 chore is overdue to be done' => '#1 Hausarbeit ist überfällig',
|
||||
'#1 battery is due to be charged within the next #2 days' => '#1 Batterie muss in den nächsten #2 Tagen geladen werden',
|
||||
'#1 battery is overdue to be charged' => '#1 Batterie ist überfällig',
|
||||
'#1 unit was automatically added and will apply in addition to the amount entered here' => '#1 Einheit wurde automatisch hinzugefügt und gilt zusätzlich der hier eingegebenen Menge',
|
||||
'in singular form' => 'in der Einzahl',
|
||||
'in plural form' => 'in der Mehrzahl',
|
||||
'Never expires' => 'Läuft nie ab',
|
||||
'This cannot be lower than #1' => 'Dies darf nicht kleiner als #1 sein',
|
||||
'-1 means that this product never expires' => '-1 bedeuet, dass dieses Produkt niemals abläuft',
|
||||
'Quantity unit' => 'Mengeneinheit',
|
||||
'Only check if a single unit is in stock (a different quantity can then be used above)' => 'Nur prüfen, ob eine einzelne Einheit vorrätig ist (eine abweichende Mengeneinheit kann dann oben verwendet werden)',
|
||||
'Are you sure to consume all ingredients needed by recipe "#1" (ingredients marked with "check only if a single unit is in stock" will be ignored)?' => 'Sicher, dass alle Zutaten die vom Rezept "#1" benötigt werden aus dem Bestand entfernt werden sollen (Zutaten markiert mit "nur prüfen, ob eine einzelne Einheit vorrätig ist" werden ignoriert)?',
|
||||
'Removed all ingredients of recipe "#1" from stock' => 'Alle Zutaten, die vom Rezept "#1" benötigt werden, wurdem aus dem Bestand entfernt',
|
||||
'Consume all ingredients needed by this recipe' => 'Alle Zutaten, die von diesem Rezept benötigt werden, aus dem Bestand enternen',
|
||||
'Click to show technical details' => 'Klick um technische Details anzuzeigen',
|
||||
'Error while saving, probably this item already exists' => 'Fehler beim Speichern, möglicherweise existiert das Element bereits',
|
||||
'Error details' => 'Fehlerdetails',
|
||||
'Tasks' => 'Aufgaben',
|
||||
'Show done tasks' => 'Erledigte Aufgaben anzeigen',
|
||||
'Task' => 'Aufgabe',
|
||||
'Due' => 'Fällig',
|
||||
'Assigned to' => 'Zugewiesen an',
|
||||
'Mark task "#1" as completed' => 'Aufgabe "#1" als erledigt markieren',
|
||||
'Uncategorized' => 'Nicht kategorisiert',
|
||||
'Task categories' => 'Aufgabenkategorien',
|
||||
'Create task' => 'Aufgabe erstellen',
|
||||
'A due date is required' => 'Ein Fälligkeitsdatum ist erforderlich',
|
||||
'Category' => 'Kategorie',
|
||||
'Edit task' => 'Aufgabe bearbeiten',
|
||||
'Are you sure to delete task "#1"?' => 'Aufgabe "#1" wirklich löschen?',
|
||||
'#1 task is due to be done within the next #2 days' => '#1 Aufgabe steht in den nächsten #2 Tagen an',
|
||||
'#1 tasks are due to be done within the next #2 days' => '#1 Aufgaben stehen in den nächsten #2 Tagen an',
|
||||
'#1 task is overdue to be done' => '#1 Aufgabe ist überfällig',
|
||||
'#1 tasks are overdue to be done' => '#1 Aufgaben sind überfällig',
|
||||
'Edit task category' => 'Aufgabenkategorie bearbeiten',
|
||||
'Create task category' => 'Aufgabenkategorie erstellen',
|
||||
'Product groups' => 'Produktgruppen',
|
||||
'Ungrouped' => 'Ungruppiert',
|
||||
'Create product group' => 'Produktgruppe erstellen',
|
||||
'Edit product group' => 'Produktgruppe bearbeiten',
|
||||
'Product group' => 'Produktgruppe',
|
||||
'Are you sure to delete product group "#1"?' => 'Produktgruppe "#1" wirklich löschen?',
|
||||
'Stay logged in permanently' => 'Dauerhaft angemeldet bleiben',
|
||||
'When not set, you will get logged out at latest after 30 days' => 'Wenn nicht gesetzt, wirst du spätestens nach 30 Tagen automatisch abgemeldet',
|
||||
'Filter by status' => 'Nach Status filtern',
|
||||
'Below min. stock amount' => 'Unter Mindestbestand',
|
||||
'Expiring soon' => 'Bald ablaufend',
|
||||
'Already expired' => 'Bereits abgelaufen',
|
||||
'Due soon' => 'Bald fällig',
|
||||
'Overdue' => 'Überfällig',
|
||||
'View settings' => 'xxx',
|
||||
'Auto reload on external changes' => 'Autom. akt. bei externen Änderungen',
|
||||
'Enable night mode' => 'Nachtmodus aktivieren',
|
||||
'Auto enable in time range' => 'Autom. akt. in diesem Zeitraum',
|
||||
'From' => 'Von',
|
||||
'in format' => 'im Format',
|
||||
'To' => 'Bis',
|
||||
'Time range goes over midnight' => 'Zeitraum geht über Mitternacht',
|
||||
|
||||
//Constants
|
||||
'manually' => 'Manuell',
|
||||
'dynamic-regular' => 'Dynamisch regelmäßig',
|
||||
|
||||
//Technical component translations
|
||||
'timeago_locale' => 'de',
|
||||
'timeago_nan' => 'vor NaN Jahren',
|
||||
'moment_locale' => 'de',
|
||||
'datatables_localization' => '{"sEmptyTable":"Keine Daten in der Tabelle vorhanden","sInfo":"_START_ bis _END_ von _TOTAL_ Einträgen","sInfoEmpty":"Keine Daten vorhanden","sInfoFiltered":"(gefiltert von _MAX_ Einträgen)","sInfoPostFix":"","sInfoThousands":".","sLengthMenu":"_MENU_ Einträge anzeigen","sLoadingRecords":"Wird geladen ..","sProcessing":"Bitte warten ..","sSearch":"Suchen","sZeroRecords":"Keine Einträge vorhanden","oPaginate":{"sFirst":"Erste","sPrevious":"Zurück","sNext":"Nächste","sLast":"Letzte"},"oAria":{"sSortAscending":": aktivieren, um Spalte aufsteigend zu sortieren","sSortDescending":": aktivieren, um Spalte absteigend zu sortieren"},"select":{"rows":{"0":"Zum Auswählen auf eine Zeile klicken","1":"1 Zeile ausgewählt","_":"%d Zeilen ausgewählt"}},"buttons":{"print":"Drucken","colvis":"Spalten","copy":"Kopieren","copyTitle":"In Zwischenablage kopieren","copyKeys":"Taste <i>ctrl</i> oder <i>⌘</i> + <i>C</i> um Tabelle<br>in Zwischenspeicher zu kopieren.<br><br>Um abzubrechen die Nachricht anklicken oder Escape drücken.","copySuccess":{"1":"1 Spalte kopiert","_":"%d Spalten kopiert"}}}',
|
||||
|
||||
//Demo data
|
||||
'Cookies' => 'Cookies',
|
||||
'Chocolate' => 'Schokolade',
|
||||
'Pantry' => 'Vorratskammer',
|
||||
'Candy cupboard' => 'Süßigkeitenschrank',
|
||||
'Tinned food cupboard' => 'Konservenschrank',
|
||||
'Fridge' => 'Kühlschrank',
|
||||
'Piece' => 'Stück',
|
||||
'Pieces' => 'Stücke',
|
||||
'Pack' => 'Packung',
|
||||
'Packs' => 'Packungen',
|
||||
'Glass' => 'Glas',
|
||||
'Glasses' => 'Gläser',
|
||||
'Tin' => 'Dose',
|
||||
'Tins' => 'Dosen',
|
||||
'Can' => 'Becher',
|
||||
'Cans' => 'Becher',
|
||||
'Bunch' => 'Bund',
|
||||
'Bunches' => 'Bunde',
|
||||
'Gummy bears' => 'Gummibärchen',
|
||||
'Crisps' => 'Chips',
|
||||
'Eggs' => 'Eier',
|
||||
'Noodles' => 'Nudeln',
|
||||
'Pickles' => 'Essiggurken',
|
||||
'Gulash soup' => 'Gulaschsuppe',
|
||||
'Yogurt' => 'Joghurt',
|
||||
'Cheese' => 'Käse',
|
||||
'Cold cuts' => 'Aufschnitt',
|
||||
'Paprika' => 'Paprika',
|
||||
'Cucumber' => 'Gurke',
|
||||
'Radish' => 'Radieschen',
|
||||
'Tomato' => 'Tomaten',
|
||||
'Changed towels in the bathroom' => 'Handtücher im Bad gewechselt',
|
||||
'Cleaned the kitchen floor' => 'Küchenboden gewischt',
|
||||
'Warranty ends' => 'Garantie endet',
|
||||
'TV remote control' => 'TV Fernbedienung',
|
||||
'Alarm clock' => 'Wecker',
|
||||
'Heat remote control' => 'Fernbedienung Heizung',
|
||||
'Lawn mowed in the garden' => 'Rasen im Garten gemäht',
|
||||
'Some good snacks' => 'Paar gute Snacks',
|
||||
'Pizza dough' => 'Pizzateig',
|
||||
'Sieved tomatoes' => 'Passierte Tomaten',
|
||||
'Salami' => 'Salami',
|
||||
'Toast' => 'Toast',
|
||||
'Minced meat' => 'Hackfleisch',
|
||||
'Pizza' => 'Pizza',
|
||||
'Spaghetti bolognese' => 'Spaghetti Bolognese',
|
||||
'Sandwiches' => 'Belegte Toasts',
|
||||
'English' => 'Englisch',
|
||||
'German' => 'Deutsch',
|
||||
'Italian' => 'Italienisch',
|
||||
'Demo in different language' => 'Demo in anderer Sprache',
|
||||
'This is the note content of the recipe ingredient' => 'Dies ist der Inhalt der Notiz der Zutat',
|
||||
'Demo User' => 'Demo Benutzer',
|
||||
'Gram' => 'Gramm',
|
||||
'Grams' => 'Gramm',
|
||||
'Flour' => 'Mehl',
|
||||
'Pancakes' => 'Pfannkuchen',
|
||||
'Sugar' => 'Zucker',
|
||||
'Home' => 'Zuhause',
|
||||
'Life' => 'Leben',
|
||||
'Projects' => 'Projekte',
|
||||
'Repair the garage door' => 'Garagentor reparieren',
|
||||
'Fork and improve grocy' => 'grocy forken und verbessern',
|
||||
'Find a solution for what to do when I forget the door keys' => 'Eine Lösung für "Haustürschlüssel vergessen" finden',
|
||||
'Sweets' => 'Süßigkeiten',
|
||||
'Bakery products' => 'Bäckerei Produkte',
|
||||
'Tinned food' => 'Konservern',
|
||||
'Butchery products' => 'Metzgerei',
|
||||
'Vegetables/Fruits' => 'Obst/Gemüse',
|
||||
'Refrigerated products' => 'Kühlregal'
|
||||
);
|
13
localization/en.php
Normal file
13
localization/en.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
return array(
|
||||
//Constants
|
||||
'manually' => 'Manually',
|
||||
'dynamic-regular' => 'Dynamic regular',
|
||||
|
||||
//Technical component translations
|
||||
'timeago_locale' => 'en',
|
||||
'timeago_nan' => 'NaN years ago',
|
||||
'moment_locale' => '',
|
||||
'datatables_localization' => '{"sEmptyTable":"No data available in table","sInfo":"Showing _START_ to _END_ of _TOTAL_ entries","sInfoEmpty":"Showing 0 to 0 of 0 entries","sInfoFiltered":"(filtered from _MAX_ total entries)","sInfoPostFix":"","sInfoThousands":",","sLengthMenu":"Show _MENU_ entries","sLoadingRecords":"Loading...","sProcessing":"Processing...","sSearch":"Search:","sZeroRecords":"No matching records found","oPaginate":{"sFirst":"First","sLast":"Last","sNext":"Next","sPrevious":"Previous"},"oAria":{"sSortAscending":": activate to sort column ascending","sSortDescending":": activate to sort column descending"}}'
|
||||
);
|
192
localization/it.php
Normal file
192
localization/it.php
Normal file
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
return array(
|
||||
'Stock overview' => 'Dispensa',
|
||||
'#1 products expiring within the next #2 days' => '#1 prodotti scadranno tra #2 giorni',
|
||||
'#1 products are already expired' => '#1 prodotti scaduti',
|
||||
'#1 products are below defined min. stock amount' => '#1 prodotti sotto il limite minimo',
|
||||
'Product' => 'prodotto',
|
||||
'Amount' => 'quantità',
|
||||
'Next best before date' => 'Prossima data di scadenza',
|
||||
'Logout' => 'Logout',
|
||||
'Chores overview' => 'Riepilogo delle abitudini',
|
||||
'Batteries overview' => 'Riepilogo delle batterie',
|
||||
'Purchase' => 'Acquisti',
|
||||
'Consume' => 'Consumi',
|
||||
'Inventory' => 'Inventario',
|
||||
'Shopping list' => 'Lista della spesa',
|
||||
'Chore tracking' => 'Dati abitudini',
|
||||
'Battery tracking' => 'Dati batterie',
|
||||
'Products' => 'Prodotti',
|
||||
'Locations' => 'Posizioni',
|
||||
'Quantity units' => 'Unità di misura',
|
||||
'Chores' => 'Abitudini',
|
||||
'Batteries' => 'Batterie',
|
||||
'Chore' => 'Abitudine',
|
||||
'Next estimated tracking' => 'Prossima esecuzione',
|
||||
'Last tracked' => 'Ultima esecuzione',
|
||||
'Battery' => 'Batterie',
|
||||
'Last charged' => 'Ultima ricarica',
|
||||
'Next planned charge cycle' => 'Prossima ricarica',
|
||||
'Best before' => 'Data di scadenza',
|
||||
'OK' => 'OK',
|
||||
'Product overview' => 'Riepilogo dei prodotti',
|
||||
'Stock quantity unit' => 'Unità di misura',
|
||||
'Stock amount' => 'Quantità',
|
||||
'Last purchased' => 'Ultimo acquisto',
|
||||
'Last used' => 'Ultimo utilizzo',
|
||||
'Spoiled' => 'Scaduto',
|
||||
'Barcode lookup is disabled' => 'I codici a barre sono disabilitati',
|
||||
'will be added to the list of barcodes for the selected product on submit' => 'sarà aggiunto alla lista dei codici a barre per questo prodotto',
|
||||
'New amount' => 'Nuova quantità',
|
||||
'Note' => 'Nota',
|
||||
'Tracked time' => 'Ora di esecuzione',
|
||||
'Chore overview' => 'Riepilogo dell\'abitudine',
|
||||
'Tracked count' => 'Numero di esecuzioni',
|
||||
'Battery overview' => 'Riepilogo della batteria',
|
||||
'Charge cycles count' => 'Numero di ricariche',
|
||||
'Create shopping list item' => 'Aggiungi un prodotto alla lista della spesa',
|
||||
'Edit shopping list item' => 'Modifica un\'entrata della lista della spesa',
|
||||
'#1 units were automatically added and will apply in addition to the amount entered here' => '#1 sono state aggiunte automaticamente',
|
||||
'Save' => 'Salva',
|
||||
'Add' => 'Aggiungi',
|
||||
'Name' => 'Nome',
|
||||
'Location' => 'Posizione',
|
||||
'Min. stock amount' => 'Quantità minima',
|
||||
'QU purchase' => 'Unità di acquisto',
|
||||
'QU stock' => 'Unità di dispensa',
|
||||
'QU factor' => 'Fattore di conversione',
|
||||
'Description' => 'Descrizione',
|
||||
'Create product' => 'Aggiungi prodotto',
|
||||
'Barcode(s)' => 'Codice a barre',
|
||||
'Minimum stock amount' => 'Quantità minima',
|
||||
'Default best before days' => 'Data di scadenza standard in giorni',
|
||||
'Quantity unit purchase' => 'Unità di acquisto',
|
||||
'Quantity unit stock' => 'Unità di dispensa',
|
||||
'Factor purchase to stock quantity unit' => 'Fattore di conversione tra quantità di acquisto e di dispensa',
|
||||
'Create location' => 'Aggiungi posizione',
|
||||
'Create quantity unit' => 'Aggiungi unità di misura',
|
||||
'Period type' => 'Tipo di ripetizione',
|
||||
'Period days' => 'Periodo in giorni',
|
||||
'Create chore' => 'Aggiungi abitudine',
|
||||
'Used in' => 'Usato in',
|
||||
'Create battery' => 'Aggiungi batteria',
|
||||
'Edit battery' => 'Modifica batteria',
|
||||
'Edit chore' => 'Modifica abitudine',
|
||||
'Edit quantity unit' => 'Modifica unità di misura',
|
||||
'Edit product' => 'Modifica prodotto',
|
||||
'Edit location' => 'Modifica posizione',
|
||||
'Record data' => 'Registra dati',
|
||||
'Manage master data' => 'Gestisci dati',
|
||||
'This will apply to added products' => 'Verrà applicato ai prodotti aggiunti',
|
||||
'never' => 'mai',
|
||||
'Add products that are below defined min. stock amount' => 'Aggiungi prodotti sotti il limite minimo',
|
||||
'For purchases this amount of days will be added to today for the best before date suggestion' => 'Questo numero di giorni verrà aggiunto alla data di acquisto per la data di scadenza',
|
||||
'This means 1 #1 purchased will be converted into #2 #3 in stock' => 'Questo significa che 1 #1 acquistato diventerà #2 #3 in dispensa',
|
||||
'Login' => 'Login',
|
||||
'Username' => 'Username',
|
||||
'Password' => 'Password',
|
||||
'Invalid credentials, please try again' => 'Credenziali non valide, per favore riprova',
|
||||
'Are you sure to delete battery "#1"?' => 'Sei sicuro di voler eliminare la batteria "#1"?',
|
||||
'Yes' => 'Si',
|
||||
'No' => 'No',
|
||||
'Are you sure to delete chore "#1"?' => 'Sei sicuro di voler eliminare l\'abitudine "#1"?',
|
||||
'"#1" could not be resolved to a product, how do you want to proceed?' => '"#1" non è stato associato a nessun prodotto, vuoi procedere?',
|
||||
'Create or assign product' => 'Aggiungi o assegna prodotto',
|
||||
'Cancel' => 'Annulla',
|
||||
'Add as new product' => 'Aggiungi come nuovo prodotto',
|
||||
'Add as barcode to existing product' => 'Assegna il codice a barre ad un prodotto',
|
||||
'Add as new product and prefill barcode' => 'Aggiungi come nuovo prodotto ed assegna il codice a barre',
|
||||
'Are you sure to delete quantity unit "#1"?' => 'Sei sicuro di voler eliminare l\'unità di misura "#1"?',
|
||||
'Are you sure to delete product "#1"?' => 'Sei sicuro di voler eliminare il prodotto "#1"?',
|
||||
'Are you sure to delete location "#1"?' => 'Sei sicuro di voler eliminare la posizione "#1"?',
|
||||
'Manage API keys' => 'Gestisci le chiavi API',
|
||||
'REST API & data model documentation' => 'REST API & Documentazione del modello di dati',
|
||||
'API keys' => 'Chiavi API',
|
||||
'Create new API key' => 'Crea nuova chiave API',
|
||||
'API key' => 'Chiave API',
|
||||
'Expires' => 'Scade il',
|
||||
'Created' => 'Creata il',
|
||||
'This product is not in stock' => 'Questo prodotto non è in dispensa',
|
||||
'This means #1 will be added to stock' => '#1 sarà aggiunto alla dispensa',
|
||||
'This means #1 will be removed from stock' => '#1 sarà rimosso dalla dispensa',
|
||||
'This means it is estimated that a new execution of this chore is tracked #1 days after the last was tracked' => 'L\'esecuzione dell\'abitudine è #1 giorni dopo la precedente',
|
||||
'Removed #1 #2 of #3 from stock' => '#1 #2 su #3 rimossi dalla dispensa',
|
||||
'About grocy' => 'Riguardo grocy',
|
||||
'Close' => 'Chiudi',
|
||||
'#1 batteries are due to be charged within the next #2 days' => '#1 batterie da ricaricare entro #2 giorni',
|
||||
'#1 batteries are overdue to be charged' => '#1 batterie devono essere ricaricate',
|
||||
'#1 chores are due to be done within the next #2 days' => '#1 abitudini da eseguire entro #2 giorni',
|
||||
'#1 chores are overdue to be done' => '#1 abitudini da eseguire',
|
||||
'Released on' => 'Rilasciato il',
|
||||
'Consume #3 #1 of #2' => 'Consumati #3 #1 di #2',
|
||||
'Added #1 #2 of #3 to stock' => 'Aggiunti #1 #2 di #3',
|
||||
'Stock amount of #1 is now #2 #3' => 'La quantità in dispensa di #1 è ora #2 #3',
|
||||
'Tracked execution of chore #1 on #2' => 'Esecuzione dell\'abitudine #1 registrata il #2',
|
||||
'Tracked charge cycle of battery #1 on #2' => 'Ricarica della batteria #1 effettuata il #2',
|
||||
'Consume all #1 which are currently in stock' => 'Consuma tutto #1 in dispensa',
|
||||
'All' => 'Tutto',
|
||||
'Track charge cycle of battery #1' => 'Registra la ricarica della batteria #1',
|
||||
'Track execution of chore #1' => 'Registra l\'esecuzione dell\'abitudine #1',
|
||||
'Filter by location' => 'Filtra per posizione',
|
||||
'Search' => 'Cerca',
|
||||
'Not logged in' => 'Non autenticato',
|
||||
'You have to select a product' => 'Devi selezionare un prodotto',
|
||||
'You have to select a chore' => 'Devi selezionare un\'abitudine',
|
||||
'You have to select a battery' => 'Devi selezionare una batteria',
|
||||
'A name is required' => 'Inserisci un nome',
|
||||
'A location is required' => 'Inserisci la posizione',
|
||||
'The amount cannot be lower than #1' => 'La quantità non può essere minore di #1',
|
||||
'This cannot be negative' => 'Il numero non può essere negativo',
|
||||
'A quantity unit is required' => 'Inserisci un\'unità di misura',
|
||||
'A period type is required' => 'Seleziona un tipo di ripetizione',
|
||||
|
||||
//Constants
|
||||
'manually' => 'Manualmente',
|
||||
'dynamic-regular' => 'Regolatore dinamico',
|
||||
|
||||
//Technical component translations
|
||||
'timeago_locale' => 'it',
|
||||
'timeago_nan' => 'NaN anni fa',
|
||||
'moment_locale' => 'it',
|
||||
'datatables_localization' => '{"sEmptyTable":"Nessun dato disponibile","sInfo":"Mostrando da _START_ a _END_ di _TOTAL_ voci","sInfoEmpty":"Mostrando da 0 a 0 di 0 voci","sInfoFiltered":"(Filtrato da _MAX_ voci totali)","sInfoPostFix":"","sInfoThousands":",","sLengthMenu":"Mostra _MENU_ voci","sLoadingRecords":"Caricando...","sProcessing":"Calcolando...","sSearch":"Cerca:","sZeroRecords":"Nessun risultato trovato","oPaginate":{"sFirst":"Prima","sLast":"Ultima","sNext":"Prossima","sPrevious":"Precedente"},"oAria":{"sSortAscending":": ordine crescente","sSortDescending":": ordine decrescente"}}',
|
||||
|
||||
//Demo data
|
||||
'Cookies' => 'Biscotti',
|
||||
'Chocolate' => 'Cioccolato',
|
||||
'Pantry' => 'Vorratskammer',
|
||||
'Candy cupboard' => 'Süßigkeitenschrank',
|
||||
'Tinned food cupboard' => 'Konservenschrank',
|
||||
'Fridge' => 'Kühlschrank',
|
||||
'Piece' => 'Pezzo',
|
||||
'Pieces' => 'Pezzi',
|
||||
'Pack' => 'Pacco',
|
||||
'Packs' => 'Pacchi',
|
||||
'Glass' => 'Bicchiere',
|
||||
'Glasses' => 'Bicchieri',
|
||||
'Tin' => 'Scatola',
|
||||
'Tins' => 'Scatole',
|
||||
'Can' => 'Lattina',
|
||||
'Cans' => 'Lattine',
|
||||
'Bunch' => 'Cespo',
|
||||
'Bunches' => 'Cespi',
|
||||
'Gummy bears' => 'Caramelle',
|
||||
'Crisps' => 'Patatine',
|
||||
'Eggs' => 'Uova',
|
||||
'Noodles' => 'Spaghetti',
|
||||
'Pickles' => 'Marmellata',
|
||||
'Gulash soup' => 'Dado',
|
||||
'Yogurt' => 'Yogurt',
|
||||
'Cheese' => 'Parmigiano',
|
||||
'Cold cuts' => 'Pancetta',
|
||||
'Paprika' => 'Pepe',
|
||||
'Cucumber' => 'Zucchine',
|
||||
'Radish' => 'Radicchio',
|
||||
'Tomato' => 'Pomodori',
|
||||
'Changed towels in the bathroom' => 'Cambiare asciugamani in bagno',
|
||||
'Cleaned the kitchen floor' => 'Pulire la cucina',
|
||||
'Warranty ends' => 'Scadenza dalla garanzia',
|
||||
'TV remote control' => 'Telecomando',
|
||||
'Alarm clock' => 'Sveglia',
|
||||
'Heat remote control' => 'Termostato'
|
||||
);
|
334
localization/no.php
Normal file
334
localization/no.php
Normal file
@@ -0,0 +1,334 @@
|
||||
<?php
|
||||
|
||||
return array(
|
||||
'Stock overview' => 'Husholdning',
|
||||
'#1 products expiring within the next #2 days' => '#1 Produkt som går ut på dato innen de neste #2 dagene',
|
||||
'#1 products are already expired' => '#1 Produkt som har gått ut på dato',
|
||||
'#1 products are below defined min. stock amount' => '#1 Produkt under minimum husholdningsnivå',
|
||||
'Product' => 'Produkt',
|
||||
'Amount' => 'Antall',
|
||||
'Next best before date' => 'Kommende best før dato',
|
||||
'Logout' => 'Logg ut',
|
||||
'Chores overview' => 'Oversikt Husarbeid',
|
||||
'Batteries overview' => 'Oversikt Batteri',
|
||||
'Purchase' => 'Innkjøp',
|
||||
'Consume' => 'Forbruk produkt',
|
||||
'Inventory' => 'Endre Husholdning',
|
||||
'Shopping list' => 'Handleliste',
|
||||
'Chore tracking' => 'Logge Husarbeid',
|
||||
'Battery tracking' => 'Batteri Ladesyklus',
|
||||
'Products' => 'Produkter',
|
||||
'Locations' => 'Lokasjoner',
|
||||
'Quantity units' => 'Forpakning',
|
||||
'Chores' => 'Husarbeid',
|
||||
'Batteries' => 'Batterier',
|
||||
'Chore' => 'Husarbeid',
|
||||
'Next estimated tracking' => 'Neste handling',
|
||||
'Last tracked' => 'Sist logget',
|
||||
'Battery' => 'Batteri',
|
||||
'Last charged' => 'Sist ladet',
|
||||
'Next planned charge cycle' => 'Neste planlagte ladesyklus',
|
||||
'Best before' => 'Best før',
|
||||
'OK' => 'OK',
|
||||
'Product overview' => 'Produkt oversikt',
|
||||
'Stock quantity unit' => 'Forpakningstype i husholdningen',
|
||||
'Stock amount' => 'Husholdning',
|
||||
'Last purchased' => 'Sist kjøpt',
|
||||
'Last used' => 'Sist brukt',
|
||||
'Spoiled' => 'Produkt har gått ut på dato',
|
||||
'Barcode lookup is disabled' => 'Strekkodesøk deaktivert',
|
||||
'will be added to the list of barcodes for the selected product on submit' => 'Blir lagt til liste over strekkoder når produkt blir lagt inn.',
|
||||
'New amount' => 'Nytt antall',
|
||||
'Note' => 'Info',
|
||||
'Tracked time' => 'Tid utført/ ladet',
|
||||
'Chore overview' => 'Oversikt Husarbeid',
|
||||
'Tracked count' => 'Antall utførelser/ ladninger',
|
||||
'Battery overview' => 'Batteri Oversikt',
|
||||
'Charge cycles count' => 'Antall ladesykluser',
|
||||
'Create shopping list item' => 'Opprett handelisteoppføring',
|
||||
'Edit shopping list item' => 'Endre på handlelistoppføring',
|
||||
'#1 units were automatically added and will apply in addition to the amount entered here' => '#1 enheter ble automatisk lagt til i tillegg til hva som blir skrevet inn her',
|
||||
'Save' => 'Lagre',
|
||||
'Add' => 'Legg til',
|
||||
'Name' => 'Navn',
|
||||
'Location' => 'Lokasjon',
|
||||
'Min. stock amount' => 'Minimums antall for husholdingen',
|
||||
'QU purchase' => 'FPK innkjøp',
|
||||
'QU stock' => 'FPK husholdning',
|
||||
'QU factor' => 'FPK faktor',
|
||||
'Description' => 'Beskrivelse',
|
||||
'Create product' => 'Opprett produkt',
|
||||
'Barcode(s)' => 'Strekkode(r)',
|
||||
'Minimum stock amount' => 'Minimums antall for husholdningen',
|
||||
'Default best before days' => 'Standard antall dager best før',
|
||||
'Quantity unit purchase' => 'Forpakning kjøpt',
|
||||
'Quantity unit stock' => 'Forpakning husholdning',
|
||||
'Factor purchase to stock quantity unit' => 'Innkjøpsfaktor for forpakning',
|
||||
'Create location' => 'Opprett lokasjon',
|
||||
'Create quantity unit' => 'Opprett forpakning',
|
||||
'Period type' => 'Gjentakelse',
|
||||
'Period days' => 'Antall dager for gjentakelse',
|
||||
'Create chore' => 'Opprett husarbeid oppgave',
|
||||
'Used in' => 'Brukt',
|
||||
'Create battery' => 'Opprett batteri',
|
||||
'Edit battery' => 'Endre batteri',
|
||||
'Edit chore' => 'Endre husarbeid oppgave',
|
||||
'Edit quantity unit' => 'Endre forpakning',
|
||||
'Edit product' => 'Endre produkt',
|
||||
'Edit location' => 'Endre lokasjon',
|
||||
'Record data' => 'Logg handlinger',
|
||||
'Manage master data' => 'Administrer masterdata',
|
||||
'This will apply to added products' => 'Dette vil gjelde for produkt som blir lagt til',
|
||||
'never' => 'aldri',
|
||||
'Add products that are below defined min. stock amount' => 'Legg til produkt som er under minimumsnivå for husholdningen',
|
||||
'For purchases this amount of days will be added to today for the best before date suggestion' => 'For innkjøp vil dette antallet dager legges til bestfør forslaget',
|
||||
'This means 1 #1 purchased will be converted into #2 #3 in stock' => 'Dette betyr at 1 #1 innkjøp vil bli omgjort til #2 #3 husholdning',
|
||||
'Login' => 'Logg inn',
|
||||
'Username' => 'Brukernavn',
|
||||
'Password' => 'Passord',
|
||||
'Invalid credentials, please try again' => 'Feil brukernavn og/eller passord, prøv igjen',
|
||||
'Are you sure to delete battery "#1"?' => 'Er du sikker du ønsker å slette Batteri "#1"?',
|
||||
'Yes' => 'Ja',
|
||||
'No' => 'Nei',
|
||||
'Are you sure to delete chore "#1"?' => 'Er du sikker på du ønsker å slette husarbeid oppgave "#1"?',
|
||||
'"#1" could not be resolved to a product, how do you want to proceed?' => '"#1" kunne ikke bli tildelt et produkt, hvordan ønsker du å fortsette?',
|
||||
'Create or assign product' => 'Opprett eller tildel til produkt',
|
||||
'Cancel' => 'Avbryt',
|
||||
'Add as new product' => 'Legg til som nytt produkt',
|
||||
'Add as barcode to existing product' => 'Legg til strekkode til allerede eksisterende produkt',
|
||||
'Add as new product and prefill barcode' => 'Legg til som nytt produkt med forhåndsutfylt strekkode',
|
||||
'Are you sure to delete quantity unit "#1"?' => 'Er du sikker du ønsker å slette forpakning "#1"?',
|
||||
'Are you sure to delete product "#1"?' => 'Er du sikker du ønsker å slette produkt "#1"?',
|
||||
'Are you sure to delete location "#1"?' => 'Er du sikker du ønsker å slette lokasjon "#1"?',
|
||||
'Manage API keys' => 'Administrer API-Keys',
|
||||
'REST API & data model documentation' => 'REST-API & Datamodell Dokumentasjon',
|
||||
'API keys' => 'API-Keys',
|
||||
'Create new API key' => 'Opprett ny API-Key',
|
||||
'API key' => 'API-Key',
|
||||
'Expires' => 'Går ut',
|
||||
'Created' => 'Opprettet',
|
||||
'This product is not in stock' => 'Dette produktet er ikke i husholdningen',
|
||||
'This means #1 will be added to stock' => 'Dette betyr at #1 vil bli lagt til i husholdningen',
|
||||
'This means #1 will be removed from stock' => 'Dette betyr at #1 vil bli fjernet fra husholdningen',
|
||||
'This means it is estimated that a new execution of this chore is tracked #1 days after the last was tracked' => 'Dette betyr at det er estimert at den nye utførelsen av denne husarbeid oppgaven er logget #1 dag etter den sist var logget',
|
||||
'Removed #1 #2 of #3 from stock' => 'Fjernet #1 #2 #3 fra husholdningen',
|
||||
'About grocy' => 'Om Grocy',
|
||||
'Close' => 'Lukk',
|
||||
'#1 batteries are due to be charged within the next #2 days' => '#1 Batteri må lades innen de #2 neste dagene',
|
||||
'#1 batteries are overdue to be charged' => '#1 Batteri har gått over fristen for å bli ladet opp',
|
||||
'#1 chores are due to be done within the next #2 days' => '#1 husarbeid(s) oppgave(r) skal gjøres inne de #2 neste dagene',
|
||||
'#1 chores are overdue to be done' => '#1 husarbeid(s) oppgave(r) har gått over fristen for utførelse',
|
||||
'Released on' => 'Utgitt',
|
||||
'Consume #3 #1 of #2' => 'Forbruk #3 #1 #2',
|
||||
'Added #1 #2 of #3 to stock' => '#1 #2 #3 lagt til i husholdningen',
|
||||
'Stock amount of #1 is now #2 #3' => 'Husholdning antall #1 er nå #2 #3',
|
||||
'Tracked execution of chore #1 on #2' => 'Utførte husarbeid oppgave "#1" den #2',
|
||||
'Tracked charge cycle of battery #1 on #2' => 'Ladet #1 den #2',
|
||||
'Consume all #1 which are currently in stock' => 'Forbruk alle #1 som er i husholdningen',
|
||||
'All' => 'Alle',
|
||||
'Track charge cycle of battery #1' => '#1 ladet',
|
||||
'Track execution of chore #1' => 'Utfør husarbeid oppgave #1',
|
||||
'Filter by location' => 'Filtrér etter lokasjon',
|
||||
'Search' => 'Søk',
|
||||
'Not logged in' => 'Ikke logget inn',
|
||||
'You have to select a product' => 'Du må velge et produkt',
|
||||
'You have to select a chore' => 'Du må velge en husarbeid oppgave',
|
||||
'You have to select a battery' => 'Du må velge et batteri',
|
||||
'A name is required' => 'Vennligst fyll inn et navn',
|
||||
'A location is required' => 'En lokasjon kreves',
|
||||
'The amount cannot be lower than #1' => 'Antallet kan ikke være lavere enn #1',
|
||||
'This cannot be negative' => 'Dette kan ikke være negativt',
|
||||
'A quantity unit is required' => 'Forpakning antall/størrelse kreves',
|
||||
'A period type is required' => 'En periodetype kreves',
|
||||
'A best before date is required and must be later than today' => 'En best før dato kreves, denne må være senere enn i dag',
|
||||
'Settings' => 'Innstillinger',
|
||||
'This can only be before now' => 'Dette kan kun være før nå',
|
||||
'Calendar' => 'Kalender',
|
||||
'Recipes' => 'Oppskrifter',
|
||||
'Edit recipe' => 'Endre oppskrift',
|
||||
'New recipe' => 'Ny oppskrift',
|
||||
'Ingredients list' => 'Liste over ingredienser',
|
||||
'Add recipe ingredient' => 'Legg ingrediens til oppskrift',
|
||||
'Edit recipe ingredient' => 'Endre ingrediens i oppskrift',
|
||||
'Are you sure to delete recipe "#1"?' => 'Er du sikker du ønsker å slette oppskrift "#1"?',
|
||||
'Are you sure to delete recipe ingredient "#1"?' => 'Er du sikker du ønsker å slette ingrediens "#1" fra oppskriften?',
|
||||
'Are you sure to empty the shopping list?' => 'Er du sikker du ønsker å slette handlelisten?',
|
||||
'Clear list' => 'Tøm liste',
|
||||
'Requirements fulfilled' => 'Har jeg alt jeg trenger for denne oppskriften?',
|
||||
'Put missing products on shopping list' => 'Legg manglende produkter til handlelisten',
|
||||
'Not enough in stock, #1 ingredients missing' => 'Ikke nok i husholdningen, #1 ingredienser mangler',
|
||||
'Enough in stock' => 'Nok i husholdningen',
|
||||
'Not enough in stock, #1 ingredients missing but already on the shopping list' => 'Ikke nok i husholdningen, #1 ingrediens mangler, men denne er på handelisten',
|
||||
'Expand to fullscreen' => 'Full skjerm',
|
||||
'Ingredients' => 'Ingredienser',
|
||||
'Preparation' => 'Forberedelse / Slik gjør du',
|
||||
'Recipe' => 'Oppskrift',
|
||||
'Not enough in stock, #1 missing, #2 already on shopping list' => 'Ikke nok i husholdningen, mangler #1, er #2 på handlelisten',
|
||||
'Show notes' => 'Vis notater',
|
||||
'Put missing amount on shopping list' => 'Legg manglende til handlelisten',
|
||||
'Are you sure to put all missing ingredients for recipe "#1" on the shopping list?' => 'Er du sikker du ønsker å legge alle manglende ingredienser til oppskrift "#1"?',
|
||||
'Added for recipe #1' => 'Lagt til fra oppskrift "#1"',
|
||||
'Manage users' => 'Administrer brukere',
|
||||
'User' => 'Bruker',
|
||||
'Users' => 'Brukere',
|
||||
'Are you sure to delete user "#1"?' => 'Er du sikker på du ønsker å slette bruker, "#1"?',
|
||||
'Create user' => 'Legg til bruker',
|
||||
'Edit user' => 'Endre på bruker',
|
||||
'First name' => 'Fornavn',
|
||||
'Last name' => 'Etternavn',
|
||||
'A username is required' => 'Et brukernavn er nødvendig',
|
||||
'Confirm password' => 'Bekreft passord',
|
||||
'Passwords do not match' => 'Passord er ikke like',
|
||||
'Change password' => 'Endre passord',
|
||||
'Done by' => 'Utført av',
|
||||
'Last done by' => 'Sist utført av',
|
||||
'Unknown' => 'Ukjent',
|
||||
'Filter by chore' => 'Filtrér husarbeid',
|
||||
'Chores analysis' => 'Statistikk husarbeid',
|
||||
'0 means suggestions for the next charge cycle are disabled' => '0 betyr neste ladesyklus er avslått',
|
||||
'Charge cycle interval (days)' => 'Ladesyklysintervall (Dager)',
|
||||
'Last price' => 'Siste pris',
|
||||
'Price history' => 'Prishistorikk',
|
||||
'No price history available' => 'Ingen prishistorikk tilgjengelig',
|
||||
'Price' => 'Pris',
|
||||
'in #1 per purchase quantity unit' => 'I #1 per kjøpt forpakning ',
|
||||
'The price cannot be lower than #1' => 'Prisen kan ikke være lavere enn #1',
|
||||
'#1 product expires within the next #2 days' => '#1 Produkt går ut på dato innen de #2 neste dagene',
|
||||
'#1 product is already expired' => '#1 Produkt er allerede gått ut på dato',
|
||||
'#1 product is below defined min. stock amount' => '#1 Produkt er under minimums husholdningsnivå',
|
||||
'Unit' => 'Enhet',
|
||||
'Units' => 'Enheter',
|
||||
'#1 chore is due to be done within the next #2 days' => '#1 husarbeid oppgave(r) skal gjøres inne de #2 neste dagene',
|
||||
'#1 chore is overdue to be done' => '#1 husarbeid(s) oppgave(r) har gått over fristen for utførelse',
|
||||
'#1 battery is due to be charged within the next #2 days' => '#1 Batteri må lades innen #2 dager',
|
||||
'#1 battery is overdue to be charged' => '#1 Batteri har gått over fristen for å lades',
|
||||
'#1 unit was automatically added and will apply in addition to the amount entered here' => '#1 enhet ble automatisk lagt til i tillegg til hva som blir skrevet inn her',
|
||||
'in singular form' => 'I entall',
|
||||
'in plural form' => 'I flertall',
|
||||
'Never expires' => 'Går ikke ut på dato',
|
||||
'This cannot be lower than #1' => 'Dette kan ikke være lavere enn #1',
|
||||
'-1 means that this product never expires' => '-1 Betyr at dette produktet aldri går ut på dato',
|
||||
'Quantity unit' => 'Forpakning',
|
||||
'Only check if a single unit is in stock (a different quantity can then be used above)' => 'Huk av hvis du ønsker å bruke mindre enn forpakningsstørrelse i husholdningen',
|
||||
'Are you sure to consume all ingredients needed by recipe "#1" (ingredients marked with "check only if a single unit is in stock" will be ignored)?' => 'Er du sikker du ønsker å forbruke alle ingredienser for "#1" oppskriften? (Ingredienser merket med "bruke mindre enn forpakningsstørrelse i husholdningen" blir ignorert',
|
||||
'Removed all ingredients of recipe "#1" from stock' => 'Fjern alle ingredienser for "#1" oppskriften fra husholdningen.',
|
||||
'Consume all ingredients needed by this recipe' => 'Forbruk alle ingredienser for denne oppskriften',
|
||||
'Click to show technical details' => 'Klikk for å vise teknisk informasjon',
|
||||
'Error while saving, probably this item already exists' => 'Kunne ikke lagre, produkt er lagt til fra før',
|
||||
'Error details' => 'Detaljer om feil',
|
||||
'Tasks' => 'Oppgaver',
|
||||
'Show done tasks' => 'Vis ferdige oppgaver',
|
||||
'Task' => 'Oppgave',
|
||||
'Due' => 'Forfall',
|
||||
'Assigned to' => 'Tildelt',
|
||||
'Mark task "#1" as completed' => 'Merk oppgave "#1" som ferdig',
|
||||
'Uncategorized' => 'Mangler kategori',
|
||||
'Task categories' => 'Oppgave kategorier',
|
||||
'Create task' => 'Opprett en oppgave',
|
||||
'A due date is required' => 'En forfallsdato kreves',
|
||||
'Category' => 'Kategori',
|
||||
'Edit task' => 'Endre oppgave',
|
||||
'Are you sure to delete task "#1"?' => 'Er du sikker du ønsker slette oppgave "#1"?',
|
||||
'#1 task is due to be done within the next #2 days' => '#1 oppgave har utførelse forfall innen de neste #2 dagene',
|
||||
'#1 tasks are due to be done within the next #2 days' => '#1 oppgaver har utførelse forfall innen de neste #2 dagene',
|
||||
'#1 task is overdue to be done' => '#1 oppgave har forfalt utførelse dato',
|
||||
'#1 tasks are overdue to be done' => '#1 oppgaver har forfalt utførelse dato',
|
||||
'Edit task category' => 'Endre oppgave kategori',
|
||||
'Create task category' => 'Opprett oppgave kategori',
|
||||
'Product groups' => 'Produktgrupper',
|
||||
'Ungrouped' => 'Ikke i grupper',
|
||||
'Create product group' => 'Opprett produkt gruppe',
|
||||
'Edit product group' => 'Endre produkt gruppe',
|
||||
'Product group' => 'Produktgruppe',
|
||||
'Are you sure to delete product group "#1"?' => 'Er du sikker du ønsker å slette produktgruppe "#1"?',
|
||||
'Stay logged in permanently' => 'Alltid være innlogget',
|
||||
'When not set, you will get logged out at latest after 30 days' => 'Når den ikke er satt vil du bli logget ut etter 30 dager',
|
||||
'Filter by status' => 'Filtrér etter status',
|
||||
'Below min. stock amount' => 'Under under minimum husholdningsnivå',
|
||||
'Expiring soon' => 'Går snart ut på dato',
|
||||
'Already expired' => 'Utgått på dato',
|
||||
'Due soon' => 'Forfaller snart',
|
||||
'Overdue' => 'Forfalt',
|
||||
|
||||
//Constants
|
||||
'manually' => 'Manuel',
|
||||
'dynamic-regular' => 'Automatisk',
|
||||
|
||||
//Technical component translations
|
||||
'timeago_locale' => 'no',
|
||||
'timeago_nan' => 'for NaN År',
|
||||
'moment_locale' => 'nb',
|
||||
'datatables_localization' => '{"sEmptyTable":"Det finnes ingen data i tabellen","sInfo":"_START_ fra _END_ til _TOTAL_ skriv","sInfoEmpty":"Ingen data tilgjengelign","sInfoFiltered":"(filtrert fra _MAX_ skriv)","sInfoPostFix":"","sInfoThousands":".","sLengthMenu":"_MENU_ registrer deg","sLoadingRecords":"Laster ..","sProcessing":"Vennligst vent ..","sSearch":"Søk","sZeroRecords":"Ingen oppføringer tilgjengelig","oPaginate":{"sFirst":"Første","sPrevious":"Bakover","sNext":"Neste","sLast":"Siste"},"oAria":{"sSortAscending":": Sortér stigende","sSortDescending":": Sortér synkende"},"select":{"rows":{"0":"klikk på en linje for å velge","1":"1 linje valgt","_":"%d linger valgt"}},"buttons":{"print":"Print","colvis":"Søyle","copy":"Kopi","copyTitle":"Kopier til utklippstavlen","copyKeys":"Trykk <i>ctrl</i> eller <i>⌘</i> + <i>C</i> for å kopiere tabell<br> til utklipptavlen.<br><br>For å avbryte, klikke på meldingen eller trykk på ESC.","copySuccess":{"1":"1 Kolonne kopiert","_":"%d kolonne kopiert"}}}',
|
||||
|
||||
//Demo data
|
||||
'Cookies' => 'Cookies',
|
||||
'Chocolate' => 'Sjokolade',
|
||||
'Pantry' => 'Spiskammers',
|
||||
'Candy cupboard' => 'Godteriskapet',
|
||||
'Tinned food cupboard' => 'Boksematskapet',
|
||||
'Fridge' => 'Kjøleskapet',
|
||||
'Piece' => 'Ett',
|
||||
'Pieces' => 'Flere',
|
||||
'Pack' => 'Pakke',
|
||||
'Packs' => 'Pakker',
|
||||
'Glass' => 'Glass',
|
||||
'Glasses' => 'Glass',
|
||||
'Tin' => 'Hermetikkboks',
|
||||
'Tins' => 'Hermetikkbokser',
|
||||
'Can' => 'Boks',
|
||||
'Cans' => 'Bokser',
|
||||
'Bunch' => 'Klase',
|
||||
'Bunches' => 'Klaser',
|
||||
'Gummy bears' => 'Vingummibjørner',
|
||||
'Crisps' => 'Chips',
|
||||
'Eggs' => 'Egg',
|
||||
'Noodles' => 'Nuddler',
|
||||
'Pickles' => 'Sur agurk',
|
||||
'Gulash soup' => 'Gulasj suppe',
|
||||
'Yogurt' => 'Yoghurt',
|
||||
'Cheese' => 'Ost',
|
||||
'Cold cuts' => 'Kjøttpålegg',
|
||||
'Paprika' => 'Paprika',
|
||||
'Cucumber' => 'Agurk',
|
||||
'Radish' => 'Reddik',
|
||||
'Tomato' => 'Tomat',
|
||||
'Changed towels in the bathroom' => 'Bytt handklær på badet',
|
||||
'Cleaned the kitchen floor' => 'Vasket kjøkkengulvet',
|
||||
'Warranty ends' => 'Garanti utgår',
|
||||
'TV remote control' => 'Fjernkontroll for TV',
|
||||
'Alarm clock' => 'Alarmklokke',
|
||||
'Heat remote control' => 'Fjernkontroll for termostat',
|
||||
'Lawn mowed in the garden' => 'Kuttet gresset i hagen',
|
||||
'Some good snacks' => 'Noen gode snacks',
|
||||
'Pizza dough' => 'Pizzadeig',
|
||||
'Sieved tomatoes' => 'Tomatpuré',
|
||||
'Salami' => 'Salami',
|
||||
'Toast' => 'Ristet brød',
|
||||
'Minced meat' => 'Kjøttdeig',
|
||||
'Pizza' => 'Pizza',
|
||||
'Spaghetti bolognese' => 'Spaghetti Bolognese',
|
||||
'Sandwiches' => 'Smørbrød',
|
||||
'English' => 'Engelsk',
|
||||
'German' => 'Tysk',
|
||||
'Italian' => 'Italiensk',
|
||||
'Demo in different language' => 'Demo i annet språk',
|
||||
'This is the note content of the recipe ingredient' => 'Dette er notisen for ingrediensen i oppskriften',
|
||||
'Demo User' => 'Demo Bruker',
|
||||
'Gram' => 'Gram',
|
||||
'Grams' => 'Gram',
|
||||
'Flour' => 'Mel',
|
||||
'Pancakes' => 'Pannekaker',
|
||||
'Sugar' => 'Sukker',
|
||||
'Home' => 'Hus',
|
||||
'Life' => 'Livstil',
|
||||
'Projects' => 'Projekter',
|
||||
'Repair the garage door' => 'Reparere garasjedøren',
|
||||
'Fork and improve grocy' => 'Fork og forbedre grocy',
|
||||
'Find a solution for what to do when I forget the door keys' => 'Finne på løsning for hva jeg skal gjøre når jeg mister dørnøklene',
|
||||
'Sweets' => 'Godteri',
|
||||
'Bakery products' => 'Produkt fra bakeren ',
|
||||
'Tinned food' => 'Boksemat',
|
||||
'Butchery products' => 'Produkt fra slakteren',
|
||||
'Vegetables/Fruits' => 'Frukt/ Grønnsaker',
|
||||
'Refrigerated products' => 'Kjølte produkter'
|
||||
);
|
72
middleware/ApiKeyAuthMiddleware.php
Normal file
72
middleware/ApiKeyAuthMiddleware.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Middleware;
|
||||
|
||||
use \Grocy\Services\SessionService;
|
||||
use \Grocy\Services\ApiKeyService;
|
||||
|
||||
class ApiKeyAuthMiddleware extends BaseMiddleware
|
||||
{
|
||||
public function __construct(\Slim\Container $container, string $sessionCookieName, string $apiKeyHeaderName)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->SessionCookieName = $sessionCookieName;
|
||||
$this->ApiKeyHeaderName = $apiKeyHeaderName;
|
||||
}
|
||||
|
||||
protected $SessionCookieName;
|
||||
protected $ApiKeyHeaderName;
|
||||
|
||||
public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, callable $next)
|
||||
{
|
||||
$route = $request->getAttribute('route');
|
||||
$routeName = $route->getName();
|
||||
|
||||
if (GROCY_IS_DEMO_INSTALL || GROCY_IS_EMBEDDED_INSTALL)
|
||||
{
|
||||
define('GROCY_AUTHENTICATED', true);
|
||||
$response = $next($request, $response);
|
||||
}
|
||||
else
|
||||
{
|
||||
$validSession = true;
|
||||
$validApiKey = true;
|
||||
|
||||
$sessionService = new SessionService();
|
||||
if (!isset($_COOKIE[$this->SessionCookieName]) || !$sessionService->IsValidSession($_COOKIE[$this->SessionCookieName]))
|
||||
{
|
||||
$validSession = false;
|
||||
}
|
||||
|
||||
$apiKeyService = new ApiKeyService();
|
||||
if (!$request->hasHeader($this->ApiKeyHeaderName) || !$apiKeyService->IsValidApiKey($request->getHeaderLine($this->ApiKeyHeaderName)))
|
||||
{
|
||||
$validApiKey = false;
|
||||
}
|
||||
|
||||
if (!$validSession && !$validApiKey)
|
||||
{
|
||||
define('GROCY_AUTHENTICATED', false);
|
||||
$response = $response->withStatus(401);
|
||||
}
|
||||
elseif ($validApiKey)
|
||||
{
|
||||
$user = $apiKeyService->GetUserByApiKey($request->getHeaderLine($this->ApiKeyHeaderName));
|
||||
define('GROCY_AUTHENTICATED', true);
|
||||
define('GROCY_USER_ID', $user->id);
|
||||
|
||||
$response = $next($request, $response);
|
||||
}
|
||||
elseif ($validSession)
|
||||
{
|
||||
$user = $sessionService->GetUserBySessionKey($_COOKIE[$this->SessionCookieName]);
|
||||
define('GROCY_AUTHENTICATED', true);
|
||||
define('GROCY_USER_ID', $user->id);
|
||||
|
||||
$response = $next($request, $response);
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
17
middleware/BaseMiddleware.php
Normal file
17
middleware/BaseMiddleware.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Middleware;
|
||||
|
||||
use \Grocy\Services\ApplicationService;
|
||||
|
||||
class BaseMiddleware
|
||||
{
|
||||
public function __construct(\Slim\Container $container)
|
||||
{
|
||||
$this->AppContainer = $container;
|
||||
$this->ApplicationService = new ApplicationService();
|
||||
}
|
||||
|
||||
protected $AppContainer;
|
||||
protected $ApplicationService;
|
||||
}
|
12
middleware/JsonMiddleware.php
Normal file
12
middleware/JsonMiddleware.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Middleware;
|
||||
|
||||
class JsonMiddleware extends BaseMiddleware
|
||||
{
|
||||
public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, callable $next)
|
||||
{
|
||||
$response = $next($request, $response);
|
||||
return $response->withHeader('Content-Type', 'application/json');
|
||||
}
|
||||
}
|
63
middleware/SessionAuthMiddleware.php
Normal file
63
middleware/SessionAuthMiddleware.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Middleware;
|
||||
|
||||
use \Grocy\Services\SessionService;
|
||||
use \Grocy\Services\LocalizationService;
|
||||
|
||||
class SessionAuthMiddleware extends BaseMiddleware
|
||||
{
|
||||
public function __construct(\Slim\Container $container, string $sessionCookieName)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->SessionCookieName = $sessionCookieName;
|
||||
}
|
||||
|
||||
protected $SessionCookieName;
|
||||
|
||||
public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, callable $next)
|
||||
{
|
||||
$route = $request->getAttribute('route');
|
||||
$routeName = $route->getName();
|
||||
$sessionService = new SessionService();
|
||||
|
||||
if ($routeName === 'root')
|
||||
{
|
||||
$response = $next($request, $response);
|
||||
}
|
||||
elseif (GROCY_IS_DEMO_INSTALL || GROCY_IS_EMBEDDED_INSTALL)
|
||||
{
|
||||
$user = $sessionService->GetDefaultUser();
|
||||
define('GROCY_AUTHENTICATED', true);
|
||||
define('GROCY_USER_USERNAME', $user->username);
|
||||
|
||||
$response = $next($request, $response);
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((!isset($_COOKIE[$this->SessionCookieName]) || !$sessionService->IsValidSession($_COOKIE[$this->SessionCookieName])) && $routeName !== 'login')
|
||||
{
|
||||
define('GROCY_AUTHENTICATED', false);
|
||||
$response = $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/login'));
|
||||
}
|
||||
else
|
||||
{
|
||||
if ($routeName !== 'login')
|
||||
{
|
||||
$user = $sessionService->GetUserBySessionKey($_COOKIE[$this->SessionCookieName]);
|
||||
define('GROCY_AUTHENTICATED', true);
|
||||
define('GROCY_USER_USERNAME', $user->username);
|
||||
define('GROCY_USER_ID', $user->id);
|
||||
}
|
||||
else
|
||||
{
|
||||
define('GROCY_AUTHENTICATED', false);
|
||||
}
|
||||
|
||||
$response = $next($request, $response);
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
13
migrations/0001.sql
Normal file
13
migrations/0001.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
CREATE TABLE products (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
location_id INTEGER NOT NULL,
|
||||
qu_id_purchase INTEGER NOT NULL,
|
||||
qu_id_stock INTEGER NOT NULL,
|
||||
qu_factor_purchase_to_stock REAL NOT NULL,
|
||||
barcode TEXT,
|
||||
min_stock_amount INTEGER NOT NULL DEFAULT 0,
|
||||
default_best_before_days INTEGER NOT NULL DEFAULT 0,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
)
|
6
migrations/0002.sql
Normal file
6
migrations/0002.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE locations (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
)
|
6
migrations/0003.sql
Normal file
6
migrations/0003.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE quantity_units (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
)
|
9
migrations/0004.sql
Normal file
9
migrations/0004.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
CREATE TABLE stock (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
product_id INTEGER NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
best_before_date DATE,
|
||||
purchased_date DATE DEFAULT (datetime('now', 'localtime')),
|
||||
stock_id TEXT NOT NULL,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
)
|
12
migrations/0005.sql
Normal file
12
migrations/0005.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
CREATE TABLE stock_log (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
product_id INTEGER NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
best_before_date DATE,
|
||||
purchased_date DATE,
|
||||
used_date DATE,
|
||||
spoiled INTEGER NOT NULL DEFAULT 0,
|
||||
stock_id TEXT NOT NULL,
|
||||
transaction_type TEXT NOT NULL,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
)
|
19
migrations/0006.sql
Normal file
19
migrations/0006.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
INSERT INTO locations
|
||||
(name, description)
|
||||
VALUES
|
||||
('DefaultLocation', 'This is the first default location, edit or delete it');
|
||||
|
||||
INSERT INTO quantity_units
|
||||
(name, description)
|
||||
VALUES
|
||||
('DefaultQuantityUnit', 'This is the first default quantity unit, edit or delete it');
|
||||
|
||||
INSERT INTO products
|
||||
(name, description, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock)
|
||||
VALUES
|
||||
('DefaultProduct1', 'This is the first default product, edit or delete it', 1, 1, 1, 1);
|
||||
|
||||
INSERT INTO products
|
||||
(name, description, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock)
|
||||
VALUES
|
||||
('DefaultProduct2', 'This is the second default product, edit or delete it', 1, 1, 1, 1);
|
9
migrations/0007.sql
Normal file
9
migrations/0007.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
CREATE VIEW stock_missing_products
|
||||
AS
|
||||
SELECT p.id, MAX(p.name) AS name, p.min_stock_amount - IFNULL(SUM(s.amount), 0) AS amount_missing
|
||||
FROM products p
|
||||
LEFT JOIN stock s
|
||||
ON p.id = s.product_id
|
||||
WHERE p.min_stock_amount != 0
|
||||
GROUP BY p.id
|
||||
HAVING IFNULL(SUM(s.amount), 0) < p.min_stock_amount
|
5
migrations/0008.sql
Normal file
5
migrations/0008.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
CREATE VIEW stock_current
|
||||
AS
|
||||
SELECT product_id, SUM(amount) AS amount, MIN(best_before_date) AS best_before_date
|
||||
FROM stock
|
||||
GROUP BY product_id
|
7
migrations/0009.sql
Normal file
7
migrations/0009.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE shopping_list (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
product_id INTEGER NOT NULL UNIQUE,
|
||||
amount INTEGER NOT NULL DEFAULT 0,
|
||||
amount_autoadded INTEGER NOT NULL DEFAULT 0,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
)
|
8
migrations/0010.sql
Normal file
8
migrations/0010.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
CREATE TABLE habits (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
period_type TEXT NOT NULL,
|
||||
period_days INTEGER,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
)
|
6
migrations/0011.sql
Normal file
6
migrations/0011.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE habits_log (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
habit_id INTEGER NOT NULL,
|
||||
tracked_time DATETIME,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
)
|
5
migrations/0012.sql
Normal file
5
migrations/0012.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
CREATE VIEW habits_current
|
||||
AS
|
||||
SELECT habit_id, MAX(tracked_time) AS last_tracked_time
|
||||
FROM habits_log
|
||||
GROUP BY habit_id
|
8
migrations/0013.sql
Normal file
8
migrations/0013.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
CREATE TABLE batteries (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
used_in TEXT,
|
||||
charge_interval_days INTEGER NOT NULL DEFAULT 0,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
)
|
6
migrations/0014.sql
Normal file
6
migrations/0014.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE battery_charge_cycles (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
battery_id TEXT NOT NULL,
|
||||
tracked_time DATETIME,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
)
|
5
migrations/0015.sql
Normal file
5
migrations/0015.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
CREATE VIEW batteries_current
|
||||
AS
|
||||
SELECT battery_id, MAX(tracked_time) AS last_tracked_time
|
||||
FROM battery_charge_cycles
|
||||
GROUP BY battery_id
|
1
migrations/0016.sql
Normal file
1
migrations/0016.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE shopping_list RENAME TO shopping_list_old
|
8
migrations/0017.sql
Normal file
8
migrations/0017.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
CREATE TABLE shopping_list (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
product_id INTEGER,
|
||||
note TEXT,
|
||||
amount INTEGER NOT NULL DEFAULT 0,
|
||||
amount_autoadded INTEGER NOT NULL DEFAULT 0,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
)
|
4
migrations/0018.sql
Normal file
4
migrations/0018.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
INSERT INTO shopping_list
|
||||
(product_id, amount, amount_autoadded, row_created_timestamp)
|
||||
SELECT product_id, amount, amount_autoadded, row_created_timestamp
|
||||
FROM shopping_list_old
|
1
migrations/0019.sql
Normal file
1
migrations/0019.sql
Normal file
@@ -0,0 +1 @@
|
||||
DROP TABLE shopping_list_old
|
6
migrations/0020.sql
Normal file
6
migrations/0020.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE sessions (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
session_key TEXT NOT NULL UNIQUE,
|
||||
expires DATETIME,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
)
|
11
migrations/0021.sql
Normal file
11
migrations/0021.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
DELETE FROM locations
|
||||
WHERE name = 'DefaultLocation';
|
||||
|
||||
DELETE FROM quantity_units
|
||||
WHERE name = 'DefaultQuantityUnit';
|
||||
|
||||
DELETE FROM products
|
||||
WHERE name = 'DefaultProduct1';
|
||||
|
||||
DELETE FROM products
|
||||
WHERE name = 'DefaultProduct2';
|
7
migrations/0022.sql
Normal file
7
migrations/0022.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE api_keys (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
api_key TEXT NOT NULL UNIQUE,
|
||||
expires DATETIME,
|
||||
last_used DATETIME,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
)
|
1
migrations/0023.sql
Normal file
1
migrations/0023.sql
Normal file
@@ -0,0 +1 @@
|
||||
DELETE FROM sessions
|
2
migrations/0024.sql
Normal file
2
migrations/0024.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE sessions
|
||||
ADD COLUMN last_used DATETIME
|
50
migrations/0025.sql
Normal file
50
migrations/0025.sql
Normal file
@@ -0,0 +1,50 @@
|
||||
CREATE TABLE recipes (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
|
||||
CREATE TABLE recipes_pos (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
recipe_id INTEGER NOT NULL,
|
||||
product_id INTEGER NOT NULL,
|
||||
amount INTEGER NOT NULL DEFAULT 0,
|
||||
note TEXT,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
|
||||
CREATE VIEW recipes_fulfillment
|
||||
AS
|
||||
SELECT
|
||||
r.id AS recipe_id,
|
||||
rp.id AS recipe_pos_id,
|
||||
rp.product_id AS product_id,
|
||||
rp.amount AS recipe_amount,
|
||||
IFNULL(sc.amount, 0) AS stock_amount,
|
||||
CASE WHEN IFNULL(sc.amount, 0) >= rp.amount THEN 1 ELSE 0 END AS need_fulfilled,
|
||||
CASE WHEN IFNULL(sc.amount, 0) - IFNULL(rp.amount, 0) < 0 THEN ABS(IFNULL(sc.amount, 0) - IFNULL(rp.amount, 0)) ELSE 0 END AS missing_amount,
|
||||
IFNULL(sl.amount, 0) AS amount_on_shopping_list,
|
||||
CASE WHEN IFNULL(sc.amount, 0) + IFNULL(sl.amount, 0) >= rp.amount THEN 1 ELSE 0 END AS need_fulfilled_with_shopping_list
|
||||
FROM recipes r
|
||||
JOIN recipes_pos rp
|
||||
ON r.id = rp.recipe_id
|
||||
LEFT JOIN (
|
||||
SELECT product_id, SUM(amount + amount_autoadded) AS amount
|
||||
FROM shopping_list
|
||||
GROUP BY product_id) sl
|
||||
ON rp.product_id = sl.product_id
|
||||
LEFT JOIN stock_current sc
|
||||
ON rp.product_id = sc.product_id;
|
||||
|
||||
CREATE VIEW recipes_fulfillment_sum
|
||||
AS
|
||||
SELECT
|
||||
r.id AS recipe_id,
|
||||
IFNULL(MIN(rf.need_fulfilled), 1) AS need_fulfilled,
|
||||
IFNULL(MIN(rf.need_fulfilled_with_shopping_list), 1) AS need_fulfilled_with_shopping_list,
|
||||
(SELECT COUNT(*) FROM recipes_fulfillment WHERE recipe_id = rf.recipe_id AND need_fulfilled = 0 AND recipe_pos_id IS NOT NULL) AS missing_products_count
|
||||
FROM recipes r
|
||||
LEFT JOIN recipes_fulfillment rf
|
||||
ON rf.recipe_id = r.id
|
||||
GROUP BY r.id;
|
20
migrations/0026.sql
Normal file
20
migrations/0026.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
CREATE TABLE users (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
first_name TEXT,
|
||||
last_name TEXT,
|
||||
password TEXT NOT NULL,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
|
||||
DROP TABLE sessions;
|
||||
|
||||
CREATE TABLE sessions (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
session_key TEXT NOT NULL UNIQUE,
|
||||
user_id INTEGER NOT NULL,
|
||||
expires DATETIME,
|
||||
last_used DATETIME,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
)
|
||||
|
24
migrations/0027.php
Normal file
24
migrations/0027.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
// This is executed inside DatabaseMigrationService class/context
|
||||
|
||||
$db = $this->DatabaseService->GetDbConnection();
|
||||
|
||||
if (defined('GROCY_HTTP_USER'))
|
||||
{
|
||||
// Migrate old user defined in config file to database
|
||||
$newUserRow = $db->users()->createRow(array(
|
||||
'username' => GROCY_HTTP_USER,
|
||||
'password' => password_hash(GROCY_HTTP_PASSWORD, PASSWORD_DEFAULT)
|
||||
));
|
||||
$newUserRow->save();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create default user "admin" with password "admin"
|
||||
$newUserRow = $db->users()->createRow(array(
|
||||
'username' => 'admin',
|
||||
'password' => password_hash('admin', PASSWORD_DEFAULT)
|
||||
));
|
||||
$newUserRow->save();
|
||||
}
|
13
migrations/0028.sql
Normal file
13
migrations/0028.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
ALTER TABLE habits_log
|
||||
ADD done_by_user_id INTEGER;
|
||||
|
||||
DROP TABLE api_keys;
|
||||
|
||||
CREATE TABLE api_keys (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
api_key TEXT NOT NULL UNIQUE,
|
||||
user_id INTEGER NOT NULL,
|
||||
expires DATETIME,
|
||||
last_used DATETIME,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
5
migrations/0029.sql
Normal file
5
migrations/0029.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE stock
|
||||
ADD price DECIMAL(15, 2);
|
||||
|
||||
ALTER TABLE stock_log
|
||||
ADD price DECIMAL(15, 2);
|
2
migrations/0030.sql
Normal file
2
migrations/0030.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE quantity_units
|
||||
ADD name_plural TEXT;
|
32
migrations/0031.php
Normal file
32
migrations/0031.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
// This is executed inside DatabaseMigrationService class/context
|
||||
|
||||
use \Grocy\Services\LocalizationService;
|
||||
$localizationService = new LocalizationService(GROCY_CULTURE);
|
||||
|
||||
$db = $this->DatabaseService->GetDbConnection();
|
||||
|
||||
if ($db->quantity_units()->count() === 0)
|
||||
{
|
||||
// Create 2 default quantity units
|
||||
$newRow = $db->quantity_units()->createRow(array(
|
||||
'name' => $localizationService->Localize('Piece'),
|
||||
'name_plural' => $localizationService->Localize('Pieces')
|
||||
));
|
||||
$newRow->save();
|
||||
$newRow = $db->quantity_units()->createRow(array(
|
||||
'name' => $localizationService->Localize('Pack'),
|
||||
'name_plural' => $localizationService->Localize('Packs')
|
||||
));
|
||||
$newRow->save();
|
||||
}
|
||||
|
||||
if ($db->locations()->count() === 0)
|
||||
{
|
||||
// Create a default location
|
||||
$newRow = $db->locations()->createRow(array(
|
||||
'name' => $localizationService->Localize('Fridge')
|
||||
));
|
||||
$newRow->save();
|
||||
}
|
20
migrations/0032.sql
Normal file
20
migrations/0032.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
DROP VIEW stock_current;
|
||||
CREATE VIEW stock_current
|
||||
AS
|
||||
SELECT product_id, SUM(amount) AS amount, MIN(best_before_date) AS best_before_date
|
||||
FROM stock
|
||||
GROUP BY product_id;
|
||||
|
||||
DROP VIEW habits_current;
|
||||
CREATE VIEW habits_current
|
||||
AS
|
||||
SELECT habit_id, MAX(tracked_time) AS last_tracked_time
|
||||
FROM habits_log
|
||||
GROUP BY habit_id;
|
||||
|
||||
DROP VIEW batteries_current;
|
||||
CREATE VIEW batteries_current
|
||||
AS
|
||||
SELECT battery_id, MAX(tracked_time) AS last_tracked_time
|
||||
FROM battery_charge_cycles
|
||||
GROUP BY battery_id;
|
29
migrations/0033.sql
Normal file
29
migrations/0033.sql
Normal file
@@ -0,0 +1,29 @@
|
||||
DROP VIEW habits_current;
|
||||
CREATE VIEW habits_current
|
||||
AS
|
||||
SELECT
|
||||
h.id AS habit_id,
|
||||
MAX(l.tracked_time) AS last_tracked_time,
|
||||
CASE h.period_type
|
||||
WHEN 'manually' THEN '2999-12-31 23:59:59'
|
||||
WHEN 'dynamic-regular' THEN datetime(MAX(l.tracked_time), '+' || CAST(h.period_days AS TEXT) || ' day')
|
||||
END AS next_estimated_execution_time
|
||||
FROM habits h
|
||||
LEFT JOIN habits_log l
|
||||
ON h.id = l.habit_id
|
||||
GROUP BY h.id, h.period_days;
|
||||
|
||||
DROP VIEW batteries_current;
|
||||
CREATE VIEW batteries_current
|
||||
AS
|
||||
SELECT
|
||||
b.id AS battery_id,
|
||||
MAX(l.tracked_time) AS last_tracked_time,
|
||||
CASE WHEN b.charge_interval_days = 0
|
||||
THEN '2999-12-31 23:59:59'
|
||||
ELSE datetime(MAX(l.tracked_time), '+' || CAST(b.charge_interval_days AS TEXT) || ' day')
|
||||
END AS next_estimated_charge_time
|
||||
FROM batteries b
|
||||
LEFT JOIN battery_charge_cycles l
|
||||
ON b.id = l.battery_id
|
||||
GROUP BY b.id, b.charge_interval_days;
|
41
migrations/0034.sql
Normal file
41
migrations/0034.sql
Normal file
@@ -0,0 +1,41 @@
|
||||
ALTER TABLE recipes_pos
|
||||
ADD qu_id INTEGER;
|
||||
|
||||
UPDATE recipes_pos
|
||||
SET qu_id = (SELECT qu_id_stock FROM products where id = product_id);
|
||||
|
||||
CREATE TRIGGER recipes_pos_qu_id_default AFTER INSERT ON recipes_pos
|
||||
BEGIN
|
||||
UPDATE recipes_pos
|
||||
SET qu_id = (SELECT qu_id_stock FROM products where id = product_id)
|
||||
WHERE qu_id IS NULL
|
||||
AND id = NEW.id;
|
||||
END;
|
||||
|
||||
ALTER TABLE recipes_pos
|
||||
ADD only_check_single_unit_in_stock TINYINT NOT NULL DEFAULT 0;
|
||||
|
||||
DROP VIEW recipes_fulfillment;
|
||||
CREATE VIEW recipes_fulfillment
|
||||
AS
|
||||
SELECT
|
||||
r.id AS recipe_id,
|
||||
rp.id AS recipe_pos_id,
|
||||
rp.product_id AS product_id,
|
||||
rp.amount AS recipe_amount,
|
||||
IFNULL(sc.amount, 0) AS stock_amount,
|
||||
CASE WHEN IFNULL(sc.amount, 0) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE IFNULL(rp.amount, 0) END THEN 1 ELSE 0 END AS need_fulfilled,
|
||||
CASE WHEN IFNULL(sc.amount, 0) - CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE IFNULL(rp.amount, 0) END < 0 THEN ABS(IFNULL(sc.amount, 0) - CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE IFNULL(rp.amount, 0) END) ELSE 0 END AS missing_amount,
|
||||
IFNULL(sl.amount, 0) AS amount_on_shopping_list,
|
||||
CASE WHEN IFNULL(sc.amount, 0) + IFNULL(sl.amount, 0) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE IFNULL(rp.amount, 0) END THEN 1 ELSE 0 END AS need_fulfilled_with_shopping_list,
|
||||
rp.qu_id
|
||||
FROM recipes r
|
||||
JOIN recipes_pos rp
|
||||
ON r.id = rp.recipe_id
|
||||
LEFT JOIN (
|
||||
SELECT product_id, SUM(amount + amount_autoadded) AS amount
|
||||
FROM shopping_list
|
||||
GROUP BY product_id) sl
|
||||
ON rp.product_id = sl.product_id
|
||||
LEFT JOIN stock_current sc
|
||||
ON rp.product_id = sc.product_id;
|
31
migrations/0035.sql
Normal file
31
migrations/0035.sql
Normal file
@@ -0,0 +1,31 @@
|
||||
ALTER TABLE habits RENAME TO chores;
|
||||
|
||||
CREATE TABLE chores_log (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
chore_id INTEGER NOT NULL,
|
||||
tracked_time DATETIME,
|
||||
done_by_user_id INTEGER,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
|
||||
INSERT INTO chores_log
|
||||
(chore_id, tracked_time, done_by_user_id, row_created_timestamp)
|
||||
SELECT habit_id, tracked_time, done_by_user_id, row_created_timestamp
|
||||
FROM habits_log;
|
||||
|
||||
DROP TABLE habits_log;
|
||||
|
||||
DROP VIEW habits_current;
|
||||
CREATE VIEW chores_current
|
||||
AS
|
||||
SELECT
|
||||
h.id AS chore_id,
|
||||
MAX(l.tracked_time) AS last_tracked_time,
|
||||
CASE h.period_type
|
||||
WHEN 'manually' THEN '2999-12-31 23:59:59'
|
||||
WHEN 'dynamic-regular' THEN datetime(MAX(l.tracked_time), '+' || CAST(h.period_days AS TEXT) || ' day')
|
||||
END AS next_estimated_execution_time
|
||||
FROM chores h
|
||||
LEFT JOIN chores_log l
|
||||
ON h.id = l.chore_id
|
||||
GROUP BY h.id, h.period_days;
|
24
migrations/0036.sql
Normal file
24
migrations/0036.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
CREATE TABLE tasks (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
due_date DATETIME,
|
||||
done TINYINT NOT NULL DEFAULT 0 CHECK(done IN (0, 1)),
|
||||
done_timestamp DATETIME,
|
||||
category_id INTEGER,
|
||||
assigned_to_user_id INTEGER,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
|
||||
CREATE TABLE task_categories (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
|
||||
CREATE VIEW tasks_current
|
||||
AS
|
||||
SELECT *
|
||||
FROM tasks
|
||||
WHERE done = 0;
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user