64 Commits

Author SHA1 Message Date
4954947829 Merge branch 'release' 2022-03-25 15:49:12 -03:00
d0d123e12e Merge branch 'develop' into release 2022-03-18 17:50:36 -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
71975ec6d9 Merge branch 'develop' into release 2022-01-07 01:13:22 -03:00
84a3f8e2e3 Order totals for tipocuenta 2022-01-07 01:12:54 -03:00
f36399c1f4 Formatting and Fixes 2022-01-07 00:24:45 -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
1d62a0c04e Fixes 2022-01-06 15:20:37 -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
a2a90497ae Python updates 2022-01-05 21:11:16 -03:00
b17225549c Python get cambio 2022-01-05 21:10:51 -03:00
925388df92 Edit transacciones, refresh, sort cuentas 2022-01-05 16:02:40 -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
2126d30ee7 Merge branch 'develop' into release 2021-12-23 01:09:50 -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
73df98dca5 Merge branch 'develop' into release 2021-12-23 00:47:47 -03:00
4abe3448c0 Upgrades to the UI 2021-12-23 00:46:56 -03:00
f9bbbfd920 Fixes for uploaded files 2021-12-23 00:10:06 -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
aacda4d1e4 Fix 2021-12-22 23:24:19 -03:00
1a0c83fb2b FIX: check if upload subfolder exists when loading files 2021-12-22 23:20:23 -03:00
f99b984f6c Fix 2021-12-22 23:12:25 -03:00
a5428b252e FIX: Ignoring upload resources from ui 2021-12-22 23:11:51 -03:00
605c905f5d Python 2021-12-22 21:53:30 -03:00
4bc1e4ae4d Upload and manage files 2021-12-22 01:39:09 -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
430e29eaec Fixed TipoCategoria select and url importar in menu 2021-12-20 23:31:05 -03:00
79c7d5ad63 Fixes 2021-12-20 23:30:13 -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
6b03d62ce0 Merge branch 'develop' into release 2021-12-20 22:54:14 -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
960c418848 Python 2021-12-09 21:14:28 -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
25f873c453 Python 2021-12-06 22:13:57 -03:00
34b429530f Python 2021-12-06 22:13:06 -03:00
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
150 changed files with 5805 additions and 375 deletions

1
.api.env.sample Normal file
View File

@ -0,0 +1 @@
API_KEY=

View File

@ -1,3 +1,4 @@
COMPOSE_PROFILES=
MYSQL_HOST=
MYSQL_ROOT_PASSWORD=
MYSQL_DATABASE=

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.

1
api/.gitignore vendored Normal file
View File

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

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,24 @@ 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]);
}
public function info(Request $request, Response $response): Response {
ob_start();
phpinfo();
$data = ob_get_clean();
$response->getBody()->write($data);
return $response;
}
}

View File

@ -5,20 +5,43 @@ 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();
if ($categorias) {
usort($categorias, function($a, $b) {
return strcmp($a['nombre'], $b['nombre']);
});
public function __invoke(Request $request, Response $response, Factory $factory, Service $service): Response {
$categorias = $factory->find(Categoria::class)->many();
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());
}
$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);
}
@ -68,14 +91,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

@ -1,24 +1,36 @@
<?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;
use Contabilidad\Cuenta;
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) {
return $t;
}
$c = strcmp($a['categoria']['nombre'], $b['categoria']['nombre']);
if ($c == 0) {
return strcmp($a['nombre'], $b['nombre']);
if ($c != 0) {
return $c;
}
return $c;
return strcmp($a['nombre'], $b['nombre']);
});
}
$output = [
@ -72,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;
@ -90,15 +111,48 @@ class Cuentas {
];
return $this->withJson($response, $output);
}
public function transacciones(Request $request, Response $response, Factory $factory, $cuenta_id, $limit = null, $start = 0): Response {
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'] = $c->moneda()->cambiar($transaccion->fecha(), $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;
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']);
}
}
$arr['debito']['categoria'] = $transaccion->debito()->categoria()->toArray();
$arr['credito']['categoria'] = $transaccion->credito()->categoria()->toArray();*/
$transaccion = $this->transaccionToArray($service, $cuenta, $transaccion);
}
}
}
$output = [
@ -108,6 +162,55 @@ 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) {
/*$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']);
}
}
$arr['debito']['categoria'] = $transaccion->debito()->categoria()->toArray();
$arr['credito']['categoria'] = $transaccion->credito()->categoria()->toArray();*/
$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

@ -0,0 +1,49 @@
<?php
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, 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);
}
}

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,132 @@
<?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;
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();
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['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
];
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);
$arr['totales'] = $item->getTotales($service);
$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) {
$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';
$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,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

@ -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,93 @@
<?php
namespace Contabilidad\Common\Service;
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;
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;
}
protected function getWeekday(\DateTimeInterface $fecha) {
if ($fecha->weekday() == 0) {
return $fecha->subWeek()->weekday(5);
}
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
];
$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 | 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());
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,
'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, 'default' => 'ffffff'])
->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();
}
}

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;

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'
];

4
api/php.ini Normal file
View File

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

View File

@ -1,4 +1,7 @@
<?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('/info', [Base::class, 'info']);
$app->get('/', Base::class);

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,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

@ -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

@ -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['base'],
'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,58 @@
<?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')
);
},
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

@ -1,41 +1,113 @@
<?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() {
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;
}
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 $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->cuentas_of;
}
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() {
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;
$arr = parent::toArray();
$arr['tipo'] = $this->tipo()->toArray();
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,22 @@
<?php
namespace Contabilidad;
use DateTimeInterface;
use Carbon\Carbon;
use PhpParser\Node\Expr\AssignOp\Mod;
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,68 +25,128 @@ 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;
}
protected $transacciones;
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')
->select('transacciones.*')
->join('cuentas', 'cuentas.id = transacciones.debito_id OR cuentas.id = transacciones.credito_id')
->whereEqual('cuentas.id', $this->id)
->orderByAsc('transacciones.fecha');
if ($limit !== null) {
$transacciones = $transacciones->limit($limit)
->offset($start);
}
$this->transacciones = $transacciones->findMany();
foreach ($this->transacciones as &$transaccion) {
$transacciones = $transacciones->findMany();
foreach ($transacciones as &$transaccion) {
$transaccion->setFactory($this->factory);
if ($transaccion->desde_id === $this->id) {
if ($transaccion->debito_id === $this->id) {
$transaccion->valor = - $transaccion->valor;
}
}
}
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->setFactory($this->factory);
if ($transaccion->desde_id === $this->id) {
$transaccion->valor = - $transaccion->valor;
}
}
return $transacciones;
}
public function acumulacion(Carbon $date) {
$abonos = Model::factory(Transaccion::class)
->whereEqual('credito_id', $this->id)
->whereLt('fecha', $date->format('Y-m-d'))
->groupBy('credito_id')
->sum('valor');
$cargos = Model::factory(Transaccion::class)
->whereEqual('debito_id', $this->id)
->whereLt('fecha', $date->format('Y-m-d'))
->groupBy('debito_id')
->sum('valor');
if (in_array($this->tipo()->descripcion, ['activo', 'banco', 'perdida'])) {
return $abonos - $cargos;
}
return $cargos - $abonos;
}
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) {
$this->saldo = array_reduce($this->transacciones(), function($sum, $item) {
return $sum + $item->valor;
return $sum + $item->valor;
});
}
}
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;
}
}

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

@ -0,0 +1,56 @@
<?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 trim(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 === null) {
$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);
}
public function toArray(): array {
$arr = parent::toArray();
$arr['format'] = [
'prefijo' => $this->prefijo,
'sufijo' => $this->sufijo,
'decimales' => $this->decimales
];
return $arr;
}
}

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;
}
}

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

@ -0,0 +1,66 @@
<?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 $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) {
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,88 @@ 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
- .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>

16
python/Dockerfile Normal file
View File

@ -0,0 +1,16 @@
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 httpx
WORKDIR /app
COPY ./src/ /app/src/
EXPOSE 5000
WORKDIR /app/src
CMD ["python", "app.py"]
#CMD ["gunicorn", "-b 0.0.0.0:5000", "app:app"]

View File

@ -0,0 +1,4 @@
passwords:
- 0839
- 159608395
- 15960839

Binary file not shown.

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>

285
python/src/ai/dictionary.py Normal file
View File

@ -0,0 +1,285 @@
import json
import os
import numpy as np
import sklearn
import enlighten
from sklearn.preprocessing import LabelEncoder
import src.contabilidad.pdf as pdf
import src.contabilidad.text_handler as th
from src.ai.models import Phrase, phrase_factory, Word, word_factory
from src.contabilidad.log import LOG_LEVEL
class Dictionary:
def __init__(self, filename, logger):
self.filename = filename
self._logger = logger
self.__processed = []
self.__phrases = None
self.__words = None
self.load()
def load(self):
if not os.path.isfile(self.filename):
return
with open(self.filename, 'r') as file:
data = json.load(file)
if 'words' in data.keys():
self.__words = []
[self.__words.append(word_factory(w)) for w in data['words']]
if 'phrases' in data.keys():
self.__phrases = []
[self.__phrases.append(phrase_factory(ph)) for ph in data['phrases']]
if 'processed' in data.keys():
self.__processed = []
self.__processed = data['processed']
def save(self):
self.sort_words()
self.sort_phrases()
with open(self.filename, 'w') as file:
json.dump(self.to_json(), file, indent=2)
def to_data(self):
encoder = LabelEncoder()
data = encoder.fit_transform([w.get_word() for w in self.get_words()])
[self.__words[i].set_fit(f) for i, f in enumerate(data)]
print(data)
# return [ph.to_data() for ph in self.get_phrases()]
def to_json(self):
output = {
'processed': [],
'words': [],
'phrases': []
}
if self.__processed is not None and len(self.__processed) > 0:
output['processed'] = self.__processed
if self.__words is not None and len(self.__words) > 0:
output['words'] = [w.to_json() for w in self.__words]
if self.__phrases is not None and len(self.__phrases) > 0:
output['phrases'] = [p.to_json() for p in self.__phrases]
return output
def find_phrase(self, phrase: Phrase = None, phrase_dict: dict = None, phrase_list: list = None):
if not self.__phrases:
return -1
if phrase is not None:
phrase_list = [w.get_word() for w in phrase.get_words()]
elif phrase_dict is not None:
phrase_list = phrase_dict['words']
elif phrase_list is not None:
pass
else:
return -1
return find_phrase(self.__phrases, phrase_list)
def add_phrase(self, phrase: Phrase = None, phrase_dict: dict = None, phrase_list: list = None):
if self.__phrases is None:
self.__phrases = []
if phrase is not None:
pass
elif phrase_dict is not None:
phrase = phrase_factory(phrase_dict)
elif phrase_list is not None:
phrase = phrase_factory({'words': phrase_list})
else:
return self
i = self.find_phrase(phrase)
if i > -1:
self.__phrases[i].add_freq()
return self
self.__phrases.append(phrase)
return self
def add_phrases(self, phrase_list: list):
if self.__phrases is None:
self.__phrases = []
phs = [sorted(w.get_word() for w in p) for p in self.__phrases]
with enlighten.get_manager() as manager:
with manager.counter(total=len(phrase_list), desc='Phrases', unit='phrases', color='green') as bar1:
for i, phrase in enumerate(phrase_list):
# print(f'Adding phrase {i}.')
p2 = sorted([w.get_word() for w in phrase])
if p2 in phs:
k = phs.index(p2)
self.__phrases[k].add_freq()
continue
ph = phrase_factory({'words': phrase})
self.__phrases.append(ph)
phs.append(p2)
bar1.update()
def get_phrases(self):
return self.__phrases
def sort_phrases(self):
if self.__phrases is None:
return
try:
def sort_phrase(p):
if p is None:
return 0
if isinstance(p, Phrase):
return p.get_freq(), p.get_type().get_desc(), len(p.get_words())
return p['frequency'], p['type']['description'], len(p['words'])
self.__phrases = sorted(self.__phrases,
key=sort_phrase)
except Exception as e:
self._logger.log(repr(self.__phrases), LOG_LEVEL.ERROR)
self._logger.log(e)
return self
def sort_words(self):
if self.__words is None:
return
try:
def sort_word(w):
if w is None:
return 0
if isinstance(w, Word):
return w.get_freq(), w.get_type().get_desc(), w.get_word()
return w['frequency'], w['type']['description'], w['word']
self.__words = sorted(self.__words, key=sort_word, reverse=True)
except Exception as e:
self._logger.log(repr(self.__words))
self._logger.log(e)
return self
def find_word(self, word: Word = None, word_dict: dict = None, word_str: str = None):
if not self.__words:
return -1
if word is not None:
word_str = word.get_word()
elif word_dict is not None:
word_str = word_dict['word']
elif word_str is not None:
pass
else:
return -1
return find_word(self.__words, word_str)
def add_word(self, word: Word = None, word_dict: dict = None, word_str: str = None):
if self.__words is None:
self.__words = []
if word is not None:
pass
elif word_dict is not None:
word = word_factory(word_dict)
elif word_str is not None:
word = word_factory({'word': word_str})
else:
return self
i = self.find_word(word)
if i > -1:
self.__words[i].add_freq()
return self
self.__words.append(word)
return self
def add_words(self, words: list):
[self.add_word(word=w) for w in words if isinstance(w, Word)]
[self.add_word(word_dict=w) for w in words if isinstance(w, dict)]
[self.add_word(word_str=w) for w in words if isinstance(w, str)]
return self
def get_words(self):
return filter_unique_words(self.__words)
def match_words(self, word_list: list):
new_list = []
for w in word_list:
wi = self.find_word(word_str=w)
new_list.append(self.__words[wi])
return new_list
def append_to_phrase(self, seed: list = None, length: int = 1):
if seed is None:
return [self.__words[0]]
max_index = max(seed) + length
if max_index > len(self.__words):
if length == 1:
return False
return self.append_to_phrase(seed, length - 1)
return seed + self.__words[max_index]
def get_possible_phrases(self, word_list):
print('Adding words.')
self.add_words(word_list)
print('Creating phrases.')
with enlighten.get_manager() as manager:
with manager.counter(total=len(word_list)**2, desc='Phrases', unit='words', color='red') as bar1:
phrases = []
for length in range(1, len(word_list) + 1):
bar2 = bar1.add_subcounter(color='green')
for start in range(0, len(word_list)):
phrase = build_phrase(word_list, start, start + length)
phrase = self.match_words(phrase)
phrases.append(phrase)
start += length
bar2.update()
bar1.update()
print(f'Created {len(phrases)} phrases.')
phrases = sorted(phrases, key=lambda e: len(e))
print('Adding phrases.')
# Really slow (~115000 phrases in one pdf)
self.add_phrases(phrases)
return self.__phrases
def is_processed(self, filename: str):
return os.path.basename(filename) in self.__processed
def process(self, filename: str, password: str = None):
if self.is_processed(filename):
print('Already processed.')
return
t = filename.split('.')
temp = os.path.realpath(os.path.join(os.path.dirname(filename), t[0] + '-temp.pdf'))
print('Removing PDF encryption.')
pdf.remove_encryption(filename, password, temp)
print('Getting text')
obj = pdf.get_text(temp)
os.remove(temp)
print('Getting possible phrases.')
phrases = self.get_possible_phrases(th.split_words(obj))
self.__processed.append(os.path.basename(filename))
return phrases
def build_phrase(word_list, start: int, end: int = None):
if end is None:
return word_list[start:]
return word_list[start:end]
def filter_unique_words(words):
new_list = []
for w in words:
if w not in new_list:
new_list.append(w)
return new_list
def validate_phrase(phrase):
return True
def find_phrase(phrases: list, phrase: list):
phrase_list = [sorted([w.get_word() for w in p.get_words()]) for p in phrases]
sphrase = sorted(phrase)
if sphrase in phrase_list:
return phrase_list.index(sphrase)
return -1
def find_word(words: list, word: str):
word_list = [w.get_word() for w in words]
if word in word_list:
return word_list.index(word)
return -1

243
python/src/ai/models.py Normal file
View File

@ -0,0 +1,243 @@
import json
class Type:
def __init__(self, _id, _description):
self.__id = _id
self.__description = _description
def get_id(self):
return self.__id
def get_desc(self):
return self.__description
def to_json(self):
return self.get_id()
def __repr__(self):
return json.dumps({
'id': self.get_id(),
'description': self.get_desc()
})
def type_factory(_type: str, _id: int):
if _type == 'Word' or _type == 'WordType':
t = WordType()
elif _type == 'Phrase' or _type == 'PhraseType':
t = PhraseType()
else:
return None
t.load(_id)
return t
class WordType(Type):
STRING = 0
NUMERIC = 1
CURRENCY = 2
DATE = 4
def __init__(self):
super().__init__(0, 'string')
def load(self, word_type: int):
if word_type == self.STRING:
self.__description = 'string'
elif word_type == self.NUMERIC:
self.__description = 'numeric'
elif word_type == self.CURRENCY:
self.__description = 'currency'
elif word_type == self.DATE:
self.__description = 'date'
return self
class PhraseType(Type):
TEXT = 0
TITLE = 1
HEADER = 2
MOVEMENT = 4
INVALID = 99
def __init__(self):
super(PhraseType, self).__init__(0, 'text')
def load(self, phrase_type: int):
if phrase_type == self.TEXT:
self.__description = 'text'
elif phrase_type == self.TITLE:
self.__description = 'title'
elif phrase_type == self.HEADER:
self.__description = 'header'
class Word:
def __init__(self):
self.__id = 0
self.__word = None
self.__type_id = 0
self.__type = None
self.__frequency = 1
def set_id(self, idx: int):
self.__id = idx
return self
def set_word(self, word: str):
self.__word = word
return self
def set_type(self, word_type):
if isinstance(word_type, WordType):
self.__type_id = word_type.get_id()
# self.__type = word_type
if isinstance(word_type, int):
self.__type_id = word_type
# self.__type = type_factory('Word', word_type)
return self
def add_freq(self, amount: int = 1):
self.__frequency += amount
return self
def get_id(self) -> int:
return self.__id
def get_word(self) -> str:
return self.__word
def get_type_id(self) -> int:
return self.__type_id
def get_type(self) -> WordType:
if self.__type is None:
self.__type = type_factory('Word', self.__type_id)
return self.__type
def get_freq(self) -> int:
return self.__frequency
def to_json(self) -> dict:
output = {
'id': self.get_id(),
'word': self.get_word(),
'type': self.get_type_id(),
'freq': self.get_freq()
}
return output
def __repr__(self):
return json.dumps(self.to_json())
def word_factory(word: dict) -> Word:
w = Word()
w.set_id(word['id'])
w.set_word(word['word'])
if 'type' in word:
w.set_type(word['type'])
if 'freq' in word:
w.add_freq(word['freq'] - 1)
return w
class Phrase:
def __init__(self):
self.__id = 0
self.__words = None
self.__type_id = 0
self.__type = None
self.__frequency = 1
def set_id(self, idx: int):
self.__id = idx
return self
def add_word(self, word):
if isinstance(word, Word):
self.__words.append(word.get_id())
if isinstance(word, dict):
if 'id' in word:
self.__words.append(word['id'])
if isinstance(word, int):
self.__words.append(word)
return self
def set_words(self, words: list):
if self.__words is None:
self.__words = []
for w in words:
if isinstance(w, Word):
self.add_word(w)
if isinstance(w, dict):
self.add_word(w)
if isinstance(w, int):
self.add_word(w)
return self
def set_type(self, phrase_type):
if isinstance(phrase_type, PhraseType):
self.__type_id = phrase_type.get_id()
# self.__type = phrase_type
if isinstance(phrase_type, int):
self.__type_id = phrase_type
# self.__type = type_factory('Phrase', phrase_type)
return self
def add_freq(self, amount: int = 1):
self.__frequency += amount
return self
def get_id(self) -> int:
return self.__id
def get_words(self) -> list:
return self.__words
def get_type_id(self) -> int:
return self.__type_id
def get_type(self) -> PhraseType:
if self.__type is None:
self.__type = type_factory('Phrase', self.__type_id)
return self.__type
def get_freq(self) -> int:
return self.__frequency
def match(self, word_list: list):
if len(word_list) != len(self.__words):
return False
new_words = sorted(self.__words)
new_list = sorted(word_list)
if new_words == new_list:
return True
return False
def to_json(self):
output = {
'id': self.get_id(),
'words': self.get_words(),
'type': self.get_type_id(),
'freq': self.get_freq()
}
return output
def __repr__(self):
return json.dumps(self.to_json())
def __len__(self):
return len(self.get_words())
def phrase_factory(phrase: dict) -> Phrase:
ph = Phrase()
ph.set_id(phrase['id'])
ph.set_words(phrase['words'])
if 'type' in phrase:
ph.set_type(phrase['type'])
if 'freq' in phrase:
ph.add_freq(phrase['freq'] - 1)
return ph

126
python/src/ai/network.py Normal file
View File

@ -0,0 +1,126 @@
import json
import os
import time
import timeit
import tensorflow as tf
import sklearn
import numpy as np
from sklearn.preprocessing import LabelEncoder
import src.contabilidad.pdf as pdf
import src.contabilidad.text_handler as th
class Layer:
def __init__(self):
self.__weights = None
self.__bias = None
def set_size(self, inputs: int, size: int):
self.__weights = [[0 for j in range(0, inputs)] for i in range(0, size)]
self.__bias = [0 for i in range(0, size)]
def add_weight(self, vector: list, idx: int = None):
if idx is None:
self.__weights.append(vector)
return self
self.__weights = self.__weights[:idx] + [vector] + self.__weights[idx:]
return self
def set_weight(self, value: float, weight_index: int, input_index: int):
self.__weights[weight_index][input_index] = value
def set_bias(self, value: list):
self.__bias = value
def train(self, input_values: list, output_values: list):
output = self.get_output(input_values)
errors = []
for i, v in enumerate(output):
error = (output_values[i] - v) / output_values[i]
new_value = v * error
def to_json(self):
return {
'bias': self.__bias,
'weights': self.__weights
}
def get_output(self, vector: list):
output = []
for i, weight in enumerate(self.__weights):
val = 0
for j, v in enumerate(weight):
val += v * vector[j]
output[i] = val + self.__bias[i]
return output
def layer_factory(layer_dict: dict):
layer = Layer()
layer.set_bias(layer_dict['bias'])
[layer.add_weight(w) for w in layer_dict['weights']]
return layer
class Network:
def __init__(self, filename: str):
self._filename = filename
self.__layers = None
def load(self):
with open(self._filename) as f:
data = json.load(f)
if 'layers' in data.keys():
self.add_layers(data['layers'])
def add_layers(self, layers: list):
for lr in layers:
layer = layer_factory(lr)
self.__layers.append(layer)
class AI:
def __init__(self, dictionary_filename, logger):
self.__dict = None
self.__network = None
self.__sources = None
self._phrases = None
self.filename = ''
def add_source(self, text):
if self.__sources is None:
self.__sources = []
self.__sources.append(text)
return self
def set_filename(self, filename: str):
self.filename = filename
return self
def process_sources(self):
for source in self.__sources:
self.process(**source)
def process(self, filename, password):
encoder = LabelEncoder()
t = filename.split('.')
temp = os.path.realpath(os.path.join(os.path.dirname(filename), t[0] + '-temp.pdf'))
pdf.remove_encryption(filename, password, temp)
obj = pdf.get_text(temp)
os.remove(temp)
word_list = th.split_words(obj)
fits = encoder.fit_transform(word_list)
phrases = []
for length in range(1, len(word_list) + 1):
for start in range(0, len(word_list)):
phrase = word_list[start:(start + length)]
phrase = np.append(np.array([fits[word_list.index(w)] for w in phrase]),
np.zeros([len(word_list) - len(phrase)]))
phrases.append(phrase)
phrases = np.array(phrases)
self._phrases = phrases
def active_train(self):
pass

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

@ -0,0 +1,109 @@
import json
import os
import sys
import httpx
from flask import Flask, request, jsonify
import contabilidad.pdf as pdf
import contabilidad.passwords as passwords
import contabilidad.text_handler as th
from contabilidad.log import Log
app = Flask(__name__)
log = Log('/var/log/python/contabilidad.log')
api_key = os.environ.get('PYTHON_KEY')
def validate_key(request_obj):
if 'Authorization' in request_obj.headers:
auth = request_obj.headers.get('Authorization')
if isinstance(auth, list):
auth = auth[0]
if 'Bearer' in auth:
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
if 'api_key' in request_obj.values:
return request_obj.values.get('api_key') == api_key
return False
@app.route('/pdf/parse', methods=['POST'])
def pdf_parse():
if not validate_key(request):
return jsonify({'message': 'Not Authorized'})
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)
try:
text = th.text_cleanup(pdf.get_text(temp))
except IndexError as ie:
print(ie, file=sys.stderr)
continue
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({'bank': text['bank'], 'filename': file['filename'], 'tables': outputs, 'text': text['text']})
return jsonify(output)
@app.route('/cambio/get', methods=['POST'])
def cambios():
if not validate_key(request):
return jsonify({'message': 'Not Authorized'})
data = request.get_json()
valid = {
"CLF": "uf",
"IVP": "ivp",
"USD": "dolar",
"USDo": "dolar_intercambio",
"EUR": "euro",
"IPC": "ipc",
"UTM": "utm",
"IMACEC": "imacec",
"TPM": "tpm",
"CUP": "libra_cobre",
"TZD": "tasa_desempleo",
"BTC": "bitcoin"
}
base_url = 'https://mindicador.cl/api/'
url = f"{base_url}{valid[data['desde']]}/{'-'.join(list(reversed(data['fecha'].split('-'))))}"
res = httpx.get(url)
if res.status_code != httpx.codes.OK:
return jsonify({'error': 'Valor no encontrado.'})
return jsonify(res.json())
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True)

View File

@ -0,0 +1,65 @@
import os.path
import time
import traceback
class LOG_LEVEL:
INFO = 0
WARNING = 1
DEBUG = 2
ERROR = 4
@staticmethod
def desc(level):
mapping = {
LOG_LEVEL.INFO: 'INFO',
LOG_LEVEL.WARNING: 'WARNING',
LOG_LEVEL.DEBUG: 'DEBUG',
LOG_LEVEL.ERROR: 'ERROR'
}
return mapping[level]
class Logger:
def __init__(self):
self._logs = []
def add_log(self, filename: str, min_level: int = LOG_LEVEL.INFO):
self._logs.append({'log': Log(filename), 'level': min_level})
self._logs.sort(key=lambda e: e['level'])
return self
def log(self, message, level: int = LOG_LEVEL.INFO):
for log in self._logs:
if log['level'] >= level:
log['log'].log(message, level)
class Log:
MAX_SIZE = 10 * 1024 * 1024
def __init__(self, filename: str = '/var/log/python/error.log'):
self._filename = filename
def log(self, message, level: int = LOG_LEVEL.INFO):
if isinstance(message, Exception):
message = traceback.format_exc()
if level < LOG_LEVEL.ERROR:
level = LOG_LEVEL.ERROR
self.rotate_file()
with open(self._filename, 'a') as f:
f.write(time.strftime('[%Y-%m-%d %H:%M:%S] ') + ' - ' + LOG_LEVEL.desc(level=level) + ': ' + message + "\n")
def rotate_file(self):
if not os.path.isfile(self._filename):
return
file_size = os.path.getsize(self._filename)
if file_size > self.MAX_SIZE:
self.next_file()
def next_file(self):
name = self._filename.split('.')
n = 1
if name[-2].isnumeric():
n = int(name[-2]) + 1
self._filename = '.'.join([name[0], str(n), name[-1]])

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,172 @@
def text_cleanup(text: str):
if isinstance(text, list):
text = "\n\n\n".join(text)
if 'bice' in text.lower():
return {'bank': 'BICE', 'text': bice(text)}
if 'scotiabank' in text.lower():
return {'bank': 'Scotiabank', 'text': scotiabank(text)}
if 'TARJETA' in text:
return {'bank': 'Scotiabank', 'text': tarjeta(text)}
return {'bank': 'unknown', 'text': basic(text)}
def bice(text):
lines = [t2.strip() for t in text.split("\n\n\n")
for t1 in t.split("\n\n") for t2 in t1.split("\n") if t2.strip() != '']
output = []
output += extract_from_to(lines, 'NOMBRE DEL CLIENTE', end='LAS CONDES', line_length=3)
ti = [t for t in lines if 'MOVIMIENTOS DE LA CUENTA CORRIENTE' in t][0]
output += extract_from_to(lines, 'LAS CONDES', end=ti, line_length=3)
output += [ti]
ti = [i for i, t in enumerate(lines) if 'FECHA' in t]
output += extract_from_to(lines, ti[0], end=ti[1], line_length=4)
output += extract_from_to(lines, 'RESUMEN DEL PERIODO', end='SALDO INICIAL', line_length=1)
output += extract_from_to(lines, 'SALDO INICIAL', end='LINEA SOBREGIRO AUTORIZADA', line_length=4)
output += extract_from_to(lines, 'LINEA SOBREGIRO AUTORIZADA', end='OBSERVACIONES', line_length=3)
output += extract_from_to(lines, 'OBSERVACIONES', line_length=1)
return output
def scotiabank(text):
words = split_words(text)
output = [words[0]]
output += extract_from_to(words, 'No. CTA.', end='VENCIMIENTO LINEA DE CREDITO', line_length=3)
output += extract_from_to(words, 'VENCIMIENTO LINEA DE CREDITO',
end='NOMBRE EJECUTIVO: LILIAN AVILA MANRIQUEZ', line_length=2)
output += extract_from_to(words, 'NOMBRE EJECUTIVO: LILIAN AVILA MANRIQUEZ', end='SALDO ANTERIOR',
line_length=1)
output += extract_from_to(words, 'SALDO ANTERIOR', end='FECHA', line_length=4)
output += extract_data(words, 'FECHA', end='ACTUALICE SIEMPRE ANTECEDENTES LEGALES, ', line_length=6,
merge_list=[['DOCTO', 'No.'], ['SALDO', 'DIARIO']])
output += extract_from_to(words, 'ACTUALICE SIEMPRE ANTECEDENTES LEGALES, ', 1)
return output
def tarjeta(text):
words = split_words(text)
output = ['ESTADO DE CUENTA NACIONAL DE TARJETA DE CRÉDITO']
i = [i for i, w in enumerate(words) if 'FECHA ESTADO DE CUENTA' in w][0] + 2
output += extract_from_to(words, 'NOMBRE DEL TITULAR', end=i, line_length=2)
output += ['I. INFORMACIóN GENERAL']
i = [i for i, w in enumerate(words) if 'CUPO TOTAL' in w][1]
output += extract_from_to(words, 'CUPO TOTAL', end=i, line_length=3)
output += extract_from_to(words, i, end='ROTATIVO', line_length=4)
output += extract_from_to(words, 'ROTATIVO', end='TASA INTERÉS VIGENTE', line_length=3)
output += extract_from_to(words, 'TASA INTERÉS VIGENTE',
end='CAE se calcula sobre un supuesto de gasto mensual de UF 20 y pagadero en 12 cuotas.',
line_length=4)
output += extract_from_to(words, 'DESDE', end='PERÍODO FACTURADO', line_length=2)
output += extract_from_to(words, 'PERÍODO FACTURADO', end='II.', line_length=3)
output += ['II. DETALLE']
output += extract_from_to(words, '1. PERÍODO ANTERIOR', end='SALDO ADEUDADO INICIO PERÍODO ANTERIOR', line_length=3)
i = words.index('2. PERÍODO ACTUAL')
output += extract_from_to(words, 'SALDO ADEUDADO INICIO PERÍODO ANTERIOR', end=i - 1, line_length=2,
merge_list=[['MONTO FACTURADO A PAGAR (PERÍODO ANTERIOR)', '(A)']], merge_character=" ")
output += ['2. PERÍODO ACTUAL']
output += extract_from_to(words, 'LUGAR DE', end='1.TOTAL OPERACIONES', line_length=7,
merge_list=[['OPERACIÓN', 'O COBRO'], ['TOTAL A', 'PAGAR'], ['VALOR CUOTA', 'MENSUAL']])
i = words.index('1.TOTAL OPERACIONES') + 3
output += extract_from_to(words, '1.TOTAL OPERACIONES', end=i, line_length=3)
output += extract_from_to(words, i, end='TOTAL PAGOS A LA CUENTA', line_length=7)
i = words.index('TOTAL PAGOS A LA CUENTA') + 2
output += extract_from_to(words, 'TOTAL PAGOS A LA CUENTA', end=i, line_length=2)
output += extract_from_to(words, i, end='TOTAL PAT A LA CUENTA', line_length=8)
i = words.index('TOTAL PAT A LA CUENTA') + 2
output += extract_from_to(words, 'TOTAL PAT A LA CUENTA', end=i, line_length=2)
output += extract_from_to(words, i, end=i + 3, line_length=2,
merge_list=[
['2.PRODUCTOS O SERVICIOS VOLUNTARIAMENTE CONTRATADOS SIN MOVIMIENTOS', '(C)']],
merge_character=" ")
if '3.CARGOS, COMISIONES, IMPUESTOS Y ABONOS' in words:
i = words.index('3.CARGOS, COMISIONES, IMPUESTOS Y ABONOS') + 3
output += extract_from_to(words, '3.CARGOS, COMISIONES, IMPUESTOS Y ABONOS', end=i, line_length=3)
return output
def basic(text):
return split_words(text)
def split_words(text):
if isinstance(text, list):
text = "\n\n\n".join(text)
words = [t.strip() for t in text.split("\n") if t.strip() != '']
return words
def extract_from_to(word_list, start, line_length, end=None, merge_list=None, merge_character="\n"):
if not isinstance(start, int):
start = word_list.index(start)
if end is not None:
if not isinstance(end, int):
end = word_list.index(end)
return extract_by_line(word_list[start:end], line_length, merge_list, merge_character)
return extract_by_line(word_list[start:], line_length, merge_list, merge_character)
def extract_by_line(word_list, line_length, merge_list=None, merge_character="\n"):
if merge_list is not None:
word_list = merge_words(word_list, merge_list, merge_character)
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, merge_character):
for m in merge_list:
ixs = find_words(word_list, m)
if ixs is None:
continue
for i in ixs:
word_list = word_list[:i] + [merge_character.join(m)] + word_list[i + len(m):]
return word_list
def find_words(word_list, find_list):
ixs = [i for i, w in enumerate(word_list) if find_list[0] == w]
output = []
for i in ixs:
mistake = False
for k, m in enumerate(find_list):
if m != word_list[i + k]:
mistake = True
break
if mistake:
continue
output.append(i)
return output
def extract_data(word_list, start, line_length, end=None, merge_list=None, merge_character="\n", 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, merge_character)
output = []
line = []
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
output.append(line)
return output

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

@ -0,0 +1,53 @@
import argparse
import os
import contabilidad.pdf as pdf
import contabilidad.text_handler as th
from contabilidad.log import Logger, LOG_LEVEL
import ai.dictionary as dictionary
from ai.network import AI
def parse_settings(args):
output = {'filename': args.filename}
if not os.path.isfile(output['filename']):
output['filename'] = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'data', args.filename))
t = args.filename.split('.')
output['temp'] = os.path.realpath(os.path.join(os.path.dirname(output['filename']), t[0] + '-temp.pdf'))
output['dictionary'] = os.path.join(os.path.dirname(output['filename']), 'dictionary.json')
output['network'] = os.path.join(os.path.dirname(output['filename']), 'network.json')
output['log_file'] = args.log_file
if not os.path.isfile(output['log_file']):
output['log_file'] = os.path.join(os.path.dirname(os.path.dirname(output['filename'])), output['log_file'])
output['error_log_file'] = os.path.join(os.path.dirname(output['log_file']), 'error.log')
output['logger'] = Logger()
output['logger'].add_log(output['log_file']).add_log(output['error_log_file'], LOG_LEVEL.ERROR)
return output
def main(args):
settings = parse_settings(args)
print('Loading AI')
network = AI(settings['dictionary'], settings['logger'])
network.set_filename(settings['network'])
network.add_source({'filename': settings['filename'], 'password': args.password})
network.process_sources()
exit()
print('Loading dictionary.')
dictio = dictionary.Dictionary(settings['dictionary'], settings['logger'])
print('Getting possible phrases.')
dictio.process(settings['filename'], args.password)
dictio.to_data()
# print('Saving dictionary.')
# dictio.save()
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-f', '--filename', type=str)
parser.add_argument('-p', '--password', type=str, default='')
parser.add_argument('-l', '--log_file', type=str, default=None)
_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

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