Compare commits

...

11 Commits

Author SHA1 Message Date
9d2504f016 UI 2021-12-06 22:10:57 -03:00
8ef4ab1c7d API 2021-12-06 22:10:41 -03:00
10b2485cfd Ignore uploads 2021-12-06 22:10:30 -03:00
0382f8c286 Dockerfile 2021-12-06 22:10:12 -03:00
a3311f805e Env files samples 2021-12-06 22:08:48 -03:00
378de3ed86 Docker 2021-12-06 22:08:05 -03:00
69c2cffa6c Docker 2021-12-06 22:05:13 -03:00
61448a2521 Python working 2021-11-02 22:12:25 -03:00
b0f7c9b2b1 Cleanup 2021-11-02 22:12:11 -03:00
0c44554375 Camelot reading 2021-11-02 15:37:36 -03:00
5ee267568a PDF reading with python 2021-11-01 11:09:26 -03:00
133 changed files with 3856 additions and 306 deletions

1
.api.env.sample Normal file
View File

@ -0,0 +1 @@
API_KEY=

2
.gitignore vendored
View File

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

8
.idea/.gitignore generated vendored Normal file
View File

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

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

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

81
.idea/contabilidad.iml generated Normal file
View File

@ -0,0 +1,81 @@
<?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 Normal file
View File

@ -0,0 +1,8 @@
<?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 Normal file
View File

@ -0,0 +1,87 @@
<?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>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

1
.python.env.sample Normal file
View File

@ -0,0 +1 @@
PYTHON_KEY=

11
TODO.md Normal file
View File

@ -0,0 +1,11 @@
# Contabilidad
1. Obtener pdf de cartolas desde email.
1. Conectar a Email por IMAP.
1. Buscar emails con cartolas.
1. Descargar cartolas.
1. Guardar de forma ordenada.
1. Extraer información e ingresar a base de datos a traves de API.
1. Abrir archivos y leer.
1. Formatear datos.
1. Mandar a API.

View File

@ -1,5 +1,9 @@
FROM php:8-fpm
RUN docker-php-ext-install pdo pdo_mysql
RUN apt-get update -y && apt-get install -y git libzip-dev zip libpng-dev libfreetype6-dev libjpeg62-turbo-dev tesseract-ocr
RUN docker-php-ext-configure gd --with-freetype --with-jpeg && docker-php-ext-install pdo pdo_mysql zip gd
COPY --from=composer /usr/bin/composer /usr/bin/composer
WORKDIR /app

View File

@ -0,0 +1,6 @@
<?php
namespace Contabilidad\Common\Alias;
interface DocumentHandler {
public function load(): ?array;
}

View File

@ -0,0 +1,11 @@
<?php
namespace Contabilidad\Common\Concept;
use Contabilidad\Common\Alias\DocumentHandler as HandlerInterface;
abstract class DocumentHandler implements HandlerInterface {
protected string $folder;
public function __construct(string $source_folder) {
$this->folder = $source_folder;
}
}

View File

@ -11,4 +11,17 @@ class Base {
public function __invoke(Request $request, Response $response): Response {
return $this->withJson($response, []);
}
public function generate_key(Request $request, Response $response): Response {
$server_addr = explode('.', $request->getServerParams()['SERVER_ADDR']);
$remote_addr = explode('.', $request->getServerParams()['REMOTE_ADDR']);
for ($i = 0; $i < 3; $i ++) {
if ($server_addr[$i] != $remote_addr[$i]) {
throw new \InvalidArgumentException('Invalid connection address.');
}
}
$salt = mt_rand();
$signature = hash_hmac('sha256', $salt, 'contabilidad', true);
$key = urlencode(base64_encode($signature));
return $this->withJson($response, ['key' => $key]);
}
}

View File

@ -5,13 +5,34 @@ use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use ProVM\Common\Define\Controller\Json;
use ProVM\Common\Factory\Model as Factory;
use Contabilidad\Common\Service\TiposCambios as Service;
use Contabilidad\Categoria;
class Categorias {
use Json;
public function __invoke(Request $request, Response $response, Factory $factory): Response {
$categorias = $factory->find(Categoria::class)->array();
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;
}
$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']);
@ -68,14 +89,14 @@ class Categorias {
];
return $this->withJson($response, $output);
}
public function cuentas(Request $request, Response $response, Factory $factory, $categoria_id): Response {
public function cuentas(Request $request, Response $response, Factory $factory, Service $service, $categoria_id): Response {
$categoria = $factory->find(Categoria::class)->one($categoria_id);
$cuentas = null;
if ($categoria !== null) {
$cuentas = $categoria->cuentas();
if ($cuentas !== null) {
array_walk($cuentas, function(&$item) {
$item = $item->toArray();
array_walk($cuentas, function(&$item) use ($service) {
$item = $item->toArray($service);
});
}
}

View File

@ -5,6 +5,7 @@ use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use ProVM\Common\Define\Controller\Json;
use ProVM\Common\Factory\Model as Factory;
use Contabilidad\Common\Service\TiposCambios as Service;
use Contabilidad\Cuenta;
class Cuentas {
@ -14,11 +15,15 @@ class Cuentas {
$cuentas = $factory->find(Cuenta::class)->array();
if ($cuentas) {
usort($cuentas, function($a, $b) {
$c = strcmp($a['categoria']['nombre'], $b['categoria']['nombre']);
if ($c == 0) {
return strcmp($a['nombre'], $b['nombre']);
$t = strcmp($a['tipo']['descripcion'], $b['tipo']['descripcion']);
if ($t != 0) {
return $t;
}
$c = strcmp($a['categoria']['nombre'], $b['categoria']['nombre']);
if ($c != 0) {
return $c;
}
return strcmp($a['nombre'], $b['nombre']);
});
}
$output = [
@ -90,15 +95,28 @@ class Cuentas {
];
return $this->withJson($response, $output);
}
public function transacciones(Request $request, Response $response, Factory $factory, $cuenta_id, $limit = null, $start = 0): Response {
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;
if ($cuenta !== null) {
$transacciones = $cuenta->transacciones($limit, $start);
if (count($transacciones)) {
array_walk($transacciones, function(&$item) {
$item = $item->toArray();
});
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;
}
}
}
$output = [

View File

@ -0,0 +1,21 @@
<?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 ProVM\Common\Factory\Model as Factory;
use Contabilidad\Common\Service\DocumentHandler as Handler;
class Import {
use Json;
public function __invoke(Request $request, Response $response, Factory $factory): Response {
$post = $request->getParsedBody();
return $this->withJson($response, $post);
}
public function uploads(Request $request, Response $response, Handler $handler): Response {
$output = $handler->handle();
return $this->withJson($response, $output);
}
}

View File

@ -0,0 +1,71 @@
<?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 ProVM\Common\Factory\Model as Factory;
use Contabilidad\Moneda;
class Monedas {
use Json;
public function __invoke(Request $request, Response $response, Factory $factory): Response {
$monedas = $factory->find(Moneda::class)->array();
if ($monedas) {
usort($monedas, function($a, $b) {
return strcmp($a['denominacion'], $b['denominacion']);
});
}
$output = [
'monedas' => $monedas
];
return $this->withJson($response, $output);
}
public function show(Request $request, Response $response, Factory $factory, $moneda_id): Response {
$moneda = $factory->find(Moneda::class)->one($moneda_id);
$output = [
'input' => $moneda_id,
'moneda' => $moneda?->toArray()
];
return $this->withJson($response, $output);
}
public function add(Request $request, Response $response, Factory $factory): Response {
$input = json_decode($request->getBody());
$results = [];
if (is_array($input)) {
foreach ($input as $in) {
$moneda = Moneda::add($factory, $in);
$results []= ['moneda' => $moneda?->toArray(), 'agregado' => $moneda?->save()];
}
} else {
$moneda = Moneda::add($factory, $input);
$results []= ['moneda' => $moneda?->toArray(), 'agregado' => $moneda?->save()];
}
$output = [
'input' => $input,
'monedas' => $results
];
return $this->withJson($response, $output);
}
public function edit(Request $request, Response $response, Factory $factory, $moneda_id): Response {
$moneda = $factory->find(Moneda::class)->one($moneda_id);
$output = [
'input' => $moneda_id,
'old' => $moneda->toArray()
];
$input = json_decode($request->getBody());
$moneda->edit($input);
$output['moneda'] = $moneda->toArray();
return $this->withJson($response, $output);
}
public function delete(Request $request, Response $response, Factory $factory, $moneda_id): Response {
$moneda = $factory->find(Moneda::class)->one($moneda_id);
$output = [
'input' => $moneda_id,
'moneda' => $moneda->toArray(),
'eliminado' => $moneda->delete()
];
return $this->withJson($response, $output);
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace Contabilidad\Common\Controller;
use Carbon\Carbon;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use ProVM\Common\Define\Controller\Json;
use ProVM\Common\Factory\Model as Factory;
use Contabilidad\Common\Service\TiposCambios as Service;
use Contabilidad\TipoCambio;
class TiposCambios {
use Json;
public function __invoke(Request $request, Response $response, Factory $factory): Response {
$tipos = $factory->find(TipoCambio::class)->array();
if ($tipos) {
usort($tipos, function($a, $b) {
return strcmp($a['fecha'], $b['fecha']);
});
}
$output = [
'tipos' => $tipos
];
return $this->withJson($response, $output);
}
public function show(Request $request, Response $response, Factory $factory, $tipo_id): Response {
$tipo = $factory->find(TipoCambio::class)->one($tipo_id);
$output = [
'input' => $tipo_id,
'tipo' => $tipo?->toArray()
];
return $this->withJson($response, $output);
}
public function add(Request $request, Response $response, Factory $factory): Response {
$input = json_decode($request->getBody());
$results = [];
if (is_array($input)) {
foreach ($input as $in) {
$tipo = TipoCambio::add($factory, $in);
$results []= ['tipo' => $tipo?->toArray(), 'agregado' => $tipo?->save()];
}
} else {
$tipo = TipoCambio::add($factory, $input);
$results []= ['tipo' => $tipo?->toArray(), 'agregado' => $tipo?->save()];
}
$output = [
'input' => $input,
'tipos' => $results
];
return $this->withJson($response, $output);
}
public function edit(Request $request, Response $response, Factory $factory, $tipo_id): Response {
$tipo = $factory->find(TipoCambio::class)->one($tipo_id);
$output = [
'input' => $tipo_id,
'old' => $tipo->toArray()
];
$input = json_decode($request->getBody());
$tipo->edit($input);
$output['tipo'] = $tipo->toArray();
return $this->withJson($response, $output);
}
public function delete(Request $request, Response $response, Factory $factory, $tipo_id): Response {
$tipo = $factory->find(TipoCambio::class)->one($tipo_id);
$output = [
'input' => $tipo_id,
'tipo' => $tipo->toArray(),
'eliminado' => $tipo->delete()
];
return $this->withJson($response, $output);
}
public function obtain(Request $request, Response $response, Factory $factory, Service $service): Response {
$post = $request->getParsedBody();
$valor = $service->get($post['fecha'], $post['moneda_id']);
if ($valor === null) {
return $this->withJson($response, ['input' => $post, 'tipo' => null, 'error' => 'No se encontró valor']);
}
$data = [
'fecha' => $post['fecha'],
'desde_id' => $post['moneda_id'],
'hasta_id' => 1,
'valor' => $valor
];
$tipo = TipoCambio::add($factory, $data);
if ($tipo !== false and $tipo->is_new()) {
$tipo->save();
}
$output = [
'input' => $post,
'tipo' => $tipo?->toArray()
];
return $this->withJson($response, $output);
}
}

View File

@ -0,0 +1,139 @@
<?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 ProVM\Common\Factory\Model as Factory;
use Contabilidad\Common\Service\TiposCambios as Service;
use Contabilidad\TipoCategoria;
class TiposCategorias {
use Json;
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;
}
$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']);
});
}
$output = [
'tipos' => $tipos
];
return $this->withJson($response, $output);
}
public function add(Request $request, Response $response, Factory $factory): Response {
$input = json_decode($request->getBody());
$results = [];
if (is_array($input)) {
foreach ($input as $in) {
$tipo = TipoCategoria::add($factory, $in);
$results []= ['tipo' => $tipo?->toArray(), 'agregado' => $tipo?->save()];
}
} else {
$tipo = TipoCategoria::add($factory, $input);
$results []= ['tipo' => $tipo?->toArray(), 'agregado' => $tipo?->save()];
}
$output = [
'input' => $input,
'tipos' => $results
];
return $this->withJson($response, $output);
}
public function edit(Request $request, Response $response, Factory $factory, $tipo_id): Response {
$tipo = $factory->find(TipoCategoria::class)->one($tipo_id);
$output = [
'input' => $tipo_id,
'old' => $tipo->toArray()
];
$input = json_decode($request->getBody());
$tipo->edit($input);
$output['tipo'] = $tipo->toArray();
return $this->withJson($response, $output);
}
public function delete(Request $request, Response $response, Factory $factory, $tipo_id): Response {
$tipo = $factory->find(TipoCategoria::class)->one($tipo_id);
$output = [
'input' => $tipo_id,
'tipo' => $tipo->toArray(),
'eliminado' => $tipo->delete()
];
return $this->withJson($response, $output);
}
public function categorias(Request $request, Response $response, Factory $factory, Service $service, $tipo_id): Response {
$tipo = $factory->find(TipoCategoria::class)->one($tipo_id);
$categorias = null;
if ($tipo != null) {
$categorias = $tipo->categorias();
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);
});
}
$item = $arr;
});
}
}
$output = [
'input' => $tipo_id,
'tipo' => $tipo?->toArray(),
'categorias' => $categorias
];
return $this->withJson($response, $output);
}
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) {
$maps = ['activo', 'pasivo', 'ganancia', 'perdida'];
foreach ($maps as $m) {
$p = $m . 's';
$t = ucfirst($m);
if (!isset($sum[$p])) {
$sum[$p] = 0;
}
$cuentas = $item->getCuentasOf($t);
if ($cuentas === false or $cuentas === null) {
continue;
}
$sum[$p] += array_reduce($cuentas, function($sum, $item) use($service) {
return $sum + $item->saldo($service, true);
});
}
return $sum;
});
return $this->withJson($response, $balance);
}
}

View File

@ -0,0 +1,71 @@
<?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 ProVM\Common\Factory\Model as Factory;
use Contabilidad\TipoCuenta;
class TiposCuentas {
use Json;
public function __invoke(Request $request, Response $response, Factory $factory): Response {
$tipos = $factory->find(TipoCuenta::class)->array();
if ($tipos) {
usort($tipos, function($a, $b) {
return strcmp($a['descripcion'], $b['descripcion']);
});
}
$output = [
'tipos' => $tipos
];
return $this->withJson($response, $output);
}
public function show(Request $request, Response $response, Factory $factory, $tipo_id): Response {
$tipo = $factory->find(TipoCuenta::class)->one($tipo_id);
$output = [
'input' => $tipo_id,
'tipo' => $tipo?->toArray()
];
return $this->withJson($response, $output);
}
public function add(Request $request, Response $response, Factory $factory): Response {
$input = json_decode($request->getBody());
$results = [];
if (is_array($input)) {
foreach ($input as $in) {
$tipo = TipoCuenta::add($factory, $in);
$results []= ['tipo' => $tipo?->toArray(), 'agregado' => $tipo?->save()];
}
} else {
$tipo = TipoCuenta::add($factory, $input);
$results []= ['tipo' => $tipo?->toArray(), 'agregado' => $tipo?->save()];
}
$output = [
'input' => $input,
'tipos' => $results
];
return $this->withJson($response, $output);
}
public function edit(Request $request, Response $response, Factory $factory, $tipo_id): Response {
$tipo = $factory->find(TipoCuenta::class)->one($tipo_id);
$output = [
'input' => $tipo_id,
'old' => $tipo->toArray()
];
$input = json_decode($request->getBody());
$tipo->edit($input);
$output['tipo'] = $tipo->toArray();
return $this->withJson($response, $output);
}
public function delete(Request $request, Response $response, Factory $factory, $tipo_id): Response {
$tipo = $factory->find(TipoCuenta::class)->one($tipo_id);
$output = [
'input' => $tipo_id,
'tipo' => $tipo->toArray(),
'eliminado' => $tipo->delete()
];
return $this->withJson($response, $output);
}
}

View File

@ -0,0 +1,29 @@
<?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 Psr\Http\Message\ResponseFactoryInterface as Factory;
use Contabilidad\Common\Service\Auth as Service;
class Auth {
protected Factory $factory;
protected Service $service;
public function __construct(Factory $factory, Service $service) {
$this->factory = $factory;
$this->service = $service;
}
public function __invoke(Request $request, Handler $handler): Response {
if ($request->getMethod() == 'OPTIONS') {
return $handler->handle($request);
}
if (!$this->service->isValid($request)) {
$response = $this->factory->createResponse(401);
$response->getBody()->write(json_encode(['message' => 'Invalid API KEY.']));
return $response
->withHeader('Content-Type', 'application/json');
}
return $handler->handle($request);
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace Contabilidad\Common\Service;
use Psr\Http\Message\ServerRequestInterface as Request;
class Auth {
protected string $key;
public function __construct(string $api_key) {
$this->key = $api_key;
}
public function isValid(Request $request): bool {
if ($request->hasHeader('Authorization')) {
$sent_key = $this->getAuthKey($request->getHeader('Authorization'));
return $this->key == $sent_key;
}
if (isset($request->getParsedBody()['api_key'])) {
$sent_key = $request->getParsedBody()['api_key'];
return $this->key == $sent_key;
}
$post = $request->getParsedBody() ?? json_decode($request->getBody());
$sent_key = $this->getArrayKey($post);
if ($sent_key !== null) {
return $this->key == $sent_key;
}
$sent_key = $this->getArrayKey($request->getQueryParams());
return $this->key == $sent_key;
}
protected function getAuthKey($auth) {
if (is_array($auth)) {
$auth = $auth[0];
}
if (str_contains($auth, 'Bearer')) {
$auth = explode(' ', $auth)[1];
}
return $auth;
}
protected function getArrayKey($array) {
$posible_keys = [
'API_KEY',
'api_key',
];
foreach ($posible_keys as $key) {
if (isset($array[$key])) {
return $array[$key];
}
}
return null;
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Contabilidad\Common\Service;
use Contabilidad\Common\Concept\DocumentHandler;
class CsvHandler extends DocumentHandler {
public function load(): ?array {
$folder = $this->folder;
$files = new \DirectoryIterator($folder);
$output = [];
foreach ($files as $file) {
if ($file->isDir() or $file->getExtension() != 'csv') {
continue;
}
$bank = 'unknown';
$text = trim(file_get_contents($file->getRealPath()));
if (str_contains($text, 'SCOTIABANK')) {
$bank = 'Scotiabank';
}
if (str_contains($text, 'BICE')) {
$bank = 'BICE';
}
$data = explode(PHP_EOL, $text);
array_walk($data, function(&$item) {
$item = trim($item, '; ');
if (str_contains($item, ';') !== false) {
$item = explode(';', $item);
}
});
$output []= ['bank' => $bank, 'filename' => $file->getBasename(), 'data' => $data];
}
return $this->build($output);
}
protected function build(array $data): ?array {
return $data;
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Contabilidad\Common\Service;
class DocumentHandler {
protected array $handlers;
public function __construct(array $handlers) {
$this->handlers = $handlers;
}
public function handle(): array {
$output = [];
foreach ($this->handlers as $handler) {
$output = array_merge($output, $handler->load());
}
return $output;
}
}

View File

@ -0,0 +1,75 @@
<?php
namespace Contabilidad\Common\Service;
use Contabilidad\Common\Concept\DocumentHandler;
use GuzzleHttp\Client;
class PdfHandler extends DocumentHandler {
protected Client $client;
protected string $url;
public function __construct(Client $client, string $pdf_folder, string $url) {
parent::__construct($pdf_folder);
$this->client = $client;
$this->url = $url;
}
public function load(): ?array {
$folder = $this->folder;
$files = new \DirectoryIterator($folder);
$output = [];
foreach ($files as $file) {
if ($file->isDir() or $file->getExtension() != 'pdf') {
continue;
}
$output []= ['filename' => $file->getBasename()];
}
$response = $this->client->post($this->url, ['json' => ['files' => $output]]);
$output = json_decode($response->getBody());
return $this->build($output);
}
protected function build(array $data): ?array {
foreach ($data as &$file) {
$i = $this->findStartRow($file->text);
if ($i === -1) {
continue;
}
$e = $this->findEndRow($file->text, $i);
if ($e == $i) {
continue;
}
$file->data = array_filter($file->text, function($key) use ($i, $e) {
return ($key >= $i) and ($key <= $e);
}, ARRAY_FILTER_USE_KEY);
}
return $data;
}
protected function findStartRow(array $data): int {
foreach ($data as $i => $row) {
if (!is_array($row)) {
continue;
}
$maybe = false;
foreach ($row as $cell) {
if (str_contains($cell, '/')) {
$maybe = true;
}
if ($maybe and str_contains($cell, '$')) {
return $i - 1;
}
}
}
return -1;
}
protected function findEndRow(array $data, int $start): int {
$l = count($data[$start]);
for ($i = $start; $i < count($data); $i ++) {
if (!is_array($data[$i])) {
return $i - 1;
}
if (count($data[$i]) != $l) {
return $i - 1;
}
}
return $start;
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace Contabilidad\Common\Service;
use Carbon\Carbon;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use ProVM\Common\Factory\Model as Factory;
use Contabilidad\Moneda;
use Contabilidad\TipoCambio;
class TiposCambios {
protected $client;
protected $factory;
protected $base_url;
protected $key;
public function __construct(Client $client, Factory $factory, $api_url, $api_key) {
$this->client = $client;
$this->factory = $factory;
$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);
}
}
$cambio = $moneda->cambio($fecha);
if ($cambio) {
if ($cambio->desde()->id != $moneda->id) {
return 1 / $cambio->valor;
}
return $cambio->valor;
}
$data = [
'fecha' => $fecha->format('Y-m-d'),
'desde' => $moneda->codigo
];
$headers = [
'Authorization' => 'Bearer ' . $this->key
];
$url = implode('/', [
$this->base_url,
'cambio',
'get'
]);
try {
$response = $this->client->request('POST', $url, ['json' => $data, 'headers' => $headers]);
} catch (ConnectException | RequestException $e) {
error_log($e);
return null;
}
if ($response->getStatusCode() !== 200) {
return null;
}
$result = json_decode($response->getBody());
$valor = $result->serie[0]->valor;
$data = [
'fecha' => $fecha->format('Y-m-d H:i:s'),
'desde_id' => $moneda->id,
'hasta_id' => 1,
'valor' => $valor
];
$tipo = TipoCambio::add($this->factory, $data);
if ($tipo !== false and $tipo->is_new()) {
$tipo->save();
}
return $valor;
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace Contabilidad\Common\Service;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
use thiagoalessio\TesseractOCR\TesseractOCR;
use Contabilidad\Common\Concept\DocumentHandler;
class XlsHandler extends DocumentHandler {
public function load(): ?array {
$folder = $this->folder;
$files = new \DirectoryIterator($folder);
$output = [];
foreach ($files as $file) {
if ($file->isDir() or $file->getExtension() != 'xls') {
continue;
}
$reader = IOFactory::createReader(ucfirst($file->getExtension()));
$xls = $reader->load($file->getRealPath());
$data = [];
$bank = 'unknown';
for ($s = 0; $s < $xls->getSheetCount(); $s ++) {
$sheet = $xls->getSheet($s);
foreach ($sheet->getRowIterator() as $row) {
$r = [];
foreach ($row->getCellIterator() as $cell) {
$r []= $cell->getValue();
}
$data []= $r;
}
foreach ($sheet->getDrawingCollection() as $drawing) {
if ($drawing instanceof MemoryDrawing) {
ob_start();
call_user_func(
$drawing->getRenderingFunction(),
$drawing->getImageResource()
);
$imageContents = ob_get_contents();
$size = ob_get_length();
ob_end_clean();
$ocr = new TesseractOCR();
$ocr->imageData($imageContents, $size);
$image = $ocr->run();
if (str_contains($image, 'BICE')) {
$bank = 'BICE';
}
if (str_contains($image, 'Scotiabank')) {
$bank = 'Scotiabank';
}
}
}
}
$output []= ['bank' => $bank, 'filename' => $file->getBasename(), 'data' => $data];
}
return $this->build($output);
}
protected function build(array $data): ?array {
return $data;
}
}

View File

@ -10,7 +10,13 @@
"zeuxisoo/slim-whoops": "^0.7.3",
"provm/controller": "^1.0",
"provm/models": "^1.0.0-rc3",
"nesbot/carbon": "^2.50"
"nesbot/carbon": "^2.50",
"robmorgan/phinx": "^0.12.9",
"odan/phinx-migrations-generator": "^5.4",
"martin-mikac/csv-to-phinx-seeder": "^1.6",
"guzzlehttp/guzzle": "^7.4",
"phpoffice/phpspreadsheet": "^1.19",
"thiagoalessio/tesseract_ocr": "^2.12"
},
"require-dev": {
"phpunit/phpunit": "^9.5",

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class TipoCategoria 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('tipos_categoria')
->addColumn('descripcion', 'string')
->addColumn('activo', 'boolean')
->create();
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class TipoEstadoConeccion 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('tipos_estado_coneccion')
->addColumn('descripcion', 'string')
->create();
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class TipoCuenta 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('tipos_cuenta')
->addColumn('descripcion', 'string')
->addColumn('color', 'string', ['length' => 6])
->create();
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class Categoria 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('categorias')
->addColumn('nombre', 'string')
->addColumn('tipo_id', 'integer')
->addForeignKey('tipo_id', 'tipos_categoria')
->create();
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class Coneccion 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('conecciones')
->addColumn('key', 'string')
->create();
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class Cuenta 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('cuentas')
->addColumn('nombre', 'string')
->addColumn('categoria_id', 'integer')
->addForeignKey('categoria_id', 'categorias')
->addColumn('tipo_id', 'integer')
->addForeignKey('tipo_id', 'tipos_cuenta')
->create();
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class EstadoConeccion 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('estados_coneccion')
->addColumn('coneccion_id', 'integer')
->addForeignKey('coneccion_id', 'conecciones')
->addColumn('fecha', 'date')
->addColumn('tipo_id', 'integer')
->addForeignKey('tipo_id', 'tipos_estado_coneccion')
->create();
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class Transaccion 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('transacciones')
->addColumn('debito_id', 'integer')
->addForeignKey('debito_id', 'cuentas')
->addColumn('credito_id', 'integer')
->addForeignKey('credito_id', 'cuentas')
->addColumn('fecha', 'datetime')
->addColumn('glosa', 'string')
->addColumn('detalle', 'text')
->addColumn('valor', 'double')
->create();
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class Moneda 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('monedas')
->addColumn('denominacion', 'string')
->addColumn('codigo', 'string', ['length' => 3])
->addColumn('prefijo', 'string', ['default' => ''])
->addColumn('sufijo', 'string', ['default' => ''])
->addColumn('decimales', 'integer', ['default' => 0])
->create();
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CuentaMoneda 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('cuentas')
->addColumn('moneda_id', 'integer')
->addForeignKey('moneda_id', 'monedas')
->update();
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class TipoCambio 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('tipos_cambio')
->addColumn('fecha', 'datetime')
->addColumn('desde_id', 'integer')
->addForeignKey('desde_id', 'monedas')
->addColumn('hasta_id', 'integer')
->addForeignKey('hasta_id', 'monedas')
->addColumn('valor', 'double')
->create();
}
}

41
api/db/seeds/Moneda.php Normal file
View File

@ -0,0 +1,41 @@
<?php
use Phinx\Seed\AbstractSeed;
class Moneda extends AbstractSeed
{
/**
* Run Method.
*
* Write your database seeder using this method.
*
* More information on writing seeders is available here:
* https://book.cakephp.org/phinx/0/en/seeding.html
*/
public function run()
{
$data = [
[
'denominacion' => 'Pesos Chilenos',
'codigo' => 'CLP',
'prefijo' => '$ '
],
[
'denominacion' => 'Dólar',
'codigo' => 'USD',
'prefijo' => 'US$ ',
'decimales' => 2
],
[
'denominacion' => 'Unidad de Fomento',
'codigo' => 'CLF',
'sufijo' => ' UF',
'decimales' => 2
]
];
$this->table('monedas')
->insert($data)
->saveData();
}
}

View File

@ -0,0 +1,36 @@
<?php
use Phinx\Seed\AbstractSeed;
class TipoCuenta extends AbstractSeed
{
/**
* Run Method.
*
* Write your database seeder using this method.
*
* More information on writing seeders is available here:
* https://book.cakephp.org/phinx/0/en/seeding.html
*/
public function run()
{
$data = [
[
'descripcion' => 'Ganancia'
],
[
'descripcion' => 'Activo'
],
[
'descripcion' => 'Pasivo'
],
[
'descripcion' => 'Perdida'
]
];
$this->table('tipos_cuenta')
->insert($data)
->saveData();
}
}

View File

@ -0,0 +1,30 @@
<?php
use Phinx\Seed\AbstractSeed;
class TipoEstadoConeccion extends AbstractSeed
{
/**
* Run Method.
*
* Write your database seeder using this method.
*
* More information on writing seeders is available here:
* https://book.cakephp.org/phinx/0/en/seeding.html
*/
public function run()
{
$data = [
[
'descripcion' => 'Activa'
],
[
'descripcion' => 'Inactiva'
]
];
$this->table('tipos_estado_coneccion')
->insert($data)
->saveData();
}
}

41
api/phinx.php Normal file
View File

@ -0,0 +1,41 @@
<?php
return
[
'paths' => [
'migrations' => '%%PHINX_CONFIG_DIR%%/db/migrations',
'seeds' => '%%PHINX_CONFIG_DIR%%/db/seeds'
],
'environments' => [
'default_migration_table' => 'phinxlog',
'default_environment' => 'development',
'production' => [
'adapter' => 'mysql',
'host' => 'db',
'name' => $_ENV['MYSQL_DATABASE'],
'user' => $_ENV['MYSQL_USER'],
'pass' => $_ENV['MYSQL_PASSWORD'],
'port' => '3306',
'charset' => 'utf8',
],
'development' => [
'adapter' => 'mysql',
'host' => 'db',
'name' => $_ENV['MYSQL_DATABASE'],
'user' => $_ENV['MYSQL_USER'],
'pass' => $_ENV['MYSQL_PASSWORD'],
'port' => '3306',
'charset' => 'utf8',
],
'testing' => [
'adapter' => 'mysql',
'host' => 'db',
'name' => $_ENV['MYSQL_DATABASE'],
'user' => $_ENV['MYSQL_USER'],
'pass' => $_ENV['MYSQL_PASSWORD'],
'port' => '3306',
'charset' => 'utf8',
]
],
'version_order' => 'creation'
];

2
api/php.ini Normal file
View File

@ -0,0 +1,2 @@
log_errors = true
error_log = /var/log/php/error.log

Binary file not shown.

Binary file not shown.

View File

@ -1,4 +1,6 @@
<?php
use Contabilidad\Common\Controller\Base;
$app->get('/key/generate[/]', [Base::class, 'generate_key']);
$app->get('/balance[/]', [Contabilidad\Common\Controller\TiposCategorias::class, 'balance']);
$app->get('/', Base::class);

View File

@ -0,0 +1,5 @@
<?php
use Contabilidad\Common\Controller\Import;
$app->post('/import', Import::class);
$app->get('/import/uploads', [Import::class, 'uploads']);

View File

@ -0,0 +1,12 @@
<?php
use Contabilidad\Common\Controller\Monedas;
$app->group('/monedas', function($app) {
$app->post('/add[/]', [Monedas::class, 'add']);
$app->get('[/]', Monedas::class);
});
$app->group('/moneda/{moneda_id}', function($app) {
$app->put('/edit', [Monedas::class, 'edit']);
$app->delete('/delete', [Monedas::class, 'delete']);
$app->get('[/]', [Monedas::class, 'show']);
});

View File

@ -0,0 +1,16 @@
<?php
$folder = implode(DIRECTORY_SEPARATOR, [
__DIR__,
'tipos'
]);
if (file_exists($folder)) {
$app->group('/tipos', function($app) use ($folder) {
$files = new DirectoryIterator($folder);
foreach ($files as $file) {
if ($file->isDir() or $file->getExtension() != 'php') {
continue;
}
include_once $file->getRealPath();
}
});
}

View File

@ -0,0 +1,13 @@
<?php
use Contabilidad\Common\Controller\TiposCambios;
$app->group('/cambios', function($app) {
$app->post('/obtener[/]', [TiposCambios::class, 'obtain']);
$app->post('/add[/]', [TiposCambios::class, 'add']);
$app->get('[/]', TiposCambios::class);
});
$app->group('/cambio/{tipo_id}', function($app) {
$app->put('/edit[/]', [TiposCambios::class, 'edit']);
$app->delete('/delete[/]', [TiposCambios::class, 'delete']);
$app->get('[/]', [TiposCambios::class, 'show']);
});

View File

@ -0,0 +1,13 @@
<?php
use Contabilidad\Common\Controller\TiposCategorias;
$app->group('/categorias', function($app) {
$app->post('/add[/]', [TiposCategorias::class, 'add']);
$app->get('[/]', TiposCategorias::class);
});
$app->group('/categoria/{tipo_id}', function($app) {
$app->get('/categorias', [TiposCategorias::class, 'categorias']);
$app->put('/edit', [TiposCategorias::class, 'edit']);
$app->delete('/delete', [TiposCategorias::class, 'delete']);
$app->get('[/]', [TiposCategorias::class, 'show']);
});

View File

@ -0,0 +1,12 @@
<?php
use Contabilidad\Common\Controller\TiposCuentas;
$app->group('/cuentas', function($app) {
$app->post('/add[/]', [TiposCuentas::class, 'add']);
$app->get('[/]', TiposCuentas::class);
});
$app->group('/cuenta/{tipo_id}', function($app) {
$app->put('/edit', [TiposCuentas::class, 'edit']);
$app->delete('/delete', [TiposCuentas::class, 'delete']);
$app->get('[/]', [TiposCuentas::class, 'show']);
});

View File

@ -31,7 +31,7 @@ $app->addRoutingMiddleware();
$app->add(new WhoopsMiddleware());
$folder = 'middlewares';
$folder = implode(DIRECTORY_SEPARATOR, [__DIR__, 'middlewares']);
if (file_exists($folder)) {
$files = new DirectoryIterator($folder);
foreach ($files as $file) {

View File

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

View File

@ -1,4 +1,7 @@
<?php
return [
'debug' => $_ENV['DEBUG'] ?? false
'debug' => $_ENV['DEBUG'] ?? false,
'api_key' => $_ENV['API_KEY'],
'python_api' => $_ENV['PYTHON_API'] ?? 'http://python:5000',
'python_key' => $_ENV['PYTHON_KEY']
];

View File

@ -14,6 +14,32 @@ return [
$arr['resources'],
'routes'
]);
$arr['public'] = implode(DIRECTORY_SEPARATOR, [
$arr['base'],
'public'
]);
$arr['uploads'] = implode(DIRECTORY_SEPARATOR, [
$arr['public'],
'uploads'
]);
$arr['pdfs'] = implode(DIRECTORY_SEPARATOR, [
$arr['uploads'],
'pdfs'
]);
$arr['csvs'] = implode(DIRECTORY_SEPARATOR, [
$arr['uploads'],
'csvs'
]);
$arr['xlss'] = implode(DIRECTORY_SEPARATOR, [
$arr['uploads'],
'xlss'
]);
return (object) $arr;
},
'urls' => function(Container $c) {
$arr = [
'python' => 'http://python:5000'
];
return (object) $arr;
}
];

View File

@ -0,0 +1,46 @@
<?php
use Psr\Container\ContainerInterface as Container;
return [
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'));
},
Contabilidad\Common\Middleware\Auth::class => function(Container $c) {
return new Contabilidad\Common\Middleware\Auth(
$c->get(Nyholm\Psr7\Factory\Psr17Factory::class),
$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\TiposCambios::class => function(Container $c) {
return new Contabilidad\Common\Service\TiposCambios(
$c->get(GuzzleHttp\Client::class),
$c->get(ProVM\Common\Factory\Model::class),
$c->get('python_api'),
$c->get('python_key')
);
}
];

View File

@ -1,15 +1,18 @@
<?php
namespace Contabilidad;
use Carbon\Carbon;
use ProVM\Common\Alias\Model;
use Contabilidad\Common\Service\TiposCambios as Service;
/**
* @property int $id
* @property string $nombre
* @property TipoCategoria $tipo_id
*/
class Categoria extends Model {
public static $_table = 'categorias';
protected static $fields = ['nombre'];
protected static $fields = ['nombre', 'tipo_id'];
protected $cuentas;
public function cuentas() {
@ -18,24 +21,72 @@ class Categoria extends Model {
}
return $this->cuentas;
}
protected $tipo;
public function tipo() {
if ($this->tipo === null) {
$this->tipo = $this->childOf(TipoCategoria::class, [Model::SELF_KEY => 'tipo_id']);
}
return $this->tipo;
}
public function getCuentasOf($tipo) {
return $this->factory->find(Cuenta::class)
->select([['cuentas', '*']])
->join([
['tipos_cuenta', 'tipos_cuenta.id', 'cuentas.tipo_id']
])
->where([
['tipos_cuenta.descripcion', $tipo],
['cuentas.categoria_id', $this->id]
])
->many();
}
protected $activos;
public function activos() {
if ($this->activos === null) {
$this->activos = $this->getCuentasOf('Activo');
}
return $this->activos();
}
protected $pasivos;
public function pasivos() {
if ($this->pasivos === null) {
$this->activos = $this->getCuentasOf('Pasivo');
}
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;
}
protected $saldo;
public function saldo() {
public function saldo(Service $service = null) {
if ($this->saldo === null) {
$this->saldo = 0;
if ($this->cuentas() !== null) {
$this->saldo = array_reduce($this->cuentas(), function($sum, $item) {
return $sum + $item->saldo();
});
$sum = 0;
$debitos = ['Activo', 'Perdida'];
foreach ($this->cuentas() as $cuenta) {
if (array_search($cuenta->tipo()->descripcion, $debitos) !== false) {
$sum -= $cuenta->saldo($service, true);
continue;
}
$sum += $cuenta->saldo($service, true);
}
$this->saldo = $sum;
}
}
return $this->saldo;
}
public function toArray(): array {
$arr = parent::toArray();
$arr['saldo'] = $this->saldo();
$arr['saldoFormateado'] = '$' . number_format($this->saldo(), 0, ',', '.');
return $arr;
}
}

21
api/src/Coneccion.php Normal file
View File

@ -0,0 +1,21 @@
<?php
namespace Contabilidad;
use ProVM\Common\Alias\Model;
/**
* @property int $id
* @property string $key
*/
class Coneccion extends Model {
public static $_table = 'conecciones';
protected static $fields = ['key'];
protected $estados;
public function estados() {
if ($this->estados === null) {
$this->estados = $this->parentOf(TipoEstadoConeccion::class, [Model::CHILD_KEY => 'coneccion_id']);
}
return $this->estados;
}
}

View File

@ -1,16 +1,20 @@
<?php
namespace Contabilidad;
use Carbon\Carbon;
use ProVM\Common\Alias\Model;
use Contabilidad\Common\Service\TiposCambios as Service;
/**
* @property int $id
* @property string $nombre
* @property Categoria $categoria_id
* @property TipoCuenta $tipo_id
* @property Moneda $moneda_id
*/
class Cuenta extends Model {
public static $_table = 'cuentas';
protected static $fields = ['nombre', 'categoria_id'];
protected static $fields = ['nombre', 'categoria_id', 'tipo_id', 'moneda_id'];
protected $categoria;
public function categoria() {
@ -19,26 +23,32 @@ class Cuenta extends Model {
}
return $this->categoria;
}
protected $entradas;
public function entradas() {
if ($this->entradas === null) {
$this->entradas = $this->parentOf(Entrada::class, [Model::CHILD_KEY => 'cuenta_id']);
protected $tipo;
public function tipo() {
if ($this->tipo === null) {
$this->tipo = $this->childOf(TipoCuenta::class, [Model::SELF_KEY => 'tipo_id']);
}
return $this->entradas;
return $this->tipo;
}
protected $moneda;
public function moneda() {
if ($this->moneda === null) {
$this->moneda = $this->childOf(Moneda::class, [Model::SELF_KEY => 'moneda_id']);
}
return $this->moneda;
}
protected $cargos;
public function cargos() {
if ($this->cargos === null) {
$this->cargos = $this->parentOf(Transaccion::class, [Model::CHILD_KEY => 'hasta_id']);
$this->cargos = $this->parentOf(Transaccion::class, [Model::CHILD_KEY => 'credito_id']);
}
return $this->cargos;
}
protected $abonos;
public function abonos() {
if ($this->abonos === null) {
$this->abonos = $this->parentOf(Transaccion::class, [Model::CHILD_KEY => 'desde_id']);
$this->abonos = $this->parentOf(Transaccion::class, [Model::CHILD_KEY => 'debito_id']);
}
return $this->abonos;
}
@ -46,7 +56,7 @@ class Cuenta extends Model {
public function transacciones($limit = null, $start = 0) {
if ($this->transacciones === null) {
$transacciones = Model::factory(Transaccion::class)
->join('cuentas', 'cuentas.id = transacciones.desde_id OR cuentas.id = transacciones.hasta_id')
->join('cuentas', 'cuentas.id = transacciones.debito_id OR cuentas.id = transacciones.credito_id')
->whereEqual('cuentas.id', $this->id)
->orderByAsc('transacciones.fecha');
if ($limit !== null) {
@ -64,7 +74,7 @@ class Cuenta extends Model {
return $this->transacciones;
}
protected $saldo;
public function saldo() {
public function saldo(Service $service = null, $in_clp = false) {
if ($this->saldo === null) {
$this->saldo = 0;
if (count($this->transacciones()) > 0) {
@ -73,14 +83,31 @@ class Cuenta extends Model {
});
}
}
if ($in_clp and $this->moneda()->codigo !== 'CLP') {
$fecha = Carbon::today();
if ($this->moneda()->codigo == 'USD') {
$fecha = match ($fecha->weekday()) {
0 => $fecha->subWeek()->weekday(5),
6 => $fecha->weekday(5),
default => $fecha
};
}
$service->get($fecha->format('Y-m-d'), $this->moneda()->id);
return $this->moneda()->cambiar($fecha, $this->saldo);
}
return $this->saldo;
}
public function toArray(): array {
public function format($valor) {
return $this->moneda()->format($valor);
}
public function toArray(Service $service = null, $in_clp = false): array {
$arr = parent::toArray();
$arr['categoria'] = $this->categoria()->toArray();
$arr['saldo'] = $this->saldo();
$arr['saldoFormateado'] = '$' . number_format($this->saldo(), 0, ',', '.');
$arr['tipo'] = $this->tipo()->toArray();
$arr['moneda'] = $this->moneda()->toArray();
$arr['saldo'] = $this->saldo($service, $in_clp);
$arr['saldoFormateado'] = $this->format($this->saldo($service, $in_clp));
return $arr;
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Contabilidad;
use ProVM\Common\Alias\Model;
/**
* @property int $id
* @property Coneccion $coneccion_id
* @property DateTime $fecha
* @property TipoEstadoConeccion $tipo_id
*/
class EstadoConeccion extends Model {
public static $_table = 'estados_coneccion';
protected static $fields = ['coneccion_id', 'fecha', 'tipo_id'];
protected $coneccion;
public function coneccion() {
if ($this->coneccion === null) {
$this->coneccion = $this->childOf(Coneccion::class, [Model::SELF_KEY => 'coneccion_id']);
}
return $this->coneccion;
}
protected $tipo;
public function tipo() {
if ($this->tipo === null) {
$this->tipo = $this->childOf(TipoEstadoConeccion::class, [Model::SELF_KEY => 'tipo_id']);
}
return $this->tipo;
}
}

46
api/src/Moneda.php Normal file
View File

@ -0,0 +1,46 @@
<?php
namespace Contabilidad;
use ProVM\Common\Alias\Model;
/**
* @property int $id
* @property string $denominacion
* @property string $codigo
* @property string $sufijo
* @property string $prefijo
* @property int $decimales
*/
class Moneda extends Model {
public static $_table = 'monedas';
protected static $fields = ['denominacion', 'codigo'];
public function format($valor) {
return implode('', [
$this->prefijo,
number_format($valor, $this->decimales, ',', '.'),
$this->sufijo
]);
}
public function cambio(\DateTime $fecha) {
$cambio = $this->factory->find(TipoCambio::class)
->where([['desde_id', $this->id], ['hasta_id', 1], ['fecha', $fecha->format('Y-m-d H:i:s')]])
->one();
if (!$cambio) {
$cambio = $this->factory->find(TipoCambio::class)
->where([['hasta_id', $this->id], ['desde_id', 1], ['fecha', $fecha->format('Y-m-d H:i:s')]])
->one();
}
return $cambio;
}
public function cambiar(\DateTime $fecha, float $valor) {
$cambio = $this->cambio($fecha);
if (!$cambio) {
return $valor;
}
if ($cambio->desde()->id != $this->id) {
return $cambio->transform($valor, TipoCambio::DESDE);
}
return $cambio->transform($valor);
}
}

50
api/src/TipoCambio.php Normal file
View File

@ -0,0 +1,50 @@
<?php
namespace Contabilidad;
use Carbon\Carbon;
use DateTime;
use ProVM\Common\Alias\Model;
/**
* @property int $id
* @property DateTime $fecha
* @property Moneda $desde_id
* @property Moneda $hasta_id
* @property float $valor
*/
class TipoCambio extends Model {
const DESDE = -1;
const HASTA = 1;
public static $_table = 'tipos_cambio';
protected static $fields = ['fecha', 'valor', 'desde_id', 'hasta_id'];
protected $desde;
public function desde() {
if ($this->desde === null) {
$this->desde = $this->childOf(Moneda::class, [Model::SELF_KEY => 'desde_id']);
}
return $this->desde;
}
protected $hasta;
public function hasta() {
if ($this->hasta === null) {
$this->hasta = $this->childOf(Moneda::class, [Model::SELF_KEY => 'hasta_id']);
}
return $this->hasta;
}
public function fecha(DateTime $fecha = null) {
if ($fecha === null) {
return Carbon::parse($this->fecha);
}
$this->fecha = $fecha->format('Y-m-d H:i:s');
return $this;
}
public function transform(float $valor, int $direction = TipoCambio::HASTA): float {
if ($direction == TipoCambio::HASTA) {
return $valor * $this->valor;
}
return $valor / $this->valor;
}
}

46
api/src/TipoCategoria.php Normal file
View File

@ -0,0 +1,46 @@
<?php
namespace Contabilidad;
use ProVM\Common\Alias\Model;
use Contabilidad\Common\Service\TiposCambios as Service;
/**
* @property int $id
* @property string $descripcion
* @property int $activo
*/
class TipoCategoria extends Model {
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']);
}
return $this->categorias;
}
public function getCuentasOf($tipo) {
return $this->factory->find(Cuenta::class)
->select([['cuentas', '*']])
->join([
['tipos_cuenta', 'tipos_cuenta.id', 'cuentas.tipo_id'],
['categorias', 'categorias.id', 'cuentas.categoria_id']
])
->where([
['tipos_cuenta.descripcion', $tipo],
['categorias.tipo_id', $this->id]
])->many();
}
protected $saldo;
public function saldo(Service $service = null) {
if ($this->saldo === null) {
$this->saldo = array_reduce($this->categorias(), function($sum, $item) use ($service) {
return $sum + $item->saldo($service);
});
}
return $this->saldo;
}
}

14
api/src/TipoCuenta.php Normal file
View File

@ -0,0 +1,14 @@
<?php
namespace Contabilidad;
use ProVM\Common\Alias\Model;
/**
* @property int $id
* @property string $descripcion
* @property string $color
*/
class TipoCuenta extends Model {
public static $_table = 'tipos_cuenta';
protected static $fields = ['descripcion', 'color'];
}

View File

@ -0,0 +1,13 @@
<?php
namespace Contabilidad;
use ProVM\Common\Alias\Model;
/**
* @property int $id
* @property string $descripcion
*/
class TipoEstadoConeccion extends Model {
public static $_table = 'tipos_estado_coneccion';
protected static $fields = ['descripcion'];
}

View File

@ -1,37 +1,38 @@
<?php
namespace Contabilidad;
use DateTime;
use Carbon\Carbon;
use ProVM\Common\Alias\Model;
/**
* @property int $id
* @property Cuenta $desde_id
* @property Cuenta $hasta_id
* @property \DateTime $fecha
* @property Cuenta $debito_id
* @property Cuenta $credito_id
* @property DateTime $fecha
* @property string $glosa
* @property string $detalle
* @property double $valor
*/
class Transaccion extends Model {
public static $_table = 'transacciones';
protected static $fields = ['desde_id', 'hasta_id', 'fecha', 'glosa', 'detalle', 'valor'];
protected static $fields = ['debito_id', 'credito_id', 'fecha', 'glosa', 'detalle', 'valor'];
protected $desde;
public function desde() {
if ($this->desde === null) {
$this->desde = $this->childOf(Cuenta::class, [Model::SELF_KEY => 'desde_id']);
protected $debito;
public function debito() {
if ($this->debito === null) {
$this->debito = $this->childOf(Cuenta::class, [Model::SELF_KEY => 'debito_id']);
}
return $this->desde;
return $this->debito;
}
protected $hasta;
public function hasta() {
if ($this->hasta === null) {
$this->hasta = $this->childOf(Cuenta::class, [Model::SELF_KEY => 'hasta_id']);
protected $credito;
public function credito() {
if ($this->credito === null) {
$this->credito = $this->childOf(Cuenta::class, [Model::SELF_KEY => 'credito_id']);
}
return $this->hasta;
return $this->credito;
}
public function fecha(\DateTime $fecha = null) {
public function fecha(DateTime $fecha = null) {
if ($fecha === null) {
return Carbon::parse($this->fecha);
}
@ -40,10 +41,10 @@ class Transaccion extends Model {
public function toArray(): array {
$arr = parent::toArray();
$arr['desde'] = $this->desde()->toArray();
$arr['hasta'] = $this->hasta()->toArray();
$arr['debito'] = $this->debito()->toArray();
$arr['credito'] = $this->credito()->toArray();
$arr['fechaFormateada'] = $this->fecha()->format('d-m-Y');
$arr['valorFormateado'] = '$' . number_format($this->valor, 0, ',', '.');
$arr['valorFormateado'] = $this->debito()->moneda()->format($this->valor);
return $arr;
}
}

View File

@ -2,44 +2,87 @@ version: '3'
services:
api:
profiles:
- api
restart: unless-stopped
image: php
build:
context: api
env_file: .env
env_file:
- .env
- .api.env
- .python.env
volumes:
- ./api/:/app/
- ./api/php.ini:/usr/local/etc/php/conf.d/php.ini
- ./logs/api/php/:/var/log/php/
api-proxy:
profiles:
- api
restart: unless-stopped
image: nginx
ports:
- 9001:80
- "9001:80"
volumes:
- ./api/nginx.conf:/etc/nginx/conf.d/default.conf
- ./logs/api/:/var/log/nginx/
- ./api/:/app/
db:
profiles:
- api
restart: unless-stopped
image: mariadb
env_file: .env
volumes:
- contabilidad_data:/var/lib/mysql
adminer:
profiles:
- api
restart: unless-stopped
image: adminer
ports:
- 9002:8080
- "9002:8080"
ui:
profiles:
- ui
restart: unless-stopped
image: php-ui
env_file:
- .api.env
build:
context: ui
volumes:
- ./ui/:/app/
- ./ui/php.ini:/usr/local/etc/php/conf.d/php.ini
- ./logs/ui/php/:/var/log/php/
ui-proxy:
profiles:
- ui
restart: unless-stopped
image: nginx
ports:
- 9000:80
- "9000:80"
volumes:
- ./ui/nginx.conf:/etc/nginx/conf.d/default.conf
- ./logs/ui/:/var/log/nginx/
- ./ui/:/app/
python:
profiles:
- python
restart: unless-stopped
build:
context: ./python
env_file:
- .python.env
ports:
- "9003:5000"
volumes:
- ./python/src/:/app/src/
- ./python/config/:/app/config/
- ./api/public/uploads/pdfs/:/app/data/
- ./logs/python/:/var/log/python/
volumes:
contabilidad_data:

1
python/.gitignore vendored Normal file
View File

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

3
python/.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
python/.idea/misc.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (contabilidad)" project-jdk-type="Python SDK" />
<component name="PyCharmProfessionalAdvertiser">
<option name="shown" value="true" />
</component>
</project>

8
python/.idea/modules.xml generated Normal file
View File

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

8
python/.idea/python.iml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.9 (contabilidad)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
python/.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

15
python/Dockerfile Normal file
View File

@ -0,0 +1,15 @@
FROM python
RUN apt-get update -y && apt-get install -y ghostscript python3-tk libgl-dev
RUN pip install flask pyyaml pypdf4 gunicorn camelot-py[cv] pikepdf
WORKDIR /app
COPY ./src/ /app/src/
EXPOSE 5000
WORKDIR /app/src
CMD ["gunicorn", "-b 0.0.0.0:5000", "app:app"]

View File

@ -0,0 +1,3 @@
passwords:
- 0839
- 159608395

Binary file not shown.

Binary file not shown.

3
python/src/.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

View File

@ -0,0 +1,12 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredIdentifiers">
<list>
<option value="property.tolist" />
</list>
</option>
</inspection_tool>
</profile>
</component>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

4
python/src/.idea/misc.xml generated Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (python)" project-jdk-type="Python SDK" />
</project>

8
python/src/.idea/modules.xml generated Normal file
View File

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

8
python/src/.idea/src.iml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.9 (python)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
python/src/.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
</component>
</project>

56
python/src/app.py Normal file
View File

@ -0,0 +1,56 @@
import io
import json
import os
import sys
from flask import Flask, request
import contabilidad.pdf as pdf
import contabilidad.passwords as passwords
import contabilidad.log as log
import contabilidad.text_handler as th
app = Flask(__name__)
log.logging['filename'] = '/var/log/python/contabilidad.log'
@app.route('/pdf/parse', methods=['POST'])
def pdf_parse():
data = request.get_json()
if not isinstance(data['files'], list):
data['files'] = [data['files']]
password_file = '/app/config/.passwords.yml'
pwds = passwords.get_passwords(password_file)
output = []
for file in data['files']:
filename = os.path.realpath(os.path.join('/app/data', file['filename']))
t = file['filename'].split('.')
temp = os.path.realpath(os.path.join('/app/data', t[0] + '-temp.pdf'))
for p in pwds:
if not pdf.check_password(filename, p):
continue
pdf.remove_encryption(filename, p, temp)
obj = pdf.get_data(temp)
outputs = []
for o in obj:
out = json.loads(o.df.to_json(orient='records'))
if out[0]['0'] == 'FECHA':
for i, line in enumerate(out):
if 'FECHA' in line['0'] or 'ACTUALICE' in line['0']:
continue
if line['0'] == '':
spl = line['1'].split(' ')
else:
spl = line['0'].split(' ')
line['0'] = ' '.join(spl[:3])
line['1'] = ' '.join(spl[3:])
out[i] = line
outputs.append(out)
os.remove(temp)
output.append({'filename': file['filename'], 'text': outputs})
return json.dumps(output)
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True)

Binary file not shown.

View File

@ -0,0 +1,19 @@
import time
logging = {
'filename': '/var/log/python/error.log'
}
class LOG_LEVEL:
INFO = 'INFO'
WARNING = 'WARNING'
DEBUG = 'DEBUG'
ERROR = 'ERROR'
def log(message, level=LOG_LEVEL.INFO):
filename = logging['filename']
with open(filename, 'a') as f:
f.write(time.strftime('[%Y-%m-%d %H:%M:%S] ') + ' - ' + level + ': ' + message)

View File

@ -0,0 +1,6 @@
import yaml
def get_passwords(filename):
with open(filename, 'r') as f:
return yaml.load(f, Loader=yaml.Loader)['passwords']

View File

@ -0,0 +1,51 @@
import camelot
import PyPDF4
import pikepdf
def is_encrypted(filename):
with open(filename, 'rb') as file:
reader = PyPDF4.PdfFileReader(file)
return reader.getIsEncrypted()
def check_password(filename, password):
if not is_encrypted(filename):
return True
with open(filename, 'rb') as file:
reader = PyPDF4.PdfFileReader(file)
status = reader.decrypt(str(password))
if status == 0:
return False
return reader.getIsEncrypted()
def remove_encryption(filename, password, new_name):
pdf = pikepdf.open(filename, password=str(password))
if '.pdf' not in new_name:
new_name += '.pdf'
pdf.save(new_name)
def get_pdf(file, password=''):
reader = PyPDF4.PdfFileReader(file)
if reader.getIsEncrypted() and password != '':
status = reader.decrypt(password=str(password))
if status == 0:
return None
return reader
def get_text(filename, password=''):
with open(filename, 'rb') as f:
reader = get_pdf(f, password)
if reader is None:
return None
texts = []
for p in range(0, reader.getNumPages()):
texts.append(reader.getPage(p).extractText())
return texts
def get_data(filename):
return camelot.read_pdf(filename, pages='all', output_format='json')

View File

@ -0,0 +1,54 @@
import argparse
import yaml
import PyPDF4
import httpx
def get_pdf(file, password=''):
reader = PyPDF4.PdfFileReader(file)
if password != '':
status = reader.decrypt(password=password)
if status == 0:
print('Not decrypted')
return reader
def send_to_parser(url, text):
res = httpx.post(url, data={'to_parse': text})
return {'status': res.status_code, 'text': res.json()}
def get_text(filename, password=''):
with open(filename, 'rb') as f:
reader = get_pdf(f, password)
texts = []
for p in range(0, reader.getNumPages()):
texts.append(reader.getPage(p).extractText())
return "\n".join(texts)
def get_config(filename):
with open(filename, 'r') as f:
return yaml.load(f, Loader=yaml.Loader)
def main(args):
password = ''
if args.config_file is not None:
config = get_config(args.config_file)
password = config['password']
if args.password is not None:
password = args.password
text = get_text(args.filename, password)
res = send_to_parser(args.url, text)
print(res)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-f', '--filename', type=str)
parser.add_argument('-p', '--password', type=str)
parser.add_argument('-c', '--config_file', type=str)
parser.add_argument('-u', '--url', type=str)
_args = parser.parse_args()
main(_args)

View File

@ -0,0 +1,90 @@
def text_cleanup(text, filename: str = None):
if isinstance(text, list):
output = []
for t in text:
output.append(text_cleanup(t, filename=filename))
return output
if filename is None:
return text
if 'bice' in filename.lower():
return bice(text)
if 'scotiabank' in filename.lower():
return scotiabank(text)
return text
def bice(text):
lines = text.split("\n\n\n")
print(lines)
return text
def scotiabank(text):
words = text.split("\n")
output = [words[0]]
output = output + extract_from_to(words, 'No. CTA.', end='VENCIMIENTO LINEA DE CREDITO', line_length=3)
output = output + extract_from_to(words, 'VENCIMIENTO LINEA DE CREDITO',
end='NOMBRE EJECUTIVO: LILIAN AVILA MANRIQUEZ', line_length=2)
output = output + extract_from_to(words, 'NOMBRE EJECUTIVO: LILIAN AVILA MANRIQUEZ', end='SALDO ANTERIOR',
line_length=1)
output = output + extract_from_to(words, 'SALDO ANTERIOR', end='FECHA', line_length=4)
output = output + extract_data(words, 'FECHA', end='ACTUALICE SIEMPRE ANTECEDENTES LEGALES, ', line_length=6,
merge_list=[['DOCTO', 'No.'], ['SALDO', 'DIARIO']])
[print(li) for li in output]
return text
def extract_from_to(word_list, start, line_length, end: str = None, merge_list=None):
if end is not None:
return extract_by_line(word_list[word_list.index(start):word_list.index(end)], line_length, merge_list)
return extract_by_line(word_list[word_list.index(start):], line_length, merge_list)
def extract_by_line(word_list, line_length, merge_list=None):
if merge_list is not None:
word_list = merge_words(word_list, merge_list)
output = []
line = []
for k, w in enumerate(word_list):
if k > 0 and k % line_length == 0:
output.append(line)
line = []
line.append(w)
output.append(line)
return output
def merge_words(word_list, merge_list):
for m in merge_list:
i = word_list.index(m[0])
word_list = word_list[:i] + ["\n".join(m)] + word_list[i+len(m):]
return word_list
def extract_data(word_list, start, line_length, end=None, merge_list=None, date_sep='/'):
word_list = word_list[word_list.index(start):]
if end is not None:
word_list = word_list[:word_list.index(end)]
if merge_list is not None:
word_list = merge_words(word_list, merge_list)
output = []
line = []
line_num = 0
col = 0
for k, w in enumerate(word_list):
if col > 0 and col % line_length == 0:
output.append(line)
line = []
col = 0
if col > 0 and date_sep in w and len(line) < line_length:
cnt = 0
for i in range(len(line), line_length):
line.append('')
cnt += 1
output.append(line)
line = [w]
col += cnt + 1
continue
line.append(w)
col += 1
return output

24
python/src/main.py Normal file
View File

@ -0,0 +1,24 @@
import argparse
import os
import contabilidad.pdf as pdf
import contabilidad.text_handler as th
def main(args):
filename = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'data', args.filename))
temp = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'data', args.temp_filename))
pdf.remove_encryption(filename, args.password, temp)
obj = pdf.get_data(temp)
obj = pdf.get_text(filename, args.password)
text = th.text_cleanup(obj, filename=str(args.filename))
os.remove(temp)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-f', '--filename', type=str)
parser.add_argument('-p', '--password', type=str, default='')
parser.add_argument('-t', '--temp_filename', type=str)
_args = parser.parse_args()
main(_args)

View File

@ -0,0 +1,35 @@
# Tests
### 1. Conductor
+ Set start event
+ Get ready events
+ Set first step event
+ Get first step ready
+ Set second step event
+ Get second step ready
### 2. Email
+ Connect to IMAP
+ Wrong data
+ Wrong configuration
+ Get mailboxes
+ Get mail ids with search
+ Get mails by id
+ Get mail by id
+ Get attachment
+ Close connection
### 3. API Sender
+ Get attachments
+ Process
+ Send to API
## Steps
1. Start
+ Connect
+ Standby
2. Find emails, get attachments
3. Process attachments
4. Send to API
5. Close

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 Config {
public function __invoke(Request $request, Response $response, View $view): Response {
return $view->render($response, 'config.list');
}
}

View File

@ -9,4 +9,7 @@ class Home {
public function __invoke(Request $request, Response $response, View $view): Response {
return $view->render($response, 'home');
}
public function info(Request $request, Response $response, View $view): Response {
return $view->render($response, 'info');
}
}

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