52 Commits

Author SHA1 Message Date
560fb356fa Move to today in cuentas.show 2022-04-02 23:32:27 -04:00
10e5383a3e FIX: modalToAdd in cuentas.show 2022-04-02 23:58:57 -03:00
bc5338b1f1 FIX: consolidado not changed with tipocambio 2022-04-02 23:40:53 -03:00
4db8161c70 FIX: withJson requires 2nd parameter 2022-03-26 20:55:57 -03:00
d94f076946 Script structure and searchable cuentas 2022-03-25 20:54:02 -03:00
8009433958 FIX: update buttons 2022-03-25 17:52:21 -03:00
d7b406ef25 FIX: double transform value 2022-03-25 17:45:58 -03:00
cddf768b1e FIX: show cuentas missing moneda 2022-03-25 17:39:12 -03:00
8b86560973 Error catching in UI 2022-03-25 16:36:58 -03:00
4ea5c6de3e Env files 2022-03-25 16:21:38 -03:00
91cc28e681 Merge branch 'feature/commands' into develop 2022-03-25 16:18:38 -03:00
ef01ab3c55 Migrations 2022-03-25 16:18:09 -03:00
e41ffd505b Migrations fixed 2022-03-25 16:16:31 -03:00
2986806137 Merge branch 'feature/commands' into develop 2022-03-25 15:04:53 -03:00
f9076d3bac Error catching in API 2022-03-25 15:04:42 -03:00
ee3133da72 Consolidar command 2022-03-25 15:04:10 -03:00
dbad283e14 Command queue 2022-03-25 15:03:49 -03:00
e3737aba27 Consolidar 2022-03-25 10:11:02 -03:00
fcc84ac09c Console application 2022-03-25 10:10:43 -03:00
2b3b475d91 Show one month at a time, and change month with calendar and buttons 2022-03-18 17:50:21 -03:00
e5cf3cfa07 Update blade view 2022-03-18 17:43:26 -03:00
84a3f8e2e3 Order totals for tipocuenta 2022-01-07 01:12:54 -03:00
5a9dc6602c Bold results 2022-01-07 00:24:08 -03:00
d7dfc2d221 FIX:Unavailable value for tipocambio crashed the loading of values 2022-01-07 00:23:52 -03:00
c1eeba04a2 Resultado y fix cuentas 2022-01-06 15:20:21 -03:00
3cb2a877de FIX: transacciones, tipocambio and headers 2022-01-06 15:19:36 -03:00
b17225549c Python get cambio 2022-01-05 21:10:51 -03:00
af78106700 Cleaner code 2022-01-05 16:01:27 -03:00
665f426011 Sort cuentas in home 2022-01-05 15:58:03 -03:00
56b371d20c Refresh and edit transacciones 2022-01-05 15:56:03 -03:00
71b4211fc3 Missing categoria from cuentas 2021-12-23 01:09:35 -03:00
0378a2cf09 FIX: Double tables 2021-12-23 01:09:15 -03:00
4abe3448c0 Upgrades to the UI 2021-12-23 00:46:56 -03:00
9e29dd09b7 Remove ignored files 2021-12-23 00:09:23 -03:00
52443c2226 FIX: Clear form and table 2021-12-23 00:09:09 -03:00
572a9dc87c FIX: Renaming with correct extension 2021-12-23 00:08:49 -03:00
66882d9f85 FIX: Upload valid file check 2021-12-23 00:08:28 -03:00
1a0c83fb2b FIX: check if upload subfolder exists when loading files 2021-12-22 23:20:23 -03:00
a5428b252e FIX: Ignoring upload resources from ui 2021-12-22 23:11:51 -03:00
93f77bfbb8 Upload files 2021-12-22 01:38:34 -03:00
fddba2fb87 Manage uploaded files 2021-12-22 01:38:23 -03:00
e6ebb2c279 FIX: url importar 2021-12-20 23:29:40 -03:00
45952bb3ac FIX: Select cuentas 2021-12-20 23:14:23 -03:00
894cc26b21 Upload files 2021-12-20 22:51:15 -03:00
64ffb53f0c Composer for UI 2021-12-20 22:45:00 -03:00
42310ef0e4 Formatting 2021-12-20 22:44:47 -03:00
a6362a6770 FIX: Empty results threw errors 2021-12-20 22:44:29 -03:00
e9c63abc3a PHP info 2021-12-20 22:43:43 -03:00
9f47c8a85f Default value for seed 2021-12-20 22:43:32 -03:00
34eedb93d7 FIX: .env not loaded in ui 2021-12-20 21:35:47 -03:00
0e5714edc8 FIX: cuentas 2021-12-07 09:13:20 -03:00
f33bddfbea Added Docker profiles 2021-12-06 22:22:54 -03:00
104 changed files with 2644 additions and 751 deletions

1
.console.env.sample Normal file
View File

@ -0,0 +1 @@
API_URL=http://api-proxy

5
.db.env.sample Normal file
View File

@ -0,0 +1,5 @@
MYSQL_HOST=
MYSQL_ROOT_PASSWORD=
MYSQL_DATABASE=
MYSQL_USER=
MYSQL_PASSWORD=

View File

@ -1,5 +1 @@
MYSQL_HOST=
MYSQL_ROOT_PASSWORD=
MYSQL_DATABASE=
MYSQL_USER=
MYSQL_PASSWORD=
COMPOSE_PROFILES=

2
.gitignore vendored
View File

@ -13,5 +13,3 @@
# Python
**/.idea/
**/uploads/

8
.idea/.gitignore generated vendored
View File

@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

View File

@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

81
.idea/contabilidad.iml generated
View File

@ -1,81 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/kint-php/kint" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/zeuxisoo/slim-whoops" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/phar-io/version" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/phar-io/manifest" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/theseer/tokenizer" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/webmozart/assert" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/philo/laravel-blade" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/nesbot/carbon" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/type" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/diff" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/illuminate/support" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/lines-of-code" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/illuminate/filesystem" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/object-enumerator" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/illuminate/events" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/comparator" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/illuminate/contracts" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/psr/simple-cache" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/code-unit" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/illuminate/view" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/psr/http-server-handler" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/doctrine/instantiator" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/exporter" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/illuminate/container" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/psr/http-factory" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/doctrine/inflector" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/cli-parser" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/psr/container" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/version" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/psr/http-message" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/complexity" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/psr/http-server-middleware" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/recursion-context" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/psr/log" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/resource-operations" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/global-state" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/composer" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/environment" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/phpdocumentor/reflection-docblock" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/code-unit-reverse-lookup" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/phpdocumentor/type-resolver" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/symfony/polyfill-mbstring" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/sebastian/object-reflector" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/phpdocumentor/reflection-common" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/symfony/finder" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/symfony/deprecation-contracts" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/symfony/translation-contracts" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/filp/whoops" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/symfony/polyfill-ctype" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/symfony/debug" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/nyholm/psr7-server" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/symfony/translation" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/nyholm/psr7" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/symfony/polyfill-php80" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/nikic/php-parser" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/nikic/fast-route" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/php-di/php-di" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/php-di/phpdoc-reader" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/php-di/slim-bridge" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/php-di/invoker" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/phpunit/php-code-coverage" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/phpunit/phpunit" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/phpunit/php-text-template" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/phpunit/php-invoker" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/phpunit/php-file-iterator" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/phpunit/php-timer" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/myclabs/deep-copy" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/phpspec/prophecy" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/opis/closure" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/php-http/message-factory" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/rubellum/slim-blade-view" />
<excludeFolder url="file://$MODULE_DIR$/ui/vendor/slim/slim" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated
View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/contabilidad.iml" filepath="$PROJECT_DIR$/.idea/contabilidad.iml" />
</modules>
</component>
</project>

87
.idea/php.xml generated
View File

@ -1,87 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PhpIncludePathManager">
<include_path>
<path value="$PROJECT_DIR$/ui/vendor/kint-php/kint" />
<path value="$PROJECT_DIR$/ui/vendor/zeuxisoo/slim-whoops" />
<path value="$PROJECT_DIR$/ui/vendor/phar-io/version" />
<path value="$PROJECT_DIR$/ui/vendor/phar-io/manifest" />
<path value="$PROJECT_DIR$/ui/vendor/theseer/tokenizer" />
<path value="$PROJECT_DIR$/ui/vendor/webmozart/assert" />
<path value="$PROJECT_DIR$/ui/vendor/philo/laravel-blade" />
<path value="$PROJECT_DIR$/ui/vendor/nesbot/carbon" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/type" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/diff" />
<path value="$PROJECT_DIR$/ui/vendor/illuminate/support" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/lines-of-code" />
<path value="$PROJECT_DIR$/ui/vendor/illuminate/filesystem" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/object-enumerator" />
<path value="$PROJECT_DIR$/ui/vendor/illuminate/events" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/comparator" />
<path value="$PROJECT_DIR$/ui/vendor/illuminate/contracts" />
<path value="$PROJECT_DIR$/ui/vendor/psr/simple-cache" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/code-unit" />
<path value="$PROJECT_DIR$/ui/vendor/illuminate/view" />
<path value="$PROJECT_DIR$/ui/vendor/psr/http-server-handler" />
<path value="$PROJECT_DIR$/ui/vendor/doctrine/instantiator" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/exporter" />
<path value="$PROJECT_DIR$/ui/vendor/illuminate/container" />
<path value="$PROJECT_DIR$/ui/vendor/psr/http-factory" />
<path value="$PROJECT_DIR$/ui/vendor/doctrine/inflector" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/cli-parser" />
<path value="$PROJECT_DIR$/ui/vendor/psr/container" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/version" />
<path value="$PROJECT_DIR$/ui/vendor/psr/http-message" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/complexity" />
<path value="$PROJECT_DIR$/ui/vendor/psr/http-server-middleware" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/recursion-context" />
<path value="$PROJECT_DIR$/ui/vendor/psr/log" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/resource-operations" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/global-state" />
<path value="$PROJECT_DIR$/ui/vendor/composer" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/environment" />
<path value="$PROJECT_DIR$/ui/vendor/phpdocumentor/reflection-docblock" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/code-unit-reverse-lookup" />
<path value="$PROJECT_DIR$/ui/vendor/phpdocumentor/type-resolver" />
<path value="$PROJECT_DIR$/ui/vendor/symfony/polyfill-mbstring" />
<path value="$PROJECT_DIR$/ui/vendor/sebastian/object-reflector" />
<path value="$PROJECT_DIR$/ui/vendor/phpdocumentor/reflection-common" />
<path value="$PROJECT_DIR$/ui/vendor/symfony/finder" />
<path value="$PROJECT_DIR$/ui/vendor/symfony/deprecation-contracts" />
<path value="$PROJECT_DIR$/ui/vendor/symfony/translation-contracts" />
<path value="$PROJECT_DIR$/ui/vendor/filp/whoops" />
<path value="$PROJECT_DIR$/ui/vendor/symfony/polyfill-ctype" />
<path value="$PROJECT_DIR$/ui/vendor/symfony/debug" />
<path value="$PROJECT_DIR$/ui/vendor/nyholm/psr7-server" />
<path value="$PROJECT_DIR$/ui/vendor/symfony/translation" />
<path value="$PROJECT_DIR$/ui/vendor/nyholm/psr7" />
<path value="$PROJECT_DIR$/ui/vendor/symfony/polyfill-php80" />
<path value="$PROJECT_DIR$/ui/vendor/nikic/php-parser" />
<path value="$PROJECT_DIR$/ui/vendor/nikic/fast-route" />
<path value="$PROJECT_DIR$/ui/vendor/php-di/php-di" />
<path value="$PROJECT_DIR$/ui/vendor/php-di/phpdoc-reader" />
<path value="$PROJECT_DIR$/ui/vendor/php-di/slim-bridge" />
<path value="$PROJECT_DIR$/ui/vendor/php-di/invoker" />
<path value="$PROJECT_DIR$/ui/vendor/phpunit/php-code-coverage" />
<path value="$PROJECT_DIR$/ui/vendor/phpunit/phpunit" />
<path value="$PROJECT_DIR$/ui/vendor/phpunit/php-text-template" />
<path value="$PROJECT_DIR$/ui/vendor/phpunit/php-invoker" />
<path value="$PROJECT_DIR$/ui/vendor/phpunit/php-file-iterator" />
<path value="$PROJECT_DIR$/ui/vendor/phpunit/php-timer" />
<path value="$PROJECT_DIR$/ui/vendor/myclabs/deep-copy" />
<path value="$PROJECT_DIR$/ui/vendor/phpspec/prophecy" />
<path value="$PROJECT_DIR$/ui/vendor/opis/closure" />
<path value="$PROJECT_DIR$/ui/vendor/php-http/message-factory" />
<path value="$PROJECT_DIR$/ui/vendor/rubellum/slim-blade-view" />
<path value="$PROJECT_DIR$/ui/vendor/slim/slim" />
</include_path>
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="8.0">
<option name="suggestChangeDefaultLanguageLevel" value="false" />
</component>
<component name="PhpUnit">
<phpunit_settings>
<PhpUnitSettings custom_loader_path="$PROJECT_DIR$/ui/vendor/autoload.php" />
</phpunit_settings>
</component>
</project>

1
api/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
**/uploads/

View File

@ -24,4 +24,11 @@ class Base {
$key = urlencode(base64_encode($signature));
return $this->withJson($response, ['key' => $key]);
}
public function info(Request $request, Response $response): Response {
ob_start();
phpinfo();
$data = ob_get_clean();
$response->getBody()->write($data);
return $response;
}
}

View File

@ -13,33 +13,35 @@ class Categorias {
public function __invoke(Request $request, Response $response, Factory $factory, Service $service): Response {
$categorias = $factory->find(Categoria::class)->many();
array_walk($categorias, function(&$item) use ($service) {
$arr = $item->toArray();
$arr['cuentas'] = array_map(function($item) {
return $item->toArray();
}, $item->cuentas());
$maps = ['activo', 'pasivo', 'ganancia', 'perdida'];
foreach ($maps as $m) {
$p = $m . 's';
$t = ucfirst($m);
$cuentas = $item->getCuentasOf($t);
if ($cuentas === false or $cuentas === null) {
$arr[$p] = 0;
continue;
if ($categorias !== null) {
array_walk($categorias, function(&$item) use ($service) {
$arr = $item->toArray();
if ($item->cuentas()) {
$arr['cuentas'] = array_map(function($item) {
return $item->toArray();
}, $item->cuentas());
}
$arr[$p] = array_reduce($cuentas, function($sum, $item) use($service) {
return $sum + $item->saldo($service, true);
});
}
$item = $arr;
});
if ($categorias) {
usort($categorias, function($a, $b) {
return strcmp($a['nombre'], $b['nombre']);
});
$maps = ['activo', 'pasivo', 'ganancia', 'perdida'];
foreach ($maps as $m) {
$p = $m . 's';
$t = ucfirst($m);
$cuentas = $item->getCuentasOf($t);
if ($cuentas === false or $cuentas === null) {
$arr[$p] = 0;
continue;
}
$arr[$p] = array_reduce($cuentas, function($sum, $item) use($service) {
return $sum + $item->saldo($service, true);
});
}
$item = $arr;
});
usort($categorias, function($a, $b) {
return strcmp($a['nombre'], $b['nombre']);
});
}
$output = [
'categorias' => $categorias
'categorias' => $categorias
];
return $this->withJson($response, $output);
}

View File

@ -0,0 +1,53 @@
<?php
namespace Contabilidad\Common\Controller;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use ProVM\Common\Factory\Model as Factory;
use ProVM\Common\Define\Controller\Json;
use Contabilidad\Common\Service\Consolidar as Service;
use Contabilidad\Consolidado;
class Consolidados {
use Json;
public function __invoke(Request $request, Response $response, Factory $factory, $cuenta_id): Response {
$consolidados = $factory->find(Consolidados::class)->where([['cuenta_id' => $cuenta_id]])->many();
$output = [
'consolidados' => array_map(function($item) {
return $item->toArray();
}, $consolidados)
];
return $this->withJson($response, $output);
}
public function add(Request $request, Response $response, Factory $factory): Response {
$input = json_decode($request->getBody()->getContents());
$output = [
'input' => $input,
'consolidados' => []
];
if (!is_array($input)) {
$input = [$input];
}
foreach ($input as $data) {
$consolidado = Consolidado::add($factory, $data);
$status = $consolidado->save();
$output['consolidados'] []= [
'consolidado' => $consolidado->toArray(),
'added' => $status
];
}
return $this->withJson($response, $output);
}
public function cli(Request $request, Response $response, Service $service): Response {
try {
if (!$service->isConsolidado()) {
$service->consolidar();
}
} catch (\Error | \Exception $e) {
error_log($e);
throw $e;
}
return $this->withJson($response, []);
}
}

View File

@ -1,8 +1,10 @@
<?php
namespace Contabilidad\Common\Controller;
use Contabilidad\Transaccion;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Carbon\Carbon;
use ProVM\Common\Define\Controller\Json;
use ProVM\Common\Factory\Model as Factory;
use Contabilidad\Common\Service\TiposCambios as Service;
@ -12,8 +14,13 @@ class Cuentas {
use Json;
public function __invoke(Request $request, Response $response, Factory $factory): Response {
$cuentas = $factory->find(Cuenta::class)->array();
$cuentas = $factory->find(Cuenta::class)->many();
if ($cuentas) {
array_walk($cuentas, function (&$item) {
$arr = $item->toArray();
$arr['categoria'] = $item->categoria()->toArray();
$item = $arr;
});
usort($cuentas, function($a, $b) {
$t = strcmp($a['tipo']['descripcion'], $b['tipo']['descripcion']);
if ($t != 0) {
@ -77,6 +84,15 @@ class Cuentas {
];
return $this->withJson($response, $output);
}
public function categoria(Request $request, Response $response, Factory $factory, $cuenta_id): Response {
$cuenta = $factory->find(Cuenta::class)->one($cuenta_id);
$output = [
'input' => $cuenta_id,
'cuenta' => $cuenta?->toArray(),
'categoria' => $cuenta?->categoria()->toArray()
];
return $this->withJson($response, $output);
}
public function entradas(Request $request, Response $response, Factory $factory, $cuenta_id): Response {
$cuenta = $factory->find(Cuenta::class)->one($cuenta_id);
$entradas = null;
@ -95,6 +111,24 @@ class Cuentas {
];
return $this->withJson($response, $output);
}
protected function transaccionToArray(Service $service, Cuenta $cuenta, Transaccion $transaccion): array {
$arr = $transaccion->toArray();
if ($cuenta->moneda()->codigo === 'CLP') {
if ($transaccion->debito()->moneda()->codigo !== 'CLP' or $transaccion->credito()->moneda()->codigo !== 'CLP') {
if ($transaccion->debito()->moneda()->codigo !== 'CLP') {
$c = $transaccion->debito();
} else {
$c = $transaccion->credito();
}
$service->get($transaccion->fecha(), $c->moneda()->id);
$arr['valor'] = $transaccion->valor;
$arr['valorFormateado'] = $cuenta->moneda()->format($arr['valor']);
}
}
$arr['debito']['categoria'] = $transaccion->debito()->categoria()->toArray();
$arr['credito']['categoria'] = $transaccion->credito()->categoria()->toArray();
return $arr;
}
public function transacciones(Request $request, Response $response, Factory $factory, Service $service, $cuenta_id, $limit = null, $start = 0): Response {
$cuenta = $factory->find(Cuenta::class)->one($cuenta_id);
$transacciones = null;
@ -102,20 +136,7 @@ class Cuentas {
$transacciones = $cuenta->transacciones($limit, $start);
if (count($transacciones) > 0) {
foreach ($transacciones as &$transaccion) {
$arr = $transaccion->toArray();
if ($cuenta->moneda()->codigo === 'CLP') {
if ($transaccion->debito()->moneda()->codigo !== 'CLP' or $transaccion->credito()->moneda()->codigo !== 'CLP') {
if ($transaccion->debito()->moneda()->codigo !== 'CLP') {
$c = $transaccion->debito();
} else {
$c = $transaccion->credito();
}
$service->get($transaccion->fecha(), $c->moneda()->id);
$arr['valor'] = $c->moneda()->cambiar($transaccion->fecha(), $transaccion->valor);
$arr['valorFormateado'] = $cuenta->moneda()->format($arr['valor']);
}
}
$transaccion = $arr;
$transaccion = $this->transaccionToArray($service, $cuenta, $transaccion);
}
}
}
@ -126,6 +147,40 @@ class Cuentas {
];
return $this->withJson($response, $output);
}
public function transaccionesMonth(Request $request, Response $response, Factory $factory, Service $service, $cuenta_id, $month): Response {
$cuenta = $factory->find(Cuenta::class)->one($cuenta_id);
$month = Carbon::parse($month);
$transacciones = null;
if ($cuenta !== null) {
$transacciones = $cuenta->transaccionesMonth($month);
if (count($transacciones) > 0) {
foreach ($transacciones as &$transaccion) {
$transaccion = $this->transaccionToArray($service, $cuenta, $transaccion);
}
}
}
$output = [
'input' => compact('cuenta_id', 'month'),
'cuenta' => $cuenta?->toArray(),
'transacciones' => $transacciones
];
return $this->withJson($response, $output);
}
public function transaccionesAcumulation(Request $request, Response $response, Factory $factory, $cuenta_id, $date): Response {
$cuenta = $factory->find(Cuenta::class)->one($cuenta_id);
$f = Carbon::parse($date);
$acum = 0;
if ($cuenta !== null) {
$acum = $cuenta->acumulacion($f);
}
$output = [
'input' => compact('cuenta_id', 'date'),
'cuenta' => $cuenta?->toArray(),
'format' => $cuenta->moneda()->toArray(),
'acumulation' => $acum
];
return $this->withJson($response, $output);
}
public function transaccionesAmount(Request $request, Response $response, Factory $factory, $cuenta_id): Response {
$cuenta = $factory->find(Cuenta::class)->one($cuenta_id);
$transacciones = 0;

View File

@ -0,0 +1,76 @@
<?php
namespace Contabilidad\Common\Controller;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use ProVM\Common\Define\Controller\Json;
use Contabilidad\Common\Service\FileHandler as Handler;
use ProVM\Common\Factory\Model as Factory;
use Contabilidad\Cuenta;
class Files {
use Json;
public function __invoke(Request $request, Response $response, Handler $handler): Response {
$files = $handler->listFiles();
usort($files, function($a, $b) {
$f = strcmp($a->folder, $b->folder);
if ($f == 0) {
return strcmp($a->filename, $b->filename);
}
return $f;
});
return $this->withJson($response, compact('files'));
}
public function upload(Request $request, Response $response, Handler $handler, Factory $factory): Response {
$post = $request->getParsedBody();
$cuenta = $factory->find(Cuenta::class)->one($post['cuenta']);
$file = $request->getUploadedFiles()['archivo'];
$new_name = implode(' - ', [$cuenta->nombre, $cuenta->categoria()->nombre, $post['fecha']]);
$output = [
'input' => [
'name' => $file->getClientFilename(),
'type' => $file->getClientMediaType(),
'size' => $file->getSize(),
'error' => $file->getError()
],
'new_name' => $new_name,
'uploaded' => $handler->uploadFile($file, $new_name)
];
return $this->withJson($response, $output);
}
public function get(Request $request, Response $response, Handler $handler, $folder, $filename): Response {
$file = $handler->getFile($folder, $filename);
return $response
->withHeader('Content-Type', $handler->getType($folder))
->withHeader('Content-Disposition', 'attachment; filename=' . $filename)
->withAddedHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->withHeader('Cache-Control', 'post-check=0, pre-check=0')
->withHeader('Pragma', 'no-cache')
->withBody($file);
}
public function edit(Request $request, Response $response, Handler $handler, Factory $factory, $folder, $filename): Response {
$post = json_decode($request->getBody());
$cuenta = $factory->find(Cuenta::class)->one($post->cuenta);
$new_name = implode(' - ', [$cuenta->nombre, $cuenta->categoria()->nombre, $post->fecha]);
$output = [
'input' => [
'folder' => $folder,
'filename' => $filename,
'post' => $post
],
'edited' => $handler->editFilename($folder, $filename, $new_name)
];
return $this->withJson($response, $output);
}
public function delete(Request $request, Response $response, Handler $handler, $folder, $filename): Response {
$output = [
'input' => [
'folder' => $folder,
'filename' => $filename
],
'deleted' => $handler->deleteFile($folder, $filename)
];
return $this->withJson($response, $output);
}
}

View File

@ -3,19 +3,47 @@ namespace Contabilidad\Common\Controller;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Container\ContainerInterface as Container;
use ProVM\Common\Define\Controller\Json;
use ProVM\Common\Factory\Model as Factory;
use Contabilidad\Common\Service\DocumentHandler as Handler;
use Contabilidad\Cuenta;
class Import {
use Json;
public function __invoke(Request $request, Response $response, Factory $factory): Response {
$post = $request->getParsedBody();
return $this->withJson($response, $post);
public function __invoke(Request $request, Response $response, Factory $factory, Container $container): Response {
$post =$request->getParsedBody();
$cuenta = $factory->find(Cuenta::class)->one($post['cuenta']);
$file = $request->getUploadedFiles()['archivo'];
$valid_media = [
'text/csv' => 'csvs',
'application/pdf' => 'pdfs',
'application/vnd.ms-excel' => 'xlss',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlss',
'application/json' => 'jsons'
];
if ($file->getError() === 0 and in_array($file->getClientMediaType(), array_keys($valid_media))) {
$filenfo = new \SplFileInfo($file->getClientFilename());
$new_name = implode('.', [implode(' - ', [$cuenta->nombre, $cuenta->categoria()->nombre, $post['fecha']]), $filenfo->getExtension()]);
$to = implode(DIRECTORY_SEPARATOR, [$container->get('folders')->uploads, $valid_media[$file->getClientMediaType()], $new_name]);
$file->moveTo($to);
$status = file_exists($to);
}
$output = [
'input' => [
'name' => $file->getClientFilename(),
'type' => $file->getClientMediaType(),
'size' => $file->getSize(),
'error' => $file->getError()
],
'new_name' => $new_name,
'uploaded' => $status
];
return $this->withJson($response, $output);
}
public function uploads(Request $request, Response $response, Handler $handler): Response {
$output = $handler->handle();
return $this->withJson($response, $output);
$output = $handler->handle();
return $this->withJson($response, $output);
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace Contabilidad\Common\Controller;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use ProVM\Common\Factory\Model as Factory;
use ProVM\Common\Define\Controller\Json;
use Contabilidad\Queue;
class Queues {
use Json;
public function __invoke(Request $request, Response $response, Factory $factory): Response {
$queues = $factory->find(Queue::class)->many();
$output = [
'queues' => array_map(function($item) {return $item->toArray();}, $queues)
];
return $this->withJson($response, $output);
}
public function pending(Request $request, Response $response, Factory $factory): Response {
$pending = $factory->find(Queue::class)->where([['processed', 0]])->many();
$output = [
'pending' => array_map(function($item) {return $item->toArray();}, $pending)
];
return $this->withJson($response, $output);
}
public function processed(Request $request, Response $response, Factory $factory): Response {
$input = json_decode($request->getBody()->getContents());
$output = [
'input' => $input,
'queues' => []
];
if (!is_array($input->processed)) {
$input->processed = [$input->processed];
}
foreach ($input->processed as $id) {
$queue = $factory->find(Queue::class)->one($id);
$queue->setProcessed(true);
$status = $queue->save();
$output['queues'] []= [
'queue' => $queue->toArray(),
'processed' => $status
];
}
return $this->withJson($response, $output);
}
}

View File

@ -1,6 +1,7 @@
<?php
namespace Contabilidad\Common\Controller;
use Contabilidad\TipoCuenta;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use ProVM\Common\Define\Controller\Json;
@ -13,31 +14,22 @@ class TiposCategorias {
public function __invoke(Request $request, Response $response, Factory $factory, Service $service): Response {
$tipos = $factory->find(TipoCategoria::class)->many();
array_walk($tipos, function(&$item) use ($service) {
$arr = $item->toArray();
$arr['categorias'] = array_map(function($item) {
return $item->toArray();
}, $item->categorias());
$arr['saldo'] = abs($item->saldo($service));
$maps = ['activo', 'pasivo', 'ganancia', 'perdida'];
foreach ($maps as $m) {
$p = $m . 's';
$t = ucfirst($m);
$cuentas = $item->getCuentasOf($t);
if ($cuentas === false or $cuentas === null) {
$arr[$p] = 0;
continue;
if ($tipos !== null) {
array_walk($tipos, function(&$item) use ($service) {
$arr = $item->toArray();
$arr['categorias'] = $item->categorias();
if ($arr['categorias'] !== null) {
$arr['categorias'] = array_map(function($item) {
return $item->toArray();
}, $item->categorias());
}
$arr[$p] = array_reduce($cuentas, function($sum, $item) use($service) {
return $sum + $item->saldo($service, true);
});
}
$item = $arr;
});
if ($tipos) {
usort($tipos, function($a, $b) {
return strcmp($a['descripcion'], $b['descripcion']);
});
$arr['saldo'] = abs($item->saldo($service));
$arr['totales'] = $item->getTotales($service);
$item = $arr;
});
usort($tipos, function($a, $b) {
return strcmp($a['descripcion'], $b['descripcion']);
});
}
$output = [
'tipos' => $tipos
@ -90,19 +82,7 @@ class TiposCategorias {
if ($categorias !== null) {
array_walk($categorias, function(&$item) use ($service) {
$arr = $item->toArray($service);
$maps = ['activo', 'pasivo', 'ganancia', 'perdida'];
foreach ($maps as $m) {
$p = $m . 's';
$t = ucfirst($m);
$cuentas = $item->getCuentasOf($t);
if ($cuentas === false or $cuentas === null) {
$arr[$p] = 0;
continue;
}
$arr[$p] = array_reduce($cuentas, function($sum, $item) use($service) {
return $sum + $item->saldo($service, true);
});
}
$arr['totales'] = $item->getTotales($service);
$item = $arr;
});
}
@ -117,6 +97,19 @@ class TiposCategorias {
public function balance(Request $request, Response $response, Factory $factory, Service $service): Response {
$tipos = $factory->find(TipoCategoria::class)->many();
$balance = array_reduce($tipos, function($sum, $item) use ($service) {
$totales = $item->getTotales($service);
if (!is_array($sum)) {
$sum = [];
}
foreach ($totales as $p => $total) {
if (!isset($sum[$p])) {
$sum[$p] = 0;
}
$sum[$p] += $total;
}
return $sum;
});
/*$balance = array_reduce($tipos, function($sum, $item) use ($service) {
$maps = ['activo', 'pasivo', 'ganancia', 'perdida'];
foreach ($maps as $m) {
$p = $m . 's';
@ -133,7 +126,7 @@ class TiposCategorias {
});
}
return $sum;
});
});*/
return $this->withJson($response, $balance);
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace Contabilidad\Common\Middleware;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as Handler;
use Psr\Http\Message\ResponseInterface as Response;
use Contabilidad\Common\Service\Consolidar as ConsolidarService;
class Consolidar {
protected $service;
public function __construct(ConsolidarService $service) {
$this->service = $service;
}
public function __invoke(Request $request, Handler $handler): Response {
if (!$this->service->isConsolidado()) {
$this->service->queue();
}
return $handler->handle($request);
}
}

View File

@ -0,0 +1,155 @@
<?php
namespace Contabilidad\Common\Service;
use Carbon\Carbon;
use Contabilidad\Queue;
use \Model;
use ProVM\Common\Factory\Model as ModelFactory;
use Contabilidad\Consolidado;
use Contabilidad\Cuenta;
use Contabilidad\Transaccion;
class Consolidar {
protected $factory;
public function __construct(ModelFactory $factory) {
$this->factory = $factory;
}
protected $cuentas;
public function getCuentas() {
if ($this->cuentas === null) {
$this->cuentas = $this->factory->find(Cuenta::class)->many();
}
return $this->cuentas;
}
public function isConsolidado(): bool {
$cuentas = $this->getCuentas();
if ($cuentas === null) {
return true;
}
foreach ($cuentas as $cuenta) {
$transacciones = $cuenta->hasTransacciones();
if (!$transacciones) {
continue;
}
$pendientes = $cuenta->hasConsolidadosPending();
if ($pendientes) {
return false;
}
}
return true;
}
public function queue() {
$data = [
'command' => 'consolidar',
'created' => Carbon::now()->format('Y-m-d H:i:s')
];
$queue = Queue::add($this->factory, $data);
$queue->save();
}
public function consolidar() {
ini_set('max_execution_time', 60*5);
foreach ($this->getCuentas() as $cuenta) {
if (!$cuenta->hasTransacciones()) {
continue;
}
$first = $this->getFirst($cuenta);
$last = $this->getLast($cuenta);
for ($current = $first->copy()->startOfMonth(); $current < $last->copy()->addMonthWithoutOverflow()->endOfMonth(); $current = $current->copy()->addMonthWithoutOverflow()) {
$transacciones = $this->getTransacciones($cuenta, $current);
if (count($transacciones) == 0) {
continue;
}
$f = $this->factory;
array_walk($transacciones, function(&$item) use ($cuenta, $f) {
$item->setFactory($f);
$item->valor = $item->transformar($cuenta->moneda());
});
$saldo = array_reduce($transacciones, function($sum, $item) {
return $sum + $item->valor;
});
$data = [
'cuenta_id' => $cuenta->id,
'fecha' => $current->format('Y-m-1'),
'periodo' => 'P1M',
'saldo' => $saldo
];
$consolidado = $this->factory->create(Consolidado::class, $data);
$consolidado->save();
}
}
}
public function getFirst(Cuenta $cuenta): ?Carbon {
$first = [
Model::factory(Transaccion::class)
->select('fecha')
->whereEqual('debito_id', $cuenta->id)
->orderByAsc('fecha')
->findOne(),
Model::factory(Transaccion::class)
->select('fecha')
->whereEqual('credito_id', $cuenta->id)
->orderByAsc('fecha')
->findOne()
];
if (!$first[0]) {
if (!$first[1]) {
return null;
}
return $first[1]->fecha();
}
if (!$first[1]) {
return $first[0]->fecha();
}
if ($first[0]->fecha() < $first[1]->fecha()) {
return $first[0]->fecha();
}
return $first[1]->fecha();
}
public function getLast(Cuenta $cuenta): ?Carbon {
$fechas = [
Model::factory(Transaccion::class)
->select('fecha')
->whereEqual('debito_id', $cuenta->id)
->orderByDesc('fecha')
->findOne(),
Model::factory(Transaccion::class)
->select('fecha')
->whereEqual('credito_id', $cuenta->id)
->orderByDesc('fecha')
->findOne()
];
if (!$fechas[0]) {
if (!$fechas[1]) {
return null;
}
return $fechas[1]->fecha();
}
if (!$fechas[1]) {
return $fechas[0]->fecha();
}
if ($fechas[0]->fecha() > $fechas[1]->fecha()) {
return $fechas[0]->fecha();
}
return $fechas[1]->fecha();
}
public function getTransacciones(Cuenta $cuenta, Carbon $fecha) {
$start = $fecha->copy()->startOfMonth();
$end = $fecha->copy()->endOfMonth();
$debitos = Model::factory(Transaccion::class)
->whereEqual('debito_id', $cuenta->id)
->whereRaw("fecha BETWEEN '{$start->format('Y-m-d')}' AND '{$end->format('Y-m-d')}'")
->findMany();
if ($debitos) {
array_walk($debitos, function(&$item) {
$item->valor *= -1;
});
}
$creditos = Model::factory(Transaccion::class)
->whereEqual('credito_id', $cuenta->id)
->whereRaw("fecha BETWEEN '{$start->format('Y-m-d')}' AND '{$end->format('Y-m-d')}'")
->findMany();
return array_merge($debitos, $creditos);
}
}

View File

@ -0,0 +1,112 @@
<?php
namespace Contabilidad\Common\Service;
use Psr\Http\Message\UploadedFileInterface;
use Psr\Http\Message\StreamInterface;
use Nyholm\Psr7\Stream;
class FileHandler {
protected $base_folder;
protected $valid_types;
protected $folders;
public function __construct(object $params) {
$this->base_folder = $params->folder;
$this->addValidTypes(array_keys($params->types));
$this->addFolders($params->types);
}
public function addFolders(array $folders): FileHandler {
foreach ($folders as $type => $folder) {
$this->addFolder($type, $folder);
}
return $this;
}
public function addFolder(string $type, string $folder): FileHandler {
$this->folders[$type] = $folder;
return $this;
}
public function addValidTypes(array $valid_types): FileHandler {
foreach ($valid_types as $type) {
$this->addValidType($type);
}
return $this;
}
public function addValidType(string $type): FileHandler {
$this->valid_types []= $type;
return $this;
}
public function getType(string $folder): string {
return array_search($folder, $this->folders);
}
public function uploadFile(UploadedFileInterface $file, string $new_name = null): bool {
if ($file->getError() !== UPLOAD_ERR_OK) {
return false;
}
if (!in_array($file->getClientMediaType(), $this->valid_types)) {
return false;
}
if ($new_name === null) {
$new_name = $file->getClientFilename();
}
$filenfo = new \SplFileInfo($file->getClientFilename());
if (!str_contains($new_name, $filenfo->getExtension())) {
$new_name .= '.' . $filenfo->getExtension();
}
$to = implode(DIRECTORY_SEPARATOR, [$this->base_folder, $this->folders[$file->getClientMediaType()], $new_name]);
$file->moveTo($to);
return file_exists($to);
}
public function listFiles(): array {
$output = [];
foreach ($this->folders as $f) {
$folder = implode(DIRECTORY_SEPARATOR, [$this->base_folder, $f]);
if (!file_exists($folder)) {
continue;
}
$files = new \DirectoryIterator($folder);
foreach ($files as $file) {
if ($file->isDir()) {
continue;
}
$output []= (object) ['folder' => $f, 'filename' => $file->getBasename()];
}
}
return $output;
}
protected function validateFilename(string $folder, string $filename): bool|string {
if (!in_array($folder, $this->folders)) {
return false;
}
$f = implode(DIRECTORY_SEPARATOR, [$this->base_folder, $folder, $filename]);
if (!file_exists($f)) {
return false;
}
return $f;
}
public function getInfo(string $folder, string $filename): \SplFileInfo|bool {
if (!$f = $this->validateFilename($folder, $filename)) {
return false;
}
return new \SplFileInfo($f);
}
public function getFile(string $folder, string $filename): StreamInterface|bool {
if (!$f = $this->validateFilename($folder, $filename)) {
return false;
}
return Stream::create(file_get_contents($f));
}
public function editFilename(string $folder, string $filename, string $new_name): bool {
if (!$f = $this->validateFilename($folder, $filename)) {
return false;
}
$info = new \SplFileInfo($f);
$new = implode(DIRECTORY_SEPARATOR, [$this->base_folder, $folder, $new_name . '.' . $info->getExtension()]);
return rename($f, $new);
}
public function deleteFile(string $folder, string $filename): bool {
if (!$f = $this->validateFilename($folder, $filename)) {
return false;
}
return unlink($f);
}
}

View File

@ -5,6 +5,7 @@ use Carbon\Carbon;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Exception\ServerException;
use ProVM\Common\Factory\Model as Factory;
use Contabilidad\Moneda;
use Contabilidad\TipoCambio;
@ -20,30 +21,22 @@ class TiposCambios {
$this->base_url = $api_url;
$this->key = $api_key;
}
public function get(string $fecha, int $moneda_id) {
$fecha = Carbon::parse($fecha);
$moneda = $this->factory->find(Moneda::class)->one($moneda_id);
if ($moneda->codigo == 'USD') {
if ($fecha->weekday() == 0) {
$fecha = $fecha->subWeek()->weekday(5);
}
if ($fecha->weekday() == 6) {
$fecha = $fecha->weekday(5);
}
protected function getWeekday(\DateTimeInterface $fecha) {
if ($fecha->weekday() == 0) {
return $fecha->subWeek()->weekday(5);
}
$cambio = $moneda->cambio($fecha);
if ($cambio) {
if ($cambio->desde()->id != $moneda->id) {
return 1 / $cambio->valor;
}
return $cambio->valor;
if ($fecha->weekday() == 6) {
return $fecha->weekday(5);
}
return $fecha;
}
protected function getValor(\DateTimeInterface $fecha, string $moneda_codigo) {
$data = [
'fecha' => $fecha->format('Y-m-d'),
'desde' => $moneda->codigo
'desde' => $moneda_codigo
];
$headers = [
'Authorization' => 'Bearer ' . $this->key
'Authorization' => "Bearer {$this->key}"
];
$url = implode('/', [
$this->base_url,
@ -52,15 +45,39 @@ class TiposCambios {
]);
try {
$response = $this->client->request('POST', $url, ['json' => $data, 'headers' => $headers]);
} catch (ConnectException | RequestException $e) {
} catch (ConnectException | RequestException | ServerException $e) {
error_log($e);
return null;
}
if ($response->getStatusCode() !== 200) {
error_log('Could not connect to python API.');
return null;
}
$result = json_decode($response->getBody());
$valor = $result->serie[0]->valor;
if (isset($result->message) and $result->message === 'Not Authorized') {
error_log('Not authorized for connecting to python API.');
return null;
}
return $result->serie[0]->valor;
}
public function get(string $fecha, int $moneda_id) {
$fecha = Carbon::parse($fecha);
$moneda = $this->factory->find(Moneda::class)->one($moneda_id);
if ($moneda->codigo == 'USD') {
$fecha = $this->getWeekday($fecha);
}
// If a value exists in the database
$cambio = $moneda->cambio($fecha);
if ($cambio !== null) {
if ($cambio->desde()->id != $moneda->id) {
return 1 / $cambio->valor;
}
return $cambio->valor;
}
$valor = $this->getValor($fecha, $moneda->codigo);
if ($valor === null) {
return 1;
}
$data = [
'fecha' => $fecha->format('Y-m-d H:i:s'),
'desde_id' => $moneda->id,

View File

@ -20,7 +20,7 @@ final class TipoCuenta extends AbstractMigration
{
$this->table('tipos_cuenta')
->addColumn('descripcion', 'string')
->addColumn('color', 'string', ['length' => 6])
->addColumn('color', 'string', ['length' => 6, 'default' => 'ffffff'])
->create();
}
}

View File

@ -21,7 +21,7 @@ final class Categoria extends AbstractMigration
$this->table('categorias')
->addColumn('nombre', 'string')
->addColumn('tipo_id', 'integer')
->addForeignKey('tipo_id', 'tipos_categoria')
->addForeignKey('tipo_id', 'tipos_categoria', ['delete' => 'cascade', 'update' => 'cascade'])
->create();
}
}

View File

@ -21,9 +21,9 @@ final class Cuenta extends AbstractMigration
$this->table('cuentas')
->addColumn('nombre', 'string')
->addColumn('categoria_id', 'integer')
->addForeignKey('categoria_id', 'categorias')
->addForeignKey('categoria_id', 'categorias', ['delete' => 'cascade', 'update' => 'cascade'])
->addColumn('tipo_id', 'integer')
->addForeignKey('tipo_id', 'tipos_cuenta')
->addForeignKey('tipo_id', 'tipos_cuenta', ['delete' => 'cascade', 'update' => 'cascade'])
->create();
}
}

View File

@ -20,10 +20,10 @@ final class EstadoConeccion extends AbstractMigration
{
$this->table('estados_coneccion')
->addColumn('coneccion_id', 'integer')
->addForeignKey('coneccion_id', 'conecciones')
->addForeignKey('coneccion_id', 'conecciones', ['delete' => 'cascade', 'update' => 'cascade'])
->addColumn('fecha', 'date')
->addColumn('tipo_id', 'integer')
->addForeignKey('tipo_id', 'tipos_estado_coneccion')
->addForeignKey('tipo_id', 'tipos_estado_coneccion', ['delete' => 'cascade', 'update' => 'cascade'])
->create();
}
}

View File

@ -20,9 +20,9 @@ final class Transaccion extends AbstractMigration
{
$this->table('transacciones')
->addColumn('debito_id', 'integer')
->addForeignKey('debito_id', 'cuentas')
->addForeignKey('debito_id', 'cuentas', ['delete' => 'cascade', 'update' => 'cascade'])
->addColumn('credito_id', 'integer')
->addForeignKey('credito_id', 'cuentas')
->addForeignKey('credito_id', 'cuentas', ['delete' => 'cascade', 'update' => 'cascade'])
->addColumn('fecha', 'datetime')
->addColumn('glosa', 'string')
->addColumn('detalle', 'text')

View File

@ -20,7 +20,7 @@ final class CuentaMoneda extends AbstractMigration
{
$this->table('cuentas')
->addColumn('moneda_id', 'integer')
->addForeignKey('moneda_id', 'monedas')
->addForeignKey('moneda_id', 'monedas', ['delete' => 'cascade', 'update' => 'cascade'])
->update();
}
}

View File

@ -21,9 +21,9 @@ final class TipoCambio extends AbstractMigration
$this->table('tipos_cambio')
->addColumn('fecha', 'datetime')
->addColumn('desde_id', 'integer')
->addForeignKey('desde_id', 'monedas')
->addForeignKey('desde_id', 'monedas', ['delete' => 'cascade', 'update' => 'cascade'])
->addColumn('hasta_id', 'integer')
->addForeignKey('hasta_id', 'monedas')
->addForeignKey('hasta_id', 'monedas', ['delete' => 'cascade', 'update' => 'cascade'])
->addColumn('valor', 'double')
->create();
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class Queue extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
$this->table('queue')
->addColumn('command', 'string')
->addColumn('created', 'datetime')
->addColumn('processed', 'boolean', ['default' => 0])
->create();
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class QueueArguments extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
$this->table('queue_arguments')
->addColumn('queue_id', 'integer')
->addForeignKey('queue_id', 'queue', ['delete' => 'cascade', 'update' => 'cascade'])
->addColumn('argument', 'string', ['length' => 100])
->addColumn('value', 'string')
->create();
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class Consolidados extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
$this->table('consolidados')
->addColumn('cuenta_id', 'integer')
->addForeignKey('cuenta_id', 'cuentas', ['delete' => 'cascade', 'update' => 'cascade'])
->addColumn('fecha', 'date')
->addColumn('periodo', 'string', ['length' => 50])
->addColumn('saldo', 'double')
->create();
}
}

View File

@ -5,25 +5,26 @@ server {
access_log /var/log/nginx/access.log;
root /app/public;
client_max_body_size 50M;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Headers' 'Authorization,Accept,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,
X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
add_header 'Access-Control-Allow-Headers' 'Authorization,Accept,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE,PATCH';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'application/json';
add_header 'Content-Length' 0;
return 204;
}
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Headers' 'Authorization,Accept,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,
X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
add_header 'Access-Control-Allow-Headers' 'Authorization,Accept,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE,PATCH';
try_files $uri =404;

View File

@ -1,2 +1,4 @@
log_errors = true
error_log = /var/log/php/error.log
error_log = /var/log/php/error.log
upload_max_filesize = 50M
max_input_vars = 5000

View File

@ -1,7 +1,12 @@
<?php
require_once implode(DIRECTORY_SEPARATOR, [
dirname(__DIR__),
'setup',
'app.php'
]);
$app->run();
try {
require_once implode(DIRECTORY_SEPARATOR, [
dirname(__DIR__),
'setup',
'app.php'
]);
$app->run();
} catch (Error | Exception $e) {
error_log($e);
throw $e;
}

View File

@ -3,4 +3,5 @@ use Contabilidad\Common\Controller\Base;
$app->get('/key/generate[/]', [Base::class, 'generate_key']);
$app->get('/balance[/]', [Contabilidad\Common\Controller\TiposCategorias::class, 'balance']);
$app->get('/info', [Base::class, 'info']);
$app->get('/', Base::class);

View File

@ -0,0 +1,4 @@
<?php
use Contabilidad\Common\Controller\Consolidados;
$app->get('/consolidar', [Consolidados::class, 'cli']);

View File

@ -9,8 +9,11 @@ $app->group('/cuenta/{cuenta_id}', function($app) {
$app->get('/entradas', [Cuentas::class, 'entradas']);
$app->group('/transacciones', function($app) {
$app->get('/amount', [Cuentas::class, 'transaccionesAmount']);
$app->get('/month/{month}', [Cuentas::class, 'transaccionesMonth']);
$app->get('/acum/{date}', [Cuentas::class, 'transaccionesAcumulation']);
$app->get('[/{limit:[0-9]+}[/{start:[0-9]+}]]', [Cuentas::class, 'transacciones']);
});
$app->get('/categoria', [Cuentas::class, 'categoria']);
$app->put('/edit', [Cuentas::class, 'edit']);
$app->delete('/delete', [Cuentas::class, 'delete']);
$app->get('[/]', [Cuentas::class, 'show']);

View File

@ -0,0 +1,8 @@
<?php
use Contabilidad\Common\Controller\Queues;
$app->group('/queues', function($app) {
$app->get('/pending', [Queues::class, 'pending']);
$app->post('/processed', [Queues::class, 'processed']);
$app->get('[/]', Queues::class);
});

View File

@ -0,0 +1,12 @@
<?php
use Contabilidad\Common\Controller\Files;
$app->group('/uploads', function($app) {
$app->post('/add[/]', [Files::class, 'upload']);
$app->get('[/]', Files::class);
});
$app->group('/upload/{folder}/{filename}', function($app) {
$app->put('[/]', [Files::class, 'edit']);
$app->delete('[/]', [Files::class, 'delete']);
$app->get('[/]', [Files::class, 'get']);
});

View File

@ -0,0 +1,4 @@
<?php
use Contabilidad\Common\Middleware\Consolidar;
$app->add(new Consolidar($app->getContainer()->get(\Contabilidad\Common\Service\Consolidar::class)));

View File

@ -19,7 +19,7 @@ return [
'public'
]);
$arr['uploads'] = implode(DIRECTORY_SEPARATOR, [
$arr['public'],
$arr['base'],
'uploads'
]);
$arr['pdfs'] = implode(DIRECTORY_SEPARATOR, [

View File

@ -2,9 +2,9 @@
use Psr\Container\ContainerInterface as Container;
return [
GuzzleHttp\Client::class => function(Container $c) {
return new GuzzleHttp\Client();
},
GuzzleHttp\Client::class => function(Container $c) {
return new GuzzleHttp\Client();
},
Contabilidad\Common\Service\Auth::class => function(Container $c) {
return new Contabilidad\Common\Service\Auth($c->get('api_key'));
},
@ -14,27 +14,27 @@ return [
$c->get(Contabilidad\Common\Service\Auth::class)
);
},
Contabilidad\Common\Service\PdfHandler::class => function(Container $c) {
return new Contabilidad\Common\Service\PdfHandler($c->get(GuzzleHttp\Client::class), $c->get('folders')->pdfs, implode('/', [
$c->get('urls')->python,
'pdf',
'parse'
]));
},
Contabilidad\Common\Service\CsvHandler::class => function(Container $c) {
return new Contabilidad\Common\Service\CsvHandler($c->get('folders')->csvs);
},
Contabilidad\Common\Service\XlsHandler::class => function(Container $c) {
return new Contabilidad\Common\Service\XlsHandler($c->get('folders')->xlss);
},
Contabilidad\Common\Service\DocumentHandler::class => function(Container $c) {
$handlers = [
$c->get(Contabilidad\Common\Service\XlsHandler::class),
$c->get(Contabilidad\Common\Service\CsvHandler::class),
$c->get(Contabilidad\Common\Service\PdfHandler::class)
];
return new Contabilidad\Common\Service\DocumentHandler($handlers);
},
Contabilidad\Common\Service\PdfHandler::class => function(Container $c) {
return new Contabilidad\Common\Service\PdfHandler($c->get(GuzzleHttp\Client::class), $c->get('folders')->pdfs, implode('/', [
$c->get('urls')->python,
'pdf',
'parse'
]));
},
Contabilidad\Common\Service\CsvHandler::class => function(Container $c) {
return new Contabilidad\Common\Service\CsvHandler($c->get('folders')->csvs);
},
Contabilidad\Common\Service\XlsHandler::class => function(Container $c) {
return new Contabilidad\Common\Service\XlsHandler($c->get('folders')->xlss);
},
Contabilidad\Common\Service\DocumentHandler::class => function(Container $c) {
$handlers = [
$c->get(Contabilidad\Common\Service\XlsHandler::class),
$c->get(Contabilidad\Common\Service\CsvHandler::class),
$c->get(Contabilidad\Common\Service\PdfHandler::class)
];
return new Contabilidad\Common\Service\DocumentHandler($handlers);
},
Contabilidad\Common\Service\TiposCambios::class => function(Container $c) {
return new Contabilidad\Common\Service\TiposCambios(
$c->get(GuzzleHttp\Client::class),
@ -42,5 +42,17 @@ return [
$c->get('python_api'),
$c->get('python_key')
);
},
Contabilidad\Common\Service\FileHandler::class => function(Container $c) {
return new Contabilidad\Common\Service\FileHandler((object) [
'folder' => $c->get('folders')->uploads,
'types' => [
'text/csv' => 'csvs',
'application/pdf' => 'pdfs',
'application/vnd.ms-excel' => 'xlss',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlss',
'application/json' => 'jsons'
]
]);
}
];

View File

@ -0,0 +1,8 @@
<?php
use Psr\Container\ContainerInterface as Container;
return [
\Contabilidad\Common\Service\Consolidar::class => function(Container $container) {
return new \Contabilidad\Common\Service\Consolidar($container->get(\ProVM\Common\Factory\Model::class));
}
];

View File

@ -18,6 +18,11 @@ class Categoria extends Model {
public function cuentas() {
if ($this->cuentas === null) {
$this->cuentas = $this->parentOf(Cuenta::class, [Model::CHILD_KEY => 'categoria_id']);
if ($this->cuentas !== null) {
usort($this->cuentas, function($a, $b) {
return strcmp($a->nombre, $b->nombre);
});
}
}
return $this->cuentas;
}
@ -41,33 +46,43 @@ class Categoria extends Model {
])
->many();
}
protected $activos;
public function activos() {
if ($this->activos === null) {
$this->activos = $this->getCuentasOf('Activo');
protected $cuentas_of;
public function getCuentas() {
if ($this->cuentas_of === null) {
$tipos = $this->factory->find(TipoCuenta::class)->many();
$cos = [];
foreach ($tipos as $tipo) {
$p = strtolower($tipo->descripcion) . 's';
$cos[$p] = [];
$cuentas = $this->getCuentasOf($tipos->descripcion);
if ($cuentas === null) {
continue;
}
$cos[$p] = $cuentas;
}
$this->cuentas_of = $cos;
}
return $this->activos();
return $this->cuentas_of;
}
protected $pasivos;
public function pasivos() {
if ($this->pasivos === null) {
$this->activos = $this->getCuentasOf('Pasivo');
protected $totales;
public function getTotales(Service $service) {
if ($this->totales === null) {
$tipos = $this->factory->find(TipoCuenta::class)->many();
$totals = [];
foreach ($tipos as $tipo) {
$p = strtolower($tipo->descripcion) . 's';
$totals[$p] = 0;
$cuentas = $this->getCuentasOf($tipo->descripcion);
if ($cuentas === null) {
continue;
}
$totals[$p] = array_reduce($cuentas, function($sum, $item) use ($service) {
return $sum + $item->saldo($service, true);
});
}
$this->totales = $totals;
}
return $this->pasivos;
}
protected $ganancias;
public function ganancias() {
if ($this->ganancias === null) {
$this->ganancias = $this->getCuentasOf('Ganancia');
}
return $this->ganancias;
}
protected $perdidas;
public function perdidas() {
if ($this->perdidas === null) {
$this->perdidas = $this->getCuentasOf('Perdida');
}
return $this->perdidas;
return $this->totales;
}
protected $saldo;
@ -89,4 +104,10 @@ class Categoria extends Model {
}
return $this->saldo;
}
public function toArray(): array {
$arr = parent::toArray();
$arr['tipo'] = $this->tipo()->toArray();
return $arr;
}
}

48
api/src/Consolidado.php Normal file
View File

@ -0,0 +1,48 @@
<?php
namespace Contabilidad;
use DateTimeInterface;
use DateInterval;
use Carbon\Carbon;
use Carbon\CarbonInterval;
use ProVM\Common\Alias\Model;
/**
* @property int $id
* @property Cuenta $cuenta_id
* @property DateTimeInterface $fecha
* @property DateInterval $periodo
* @property float $saldo
*/
class Consolidado extends Model {
public static $_table = 'consolidados';
protected $cuenta;
public function cuenta() {
if ($this->cuenta === null) {
$this->cuenta = $this->childOf(Cuenta::class, [Model::SELF_KEY => 'cuenta_id']);
}
return $this->cuenta;
}
public function fecha(DateTimeInterface $fecha = null) {
if ($fecha === null) {
return Carbon::parse($this->fecha);
}
if ($this->periodo()->days > 31) {
$this->fecha = $fecha->format('Y-1-1');
} else {
$this->fecha = $fecha->format('Y-m-1');
}
return $this;
}
public function periodo(DateInterval $periodo = null) {
if ($periodo === null) {
return new CarbonInterval($this->periodo);
}
$this->periodo = CarbonInterval::getDateIntervalSpec($periodo);
return $this;
}
public function saldo() {
return $this->saldo;
}
}

View File

@ -52,10 +52,46 @@ class Cuenta extends Model {
}
return $this->abonos;
}
protected $consolidados;
public function consolidados() {
if ($this->consolidados === null) {
$this->consolidados = $this->parentOf(Consolidado::class, [Model::CHILD_KEY => 'cuenta_id']);
}
return $this->consolidados;
}
public function hasConsolidados(): bool {
$t = Carbon::now();
return (bool) Model::factory(Consolidado::class)
->whereEqual('cuenta_id', $this->id)
->count('id');
}
public function hasConsolidadosPending(): bool {
$t = Carbon::now();
return !(bool) Model::factory(Consolidado::class)
->whereEqual('cuenta_id', $this->id)
->whereGte('fecha', $t->copy()->subMonthNoOverflow()->startOfMonth()->format('Y-m-d'))
->orderByDesc('fecha')
->count('id');
}
public function hasTransacciones(): bool {
return (bool) Model::factory(Transaccion::class)
->select('transacciones.*')
->join('cuentas', 'cuentas.id = transacciones.debito_id OR cuentas.id = transacciones.credito_id')
->whereEqual('cuentas.id', $this->id)
->count('transacciones.id');
}
protected $transacciones;
protected function parseTransaccion(Transaccion $transaccion) {
$transaccion->setFactory($this->factory);
if ($transaccion->debito_id === $this->id) {
$transaccion->valor = - $transaccion->valor;
}
$transaccion->valor = $transaccion->transformar($this->moneda());
return $transaccion;
}
public function transacciones($limit = null, $start = 0) {
if ($this->transacciones === null) {
$transacciones = Model::factory(Transaccion::class)
->select('transacciones.*')
->join('cuentas', 'cuentas.id = transacciones.debito_id OR cuentas.id = transacciones.credito_id')
->whereEqual('cuentas.id', $this->id)
->orderByAsc('transacciones.fecha');
@ -63,15 +99,43 @@ class Cuenta extends Model {
$transacciones = $transacciones->limit($limit)
->offset($start);
}
$this->transacciones = $transacciones->findMany();
foreach ($this->transacciones as &$transaccion) {
$transaccion->setFactory($this->factory);
if ($transaccion->desde_id === $this->id) {
$transaccion->valor = - $transaccion->valor;
}
$transacciones = $transacciones->findMany();
foreach ($transacciones as &$transaccion) {
$transaccion = $this->parseTransaccion($transaccion);
}
}
return $this->transacciones;
return $transacciones;
}
public function transaccionesMonth(Carbon $month) {
$start = $month->copy()->startOfMonth();
$end = $month->copy()->endOfMonth();
$transacciones = Model::factory(Transaccion::class)
->select('transacciones.*')
->join('cuentas', 'cuentas.id = transacciones.debito_id OR cuentas.id = transacciones.credito_id')
->whereEqual('cuentas.id', $this->id)
->whereRaw("transacciones.fecha BETWEEN '{$start->format('Y-m-d')}' AND '{$end->format('Y-m-d')}'")
->orderByAsc('transacciones.fecha');
$transacciones = $transacciones->findMany();
foreach ($transacciones as &$transaccion) {
$transaccion = $this->parseTransaccion($transaccion);
}
return $transacciones;
}
public function acumulacion(Carbon $date) {
$consolidados = $this->consolidados();
if ($consolidados === null) {
return 0;
}
$saldo = 0;
foreach ($consolidados as $consolidado) {
if ($consolidado->fecha() >= $date) {
continue;
}
$saldo += $consolidado->saldo();
}
return $saldo;
}
protected $saldo;
public function saldo(Service $service = null, $in_clp = false) {

View File

@ -16,25 +16,28 @@ class Moneda extends Model {
protected static $fields = ['denominacion', 'codigo'];
public function format($valor) {
return implode('', [
return trim(implode('', [
$this->prefijo,
number_format($valor, $this->decimales, ',', '.'),
$this->sufijo
]);
]));
}
public function cambio(\DateTime $fecha) {
public function cambio(\DateTime $fecha, Moneda $moneda = null) {
if ($moneda === null) {
$moneda = $this->factory->find(Moneda::class)->one(1);
}
$cambio = $this->factory->find(TipoCambio::class)
->where([['desde_id', $this->id], ['hasta_id', 1], ['fecha', $fecha->format('Y-m-d H:i:s')]])
->where([['desde_id', $this->id], ['hasta_id', $moneda->id], ['fecha', $fecha->format('Y-m-d H:i:s')]])
->one();
if (!$cambio) {
if ($cambio === null) {
$cambio = $this->factory->find(TipoCambio::class)
->where([['hasta_id', $this->id], ['desde_id', 1], ['fecha', $fecha->format('Y-m-d H:i:s')]])
->where([['hasta_id', $this->id], ['desde_id', $moneda->id], ['fecha', $fecha->format('Y-m-d H:i:s')]])
->one();
}
return $cambio;
}
public function cambiar(\DateTime $fecha, float $valor) {
$cambio = $this->cambio($fecha);
public function cambiar(\DateTime $fecha, float $valor, Moneda $moneda = null) {
$cambio = $this->cambio($fecha, $moneda);
if (!$cambio) {
return $valor;
}
@ -43,4 +46,14 @@ class Moneda extends Model {
}
return $cambio->transform($valor);
}
public function toArray(): array {
$arr = parent::toArray();
$arr['format'] = [
'prefijo' => $this->prefijo,
'sufijo' => $this->sufijo,
'decimales' => $this->decimales
];
return $arr;
}
}

83
api/src/Queue.php Normal file
View File

@ -0,0 +1,83 @@
<?php
namespace Contabilidad;
use DateTimeInterface;
use Carbon\Carbon;
use ProVM\Common\Alias\Model;
use ProVM\Common\Factory\Model as Factory;
/**
* @property int $id
* @property string $command
* @property DateTimeInterface $created
* @property bool $processed
*/
class Queue extends Model {
public static $_table = 'queue';
protected static $fields = ['command', 'created', 'processed'];
public function created(DateTimeInterface $fecha = null) {
if ($fecha === null) {
return Carbon::parse($this->created);
}
$this->created = $fecha->format('Y-m-d H:i:s');
return $this;
}
public function hasArguments() {
return Model::factory(QueueArgument::class)
->whereEqual('queue_id', $this->id)
->groupBy('queue_id')
->count('id') > 0;
}
protected $arguments;
public function arguments() {
if ($this->arguments === null) {
$this->arguments = $this->parentOf(QueueArgument::class, [Model::CHILD_KEY => 'queue_id']);
}
return $this->arguments;
}
public function isProcessed() {
return $this->processed > 0;
}
public function setProcessed(bool $processed) {
$this->processed = $processed ? 1 : 0;
return $this;
}
public static function find(Factory $factory, $data) {
$where = [
'command' => $data['command'],
'processed' => $data['processed'] ?? 0
];
array_walk($where, function(&$item, $key) {
$item = [$key, $item];
});
$where = array_values($where);
return $factory->find(Queue::class)->where($where)->one();
}
public function toArray(): array
{
$arr = parent::toArray();
$cmd = [(string) $this];
$arr['arguments'] = [];
if ($this->hasArguments()) {
$arr['arguments'] = array_map(function($item) use (&$cmd) {
$cmd []= (string) $item;
return $item->toArray();
}, $this->arguments());
}
$arr['cmd'] = implode(' ', $cmd);
return $arr;
}
public function __toString(): string {
$str = "{$this->command}";
$arguments = $this->arguments();
if ($arguments !== null and count($arguments) > 0) {
$arguments = implode(' ', array_map(function($item) {return (string) $item;}, $arguments));
$str .= " {$arguments}";
}
return $str;
}
}

26
api/src/QueueArgument.php Normal file
View File

@ -0,0 +1,26 @@
<?php
namespace Contabilidad;
use ProVM\Common\Alias\Model;
/**
* @property int $id
* @property Queue $queue_id
* @property string $argument
* @property string $value
*/
class QueueArgument extends Model {
public static $_table = 'queue_arguments';
protected $queue;
public function queue() {
if ($this->queue === null) {
$this->queue = $this->childOf(Queue::class, [Model::SELF_KEY => 'queue_id']);
}
return $this->queue;
}
public function __toString(): string {
return "{$this->argument}='{$this->value}'";
}
}

View File

@ -10,20 +10,20 @@ use Contabilidad\Common\Service\TiposCambios as Service;
* @property int $activo
*/
class TipoCategoria extends Model {
public static $_table = 'tipos_categoria';
protected static $fields = ['descripcion', 'activo'];
public static $_table = 'tipos_categoria';
protected static $fields = ['descripcion', 'activo'];
protected $categorias;
public function categorias() {
if ($this->categorias === null) {
$this->categorias = $this->parentOf(Categoria::class, [Model::CHILD_KEY => 'tipo_id']);
protected $categorias;
public function categorias() {
if ($this->categorias === null) {
$this->categorias = $this->parentOf(Categoria::class, [Model::CHILD_KEY => 'tipo_id']);
}
return $this->categorias;
}
return $this->categorias;
}
public function getCuentasOf($tipo) {
return $this->factory->find(Cuenta::class)
->select([['cuentas', '*']])
->select('cuentas.*')
->join([
['tipos_cuenta', 'tipos_cuenta.id', 'cuentas.tipo_id'],
['categorias', 'categorias.id', 'cuentas.categoria_id']
@ -33,11 +33,31 @@ class TipoCategoria extends Model {
['categorias.tipo_id', $this->id]
])->many();
}
protected $totales;
public function getTotales(Service $service) {
if ($this->totales === null) {
$tipos = $this->factory->find(TipoCuenta::class)->many();
$totals = [];
foreach ($tipos as $tipo) {
$p = strtolower($tipo->descripcion) . 's';
$totals[$p] = 0;
$cuentas = $this->getCuentasOf($tipo->descripcion);
if ($cuentas === null) {
continue;
}
$totals[$p] = array_reduce($cuentas, function($sum, $item) use ($service) {
return $sum + $item->saldo($service, true);
});
}
$this->totales = $totals;
}
return $this->totales;
}
protected $saldo;
public function saldo(Service $service = null) {
if ($this->saldo === null) {
$this->saldo = array_reduce($this->categorias(), function($sum, $item) use ($service) {
$this->saldo = array_reduce($this->categorias() ?? [], function($sum, $item) use ($service) {
return $sum + $item->saldo($service);
});
}

View File

@ -13,10 +13,11 @@ use ProVM\Common\Alias\Model;
* @property string $glosa
* @property string $detalle
* @property double $valor
* @property Moneda $moneda_id
*/
class Transaccion extends Model {
public static $_table = 'transacciones';
protected static $fields = ['debito_id', 'credito_id', 'fecha', 'glosa', 'detalle', 'valor'];
protected static $fields = ['debito_id', 'credito_id', 'fecha', 'glosa', 'detalle', 'valor', 'moneda_id'];
protected $debito;
public function debito() {
@ -37,6 +38,21 @@ class Transaccion extends Model {
return Carbon::parse($this->fecha);
}
$this->fecha = $fecha->format('Y-m-d');
return $this;
}
protected $moneda;
public function moneda() {
if ($this->moneda === null) {
$this->moneda = $this->childOf(Moneda::class, [Model::SELF_KEY => 'moneda_id']);
}
return $this->moneda;
}
public function transformar(Moneda $moneda = null) {
if (($moneda !== null and $this->moneda()->id === $moneda->id) or ($moneda === null and $this->moneda()->id === 1)) {
return $this->valor;
}
return $this->moneda()->cambiar($this->fecha(), $this->valor, $moneda);
}
public function toArray(): array {

12
console/Dockerfile Normal file
View File

@ -0,0 +1,12 @@
FROM php:8-cli
RUN apt-get update -y && apt-get install -y cron git libzip-dev zip
RUN docker-php-ext-install zip
COPY --from=composer /usr/bin/composer /usr/bin/composer
WORKDIR /app
CMD ["cron", "-f", "-l", "2"]
ENTRYPOINT ["cron", "-f", "-l", "2"]

7
console/bin/console Normal file
View File

@ -0,0 +1,7 @@
<?php
$app = require_once implode(DIRECTORY_SEPARATOR, [
dirname(__FILE__, 2),
'setup',
'app.php'
]);
$app->run();

View File

@ -0,0 +1,37 @@
<?php
namespace Contabilidad\Common\Command;
use Psr\Http\Client\ClientInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class Consolidar extends Command {
protected $client;
public function __construct(ClientInterface $client = null, string $name = null) {
parent::__construct($name);
$this->setClient($client);
}
public function setClient(ClientInterface $client) {
$this->client = $client;
return $this;
}
public function getClient(): ClientInterface {
return $this->client;
}
public function execute(InputInterface $input, OutputInterface $output) {
try {
$response = $this->getClient()->get('/consolidar');
if ($response->getStatusCode() === 200) {
return Command::SUCCESS;
}
error_log($response->getReasonPhrase());
return Command::FAILURE;
} catch (\Exception $e) {
error_log($e);
return Command::FAILURE;
}
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace Contabilidad\Common\Command;
use Psr\Http\Client\ClientInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class Queue extends Command {
protected $client;
public function __construct(ClientInterface $client = null, string $name = null) {
parent::__construct($name);
$this->setClient($client);
}
public function setClient(ClientInterface $client) {
$this->client = $client;
return $this;
}
public function getClient(): ClientInterface {
return $this->client;
}
public function execute(InputInterface $input, OutputInterface $output)
{
$response = $this->getClient()->get('/queues/pending');
if ($response->getStatusCode() !== 200) {
return Command::FAILURE;
}
$input = json_decode($response->getBody()->getContents());
$output = [
'input' => $input,
'processed' => []
];
foreach ($input->pending as $queue) {
$log = "Running {$queue->command} from queue. Created in {$queue->created}.";
error_log($log);
$cmd = '/usr/local/bin/php /app/bin/console ' . $queue->cmd;
exec($cmd, $result, $code);
if ($code != Command::SUCCESS) {
error_log(var_export($queue, true));
error_log(var_export($result, true));
continue;
}
$output['processed'] []= $queue->id;
}
$response = $this->getClient()->post('/queues/processed', ['json' => $output]);
if ($response->getStatusCode() !== 200) {
return Command::FAILURE;
}
return Command::SUCCESS;
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace Contabilidad\Common\Define;
use Psr\Container\ContainerInterface as Container;
use Symfony\Component\Console\Application as Base;
class Application extends Base {
public function __construct(Container $container = null, string $name = 'UNKNOWN', string $version = 'UNKNOWN')
{
parent::__construct($name, $version);
$this->setContainer($container);
}
protected $container;
public function setContainer(Container $container) {
$this->container = $container;
return $this;
}
public function getContainer() {
return $this->container;
}
}

25
console/composer.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "provm/contabilidad-console",
"type": "project",
"require": {
"symfony/console": "^6.0",
"php-di/php-di": "^6.3",
"nesbot/carbon": "^2.57",
"guzzlehttp/guzzle": "^7.4"
},
"require-dev": {
"phpunit/phpunit": "^9.5",
"kint-php/kint": "^4.1"
},
"authors": [
{
"name": "Aldarien",
"email": "aldarien85@gmail.com"
}
],
"autoload": {
"psr-4": {
"Contabilidad\\Common\\": "common/"
}
}
}

24
console/crontab Normal file
View File

@ -0,0 +1,24 @@
# Edit this file to introduce tasks to be run by cron.
#
# Each task to run has to be defined through a single line
# indicating with different fields when the task will be run
# and what command to run for the task
#
# To define the time you can provide concrete values for
# minute (m), hour (h), day of month (dom), month (mon),
# and day of week (dow) or use '*' in these fields (for 'any').
#
# Notice that tasks will be started based on the cron's system
# daemon's notion of time and timezones.
#
# Output of the crontab jobs (including errors) is sent through
# email to the user the crontab file belongs to (unless redirected).
#
# For example, you can run a backup of all your user accounts
# at 5 a.m every week with:
# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
#
# For more information see the manual pages of crontab(5) and cron(8)
#
# m h dom mon dow command
0 2 * * * /usr/local/bin/php /app/bin/console queue

4
console/php.ini Normal file
View File

@ -0,0 +1,4 @@
[PHP]
display_errors = E_ALL
log_errors = true
error_log = /var/log/php/error.log

39
console/setup/app.php Normal file
View File

@ -0,0 +1,39 @@
<?php
use DI\ContainerBuilder as Builder;
use Contabilidad\Common\Define\Application;
require_once 'composer.php';
$builder = new Builder();
$folders = [
'settings',
'setups'
];
foreach ($folders as $f) {
$folder = implode(DIRECTORY_SEPARATOR, [__DIR__, $f]);
if (!file_exists($folder)) {
continue;
}
$files = new DirectoryIterator($folder);
foreach ($files as $file) {
if ($file->isDir()) {
continue;
}
$builder->addDefinitions($file->getRealPath());
}
}
$container = $builder->build();
$app = new Application($container);
$folder = implode(DIRECTORY_SEPARATOR, [__DIR__, 'commands']);
if (file_exists($folder)) {
$files = new DirectoryIterator($folder);
foreach ($files as $file) {
if ($file->isDir() or $file->getExtension() != 'php') {
continue;
}
include_once $file->getRealPath();
}
}
return $app;

View File

@ -0,0 +1,4 @@
<?php
use Contabilidad\Common\Command\Queue;
$app->add(new Queue($app->getContainer()->get(\Psr\Http\Client\ClientInterface::class), 'queue'));

View File

@ -0,0 +1,4 @@
<?php
use Contabilidad\Common\Command\Consolidar;
$app->add(new Consolidar($app->getContainer()->get(\Psr\Http\Client\ClientInterface::class), 'consolidar'));

View File

@ -0,0 +1,6 @@
<?php
require_once implode(DIRECTORY_SEPARATOR, [
dirname(__DIR__),
'vendor',
'autoload.php'
]);

View File

@ -0,0 +1,5 @@
<?php
return [
'api_url' => $_ENV['API_URL'],
'api_key' => $_ENV['API_KEY']
];

View File

@ -0,0 +1,13 @@
<?php
use Psr\Container\ContainerInterface as Container;
return [
\Psr\Http\Client\ClientInterface::class => function(Container $container) {
return new \GuzzleHttp\Client([
'base_uri' => $container->get('api_url'),
'headers' => [
'Authorization' => "Bearer {$container->get('api_key')}"
]
]);
}
];

View File

@ -1,15 +1,18 @@
version: '3'
x-restart: &restart
restart: unless-stopped
services:
api:
profiles:
- api
restart: unless-stopped
<<: *restart
image: php
build:
context: api
env_file:
- .env
env_file:
- .db.env
- .api.env
- .python.env
volumes:
@ -19,26 +22,26 @@ services:
api-proxy:
profiles:
- api
restart: unless-stopped
<<: *restart
image: nginx
ports:
- "9001:80"
volumes:
- ./api/nginx.conf:/etc/nginx/conf.d/default.conf
- ./logs/api/:/var/log/nginx/
- ./logs/api/proxy/:/var/log/nginx/
- ./api/:/app/
db:
profiles:
- api
restart: unless-stopped
<<: *restart
image: mariadb
env_file: .env
env_file: .db.env
volumes:
- contabilidad_data:/var/lib/mysql
adminer:
profiles:
- api
restart: unless-stopped
<<: *restart
image: adminer
ports:
- "9002:8080"
@ -46,10 +49,11 @@ services:
ui:
profiles:
- ui
restart: unless-stopped
<<: *restart
image: php-ui
env_file:
- .api.env
- .env
build:
context: ui
volumes:
@ -59,19 +63,19 @@ services:
ui-proxy:
profiles:
- ui
restart: unless-stopped
<<: *restart
image: nginx
ports:
- "9000:80"
volumes:
- ./ui/nginx.conf:/etc/nginx/conf.d/default.conf
- ./logs/ui/:/var/log/nginx/
- ./logs/ui/proxy/:/var/log/nginx/
- ./ui/:/app/
python:
profiles:
- python
restart: unless-stopped
<<: *restart
build:
context: ./python
env_file:
@ -84,5 +88,21 @@ services:
- ./api/public/uploads/pdfs/:/app/data/
- ./logs/python/:/var/log/python/
console:
profiles:
- console
<<: *restart
build:
context: ./console
env_file:
- .api.env
- .console.env
- .db.env
volumes:
- ./console/:/app/
- ./console/php.ini:/usr/local/etc/php/conf.d/php.ini
- ./logs/console/:/var/log/php/
- ./console/crontab:/var/spool/cron/crontabs/root
volumes:
contabilidad_data:

View File

@ -22,7 +22,10 @@ def validate_key(request_obj):
if isinstance(auth, list):
auth = auth[0]
if 'Bearer' in auth:
auth = auth.split(' ')[1]
try:
auth = auth.split(' ')[1]
except:
return False
return auth == api_key
if 'API_KEY' in request_obj.values:
return request_obj.values.get('API_KEY') == api_key

View File

@ -1,3 +1,9 @@
FROM php:8-fpm
RUN apt-get update -y && apt-get install -y git libzip-dev zip
RUN docker-php-ext-install zip
COPY --from=composer /usr/bin/composer /usr/bin/composer
WORKDIR /app

View File

@ -10,7 +10,8 @@ class Cuentas {
return $view->render($response, 'cuentas.list');
}
public function show(Request $request, Response $response, View $view, $cuenta_id): Response {
return $view->render($response, 'cuentas.show', compact('cuenta_id'));
$max_transacciones = 100;
return $view->render($response, 'cuentas.show', compact('cuenta_id', 'max_transacciones'));
}
public function add(Request $request, Response $response, View $view): Response {
return $view->render($response, 'cuentas.add');

View File

@ -0,0 +1,12 @@
<?php
namespace Contabilidad\Common\Controller;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Slim\Views\Blade as View;
class Importar {
public function __invoke(Request $request, Response $response, View $view): Response {
return $view->render($response, 'importar');
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Contabilidad\Common\Controller;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Slim\Views\Blade as View;
use GuzzleHttp\Client;
class Uploads {
public function __invoke(Request $request, Response $response, View $view): Response {
return $view->render($response, 'uploads.list');
}
public function get(Request $request, Response $response, Client $client, $folder, $filename): Response {
$resp = $client->get(implode('/', ['upload', $folder, $filename]));
$file = $resp->getBody();
return $response
->withHeader('Content-Type', $resp->getHeader('Content-Type'))
->withHeader('Content-Disposition', 'attachment; filename=' . $filename)
->withAddedHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->withHeader('Cache-Control', 'post-check=0, pre-check=0')
->withHeader('Pragma', 'no-cache')
->withBody($file);
}
}

View File

@ -5,10 +5,11 @@
"require": {
"php-di/php-di": "^6.3",
"php-di/slim-bridge": "^3.1",
"rubellum/slim-blade-view": "^0.1.1",
"nyholm/psr7-server": "^1.0",
"zeuxisoo/slim-whoops": "^0.7.3",
"nyholm/psr7": "^1.4"
"nyholm/psr7": "^1.4",
"guzzlehttp/guzzle": "^7.4",
"berrnd/slim-blade-view": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^9.5",
@ -24,11 +25,5 @@
"psr-4": {
"Contabilidad\\Common\\": "common"
}
},
"repositories": [
{
"type": "git",
"url": "http://git.provm.cl/ProVM/controller.git"
}
]
}
}

View File

@ -90,32 +90,37 @@ const tipos_categorias = {
this.draw()
})
},
getParent: function() {
let parent = $(this.id).find('tbody')
if (parent.length === 0) {
const table = $('<table></table>').attr('class', 'ui table').append(
$('<thead></thead>').append(
$('<tr></tr>').append(
$('<th></th>').attr('class', 'twelve wide').html('Tipo Categoría')
).append(
$('<th></th>').attr('class', 'two wide').html('Activo')
).append(
$('<th></th>').attr('class', 'two wide right aligned').append(
$('<button></button>').attr('class', 'ui tiny green circular icon button').append(
$('<i></i>').attr('class', 'plus icon')
)
buildParent: function(segment) {
const table = $('<table></table>').attr('class', 'ui table').append(
$('<thead></thead>').append(
$('<tr></tr>').append(
$('<th></th>').attr('class', 'twelve wide').html('Tipo Categoría')
).append(
$('<th></th>').attr('class', 'two wide').html('Activo')
).append(
$('<th></th>').attr('class', 'two wide right aligned').append(
$('<button></button>').attr('class', 'ui tiny green circular icon button').append(
$('<i></i>').attr('class', 'plus icon')
)
)
)
)
table.find('.ui.button').click((e) => {
e.preventDefault()
this.add()
return false
})
parent = $('<tbody></tbody>')
table.append(parent)
$(this.id).append(table)
)
table.find('.ui.button').click((e) => {
e.preventDefault()
this.add()
return false
})
parent = $('<tbody></tbody>')
table.append(parent)
segment.append(table)
return parent
},
getParent: function() {
const segment = $(this.id)
let parent = segment.find('tbody')
if (parent.length === 0) {
parent = this.buildParent(segment)
}
return parent
},

View File

@ -1,231 +0,0 @@
class Transaccion {
constructor({id, debito_id, credito_id, fecha, glosa, detalle, valor, debito, credito, fechaFormateada, valorFormateado}) {
this.id = id
this.debito_id = debito_id
this.credito_id = credito_id
this.fecha = {
fecha,
formateada: fechaFormateada
}
this.glosa = glosa
this.detalle = detalle
this.valor = {
valor,
formateado: valorFormateado
}
this.debito = debito
this.credito = credito
this.modal = null
}
setCuenta(cuenta) {
this.cuenta = cuenta
}
setModal(modal) {
this.modal = modal
}
isDebito() {
return this.debito.id === this.cuenta.id;
}
isIncrement() {
const debits = ['Activo', 'Perdida']
if (debits.indexOf(this.cuenta.tipo.descripcion)) {
return this.isDebito()
}
return !this.isDebito()
}
draw({saldo, format}) {
const fuente = (this.isDebito()) ? this.credito : this.debito
return $('<tr></tr>').append(
$('<td></td>').html(this.fecha.formateada)
).append(
$('<td></td>').append(
$('<a></a>').attr('href', _urls.base + 'cuenta/' + fuente.id).html(fuente.nombre + ' (' + fuente.categoria.nombre + ')')
)
).append(
$('<td></td>').html(this.glosa + '<br />' + this.detalle)
).append(
$('<td></td>').attr('class', 'right aligned').html((this.isIncrement()) ? '' : format.format(this.valor.valor))
).append(
$('<td></td>').attr('class', 'right aligned').html((this.isIncrement()) ? format.format(this.valor.valor) : '')
).append(
$('<td></td>').attr('class', 'right aligned').html(format.format(saldo))
).append(
$('<td></td>').attr('class', 'right aligned')/*.append(
$('<button></button>').attr('class', 'ui tiny circular icon button').append(
$('<i></i>').attr('class', 'edit icon')
).click((e) => {
e.preventDefault()
this.edit()
return false
})
)*/.append(
$('<button></button>').attr('class', 'ui tiny circular red icon button').append(
$('<i></i>').attr('class', 'remove icon')
).click((e) => {
e.preventDefault()
this.remove()
return false
})
)
)
}
edit() {
const form = this.modal.find('form')
form.find("[name='fecha']")
}
remove() {
sendDelete(_urls.api + '/transaccion/' + this.id + '/delete').then(() => {
transacciones.get().transacciones()
})
}
}
const transacciones = {
id: '#transacciones',
cuenta_id: 0,
cuenta: null,
transacciones: [],
cuentas: [],
saldo: 0,
get: function() {
return {
transacciones: () => {
let promises = []
sendGet(_urls.api + '/cuenta/' + this.cuenta_id + '/transacciones/amount').then((data) => {
if (data.cuenta === null) {
return
}
this.cuenta = data.cuenta
this.saldo = this.cuenta.saldo
$('#cuenta').html(this.cuenta.nombre + ' (' + this.cuenta.categoria.nombre + ')').append(
$('<i></i>').attr('class', 'square full icon').css('color', '#' + this.cuenta.tipo.color)
)
const amount = data.transacciones
const step = 50
for (let i = 0; i < amount; i += step) {
promises.push(
sendGet(_urls.api + '/cuenta/' + this.cuenta_id + '/transacciones/' + step + '/' + i)
)
}
if (promises.length > 0) {
Promise.all(promises).then((data_arr) => {
this.transacciones = []
data_arr.forEach(data => {
if (data.transacciones === null || data.transacciones.length === 0) {
return
}
$.each(data.transacciones, (i, el) => {
const tr = new Transaccion(el)
tr.setCuenta(this.cuenta)
tr.setModal(this.modal)
this.transacciones.push(tr)
})
})
this.transacciones.sort((a, b) => {
return (new Date(b.fecha)) - (new Date(a.fecha))
})
}).then(() => {
this.draw()
})
} else {
this.draw()
}
})
},
cuentas: () => {
return sendGet(_urls.api + '/cuentas').then((data) => {
if (data.cuentas === null || data.cuentas.length === 0) {
return
}
this.cuentas = data.cuentas
}).then(() => {
const select = this.modal.find("[name='cuenta']")
$.each(this.cuentas, (i, el) => {
select.append(
$('<option></option>').attr('value', el.id).html(el.nombre + ' (' + el.categoria.nombre + ')')
)
})
})
}
}
},
draw: function() {
const format = Intl.NumberFormat('es-CL', {style: 'currency', currency: this.cuenta.moneda.codigo})
const parent = $(this.id)
parent.html('')
$.each(this.transacciones, (i, el) => {
parent.append(el.draw({saldo: this.saldo, format: format}))
this.saldo = this.saldo + parseInt(el.valor.valor) * ((el.isIncrement()) ? 1 : -1)
})
},
add: function() {
return {
show: () => {
this.modal.find('form').trigger('reset')
this.modal.modal('show')
},
exec: () => {
const fecha = $("[name='fecha']").val()
const data1 = JSON.stringify({
desde_id: $("[name='moneda']").val(),
hasta_id: 1,
fecha: fecha,
valor: $("[name='cambio']").val()
})
sendPut(_urls.api + '/tipos/cambios/add', data1)
const valor = $("[name='valor']").val()
const cuenta = $("[name='cuenta']").val()
const data = JSON.stringify({
debito_id: (valor < 0) ? this.cuenta_id : cuenta,
credito_id: (valor < 0) ? cuenta : this.cuenta_id,
fecha: fecha,
glosa: $("[name='glosa']").val(),
detalle: $("[name='detalle']").val(),
valor: (valor < 0) ? -valor : valor
})
return sendPost(_urls.api + '/transacciones/add', data).then(() => {
this.modal.modal('hide')
this.get().transacciones()
})
}
}
},
build: function() {
return {
modal: () => {
this.modal = $('.ui.modal')
this.modal.modal()
this.modal.find('.close.icon').click(() => {
this.modal.modal('hide')
})
this.modal.find('form').submit((e) => {
e.preventDefault()
this.add().exec()
return false
})
this.modal.find('.ui.calendar').calendar({
type: 'date',
formatter: {
date: function(date, settings) {
if (!date) return ''
let day = ('00' + date.getDate()).slice(-2)
let month = ('00' + (date.getMonth() + 1)).slice(-2)
let year = date.getFullYear()
return year + '/' + month + '/' + day
}
},
maxDate: new Date()
})
this.get().cuentas()
}
}
},
setup: function() {
this.build().modal()
$(this.id).parent().find('.ui.button').click(() => {
this.add().show()
})
this.get().transacciones()
}
}

View File

@ -0,0 +1,466 @@
class Transaccion {
constructor({id, debito_id, credito_id, fecha, glosa, detalle, valor, debito, credito, fechaFormateada, valorFormateado, format, moneda_id}) {
this.id = id
this.debito_id = debito_id
this.credito_id = credito_id
this.fecha = {
fecha,
formateada: fechaFormateada
}
this.glosa = glosa
this.detalle = detalle
this.valor = {
valor,
formateado: valorFormateado
}
this.format_array = format
this.debito = debito
this.credito = credito
this.modal = null
}
setCuenta(cuenta) {
this.cuenta = cuenta
}
setModal(modal) {
this.modal = modal
}
isDebito() {
return this.debito.id === this.cuenta.id;
}
isIncrement() {
const debits = ['Activo', 'Perdida']
if (debits.indexOf(this.cuenta.tipo.descripcion)) {
return this.isDebito()
}
return !this.isDebito()
}
draw({saldo, format, format_array, format_call}) {
const fuente = (this.isDebito()) ? this.credito : this.debito
return $('<tr></tr>').append(
$('<td></td>').html(this.fecha.formateada)
).append(
$('<td></td>').append(
$('<a></a>').attr('href', _urls.base + 'cuenta/' + fuente.id).html(fuente.nombre + ' (' + fuente.categoria.nombre + ')')
)
).append(
$('<td></td>').html(this.glosa + '<br />' + this.detalle)
).append(
$('<td></td>').attr('class', 'right aligned').html((this.isIncrement()) ? '' : format_call({value: this.valor.valor, format, format_array}))
).append(
$('<td></td>').attr('class', 'right aligned').html((this.isIncrement()) ? format_call({value: this.valor.valor, format, format_array}) : '')
).append(
$('<td></td>').attr('class', 'right aligned').html(format_call({value: saldo, format_array, format}))
).append(
$('<td></td>').attr('class', 'right aligned').append(
$('<button></button>').attr('class', 'ui tiny circular icon button').append(
$('<i></i>').attr('class', 'edit icon')
).click((e) => {
e.preventDefault()
this.edit()
return false
})
).append(
$('<button></button>').attr('class', 'ui tiny circular red icon button').append(
$('<i></i>').attr('class', 'remove icon')
).click((e) => {
e.preventDefault()
this.remove()
return false
})
)
)
}
edit() {
const form = this.modal.find('form')
form.trigger('reset')
form.find("[name='id']").val(this.id)
form.find(".ui.calendar").calendar('set date', new Date(this.fecha.fecha))
form.find("[name='cuenta']").dropdown('set selected', (this.isDebito()) ? this.credito_id : this.credito_id)
form.find("[name='glosa']").val(this.glosa)
form.find("[name='detalle']").val(this.detalle)
form.find("[name='valor']").val(((this.isDebito()) ? -1 : 1) * this.valor.valor)
modalToEdit(this.modal)
this.modal.modal('show')
}
remove() {
sendDelete(_urls.api + '/transaccion/' + this.id + '/delete').then(() => {
transacciones.get().transacciones()
})
}
}
const transacciones = {
id: '#transacciones',
mes: '#mes',
buttons: {
prev: '#prev_button',
left: '#left_button',
today: '#today_button',
right: '#right_button',
next: '#next_button'
},
cuenta_id: 0,
cuenta: null,
transacciones: [],
cuentas: [],
monedas: [],
date: new Date(),
saldo: 0,
acumulation: 0,
intl_format: null,
max: null,
format: ({format_array, format, value}) => {
let output = []
if (format_array.prefijo !== '') {
output.push(format_array.prefijo)
}
output.push(format.format(Math.round(value * Math.pow(10, format_array.decimales)) / Math.pow(10, format_array.decimales)))
if (format_array.sufijo !== '') {
output.push(format_array.sufijo)
}
return output.join('')
},
get: function() {
return {
transacciones: () => {
this.draw().loading()
let promises = []
sendGet(_urls.api + '/cuenta/' + this.cuenta_id + '/transacciones/amount').then((data) => {
if (data.cuenta === null) {
return
}
this.cuenta = data.cuenta
this.intl_format = Intl.NumberFormat('es-CL')
sendGet(_urls.api + '/cuenta/' + this.cuenta_id + '/categoria').then((resp) => {
this.cuenta.categoria = resp.categoria
}).then(() => {
//this.saldo = this.cuenta.saldo
$('#cuenta').html(this.cuenta.nombre + ' (' + this.cuenta.categoria.nombre + ')').append(
$('<i></i>').attr('class', 'square full icon').css('color', '#' + this.cuenta.tipo.color)
)
promises = this.get().transaccionesMes(this.date)
if (promises.length > 0) {
Promise.all(promises).then((data_arr) => {
this.transacciones = []
data_arr.forEach(data => {
if (data.transacciones === null || data.transacciones.length === 0) {
return
}
$.each(data.transacciones, (i, el) => {
const tr = new Transaccion(el)
tr.setCuenta(this.cuenta)
tr.setModal(this.modal)
this.transacciones.push(tr)
})
})
this.transacciones.sort((a, b) => {
return (new Date(b.fecha)) - (new Date(a.fecha))
})
}).then(() => {
this.get().transaccionesAcumulacion(this.date).then((response) => {
this.acumulation = response.acumulation
this.saldo = response.acumulation
}).then(() => {
this.draw().table()
this.draw().acumulation()
})
})
} else {
this.draw().table()
}
})
})
},
transaccionesLimit: () => {
let promises = []
const amount = data.transacciones
let step = 50
let start = 0
if (this.max !== null) {
step = Math.min(50, this.max)
start = Math.max(0, amount - this.max)
}
for (let i = start; i <= amount; i += step) {
promises.push(
sendGet(_urls.api + '/cuenta/' + this.cuenta_id + '/transacciones/' + step + '/' + i)
)
}
return promises
},
transaccionesMes: (mes) => {
let promises = []
promises.push(
sendGet(_urls.api + '/cuenta/' + this.cuenta_id + '/transacciones/month/' + [mes.getFullYear(), mes.getMonth() + 1, '1'].join('-'))
)
return promises
},
transaccionesAcumulacion: (mes) => {
return sendGet(_urls.api + '/cuenta/' + this.cuenta_id + '/transacciones/acum/' + [mes.getFullYear(), mes.getMonth() + 1, '1'].join('-'))
},
cuentas: () => {
return sendGet(_urls.api + '/cuentas').then((data) => {
if (data.cuentas === null || data.cuentas.length === 0) {
return
}
this.cuentas = data.cuentas
}).then(() => {
const select = this.modal.find("[name='cuenta']")
$.each(this.cuentas, (i, el) => {
this.get().categoria(i).then(() => {
select.append(
$('<option></option>').attr('value', el.id).html(el.nombre + ' (' + el.categoria.nombre + ')')
)
select.dropdown()
})
})
this.get().monedas().then(() => {
const select = this.modal.find("[name='moneda']")
$.each(this.monedas, (i, el) => {
const option = $('<option></option>').attr('value', el.id).html(el.denominacion)
if (el.id === '1') {
option.attr('selected', 'selected')
}
select.append(option)
})
})
})
},
categoria: (idx) => {
return sendGet(_urls.api + '/cuenta/' + this.cuentas[idx].id + '/categoria').then((data) => {
this.cuentas[idx].categoria = data.categoria
})
},
monedas: () => {
return sendGet(_urls.api + '/monedas').then((response) => {
this.monedas = response.monedas
})
}
}
},
draw: function() {
return {
loading: () => {
const parent = $(this.id)
parent.html('')
parent.append(
$('<tr></tr>').append(
$('<td></td>').attr('colspan', 7).append(
$('<div></div>').attr('class', 'ui active dimmer').append(
$('<div></div>').attr('class', 'ui indeterminate elastic text loader').html('Buscando los datos')
)
)
)
)
},
table: () => {
const parent = $(this.id)
parent.html('')
$.each(this.transacciones, (i, el) => {
this.saldo = this.saldo + parseInt(el.valor.valor)// * ((el.isIncrement()) ? 1 : -1)
parent.append(el.draw({saldo: this.saldo, format: this.intl_format, format_array: this.cuenta.moneda.format, format_call: this.format}))
})
},
acumulation: () => {
const parent = $(this.id)
parent.prepend(
$('<tr></tr>').append(
$('<td></td>').html('')
).append(
$('<td></td>').html('')
).append(
$('<td></td>').html('Acumulacion Anterior')
).append(
$('<td></td>').attr('class', 'right aligned').html('')
).append(
$('<td></td>').attr('class', 'right aligned').html('')
).append(
$('<td></td>').attr('class', 'right aligned').html(this.format({
format_array: this.cuenta.moneda.format,
format: this.intl_format,
value: this.acumulation
}))
).append(
$('<td></td>').attr('class', 'right aligned').html('')
)
)
}
}
},
add: function() {
return {
show: () => {
this.modal.find('form').trigger('reset')
modalToAdd(this.modal)
this.modal.modal('show')
},
exec: () => {
const fecha = $("[name='fecha']").val()
const valor = $("[name='valor']").val()
const cuenta = $("[name='cuenta']").val()
const data = JSON.stringify({
debito_id: (valor < 0) ? this.cuenta_id : cuenta,
credito_id: (valor < 0) ? cuenta : this.cuenta_id,
fecha: fecha,
glosa: $("[name='glosa']").val(),
detalle: $("[name='detalle']").val(),
valor: (valor < 0) ? -valor : valor,
moneda_id: $("[name='moneda']").val()
})
return sendPost(_urls.api + '/transacciones/add', data).then(() => {
this.modal.modal('hide')
this.get().transacciones()
})
}
}
},
edit: function() {
const id = $("[name='id']").val()
const fecha = $("[name='fecha']").val()
const cuenta = $("[name='cuenta']").val()
const glosa = $("[name='glosa']").val()
const detalle = $("[name='detalle']").val()
const valor = $("[name='valor']").val()
const moneda_id = $("[name='moneda']").val()
const data = JSON.stringify({
debito_id: (valor < 0) ? this.cuenta_id : cuenta,
credito_id: (valor < 0) ? cuenta : this.cuenta_id,
fecha,
glosa,
detalle,
valor: (valor < 0) ? -valor : valor,
moneda_id
})
return sendPut(_urls.api + '/transaccion/' + id + '/edit', data).then(() => {
this.modal.modal('hide')
this.get().transacciones()
})
},
changeMonth: function(dif) {
let d = this.date
d.setMonth(this.date.getMonth() + dif)
this.date = d
this.checkButtons()
$(this.mes).calendar('set date', this.date)
},
today: function() {
this.date = new Date()
this.checkButtons()
$(this.mes).calendar('set date', this.date)
},
changeYear: function(dif) {
let d = this.date
d.setFullYear(this.date.getFullYear() + dif)
this.date = d
this.checkButtons()
$(this.mes).calendar('set date', this.date)
},
checkButtons: function() {
let f = new Date()
if (this.date.getMonth() === f.getMonth() && this.date.getFullYear() === f.getFullYear()) {
$(this.buttons.right).addClass('disabled')
} else {
$(this.buttons.right).removeClass('disabled')
}
if (this.date.toDateString() === f.toDateString()) {
$(this.buttons.today).addClass('disabled')
} else {
$(this.buttons.today).removeClass('disabled')
}
if (this.date.getFullYear() === f.getFullYear()) {
$(this.buttons.next).addClass('disabled')
} else {
$(this.buttons.next).removeClass('disabled')
}
},
refresh: function () {
this.get().transacciones()
this.checkButtons()
},
build: function() {
return {
modal: () => {
this.modal = $('.ui.modal')
this.modal.modal()
this.modal.find('.close.icon').click(() => {
this.modal.modal('hide')
})
this.modal.find('form').submit((e) => {
e.preventDefault()
const add = $(e.currentTarget).find('.plus.icon')
if (add.length > 0) {
this.add().exec()
} else {
this.edit()
}
return false
})
this.modal.find('.ui.calendar').calendar({
type: 'date',
formatter: {
date: function(date, settings) {
if (!date) return ''
let day = ('00' + date.getDate()).slice(-2)
let month = ('00' + (date.getMonth() + 1)).slice(-2)
let year = date.getFullYear()
return year + '/' + month + '/' + day
}
},
maxDate: new Date()
})
this.get().cuentas()
},
mes: () => {
const meses = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Dicembre']
$(this.mes).calendar({
type: 'month',
initialDate: this.date,
maxDate: new Date(),
text: {
months: meses,
monthsShort: meses.map((item) => {item.slice(0, 3)})
},
formatter: {
date: function (date, settings) {
if (!date) return '';
return meses[date.getMonth()] + ', ' + date.getFullYear()
}
},
onChange: (date) => {
this.date = date
this.refresh()
}
})
},
buttons: () => {
$(this.buttons.today).click((e) => {
this.today()
})
$(this.buttons.right).click((e) => {
this.changeMonth(1)
})
$(this.buttons.left).click((e) => {
this.changeMonth(-1)
})
$(this.buttons.next).click(() => {
this.changeYear(1)
})
$(this.buttons.prev).click(() => {
this.changeYear(-1)
})
}
}
},
setup: function() {
this.build().modal()
this.build().mes()
this.build().buttons()
$(this.id).parent().find('#refresh').click(() => {
this.refresh()
})
$(this.id).parent().find('#add').click(() => {
this.add().show()
})
this.get().transacciones()
}
}

View File

@ -55,6 +55,7 @@ class TipoCuenta {
const tipos_cuentas = {
id: '#tipos_cuentas',
tipos: [],
modal: null,
getTipos: function() {
this.tipos = []
return sendGet(_urls.api + '/tipos/cuentas').then((data) => {

View File

@ -39,22 +39,32 @@ class Cuenta {
}
tr.append(td)
})
$("[data-id='" + this.categoria_id + "'][data-class='categoria']").after(tr)
const prev = this.prev()
prev.after(tr)
}
prev() {
let prev = $("[data-id='" + this.categoria_id + "'][data-class='categoria']")
let n = 0
while (prev.next().attr('data-class') === 'cuenta') {
prev = prev.next()
n ++;
if (n >= 100) {
return prev
}
}
return prev
}
remove() {
$("[data-id='" + this.id + "'][data-class='cuenta']").remove()
}
}
class Categoria {
constructor({id, nombre, tipo_id, tipo, activos, pasivos, ganancias, perdidas}) {
constructor({id, nombre, tipo_id, tipo, totales}) {
this.id = id
this.nombre = nombre
this.tipo_id = tipo_id
this.tipo = tipo
this.activos = activos
this.pasivos = pasivos
this.ganancias = ganancias
this.perdidas = perdidas
this.totales = totales
this.is_open = false
this.cuentas = []
}
@ -62,7 +72,7 @@ class Categoria {
this.tipos = tipos
}
draw({format}) {
const button = $('<button></button>').attr('class', 'ui mini compact icon button').append(
const button = $('<button></button>').attr('class', 'ui mini compact circular icon button').append(
$('<i></i>').attr('class', down_icon + ' icon')
).click((e) => {
const plus = button.find('.' + down_icon.replace(' ', '.') + '.icon')
@ -85,7 +95,7 @@ class Categoria {
)
$.each(this.tipos, (i, el) => {
tr.append(
$('<td></td>').attr('class', 'right aligned').html(format.format(this[el.descripcion.toLowerCase() + 's']))
$('<td></td>').attr('class', 'right aligned').html(format.format(this.totales[el.descripcion.toLowerCase() + 's']))
)
})
$("[data-id='" + this.tipo_id + "'][data-class='tipo_categoria']").after(tr)
@ -143,21 +153,18 @@ class Categoria {
}
}
class TipoCategoria {
constructor({id, descripcion, activo, activos, pasivos, ganancias, perdidas}) {
constructor({id, descripcion, activo, totales}) {
this.id = id
this.descripcion = descripcion
this.activo = activo
this.activos = activos
this.pasivos = pasivos
this.ganancias = ganancias
this.perdidas = perdidas
this.totales = totales
this.categorias = []
}
setTipos(tipos) {
this.tipos = tipos
}
draw({format}) {
const button = $('<button></button>').attr('class', 'ui mini compact icon button').append(
const button = $('<button></button>').attr('class', 'ui mini compact circular icon button').append(
$('<i></i>').attr('class', down_icon + ' icon')
).click((e) => {
const plus = button.find('.' + down_icon.replace(' ', '.') + '.icon')
@ -175,7 +182,7 @@ class TipoCategoria {
)
)
$.each(this.tipos, (i, el) => {
tr.append($('<td></td>').attr('class', 'right aligned').html(format.format(this[el.descripcion.toLowerCase() + 's'])))
tr.append($('<td></td>').attr('class', 'right aligned').html(format.format(this.totales[el.descripcion.toLowerCase() + 's'])))
})
return tr
}
@ -210,29 +217,61 @@ class TipoCategoria {
}
}
const cuentas = {
id: 'cuentas',
id: '#cuentas',
balance: 0,
tipos: [],
tipos_categorias: [],
build: function() {
return {
parent: (segment) => {
const tr = $('<tr></tr>').append(
$('<th></th>').attr('colspan', 3).html('Cuenta')
)
$.each(this.tipos, (i, el) => {
tr.append(
$('<th></th>').attr('class', 'right aligned').css('color', '#' + el.color).html(el.descripcion)
)
})
const table = $('<table></table>').attr('class', 'ui striped table').append(
$('<thead></thead>').append(tr)
)
const parent = $('<tbody></tbody>')
table.append(parent)
segment.append(table)
return parent
},
resultado: (segment) => {
segment.append(
$('<table></table>').attr('class', 'ui collapsing table').append(
$('<tr></tr>').append(
$('<td></td>').html('Ganancias')
).append(
$('<td></td>').attr('data-tipo', 'ganancias')
)
).append(
$('<tr></tr>').append(
$('<td></td>').html('Perdidas')
).append(
$('<td></td>').attr('data-tipo', 'perdidas')
)
).append(
$('<tr></tr>').append(
$('<td></td>').html('<b>Resultado</b>')
).append(
$('<td></td>').attr('data-tipo', 'resultado')
)
)
)
}
}
},
get: function() {
return {
parent: () => {
let parent = $('#' + this.id)
const segment = $(this.id)
let parent = segment.find('tbody')
if (parent.length === 0) {
const tr = $('<tr></tr>').append(
$('<th></th>').attr('colspan', 3).html('Cuenta')
)
$.each(this.tipos, (i, el) => {
tr.append(
$('<th></th>').attr('class', 'right aligned').css('color', '#' + el.color).html(el.descripcion)
)
})
const table = $('<table></table>').attr('class', 'ui striped table').append(
$('<thead></thead>').append(tr)
)
parent = $('<tbody></tbody>').attr('id', this.id)
table.append(parent)
$('h1.header').after(table)
parent = this.build().parent(segment)
}
return parent
},
@ -250,7 +289,7 @@ const cuentas = {
return
}
$.each(data.tipos, (i, el) => {
tipo = new TipoCategoria(el)
const tipo = new TipoCategoria(el)
tipo.setTipos(this.tipos)
this.tipos_categorias.push(tipo)
})
@ -263,6 +302,8 @@ const cuentas = {
this.balance = data
}).then(() => {
this.draw().balance()
}).then(() => {
this.draw().resultado()
})
}
}
@ -298,6 +339,17 @@ const cuentas = {
)
})
foot.append(tr)
},
resultado: () => {
const div = $('#resultado')
if (div.find("[data-tipo='resultado']").length === 0) {
div.html('')
this.build().resultado(div)
}
const format = Intl.NumberFormat('es-CL', {style: 'currency', currency: 'CLP'})
div.find("[data-tipo='ganancias']").html(format.format(this.balance['ganancias']))
div.find("[data-tipo='perdidas']").html(format.format(this.balance['perdidas']))
div.find("[data-tipo='resultado']").html('<b>' + format.format(this.balance['ganancias'] - this.balance['perdidas']) + '</b>')
}
}
},

View File

@ -0,0 +1,221 @@
class Archivo {
constructor({folder, filename}) {
this.folder = folder
this.filename = filename
this.modal = null
}
setModal(modal) {
this.modal = modal
return this
}
draw() {
return $('<tr></tr>').append(
$('<td></td>').append(
$('<a></a>').attr('class', 'item').attr('href', _urls.base + ['upload', this.folder, this.filename].join('/')).html(this.filename)
)
).append(
$('<td></td>').attr('class', 'right aligned').append(
$('<button></button>').attr('class', 'ui mini circular icon button').append(
$('<i></i>').attr('class', 'edit icon')
).click((e) => {
e.preventDefault()
const t = e.currentTarget
this.edit()
return false
})
).append(
$('<button></button>').attr('class', 'ui mini red circular icon button').append(
$('<i></i>').attr('class', 'remove icon')
).click((e) => {
e.preventDefault()
const t = e.currentTarget
this.remove()
return false
})
)
)
}
edit() {
this.modal.find('form').trigger('reset')
this.modal.find('form').find("[name='folder']").val(this.folder)
this.modal.find('form').find("[name='old_filename']").val(this.filename)
this.modal.find('form').find("[name='filename']").val(this.filename)
this.modal.modal('show')
}
remove() {
return sendDelete([_urls.api, 'upload', this.folder, this.filename].join('/')).then((data) => {
if (data.deleted) {
archivos.get()
}
})
}
}
const archivos = {
id: '#archivos',
archivos: [],
modals: {
add: null,
edit: null
},
build: function() {
return {
parent: (segment) => {
const table = $('<table></table>').attr('class', 'ui striped table').append(
$('<thead></thead>').append(
$('<tr></tr>').append(
$('<th></th>').html('Archivo')
).append(
$('<th></th>').attr('class', 'right aligned').append(
$('<button></button>').attr('class', 'ui tiny green circular icon button').append(
$('<i></i>').attr('class', 'plus icon')
).click((e) => {
e.preventDefault()
this.add()
return false
})
)
)
)
)
const parent = $('<tbody></tbody>')
table.append(parent)
segment.append(table)
return parent
}
}
},
get: function() {
return {
parent: () => {
const segment = $(this.id)
let parent = segment.find('tbody')
if (parent.length === 0) {
parent = this.build().parent(segment)
}
return parent
},
archivos: () => {
return sendGet(_urls.api + '/uploads').then((data) => {
if (data.files === null || data.files.length === 0) {
return
}
this.archivos = []
$.each(data.files, (i, el) => {
const arch = new Archivo(el)
arch.setModal(this.modals.edit)
this.archivos.push(arch)
})
}).then(() => {
this.draw()
})
},
cuentas: () => {
return sendGet(_urls.api + '/cuentas')
}
}
},
draw: function() {
const tbody = this.get().parent()
tbody.empty()
$.each(this.archivos, (i, el) => {
tbody.append(el.draw())
})
},
add: function() {
this.modals.add.find('form').trigger('reset')
this.modals.add.find("[name='cuenta']").dropdown('clear')
this.modals.add.modal('show')
},
doAdd: function() {
const data = new FormData(this.modals.add.find('form')[0])
return sendPost(_urls.api + '/uploads/add', data, true).then((resp) => {
this.modals.add.modal('hide')
this.get().archivos()
})
},
doEdit: function() {
const folder = this.modals.edit.find("[name='folder']").val()
const filename = this.modals.edit.find("[name='old_filename']").val()
const data = JSON.stringify({
cuenta: this.modals.edit.find("[name='cuenta']").val(),
fecha: this.modals.edit.find("[name='fecha']").val()
})
sendPut([_urls.api, 'upload', folder, filename].join('/'), data).then((resp) => {
this.modals.edit.modal('hide')
if (resp.edited) {
this.get().archivos()
}
})
},
updateCalendar: function(modal) {
const today = new Date()
const start = new Date(today.getFullYear(), today.getMonth() - 1)
modal.find('.ui.calendar').calendar({
type: 'month',
initialDate: start,
maxDate: start,
months: ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'],
monthsShort: ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'],
formatter: {
date: function(date, settings) {
if (!date) return ''
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
return [year, month].join('-')
}
}
})
},
updateCuentas: function(modal, data) {
if (data.cuentas === null || data.cuentas.length === 0) {
return
}
const select = modal.find("select[name='cuenta']")
let values = []
$.each(data.cuentas, (i, el) => {
const nombre = [el.nombre, el.categoria.nombre].join(' - ')
values.push({
name: nombre,
value: el.id,
text: nombre
})
})
select.dropdown({values})
},
setupModal: function() {
this.modals.add = $('#add_modal')
this.modals.edit = $('#edit_modal')
$.each(this.modals, (i, el) => {
el.modal().find('.close.icon').click(() => {
el.modal('hide')
})
this.updateCalendar(el)
})
this.modals.add.find('form').submit((e) => {
e.preventDefault()
this.doAdd()
return false
})
this.modals.add.find('#archivo_btn').css('cursor', 'pointer').click(() => {
this.modals.add.find("[name='archivo']").trigger('click')
})
this.modals.add.find("[name='archivo']").change((e) => {
const arch = $(e.currentTarget)
const filename = arch[0].files[0].name
this.modals.add.find('#archivo_btn').find('input').val(filename)
})
this.modals.edit.find('form').submit((e) => {
e.preventDefault()
this.doEdit()
return false
})
this.get().cuentas().then((data) => {
this.updateCuentas(this.modals.add, data)
this.updateCuentas(this.modals.edit, data)
})
},
setup: function() {
this.setupModal()
this.get().archivos()
}
}

View File

@ -1,7 +1,12 @@
<?php
require_once implode(DIRECTORY_SEPARATOR, [
dirname(__DIR__),
'setup',
'app.php'
]);
$app->run();
try {
require_once implode(DIRECTORY_SEPARATOR, [
dirname(__DIR__),
'setup',
'app.php'
]);
$app->run();
} catch (Error | Exception $e) {
error_log($e);
throw $e;
}

View File

@ -0,0 +1,4 @@
<?php
use Contabilidad\Common\Controller\Importar;
$app->get('/importar[/]', Importar::class);

View File

@ -0,0 +1,10 @@
<?php
use Contabilidad\Common\Controller\Uploads;
$app->group('/uploads', function($app) {
$app->get('/add', [Uploads::class, 'upload']);
$app->get('[/]', Uploads::class);
});
$app->group('/upload/{folder}/{filename}', function($app) {
$app->get('[/]', [Uploads::class, 'get']);
});

View File

@ -8,7 +8,7 @@
Categorías
@endif
</h1>
<div class="ui segment">
<div class="ui basic fitted segment">
@yield('categorias_content')
</div>
@endsection

View File

@ -28,7 +28,7 @@
@endsection
@push('scripts')
<script type="text/javascript" src="{{$urls->scripts}}/categorias.list.js"></script>
<script type="text/javascript" src="{{$urls->scripts}}/categorias/list.js"></script>
<script type="text/javascript">
$(document).ready(() => {
categorias.setup()

View File

@ -8,7 +8,7 @@
Tipos Categoría
@endif
</h1>
<div class="ui segment">
<div class="ui basic fitted segment">
@yield('tipos_categorias_content')
</div>
@endsection

View File

@ -24,7 +24,7 @@
@endsection
@push('scripts')
<script type="text/javascript" src="{{$urls->scripts}}/tipos_categorias.list.js"></script>
<script type="text/javascript" src="{{$urls->scripts}}/categorias/tipos/list.js"></script>
<script type="text/javascript">
$(document).ready(() => {
tipos_categorias.setup()

View File

@ -1 +1,8 @@
@extends('config.base')
@section('config_content')
<h3>Configuraciones Generales</h3>
<div class="ui basic segment">
</div>
@endsection

View File

@ -1,4 +1,5 @@
<div class="ui vertical fluid borderless menu">
@include('config.menu.tipos_categorias')
@include('config.menu.tipos_cuentas')
@include('config.menu.files')
</div>

View File

@ -0,0 +1,4 @@
<a class="item" href="{{$urls->base}}uploads">
Archivos
</a>

View File

@ -8,7 +8,7 @@
Cuentas
@endif
</h1>
<div class="ui segment">
<div class="ui basic fitted segment">
@yield('cuentas_content')
</div>
@endsection

View File

@ -32,7 +32,7 @@
@endsection
@push('scripts')
<script type="text/javascript" src="{{$urls->scripts}}/cuentas.list.js"></script>
<script type="text/javascript" src="{{$urls->scripts}}/cuentas/list.js"></script>
<script type="text/javascript">
$(document).ready(() => {
cuentas.setup()

View File

@ -5,6 +5,29 @@
@endsection
@section('cuentas_content')
<div class="ui right aligned header">
<div class="ui calendar" id="mes">
<div class="ui input right icon">
<input type="text" placeholder="Mes" />
<i class="calendar icon"></i>
</div>
</div>
<div class="ui icon button" id="prev_button">
<i class="double left angle icon"></i>
</div>
<div class="ui icon button" id="left_button">
<i class="left angle icon"></i>
</div>
<div class="ui icon disabled button" id="today_button">
<i class="calendar day icon"></i>
</div>
<div class="ui icon disabled button" id="right_button">
<i class="right angle icon"></i>
</div>
<div class="ui icon disabled button" id="next_button">
<i class="double right angle icon"></i>
</div>
</div>
<table class="ui striped table">
<thead>
<tr>
@ -27,23 +50,16 @@
Saldo
</th>
<th class="right aligned">
<button class="ui tiny green circular icon button">
<button class="ui tiny blue circular icon button" id="refresh">
<i class="refresh icon"></i>
</button>
<button class="ui tiny green circular icon button" id="add">
<i class="plus icon"></i>
</button>
</th>
</tr>
</thead>
<tbody id="transacciones">
<tr>
<td colspan="7">
<div class="ui active dimmer">
<div class="ui indeterminate elastic text loader">
Buscando los datos
</div>
</div>
</td>
</tr>
</tbody>
<tbody id="transacciones"></tbody>
</table>
<div class="ui modal">
@ -62,7 +78,7 @@
</div>
<div class="field">
<label>Cuenta</label>
<select name="cuenta"></select>
<select name="cuenta" class="ui searchable dropdown"></select>
</div>
<div class="field">
<label>Glosa</label>
@ -72,9 +88,15 @@
<label>Detalle</label>
<input type="text" name="detalle" />
</div>
<div class="field">
<label>Valor</label>
<input type="text" name="valor" />
<div class="fields">
<div class="field">
<label>Valor</label>
<input type="text" name="valor" />
</div>
<div class="field">
<lable>Moneda</lable>
<select name="moneda"></select>
</div>
</div>
<button class="ui icon button">
<i class="plus icon"></i>
@ -85,10 +107,11 @@
@endsection
@push('scripts')
<script type="text/javascript" src="{{$urls->scripts}}/cuentas.show.js"></script>
<script type="text/javascript" src="{{$urls->scripts}}/cuentas/show.js"></script>
<script type="text/javascript">
$(document).ready(() => {
transacciones.cuenta_id = {{$cuenta_id}}
transacciones.max = {{$max_transacciones ?? 100}}
transacciones.setup()
})
</script>

View File

@ -25,7 +25,7 @@
@push('scripts')
<script type="text/javascript" src="{{$urls->scripts}}/colorpicker.js"></script>
<script type="text/javascript" src="{{$urls->scripts}}/tipos_cuentas.list.js"></script>
<script type="text/javascript" src="{{$urls->scripts}}/cuentas/tipos/list.js"></script>
<script type="text/javascript">
$(document).ready(() => {
tipos_cuentas.setup()

View File

@ -4,6 +4,8 @@
<h1 class="ui header">
Contabilidad
</h1>
<div id="cuentas" class="ui basic fitted segment"></div>
<div id="resultado" class="ui basic fitted segment"></div>
@endsection
@push('scripts')

View File

@ -0,0 +1,92 @@
@extends('layout.base')
@section('page_title')
Importar
@endsection
@section('page_content')
<h1>Importar</h1>
<form class="ui form" action="#" method="post" id="importar_form" enctype="multipart/form-data">
<div class="two wide field">
<label>Fecha</label>
<div class="ui date calendar">
<div class="ui icon input">
<input type="text" name="fecha" />
<i class="calendar outline icon"></i>
</div>
</div>
</div>
<div class="six wide field">
<label>Cuenta</label>
<select name="cuenta"></select>
</div>
<div class="inline field">
<input type="file" name="archivo" style="display: none;" />
<div class="ui labeled icon input" id="archivo_btn">
<div class="ui label">Archivo</div>
<input type="text" readonly="" />
<i class="search icon"></i>
</div>
</div>
<button class="ui button">Importar</button>
</form>
@endsection
@push('scripts')
<script type="text/javascript">
function getCuentas() {
sendGet(_urls.api + '/cuentas').then((data) => {
if (data.cuentas === null || data.cuentas.length === 0) {
return
}
const select = $("select[name='cuenta']")
let values = []
$.each(data.cuentas, (i, el) => {
const nombre = [el.nombre, el.categoria.nombre].join(' - ')
values.push({
name: nombre,
value: el.id,
text: nombre
})
})
select.dropdown({values})
})
}
$(document).ready(() => {
getCuentas()
const today = new Date()
const start = new Date(today.getFullYear(), today.getMonth() - 1)
$('.ui.calendar').calendar({
type: 'month',
initialDate: start,
maxDate: start,
months: ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'],
monthsShort: ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'],
formatter: {
date: function(date, settings) {
if (!date) return ''
const year = date.getFullYear()
const month = date.getMonth() + 1
return [year, month].join('-')
}
}
})
$('#archivo_btn').css('cursor', 'pointer').click(() => {
$("[name='archivo']").trigger('click')
})
$("[name='archivo']").change((e) => {
const arch = $(e.currentTarget)
const filename = arch[0].files[0].name
$('#archivo_btn').find('input').val(filename)
})
$('#importar_form').submit((e) => {
e.preventDefault()
const data = new FormData(e.currentTarget)
sendPost(_urls.api + '/import', data, true).then((resp) => {
console.debug(resp)
})
return false
})
})
</script>
@endpush

View File

@ -7,7 +7,18 @@
base: '{{$urls->base}}',
api: '{{$urls->api}}'
}
function buildAjax(url, method) {
function buildAjax(url, method, files=false) {
if (files) {
return {
url: url,
headers: {
'Authorization': 'Bearer ' + API_KEY
},
method: method,
processData: false,
contentType: false
}
}
return {
url: url,
headers: {
@ -21,8 +32,8 @@
let ajax_obj = buildAjax(url, 'GET')
return $.ajax(ajax_obj)
}
function sendPost(url, data) {
let ajax_obj = buildAjax(url, 'POST')
function sendPost(url, data, files=false) {
let ajax_obj = buildAjax(url, 'POST', files)
ajax_obj['data'] = data
return $.ajax(ajax_obj)
}

Some files were not shown because too many files have changed in this diff Show More