diff --git a/.gitignore b/.gitignore
index b1cd869..f703fb2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,5 +13,3 @@
# Python
**/.idea/
-
-**/uploads/
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 73f69e0..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
-# Datasource local storage ignored files
-/dataSources/
-/dataSources.local.xml
-# Editor-based HTTP Client requests
-/httpRequests/
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
deleted file mode 100644
index a55e7a1..0000000
--- a/.idea/codeStyles/codeStyleConfig.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/contabilidad.iml b/.idea/contabilidad.iml
deleted file mode 100644
index 8558fe5..0000000
--- a/.idea/contabilidad.iml
+++ /dev/null
@@ -1,81 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index 2081fb2..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/php.xml b/.idea/php.xml
deleted file mode 100644
index e783343..0000000
--- a/.idea/php.xml
+++ /dev/null
@@ -1,87 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/api/.gitignore b/api/.gitignore
new file mode 100644
index 0000000..3b6c4bf
--- /dev/null
+++ b/api/.gitignore
@@ -0,0 +1 @@
+**/uploads/
diff --git a/api/common/Controller/Cuentas.php b/api/common/Controller/Cuentas.php
index 0100df3..3071c17 100644
--- a/api/common/Controller/Cuentas.php
+++ b/api/common/Controller/Cuentas.php
@@ -1,8 +1,10 @@
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;
@@ -100,6 +111,24 @@ class Cuentas {
];
return $this->withJson($response, $output);
}
+ protected function transaccionToArray(Service $service, Cuenta $cuenta, Transaccion $transaccion): array {
+ $arr = $transaccion->toArray();
+ if ($cuenta->moneda()->codigo === 'CLP') {
+ if ($transaccion->debito()->moneda()->codigo !== 'CLP' or $transaccion->credito()->moneda()->codigo !== 'CLP') {
+ if ($transaccion->debito()->moneda()->codigo !== 'CLP') {
+ $c = $transaccion->debito();
+ } else {
+ $c = $transaccion->credito();
+ }
+ $service->get($transaccion->fecha(), $c->moneda()->id);
+ $arr['valor'] = $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;
@@ -107,7 +136,7 @@ class Cuentas {
$transacciones = $cuenta->transacciones($limit, $start);
if (count($transacciones) > 0) {
foreach ($transacciones as &$transaccion) {
- $arr = $transaccion->toArray();
+ /*$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') {
@@ -120,7 +149,9 @@ class Cuentas {
$arr['valorFormateado'] = $cuenta->moneda()->format($arr['valor']);
}
}
- $transaccion = $arr;
+ $arr['debito']['categoria'] = $transaccion->debito()->categoria()->toArray();
+ $arr['credito']['categoria'] = $transaccion->credito()->categoria()->toArray();*/
+ $transaccion = $this->transaccionToArray($service, $cuenta, $transaccion);
}
}
}
@@ -131,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;
diff --git a/api/common/Controller/Files.php b/api/common/Controller/Files.php
new file mode 100644
index 0000000..e227dc5
--- /dev/null
+++ b/api/common/Controller/Files.php
@@ -0,0 +1,76 @@
+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);
+ }
+}
diff --git a/api/common/Controller/TiposCategorias.php b/api/common/Controller/TiposCategorias.php
index 1d34274..f023410 100644
--- a/api/common/Controller/TiposCategorias.php
+++ b/api/common/Controller/TiposCategorias.php
@@ -1,6 +1,7 @@
categorias());
}
$arr['saldo'] = abs($item->saldo($service));
- $maps = ['activo', 'pasivo', 'ganancia', 'perdida'];
- foreach ($maps as $m) {
- $p = $m . 's';
- $t = ucfirst($m);
- $cuentas = $item->getCuentasOf($t);
- if ($cuentas === false or $cuentas === null) {
- $arr[$p] = 0;
- continue;
- }
- $arr[$p] = array_reduce($cuentas, function($sum, $item) use($service) {
- return $sum + $item->saldo($service, true);
- });
- }
+ $arr['totales'] = $item->getTotales($service);
$item = $arr;
});
usort($tipos, function($a, $b) {
@@ -93,19 +82,7 @@ class TiposCategorias {
if ($categorias !== null) {
array_walk($categorias, function(&$item) use ($service) {
$arr = $item->toArray($service);
- $maps = ['activo', 'pasivo', 'ganancia', 'perdida'];
- foreach ($maps as $m) {
- $p = $m . 's';
- $t = ucfirst($m);
- $cuentas = $item->getCuentasOf($t);
- if ($cuentas === false or $cuentas === null) {
- $arr[$p] = 0;
- continue;
- }
- $arr[$p] = array_reduce($cuentas, function($sum, $item) use($service) {
- return $sum + $item->saldo($service, true);
- });
- }
+ $arr['totales'] = $item->getTotales($service);
$item = $arr;
});
}
@@ -120,6 +97,19 @@ class TiposCategorias {
public function balance(Request $request, Response $response, Factory $factory, Service $service): Response {
$tipos = $factory->find(TipoCategoria::class)->many();
$balance = array_reduce($tipos, function($sum, $item) use ($service) {
+ $totales = $item->getTotales($service);
+ if (!is_array($sum)) {
+ $sum = [];
+ }
+ foreach ($totales as $p => $total) {
+ if (!isset($sum[$p])) {
+ $sum[$p] = 0;
+ }
+ $sum[$p] += $total;
+ }
+ return $sum;
+ });
+ /*$balance = array_reduce($tipos, function($sum, $item) use ($service) {
$maps = ['activo', 'pasivo', 'ganancia', 'perdida'];
foreach ($maps as $m) {
$p = $m . 's';
@@ -136,7 +126,7 @@ class TiposCategorias {
});
}
return $sum;
- });
+ });*/
return $this->withJson($response, $balance);
}
}
diff --git a/api/common/Service/FileHandler.php b/api/common/Service/FileHandler.php
new file mode 100644
index 0000000..79ee9e7
--- /dev/null
+++ b/api/common/Service/FileHandler.php
@@ -0,0 +1,112 @@
+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);
+ }
+}
diff --git a/api/common/Service/TiposCambios.php b/api/common/Service/TiposCambios.php
index be9f88d..26f4b7b 100644
--- a/api/common/Service/TiposCambios.php
+++ b/api/common/Service/TiposCambios.php
@@ -5,6 +5,7 @@ use Carbon\Carbon;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
+use GuzzleHttp\Exception\ServerException;
use ProVM\Common\Factory\Model as Factory;
use Contabilidad\Moneda;
use Contabilidad\TipoCambio;
@@ -20,30 +21,22 @@ class TiposCambios {
$this->base_url = $api_url;
$this->key = $api_key;
}
- public function get(string $fecha, int $moneda_id) {
- $fecha = Carbon::parse($fecha);
- $moneda = $this->factory->find(Moneda::class)->one($moneda_id);
- if ($moneda->codigo == 'USD') {
- if ($fecha->weekday() == 0) {
- $fecha = $fecha->subWeek()->weekday(5);
- }
- if ($fecha->weekday() == 6) {
- $fecha = $fecha->weekday(5);
- }
+ protected function getWeekday(\DateTimeInterface $fecha) {
+ if ($fecha->weekday() == 0) {
+ return $fecha->subWeek()->weekday(5);
}
- $cambio = $moneda->cambio($fecha);
- if ($cambio) {
- if ($cambio->desde()->id != $moneda->id) {
- return 1 / $cambio->valor;
- }
- return $cambio->valor;
+ if ($fecha->weekday() == 6) {
+ return $fecha->weekday(5);
}
+ return $fecha;
+ }
+ protected function getValor(\DateTimeInterface $fecha, string $moneda_codigo) {
$data = [
'fecha' => $fecha->format('Y-m-d'),
- 'desde' => $moneda->codigo
+ 'desde' => $moneda_codigo
];
$headers = [
- 'Authorization' => 'Bearer ' . $this->key
+ 'Authorization' => "Bearer {$this->key}"
];
$url = implode('/', [
$this->base_url,
@@ -52,15 +45,39 @@ class TiposCambios {
]);
try {
$response = $this->client->request('POST', $url, ['json' => $data, 'headers' => $headers]);
- } catch (ConnectException | RequestException $e) {
+ } catch (ConnectException | RequestException | ServerException $e) {
error_log($e);
return null;
}
if ($response->getStatusCode() !== 200) {
+ error_log('Could not connect to python API.');
return null;
}
$result = json_decode($response->getBody());
- $valor = $result->serie[0]->valor;
+ if (isset($result->message) and $result->message === 'Not Authorized') {
+ error_log('Not authorized for connecting to python API.');
+ return null;
+ }
+ return $result->serie[0]->valor;
+ }
+ public function get(string $fecha, int $moneda_id) {
+ $fecha = Carbon::parse($fecha);
+ $moneda = $this->factory->find(Moneda::class)->one($moneda_id);
+ if ($moneda->codigo == 'USD') {
+ $fecha = $this->getWeekday($fecha);
+ }
+ // If a value exists in the database
+ $cambio = $moneda->cambio($fecha);
+ if ($cambio !== null) {
+ if ($cambio->desde()->id != $moneda->id) {
+ return 1 / $cambio->valor;
+ }
+ return $cambio->valor;
+ }
+ $valor = $this->getValor($fecha, $moneda->codigo);
+ if ($valor === null) {
+ return 1;
+ }
$data = [
'fecha' => $fecha->format('Y-m-d H:i:s'),
'desde_id' => $moneda->id,
diff --git a/api/nginx.conf b/api/nginx.conf
index 5d96382..c946d40 100644
--- a/api/nginx.conf
+++ b/api/nginx.conf
@@ -12,20 +12,19 @@ server {
}
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;
diff --git a/api/public/uploads/pdfs/BICE-CC-2021-09.pdf b/api/public/uploads/pdfs/BICE-CC-2021-09.pdf
deleted file mode 100644
index d521655..0000000
Binary files a/api/public/uploads/pdfs/BICE-CC-2021-09.pdf and /dev/null differ
diff --git a/api/public/uploads/pdfs/Scotiabank-CC-2021-10.pdf b/api/public/uploads/pdfs/Scotiabank-CC-2021-10.pdf
deleted file mode 100644
index 8fef2f3..0000000
Binary files a/api/public/uploads/pdfs/Scotiabank-CC-2021-10.pdf and /dev/null differ
diff --git a/api/resources/routes/cuentas.php b/api/resources/routes/cuentas.php
index 06ea592..76d6d03 100644
--- a/api/resources/routes/cuentas.php
+++ b/api/resources/routes/cuentas.php
@@ -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']);
diff --git a/api/resources/routes/uploads.php b/api/resources/routes/uploads.php
new file mode 100644
index 0000000..6138be8
--- /dev/null
+++ b/api/resources/routes/uploads.php
@@ -0,0 +1,12 @@
+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']);
+});
diff --git a/api/setup/settings/02_common.php b/api/setup/settings/02_common.php
index 1185ad3..eeb2bd9 100644
--- a/api/setup/settings/02_common.php
+++ b/api/setup/settings/02_common.php
@@ -19,7 +19,7 @@ return [
'public'
]);
$arr['uploads'] = implode(DIRECTORY_SEPARATOR, [
- $arr['public'],
+ $arr['base'],
'uploads'
]);
$arr['pdfs'] = implode(DIRECTORY_SEPARATOR, [
diff --git a/api/setup/setups/02_common.php b/api/setup/setups/02_common.php
index 27836d5..56eabc2 100644
--- a/api/setup/setups/02_common.php
+++ b/api/setup/setups/02_common.php
@@ -14,27 +14,27 @@ return [
$c->get(Contabilidad\Common\Service\Auth::class)
);
},
- Contabilidad\Common\Service\PdfHandler::class => function(Container $c) {
- return new Contabilidad\Common\Service\PdfHandler($c->get(GuzzleHttp\Client::class), $c->get('folders')->pdfs, implode('/', [
- $c->get('urls')->python,
- 'pdf',
- 'parse'
- ]));
- },
- Contabilidad\Common\Service\CsvHandler::class => function(Container $c) {
- return new Contabilidad\Common\Service\CsvHandler($c->get('folders')->csvs);
- },
- Contabilidad\Common\Service\XlsHandler::class => function(Container $c) {
- return new Contabilidad\Common\Service\XlsHandler($c->get('folders')->xlss);
- },
- Contabilidad\Common\Service\DocumentHandler::class => function(Container $c) {
- $handlers = [
- $c->get(Contabilidad\Common\Service\XlsHandler::class),
- $c->get(Contabilidad\Common\Service\CsvHandler::class),
- $c->get(Contabilidad\Common\Service\PdfHandler::class)
- ];
- return new Contabilidad\Common\Service\DocumentHandler($handlers);
- },
+ Contabilidad\Common\Service\PdfHandler::class => function(Container $c) {
+ return new Contabilidad\Common\Service\PdfHandler($c->get(GuzzleHttp\Client::class), $c->get('folders')->pdfs, implode('/', [
+ $c->get('urls')->python,
+ 'pdf',
+ 'parse'
+ ]));
+ },
+ Contabilidad\Common\Service\CsvHandler::class => function(Container $c) {
+ return new Contabilidad\Common\Service\CsvHandler($c->get('folders')->csvs);
+ },
+ Contabilidad\Common\Service\XlsHandler::class => function(Container $c) {
+ return new Contabilidad\Common\Service\XlsHandler($c->get('folders')->xlss);
+ },
+ Contabilidad\Common\Service\DocumentHandler::class => function(Container $c) {
+ $handlers = [
+ $c->get(Contabilidad\Common\Service\XlsHandler::class),
+ $c->get(Contabilidad\Common\Service\CsvHandler::class),
+ $c->get(Contabilidad\Common\Service\PdfHandler::class)
+ ];
+ return new Contabilidad\Common\Service\DocumentHandler($handlers);
+ },
Contabilidad\Common\Service\TiposCambios::class => function(Container $c) {
return new Contabilidad\Common\Service\TiposCambios(
$c->get(GuzzleHttp\Client::class),
@@ -42,5 +42,17 @@ return [
$c->get('python_api'),
$c->get('python_key')
);
+ },
+ Contabilidad\Common\Service\FileHandler::class => function(Container $c) {
+ return new Contabilidad\Common\Service\FileHandler((object) [
+ 'folder' => $c->get('folders')->uploads,
+ 'types' => [
+ 'text/csv' => 'csvs',
+ 'application/pdf' => 'pdfs',
+ 'application/vnd.ms-excel' => 'xlss',
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlss',
+ 'application/json' => 'jsons'
+ ]
+ ]);
}
];
diff --git a/api/src/Categoria.php b/api/src/Categoria.php
index be3cd78..cb22e21 100644
--- a/api/src/Categoria.php
+++ b/api/src/Categoria.php
@@ -18,6 +18,11 @@ class Categoria extends Model {
public function cuentas() {
if ($this->cuentas === null) {
$this->cuentas = $this->parentOf(Cuenta::class, [Model::CHILD_KEY => 'categoria_id']);
+ if ($this->cuentas !== null) {
+ usort($this->cuentas, function($a, $b) {
+ return strcmp($a->nombre, $b->nombre);
+ });
+ }
}
return $this->cuentas;
}
@@ -41,33 +46,43 @@ class Categoria extends Model {
])
->many();
}
- protected $activos;
- public function activos() {
- if ($this->activos === null) {
- $this->activos = $this->getCuentasOf('Activo');
+ protected $cuentas_of;
+ public function getCuentas() {
+ if ($this->cuentas_of === null) {
+ $tipos = $this->factory->find(TipoCuenta::class)->many();
+ $cos = [];
+ foreach ($tipos as $tipo) {
+ $p = strtolower($tipo->descripcion) . 's';
+ $cos[$p] = [];
+ $cuentas = $this->getCuentasOf($tipos->descripcion);
+ if ($cuentas === null) {
+ continue;
+ }
+ $cos[$p] = $cuentas;
+ }
+ $this->cuentas_of = $cos;
}
- return $this->activos();
+ return $this->cuentas_of;
}
- protected $pasivos;
- public function pasivos() {
- if ($this->pasivos === null) {
- $this->activos = $this->getCuentasOf('Pasivo');
+ protected $totales;
+ public function getTotales(Service $service) {
+ if ($this->totales === null) {
+ $tipos = $this->factory->find(TipoCuenta::class)->many();
+ $totals = [];
+ foreach ($tipos as $tipo) {
+ $p = strtolower($tipo->descripcion) . 's';
+ $totals[$p] = 0;
+ $cuentas = $this->getCuentasOf($tipo->descripcion);
+ if ($cuentas === null) {
+ continue;
+ }
+ $totals[$p] = array_reduce($cuentas, function($sum, $item) use ($service) {
+ return $sum + $item->saldo($service, true);
+ });
+ }
+ $this->totales = $totals;
}
- return $this->pasivos;
- }
- protected $ganancias;
- public function ganancias() {
- if ($this->ganancias === null) {
- $this->ganancias = $this->getCuentasOf('Ganancia');
- }
- return $this->ganancias;
- }
- protected $perdidas;
- public function perdidas() {
- if ($this->perdidas === null) {
- $this->perdidas = $this->getCuentasOf('Perdida');
- }
- return $this->perdidas;
+ return $this->totales;
}
protected $saldo;
diff --git a/api/src/Cuenta.php b/api/src/Cuenta.php
index 4caa29b..46f6d18 100644
--- a/api/src/Cuenta.php
+++ b/api/src/Cuenta.php
@@ -1,7 +1,9 @@
transacciones === null) {
$transacciones = Model::factory(Transaccion::class)
+ ->select('transacciones.*')
->join('cuentas', 'cuentas.id = transacciones.debito_id OR cuentas.id = transacciones.credito_id')
->whereEqual('cuentas.id', $this->id)
->orderByAsc('transacciones.fecha');
@@ -63,15 +65,52 @@ class Cuenta extends Model {
$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(Service $service = null, $in_clp = false) {
diff --git a/api/src/Moneda.php b/api/src/Moneda.php
index 97602a2..9405f41 100644
--- a/api/src/Moneda.php
+++ b/api/src/Moneda.php
@@ -16,17 +16,17 @@ class Moneda extends Model {
protected static $fields = ['denominacion', 'codigo'];
public function format($valor) {
- return implode('', [
+ return trim(implode('', [
$this->prefijo,
number_format($valor, $this->decimales, ',', '.'),
$this->sufijo
- ]);
+ ]));
}
public function cambio(\DateTime $fecha) {
$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) {
+ 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();
@@ -43,4 +43,14 @@ class Moneda extends Model {
}
return $cambio->transform($valor);
}
+
+ public function toArray(): array {
+ $arr = parent::toArray();
+ $arr['format'] = [
+ 'prefijo' => $this->prefijo,
+ 'sufijo' => $this->sufijo,
+ 'decimales' => $this->decimales
+ ];
+ return $arr;
+ }
}
diff --git a/api/src/TipoCategoria.php b/api/src/TipoCategoria.php
index bd4b778..b6048d7 100644
--- a/api/src/TipoCategoria.php
+++ b/api/src/TipoCategoria.php
@@ -33,6 +33,26 @@ class TipoCategoria extends Model {
['categorias.tipo_id', $this->id]
])->many();
}
+ protected $totales;
+ public function getTotales(Service $service) {
+ if ($this->totales === null) {
+ $tipos = $this->factory->find(TipoCuenta::class)->many();
+ $totals = [];
+ foreach ($tipos as $tipo) {
+ $p = strtolower($tipo->descripcion) . 's';
+ $totals[$p] = 0;
+ $cuentas = $this->getCuentasOf($tipo->descripcion);
+ if ($cuentas === null) {
+ continue;
+ }
+ $totals[$p] = array_reduce($cuentas, function($sum, $item) use ($service) {
+ return $sum + $item->saldo($service, true);
+ });
+ }
+ $this->totales = $totals;
+ }
+ return $this->totales;
+ }
protected $saldo;
public function saldo(Service $service = null) {
diff --git a/python/Dockerfile b/python/Dockerfile
index 1eedf44..96ee6c3 100644
--- a/python/Dockerfile
+++ b/python/Dockerfile
@@ -2,7 +2,7 @@ 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
+RUN pip install flask pyyaml pypdf4 gunicorn camelot-py[cv] pikepdf httpx
WORKDIR /app
@@ -12,4 +12,5 @@ EXPOSE 5000
WORKDIR /app/src
-CMD ["gunicorn", "-b 0.0.0.0:5000", "app:app"]
+CMD ["python", "app.py"]
+#CMD ["gunicorn", "-b 0.0.0.0:5000", "app:app"]
diff --git a/python/config/.passwords.yml b/python/config/.passwords.yml
index f44e275..1acd267 100644
--- a/python/config/.passwords.yml
+++ b/python/config/.passwords.yml
@@ -1,3 +1,4 @@
passwords:
- 0839
- 159608395
+ - 15960839
diff --git a/python/data/EECCvirtual-Visa.pdf b/python/data/EECCvirtual-Visa.pdf
new file mode 100644
index 0000000..7ea5ef8
Binary files /dev/null and b/python/data/EECCvirtual-Visa.pdf differ
diff --git a/python/src/ai/dictionary.py b/python/src/ai/dictionary.py
new file mode 100644
index 0000000..c42caef
--- /dev/null
+++ b/python/src/ai/dictionary.py
@@ -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
diff --git a/python/src/ai/models.py b/python/src/ai/models.py
new file mode 100644
index 0000000..184a0ba
--- /dev/null
+++ b/python/src/ai/models.py
@@ -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
diff --git a/python/src/ai/network.py b/python/src/ai/network.py
new file mode 100644
index 0000000..ae0345a
--- /dev/null
+++ b/python/src/ai/network.py
@@ -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
diff --git a/python/src/app.py b/python/src/app.py
index 5722eb2..4c6bc1b 100644
--- a/python/src/app.py
+++ b/python/src/app.py
@@ -1,22 +1,43 @@
-import io
import json
import os
import sys
-from flask import Flask, request
+import httpx
+from flask import Flask, request, jsonify
import contabilidad.pdf as pdf
import contabilidad.passwords as passwords
-import contabilidad.log as log
import contabilidad.text_handler as th
+from contabilidad.log import Log
app = Flask(__name__)
-log.logging['filename'] = '/var/log/python/contabilidad.log'
+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']]
@@ -32,6 +53,11 @@ def pdf_parse():
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'))
@@ -48,8 +74,35 @@ def pdf_parse():
out[i] = line
outputs.append(out)
os.remove(temp)
- output.append({'filename': file['filename'], 'text': outputs})
- return json.dumps(output)
+ 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__':
diff --git a/python/src/contabilidad/__pycache__/log.cpython-39.pyc b/python/src/contabilidad/__pycache__/log.cpython-39.pyc
deleted file mode 100644
index 36d64f1..0000000
Binary files a/python/src/contabilidad/__pycache__/log.cpython-39.pyc and /dev/null differ
diff --git a/python/src/contabilidad/log.py b/python/src/contabilidad/log.py
index c16024d..a1d908b 100644
--- a/python/src/contabilidad/log.py
+++ b/python/src/contabilidad/log.py
@@ -1,19 +1,65 @@
+import os.path
import time
-
-
-logging = {
- 'filename': '/var/log/python/error.log'
-}
+import traceback
class LOG_LEVEL:
- INFO = 'INFO'
- WARNING = 'WARNING'
- DEBUG = 'DEBUG'
- ERROR = 'ERROR'
+ 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]
-def log(message, level=LOG_LEVEL.INFO):
- filename = logging['filename']
- with open(filename, 'a') as f:
- f.write(time.strftime('[%Y-%m-%d %H:%M:%S] ') + ' - ' + level + ': ' + message)
+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]])
diff --git a/python/src/contabilidad/text_handler.py b/python/src/contabilidad/text_handler.py
index 27690ad..6d5240c 100644
--- a/python/src/contabilidad/text_handler.py
+++ b/python/src/contabilidad/text_handler.py
@@ -1,48 +1,112 @@
-def text_cleanup(text, filename: str = None):
+def text_cleanup(text: str):
if isinstance(text, list):
- output = []
- for t in text:
- output.append(text_cleanup(t, filename=filename))
- return output
- if filename is None:
- return text
- if 'bice' in filename.lower():
- return bice(text)
- if 'scotiabank' in filename.lower():
- return scotiabank(text)
- return text
+ 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 = text.split("\n\n\n")
- print(lines)
- return 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 = text.split("\n")
+ words = split_words(text)
output = [words[0]]
- output = output + extract_from_to(words, 'No. CTA.', end='VENCIMIENTO LINEA DE CREDITO', line_length=3)
- output = output + extract_from_to(words, 'VENCIMIENTO LINEA DE CREDITO',
- end='NOMBRE EJECUTIVO: LILIAN AVILA MANRIQUEZ', line_length=2)
- output = output + extract_from_to(words, 'NOMBRE EJECUTIVO: LILIAN AVILA MANRIQUEZ', end='SALDO ANTERIOR',
- line_length=1)
- output = output + extract_from_to(words, 'SALDO ANTERIOR', end='FECHA', line_length=4)
- output = output + extract_data(words, 'FECHA', end='ACTUALICE SIEMPRE ANTECEDENTES LEGALES, ', line_length=6,
- merge_list=[['DOCTO', 'No.'], ['SALDO', 'DIARIO']])
- [print(li) for li in output]
- return text
+ 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 extract_from_to(word_list, start, line_length, end: str = None, merge_list=None):
+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:
- return extract_by_line(word_list[word_list.index(start):word_list.index(end)], line_length, merge_list)
- return extract_by_line(word_list[word_list.index(start):], line_length, merge_list)
+ 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):
+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)
+ word_list = merge_words(word_list, merge_list, merge_character)
output = []
line = []
for k, w in enumerate(word_list):
@@ -54,22 +118,39 @@ def extract_by_line(word_list, line_length, merge_list=None):
return output
-def merge_words(word_list, merge_list):
+def merge_words(word_list, merge_list, merge_character):
for m in merge_list:
- i = word_list.index(m[0])
- word_list = word_list[:i] + ["\n".join(m)] + word_list[i+len(m):]
+ 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 extract_data(word_list, start, line_length, end=None, merge_list=None, date_sep='/'):
+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)
+ word_list = merge_words(word_list, merge_list, merge_character)
output = []
line = []
- line_num = 0
col = 0
for k, w in enumerate(word_list):
if col > 0 and col % line_length == 0:
@@ -87,4 +168,5 @@ def extract_data(word_list, start, line_length, end=None, merge_list=None, date_
continue
line.append(w)
col += 1
+ output.append(line)
return output
diff --git a/python/src/main.py b/python/src/main.py
index 229b132..bcbd999 100644
--- a/python/src/main.py
+++ b/python/src/main.py
@@ -3,22 +3,51 @@ 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):
- filename = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'data', args.filename))
- temp = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'data', args.temp_filename))
- pdf.remove_encryption(filename, args.password, temp)
- obj = pdf.get_data(temp)
- obj = pdf.get_text(filename, args.password)
- text = th.text_cleanup(obj, filename=str(args.filename))
- os.remove(temp)
+ 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('-t', '--temp_filename', type=str)
+ parser.add_argument('-l', '--log_file', type=str, default=None)
_args = parser.parse_args()
main(_args)
diff --git a/ui/Dockerfile b/ui/Dockerfile
index 952159e..a7a3ff8 100644
--- a/ui/Dockerfile
+++ b/ui/Dockerfile
@@ -1,5 +1,9 @@
FROM php:8-fpm
+RUN apt-get update -y && apt-get install -y git libzip-dev zip
+
+RUN docker-php-ext-install zip
+
COPY --from=composer /usr/bin/composer /usr/bin/composer
WORKDIR /app
diff --git a/ui/common/Controller/Cuentas.php b/ui/common/Controller/Cuentas.php
index 332096d..19913fc 100644
--- a/ui/common/Controller/Cuentas.php
+++ b/ui/common/Controller/Cuentas.php
@@ -10,7 +10,8 @@ class Cuentas {
return $view->render($response, 'cuentas.list');
}
public function show(Request $request, Response $response, View $view, $cuenta_id): Response {
- return $view->render($response, 'cuentas.show', compact('cuenta_id'));
+ $max_transacciones = 100;
+ return $view->render($response, 'cuentas.show', compact('cuenta_id', 'max_transacciones'));
}
public function add(Request $request, Response $response, View $view): Response {
return $view->render($response, 'cuentas.add');
diff --git a/ui/common/Controller/Uploads.php b/ui/common/Controller/Uploads.php
new file mode 100644
index 0000000..c04cea0
--- /dev/null
+++ b/ui/common/Controller/Uploads.php
@@ -0,0 +1,24 @@
+render($response, 'uploads.list');
+ }
+ public function get(Request $request, Response $response, Client $client, $folder, $filename): Response {
+ $resp = $client->get(implode('/', ['upload', $folder, $filename]));
+ $file = $resp->getBody();
+ return $response
+ ->withHeader('Content-Type', $resp->getHeader('Content-Type'))
+ ->withHeader('Content-Disposition', 'attachment; filename=' . $filename)
+ ->withAddedHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
+ ->withHeader('Cache-Control', 'post-check=0, pre-check=0')
+ ->withHeader('Pragma', 'no-cache')
+ ->withBody($file);
+ }
+}
diff --git a/ui/composer.json b/ui/composer.json
index 9cfd883..85d0e18 100644
--- a/ui/composer.json
+++ b/ui/composer.json
@@ -5,10 +5,11 @@
"require": {
"php-di/php-di": "^6.3",
"php-di/slim-bridge": "^3.1",
- "rubellum/slim-blade-view": "^0.1.1",
"nyholm/psr7-server": "^1.0",
"zeuxisoo/slim-whoops": "^0.7.3",
- "nyholm/psr7": "^1.4"
+ "nyholm/psr7": "^1.4",
+ "guzzlehttp/guzzle": "^7.4",
+ "berrnd/slim-blade-view": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^9.5",
@@ -24,11 +25,5 @@
"psr-4": {
"Contabilidad\\Common\\": "common"
}
- },
- "repositories": [
- {
- "type": "git",
- "url": "http://git.provm.cl/ProVM/controller.git"
- }
- ]
+ }
}
diff --git a/ui/public/assets/scripts/cuentas.show.js b/ui/public/assets/scripts/cuentas.show.js
index c4f03c6..5f863ce 100644
--- a/ui/public/assets/scripts/cuentas.show.js
+++ b/ui/public/assets/scripts/cuentas.show.js
@@ -1,5 +1,5 @@
class Transaccion {
- constructor({id, debito_id, credito_id, fecha, glosa, detalle, valor, debito, credito, fechaFormateada, valorFormateado}) {
+ constructor({id, debito_id, credito_id, fecha, glosa, detalle, valor, debito, credito, fechaFormateada, valorFormateado, format}) {
this.id = id
this.debito_id = debito_id
this.credito_id = credito_id
@@ -13,6 +13,7 @@ class Transaccion {
valor,
formateado: valorFormateado
}
+ this.format_array = format
this.debito = debito
this.credito = credito
this.modal = null
@@ -33,7 +34,7 @@ class Transaccion {
}
return !this.isDebito()
}
- draw({saldo, format}) {
+ draw({saldo, format, format_array, format_call}) {
const fuente = (this.isDebito()) ? this.credito : this.debito
return $('
').append(
$(' | ').html(this.fecha.formateada)
@@ -44,13 +45,13 @@ class Transaccion {
).append(
$(' | ').html(this.glosa + '
' + this.detalle)
).append(
- $(' | ').attr('class', 'right aligned').html((this.isIncrement()) ? '' : format.format(this.valor.valor))
+ $(' | ').attr('class', 'right aligned').html((this.isIncrement()) ? '' : format_call({value: this.valor.valor, format, format_array}))
).append(
- $(' | ').attr('class', 'right aligned').html((this.isIncrement()) ? format.format(this.valor.valor) : '')
+ $(' | ').attr('class', 'right aligned').html((this.isIncrement()) ? format_call({value: this.valor.valor, format, format_array}) : '')
).append(
- $(' | ').attr('class', 'right aligned').html(format.format(saldo))
+ $(' | ').attr('class', 'right aligned').html(format_call({value: saldo, format_array, format}))
).append(
- $(' | ').attr('class', 'right aligned')/*.append(
+ $(' | ').attr('class', 'right aligned').append(
$('').attr('class', 'ui tiny circular icon button').append(
$('').attr('class', 'edit icon')
).click((e) => {
@@ -58,7 +59,7 @@ class Transaccion {
this.edit()
return false
})
- )*/.append(
+ ).append(
$('').attr('class', 'ui tiny circular red icon button').append(
$('').attr('class', 'remove icon')
).click((e) => {
@@ -71,7 +72,15 @@ class Transaccion {
}
edit() {
const form = this.modal.find('form')
- form.find("[name='fecha']")
+ form.trigger('reset')
+ form.find("[name='id']").val(this.id)
+ form.find(".ui.calendar").calendar('set date', new Date(this.fecha.fecha))
+ form.find("[name='cuenta']").dropdown('set selected', (this.isDebito()) ? this.credito_id : this.credito_id)
+ form.find("[name='glosa']").val(this.glosa)
+ form.find("[name='detalle']").val(this.detalle)
+ form.find("[name='valor']").val(((this.isDebito()) ? -1 : 1) * this.valor.valor)
+ modalToEdit(this.modal)
+ this.modal.modal('show')
}
remove() {
sendDelete(_urls.api + '/transaccion/' + this.id + '/delete').then(() => {
@@ -82,56 +91,110 @@ class Transaccion {
const transacciones = {
id: '#transacciones',
+ mes: '#mes',
+ buttons: {
+ prev: '#prev_button',
+ left: '#left_button',
+ right: '#right_button',
+ next: '#next_button'
+ },
cuenta_id: 0,
cuenta: null,
transacciones: [],
cuentas: [],
+ date: new Date(),
saldo: 0,
+ acumulation: 0,
+ intl_format: null,
+ max: null,
+ format: ({format_array, format, value}) => {
+ let output = []
+ if (format_array.prefijo !== '') {
+ output.push(format_array.prefijo)
+ }
+ output.push(format.format(Math.round(value * Math.pow(10, format_array.decimales)) / Math.pow(10, format_array.decimales)))
+ if (format_array.sufijo !== '') {
+ output.push(format_array.sufijo)
+ }
+ return output.join('')
+ },
get: function() {
return {
transacciones: () => {
+ this.draw().loading()
let promises = []
sendGet(_urls.api + '/cuenta/' + this.cuenta_id + '/transacciones/amount').then((data) => {
if (data.cuenta === null) {
return
}
this.cuenta = data.cuenta
- this.saldo = this.cuenta.saldo
- $('#cuenta').html(this.cuenta.nombre + ' (' + this.cuenta.categoria.nombre + ')').append(
- $('').attr('class', 'square full icon').css('color', '#' + this.cuenta.tipo.color)
- )
- const amount = data.transacciones
- const step = 50
- for (let i = 0; i < amount; i += step) {
- promises.push(
- sendGet(_urls.api + '/cuenta/' + this.cuenta_id + '/transacciones/' + step + '/' + i)
+ this.intl_format = Intl.NumberFormat('es-CL')
+ sendGet(_urls.api + '/cuenta/' + this.cuenta_id + '/categoria').then((resp) => {
+ this.cuenta.categoria = resp.categoria
+ }).then(() => {
+ //this.saldo = this.cuenta.saldo
+ $('#cuenta').html(this.cuenta.nombre + ' (' + this.cuenta.categoria.nombre + ')').append(
+ $('').attr('class', 'square full icon').css('color', '#' + this.cuenta.tipo.color)
)
- }
- if (promises.length > 0) {
- Promise.all(promises).then((data_arr) => {
- this.transacciones = []
- data_arr.forEach(data => {
- if (data.transacciones === null || data.transacciones.length === 0) {
- return
- }
- $.each(data.transacciones, (i, el) => {
- const tr = new Transaccion(el)
- tr.setCuenta(this.cuenta)
- tr.setModal(this.modal)
- this.transacciones.push(tr)
+ promises = this.get().transaccionesMes(this.date)
+ if (promises.length > 0) {
+ Promise.all(promises).then((data_arr) => {
+ this.transacciones = []
+ data_arr.forEach(data => {
+ if (data.transacciones === null || data.transacciones.length === 0) {
+ return
+ }
+ $.each(data.transacciones, (i, el) => {
+ const tr = new Transaccion(el)
+ tr.setCuenta(this.cuenta)
+ tr.setModal(this.modal)
+ this.transacciones.push(tr)
+ })
+ })
+ this.transacciones.sort((a, b) => {
+ return (new Date(b.fecha)) - (new Date(a.fecha))
+ })
+ }).then(() => {
+ this.get().transaccionesAcumulacion(this.date).then((response) => {
+ this.acumulation = response.acumulation
+ this.saldo = response.acumulation
+ }).then(() => {
+ this.draw().table()
+ this.draw().acumulation()
})
})
- this.transacciones.sort((a, b) => {
- return (new Date(b.fecha)) - (new Date(a.fecha))
- })
- }).then(() => {
- this.draw()
- })
- } else {
- this.draw()
- }
+ } else {
+ this.draw().table()
+ }
+ })
})
},
+ transaccionesLimit: () => {
+ let promises = []
+ const amount = data.transacciones
+ let step = 50
+ let start = 0
+ if (this.max !== null) {
+ step = Math.min(50, this.max)
+ start = Math.max(0, amount - this.max)
+ }
+ for (let i = start; i <= amount; i += step) {
+ promises.push(
+ sendGet(_urls.api + '/cuenta/' + this.cuenta_id + '/transacciones/' + step + '/' + i)
+ )
+ }
+ return promises
+ },
+ transaccionesMes: (mes) => {
+ let promises = []
+ promises.push(
+ sendGet(_urls.api + '/cuenta/' + this.cuenta_id + '/transacciones/month/' + [mes.getFullYear(), mes.getMonth() + 1, '1'].join('-'))
+ )
+ return promises
+ },
+ transaccionesAcumulacion: (mes) => {
+ return sendGet(_urls.api + '/cuenta/' + this.cuenta_id + '/transacciones/acum/' + [mes.getFullYear(), mes.getMonth() + 1, '1'].join('-'))
+ },
cuentas: () => {
return sendGet(_urls.api + '/cuentas').then((data) => {
if (data.cuentas === null || data.cuentas.length === 0) {
@@ -141,22 +204,69 @@ const transacciones = {
}).then(() => {
const select = this.modal.find("[name='cuenta']")
$.each(this.cuentas, (i, el) => {
- select.append(
- $('').attr('value', el.id).html(el.nombre + ' (' + el.categoria.nombre + ')')
- )
+ this.get().categoria(i).then(() => {
+ select.append(
+ $('').attr('value', el.id).html(el.nombre + ' (' + el.categoria.nombre + ')')
+ )
+ })
})
})
+ },
+ categoria: (idx) => {
+ return sendGet(_urls.api + '/cuenta/' + this.cuentas[idx].id + '/categoria').then((data) => {
+ this.cuentas[idx].categoria = data.categoria
+ })
}
}
},
draw: function() {
- const format = Intl.NumberFormat('es-CL', {style: 'currency', currency: this.cuenta.moneda.codigo})
- const parent = $(this.id)
- parent.html('')
- $.each(this.transacciones, (i, el) => {
- parent.append(el.draw({saldo: this.saldo, format: format}))
- this.saldo = this.saldo + parseInt(el.valor.valor) * ((el.isIncrement()) ? 1 : -1)
- })
+ return {
+ loading: () => {
+ const parent = $(this.id)
+ parent.html('')
+ parent.append(
+ $('
').append(
+ $(' | ').attr('colspan', 7).append(
+ $('').attr('class', 'ui active dimmer').append(
+ $('').attr('class', 'ui indeterminate elastic text loader').html('Buscando los datos')
+ )
+ )
+ )
+ )
+ },
+ table: () => {
+ const parent = $(this.id)
+ parent.html('')
+ $.each(this.transacciones, (i, el) => {
+ this.saldo = this.saldo + parseInt(el.valor.valor) * ((el.isIncrement()) ? 1 : -1)
+ parent.append(el.draw({saldo: this.saldo, format: this.intl_format, format_array: this.cuenta.moneda.format, format_call: this.format}))
+ })
+ },
+ acumulation: () => {
+ const parent = $(this.id)
+ parent.prepend(
+ $('
').append(
+ $(' | ').html('')
+ ).append(
+ $(' | ').html('')
+ ).append(
+ $(' | ').html('Acumulacion Anterior')
+ ).append(
+ $(' | ').attr('class', 'right aligned').html('')
+ ).append(
+ $(' | ').attr('class', 'right aligned').html('')
+ ).append(
+ $(' | ').attr('class', 'right aligned').html(this.format({
+ format_array: this.cuenta.moneda.format,
+ format: this.intl_format,
+ value: this.acumulation
+ }))
+ ).append(
+ $(' | ').attr('class', 'right aligned').html('')
+ )
+ )
+ }
+ }
},
add: function() {
return {
@@ -172,7 +282,7 @@ const transacciones = {
fecha: fecha,
valor: $("[name='cambio']").val()
})
- sendPut(_urls.api + '/tipos/cambios/add', data1)
+ sendPost(_urls.api + '/tipos/cambios/add', data1)
const valor = $("[name='valor']").val()
const cuenta = $("[name='cuenta']").val()
@@ -191,6 +301,56 @@ const transacciones = {
}
}
},
+ edit: function() {
+ const id = $("[name='id']").val()
+ const fecha = $("[name='fecha']").val()
+ const cuenta = $("[name='cuenta']").val()
+ const glosa = $("[name='glosa']").val()
+ const detalle = $("[name='detalle']").val()
+ const valor = $("[name='valor']").val()
+ const data = JSON.stringify({
+ debito_id: (valor < 0) ? this.cuenta_id : cuenta,
+ credito_id: (valor < 0) ? cuenta : this.cuenta_id,
+ fecha,
+ glosa,
+ detalle,
+ valor: (valor < 0) ? -valor : valor
+ })
+ return sendPut(_urls.api + '/transaccion/' + id + '/edit', data).then(() => {
+ this.modal.modal('hide')
+ this.get().transacciones()
+ })
+ },
+ changeMonth: function(dif) {
+ let d = this.date
+ d.setMonth(this.date.getMonth() + dif)
+ this.date = d
+ this.checkButtons()
+ $(this.mes).calendar('set date', this.date)
+ },
+ changeYear: function(dif) {
+ let d = this.date
+ d.setFullYear(this.date.getFullYear() + dif)
+ this.date = d
+ this.checkButtons()
+ $(this.mes).calendar('set date', this.date)
+ },
+ checkButtons: function() {
+ let f = new Date()
+ if (this.date.getMonth() === f.getMonth() && this.date.getFullYear() === f.getFullYear()) {
+ $(this.buttons.right).addClass('disabled')
+ } else {
+ $(this.buttons.right).removeClass('disabled')
+ }
+ if (this.date.getFullYear() === f.getFullYear()) {
+ $(this.buttons.next).addClass('disabled')
+ } else {
+ $(this.buttons.next).removeClass('disabled')
+ }
+ },
+ refresh: function () {
+ this.get().transacciones()
+ },
build: function() {
return {
modal: () => {
@@ -201,7 +361,12 @@ const transacciones = {
})
this.modal.find('form').submit((e) => {
e.preventDefault()
- this.add().exec()
+ const add = $(e.currentTarget).find('.plus.icon')
+ if (add.length > 0) {
+ this.add().exec()
+ } else {
+ this.edit()
+ }
return false
})
this.modal.find('.ui.calendar').calendar({
@@ -218,12 +383,53 @@ const transacciones = {
maxDate: new Date()
})
this.get().cuentas()
+ },
+ mes: () => {
+ const meses = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Dicembre']
+ $(this.mes).calendar({
+ type: 'month',
+ initialDate: this.date,
+ maxDate: new Date(),
+ text: {
+ months: meses,
+ monthsShort: meses.map((item) => {item.slice(0, 3)})
+ },
+ formatter: {
+ date: function (date, settings) {
+ if (!date) return '';
+ return meses[date.getMonth()] + ', ' + date.getFullYear()
+ }
+ },
+ onChange: (date) => {
+ this.date = date
+ this.refresh()
+ }
+ })
+ },
+ buttons: () => {
+ $(this.buttons.right).click((e) => {
+ this.changeMonth(1)
+ })
+ $(this.buttons.left).click((e) => {
+ this.changeMonth(-1)
+ })
+ $(this.buttons.next).click(() => {
+ this.changeYear(1)
+ })
+ $(this.buttons.prev).click(() => {
+ this.changeYear(-1)
+ })
}
}
},
setup: function() {
this.build().modal()
- $(this.id).parent().find('.ui.button').click(() => {
+ this.build().mes()
+ this.build().buttons()
+ $(this.id).parent().find('#refresh').click(() => {
+ this.refresh()
+ })
+ $(this.id).parent().find('#add').click(() => {
this.add().show()
})
this.get().transacciones()
diff --git a/ui/public/assets/scripts/home.js b/ui/public/assets/scripts/home.js
index 8cbe3d2..6b0214d 100644
--- a/ui/public/assets/scripts/home.js
+++ b/ui/public/assets/scripts/home.js
@@ -39,22 +39,32 @@ class Cuenta {
}
tr.append(td)
})
- $("[data-id='" + this.categoria_id + "'][data-class='categoria']").after(tr)
+ const prev = this.prev()
+ prev.after(tr)
+ }
+ prev() {
+ let prev = $("[data-id='" + this.categoria_id + "'][data-class='categoria']")
+ let n = 0
+ while (prev.next().attr('data-class') === 'cuenta') {
+ prev = prev.next()
+ n ++;
+ if (n >= 100) {
+ return prev
+ }
+ }
+ return prev
}
remove() {
$("[data-id='" + this.id + "'][data-class='cuenta']").remove()
}
}
class Categoria {
- constructor({id, nombre, tipo_id, tipo, activos, pasivos, ganancias, perdidas}) {
+ constructor({id, nombre, tipo_id, tipo, totales}) {
this.id = id
this.nombre = nombre
this.tipo_id = tipo_id
this.tipo = tipo
- this.activos = activos
- this.pasivos = pasivos
- this.ganancias = ganancias
- this.perdidas = perdidas
+ this.totales = totales
this.is_open = false
this.cuentas = []
}
@@ -62,7 +72,7 @@ class Categoria {
this.tipos = tipos
}
draw({format}) {
- const button = $('').attr('class', 'ui mini compact icon button').append(
+ const button = $('').attr('class', 'ui mini compact circular icon button').append(
$('').attr('class', down_icon + ' icon')
).click((e) => {
const plus = button.find('.' + down_icon.replace(' ', '.') + '.icon')
@@ -85,7 +95,7 @@ class Categoria {
)
$.each(this.tipos, (i, el) => {
tr.append(
- $(' | ').attr('class', 'right aligned').html(format.format(this[el.descripcion.toLowerCase() + 's']))
+ $(' | ').attr('class', 'right aligned').html(format.format(this.totales[el.descripcion.toLowerCase() + 's']))
)
})
$("[data-id='" + this.tipo_id + "'][data-class='tipo_categoria']").after(tr)
@@ -143,21 +153,18 @@ class Categoria {
}
}
class TipoCategoria {
- constructor({id, descripcion, activo, activos, pasivos, ganancias, perdidas}) {
+ constructor({id, descripcion, activo, totales}) {
this.id = id
this.descripcion = descripcion
this.activo = activo
- this.activos = activos
- this.pasivos = pasivos
- this.ganancias = ganancias
- this.perdidas = perdidas
+ this.totales = totales
this.categorias = []
}
setTipos(tipos) {
this.tipos = tipos
}
draw({format}) {
- const button = $('').attr('class', 'ui mini compact icon button').append(
+ const button = $('').attr('class', 'ui mini compact circular icon button').append(
$('').attr('class', down_icon + ' icon')
).click((e) => {
const plus = button.find('.' + down_icon.replace(' ', '.') + '.icon')
@@ -175,7 +182,7 @@ class TipoCategoria {
)
)
$.each(this.tipos, (i, el) => {
- tr.append($(' | ').attr('class', 'right aligned').html(format.format(this[el.descripcion.toLowerCase() + 's'])))
+ tr.append($(' | ').attr('class', 'right aligned').html(format.format(this.totales[el.descripcion.toLowerCase() + 's'])))
})
return tr
}
@@ -210,29 +217,61 @@ class TipoCategoria {
}
}
const cuentas = {
- id: 'cuentas',
+ id: '#cuentas',
balance: 0,
tipos: [],
tipos_categorias: [],
+ build: function() {
+ return {
+ parent: (segment) => {
+ const tr = $('
').append(
+ $(' | ').attr('colspan', 3).html('Cuenta')
+ )
+ $.each(this.tipos, (i, el) => {
+ tr.append(
+ $(' | ').attr('class', 'right aligned').css('color', '#' + el.color).html(el.descripcion)
+ )
+ })
+ const table = $('').attr('class', 'ui striped table').append(
+ $('').append(tr)
+ )
+ const parent = $('')
+ table.append(parent)
+ segment.append(table)
+ return parent
+ },
+ resultado: (segment) => {
+ segment.append(
+ $('').attr('class', 'ui collapsing table').append(
+ $('
').append(
+ $(' | ').html('Ganancias')
+ ).append(
+ $(' | ').attr('data-tipo', 'ganancias')
+ )
+ ).append(
+ $('
').append(
+ $(' | ').html('Perdidas')
+ ).append(
+ $(' | ').attr('data-tipo', 'perdidas')
+ )
+ ).append(
+ $('
').append(
+ $(' | ').html('Resultado')
+ ).append(
+ $(' | ').attr('data-tipo', 'resultado')
+ )
+ )
+ )
+ }
+ }
+ },
get: function() {
return {
parent: () => {
- let parent = $('#' + this.id)
+ const segment = $(this.id)
+ let parent = segment.find('tbody')
if (parent.length === 0) {
- const tr = $('
').append(
- $(' | ').attr('colspan', 3).html('Cuenta')
- )
- $.each(this.tipos, (i, el) => {
- tr.append(
- $(' | ').attr('class', 'right aligned').css('color', '#' + el.color).html(el.descripcion)
- )
- })
- const table = $('').attr('class', 'ui striped table').append(
- $('').append(tr)
- )
- parent = $('').attr('id', this.id)
- table.append(parent)
- $('h1.header').after(table)
+ parent = this.build().parent(segment)
}
return parent
},
@@ -250,7 +289,7 @@ const cuentas = {
return
}
$.each(data.tipos, (i, el) => {
- tipo = new TipoCategoria(el)
+ const tipo = new TipoCategoria(el)
tipo.setTipos(this.tipos)
this.tipos_categorias.push(tipo)
})
@@ -263,6 +302,8 @@ const cuentas = {
this.balance = data
}).then(() => {
this.draw().balance()
+ }).then(() => {
+ this.draw().resultado()
})
}
}
@@ -298,6 +339,17 @@ const cuentas = {
)
})
foot.append(tr)
+ },
+ resultado: () => {
+ const div = $('#resultado')
+ if (div.find("[data-tipo='resultado']").length === 0) {
+ div.html('')
+ this.build().resultado(div)
+ }
+ const format = Intl.NumberFormat('es-CL', {style: 'currency', currency: 'CLP'})
+ div.find("[data-tipo='ganancias']").html(format.format(this.balance['ganancias']))
+ div.find("[data-tipo='perdidas']").html(format.format(this.balance['perdidas']))
+ div.find("[data-tipo='resultado']").html('' + format.format(this.balance['ganancias'] - this.balance['perdidas']) + '')
}
}
},
diff --git a/ui/public/assets/scripts/tipos_categorias.list.js b/ui/public/assets/scripts/tipos_categorias.list.js
index 51c72fd..815a194 100644
--- a/ui/public/assets/scripts/tipos_categorias.list.js
+++ b/ui/public/assets/scripts/tipos_categorias.list.js
@@ -90,32 +90,37 @@ const tipos_categorias = {
this.draw()
})
},
- getParent: function() {
- let parent = $(this.id).find('tbody')
- if (parent.length === 0) {
- const table = $('').attr('class', 'ui table').append(
- $('').append(
- $('
').append(
- $(' | ').attr('class', 'twelve wide').html('Tipo Categoría')
- ).append(
- $(' | ').attr('class', 'two wide').html('Activo')
- ).append(
- $(' | ').attr('class', 'two wide right aligned').append(
- $('').attr('class', 'ui tiny green circular icon button').append(
- $('').attr('class', 'plus icon')
- )
+ buildParent: function(segment) {
+ const table = $('').attr('class', 'ui table').append(
+ $('').append(
+ $('
').append(
+ $(' | ').attr('class', 'twelve wide').html('Tipo Categoría')
+ ).append(
+ $(' | ').attr('class', 'two wide').html('Activo')
+ ).append(
+ $(' | ').attr('class', 'two wide right aligned').append(
+ $('').attr('class', 'ui tiny green circular icon button').append(
+ $('').attr('class', 'plus icon')
)
)
)
)
- table.find('.ui.button').click((e) => {
- e.preventDefault()
- this.add()
- return false
- })
- parent = $('')
- table.append(parent)
- $(this.id).append(table)
+ )
+ table.find('.ui.button').click((e) => {
+ e.preventDefault()
+ this.add()
+ return false
+ })
+ parent = $('')
+ table.append(parent)
+ segment.append(table)
+ return parent
+ },
+ getParent: function() {
+ const segment = $(this.id)
+ let parent = segment.find('tbody')
+ if (parent.length === 0) {
+ parent = this.buildParent(segment)
}
return parent
},
diff --git a/ui/public/assets/scripts/tipos_cuentas.list.js b/ui/public/assets/scripts/tipos_cuentas.list.js
index ea739e9..40d33cd 100644
--- a/ui/public/assets/scripts/tipos_cuentas.list.js
+++ b/ui/public/assets/scripts/tipos_cuentas.list.js
@@ -55,6 +55,7 @@ class TipoCuenta {
const tipos_cuentas = {
id: '#tipos_cuentas',
tipos: [],
+ modal: null,
getTipos: function() {
this.tipos = []
return sendGet(_urls.api + '/tipos/cuentas').then((data) => {
diff --git a/ui/public/assets/scripts/uploads.list.js b/ui/public/assets/scripts/uploads.list.js
new file mode 100644
index 0000000..5251968
--- /dev/null
+++ b/ui/public/assets/scripts/uploads.list.js
@@ -0,0 +1,221 @@
+class Archivo {
+ constructor({folder, filename}) {
+ this.folder = folder
+ this.filename = filename
+ this.modal = null
+ }
+ setModal(modal) {
+ this.modal = modal
+ return this
+ }
+ draw() {
+ return $('
').append(
+ $(' | ').append(
+ $('').attr('class', 'item').attr('href', _urls.base + ['upload', this.folder, this.filename].join('/')).html(this.filename)
+ )
+ ).append(
+ $(' | ').attr('class', 'right aligned').append(
+ $('').attr('class', 'ui mini circular icon button').append(
+ $('').attr('class', 'edit icon')
+ ).click((e) => {
+ e.preventDefault()
+ const t = e.currentTarget
+ this.edit()
+ return false
+ })
+ ).append(
+ $('').attr('class', 'ui mini red circular icon button').append(
+ $('').attr('class', 'remove icon')
+ ).click((e) => {
+ e.preventDefault()
+ const t = e.currentTarget
+ this.remove()
+ return false
+ })
+ )
+ )
+ }
+ edit() {
+ this.modal.find('form').trigger('reset')
+ this.modal.find('form').find("[name='folder']").val(this.folder)
+ this.modal.find('form').find("[name='old_filename']").val(this.filename)
+ this.modal.find('form').find("[name='filename']").val(this.filename)
+ this.modal.modal('show')
+ }
+ remove() {
+ return sendDelete([_urls.api, 'upload', this.folder, this.filename].join('/')).then((data) => {
+ if (data.deleted) {
+ archivos.get()
+ }
+ })
+ }
+}
+const archivos = {
+ id: '#archivos',
+ archivos: [],
+ modals: {
+ add: null,
+ edit: null
+ },
+ build: function() {
+ return {
+ parent: (segment) => {
+ const table = $('').attr('class', 'ui striped table').append(
+ $('').append(
+ $('
').append(
+ $(' | ').html('Archivo')
+ ).append(
+ $(' | ').attr('class', 'right aligned').append(
+ $('').attr('class', 'ui tiny green circular icon button').append(
+ $('').attr('class', 'plus icon')
+ ).click((e) => {
+ e.preventDefault()
+ this.add()
+ return false
+ })
+ )
+ )
+ )
+ )
+ const parent = $('')
+ table.append(parent)
+ segment.append(table)
+ return parent
+ }
+ }
+ },
+ get: function() {
+ return {
+ parent: () => {
+ const segment = $(this.id)
+ let parent = segment.find('tbody')
+ if (parent.length === 0) {
+ parent = this.build().parent(segment)
+ }
+ return parent
+ },
+ archivos: () => {
+ return sendGet(_urls.api + '/uploads').then((data) => {
+ if (data.files === null || data.files.length === 0) {
+ return
+ }
+ this.archivos = []
+ $.each(data.files, (i, el) => {
+ const arch = new Archivo(el)
+ arch.setModal(this.modals.edit)
+ this.archivos.push(arch)
+ })
+ }).then(() => {
+ this.draw()
+ })
+ },
+ cuentas: () => {
+ return sendGet(_urls.api + '/cuentas')
+ }
+ }
+ },
+ draw: function() {
+ const tbody = this.get().parent()
+ tbody.empty()
+ $.each(this.archivos, (i, el) => {
+ tbody.append(el.draw())
+ })
+ },
+ add: function() {
+ this.modals.add.find('form').trigger('reset')
+ this.modals.add.find("[name='cuenta']").dropdown('clear')
+ this.modals.add.modal('show')
+ },
+ doAdd: function() {
+ const data = new FormData(this.modals.add.find('form')[0])
+ return sendPost(_urls.api + '/uploads/add', data, true).then((resp) => {
+ this.modals.add.modal('hide')
+ this.get().archivos()
+ })
+ },
+ doEdit: function() {
+ const folder = this.modals.edit.find("[name='folder']").val()
+ const filename = this.modals.edit.find("[name='old_filename']").val()
+ const data = JSON.stringify({
+ cuenta: this.modals.edit.find("[name='cuenta']").val(),
+ fecha: this.modals.edit.find("[name='fecha']").val()
+ })
+ sendPut([_urls.api, 'upload', folder, filename].join('/'), data).then((resp) => {
+ this.modals.edit.modal('hide')
+ if (resp.edited) {
+ this.get().archivos()
+ }
+ })
+ },
+ updateCalendar: function(modal) {
+ const today = new Date()
+ const start = new Date(today.getFullYear(), today.getMonth() - 1)
+ modal.find('.ui.calendar').calendar({
+ type: 'month',
+ initialDate: start,
+ maxDate: start,
+ months: ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'],
+ monthsShort: ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'],
+ formatter: {
+ date: function(date, settings) {
+ if (!date) return ''
+ const year = date.getFullYear()
+ const month = (date.getMonth() + 1).toString().padStart(2, '0')
+ return [year, month].join('-')
+ }
+ }
+ })
+ },
+ updateCuentas: function(modal, data) {
+ if (data.cuentas === null || data.cuentas.length === 0) {
+ return
+ }
+ const select = modal.find("select[name='cuenta']")
+ let values = []
+ $.each(data.cuentas, (i, el) => {
+ const nombre = [el.nombre, el.categoria.nombre].join(' - ')
+ values.push({
+ name: nombre,
+ value: el.id,
+ text: nombre
+ })
+ })
+ select.dropdown({values})
+ },
+ setupModal: function() {
+ this.modals.add = $('#add_modal')
+ this.modals.edit = $('#edit_modal')
+ $.each(this.modals, (i, el) => {
+ el.modal().find('.close.icon').click(() => {
+ el.modal('hide')
+ })
+ this.updateCalendar(el)
+ })
+ this.modals.add.find('form').submit((e) => {
+ e.preventDefault()
+ this.doAdd()
+ return false
+ })
+ this.modals.add.find('#archivo_btn').css('cursor', 'pointer').click(() => {
+ this.modals.add.find("[name='archivo']").trigger('click')
+ })
+ this.modals.add.find("[name='archivo']").change((e) => {
+ const arch = $(e.currentTarget)
+ const filename = arch[0].files[0].name
+ this.modals.add.find('#archivo_btn').find('input').val(filename)
+ })
+ this.modals.edit.find('form').submit((e) => {
+ e.preventDefault()
+ this.doEdit()
+ return false
+ })
+ this.get().cuentas().then((data) => {
+ this.updateCuentas(this.modals.add, data)
+ this.updateCuentas(this.modals.edit, data)
+ })
+ },
+ setup: function() {
+ this.setupModal()
+ this.get().archivos()
+ }
+}
diff --git a/ui/resources/routes/uploads.php b/ui/resources/routes/uploads.php
new file mode 100644
index 0000000..af9cf9a
--- /dev/null
+++ b/ui/resources/routes/uploads.php
@@ -0,0 +1,10 @@
+group('/uploads', function($app) {
+ $app->get('/add', [Uploads::class, 'upload']);
+ $app->get('[/]', Uploads::class);
+});
+$app->group('/upload/{folder}/{filename}', function($app) {
+ $app->get('[/]', [Uploads::class, 'get']);
+});
diff --git a/ui/resources/views/categorias/base.blade.php b/ui/resources/views/categorias/base.blade.php
index 4b14c94..2146949 100644
--- a/ui/resources/views/categorias/base.blade.php
+++ b/ui/resources/views/categorias/base.blade.php
@@ -8,7 +8,7 @@
Categorías
@endif
-
+
@yield('categorias_content')
@endsection
diff --git a/ui/resources/views/categorias/tipos/base.blade.php b/ui/resources/views/categorias/tipos/base.blade.php
index a1a0944..49d6c00 100644
--- a/ui/resources/views/categorias/tipos/base.blade.php
+++ b/ui/resources/views/categorias/tipos/base.blade.php
@@ -8,7 +8,7 @@
Tipos Categoría
@endif
-
+
@yield('tipos_categorias_content')
@endsection
diff --git a/ui/resources/views/config/list.blade.php b/ui/resources/views/config/list.blade.php
index 4499e0f..408f8df 100644
--- a/ui/resources/views/config/list.blade.php
+++ b/ui/resources/views/config/list.blade.php
@@ -1 +1,8 @@
@extends('config.base')
+
+@section('config_content')
+
Configuraciones Generales
+
+
+
+@endsection
diff --git a/ui/resources/views/config/menu.blade.php b/ui/resources/views/config/menu.blade.php
index c361fbe..6137067 100644
--- a/ui/resources/views/config/menu.blade.php
+++ b/ui/resources/views/config/menu.blade.php
@@ -1,4 +1,5 @@
diff --git a/ui/resources/views/config/menu/files.blade.php b/ui/resources/views/config/menu/files.blade.php
new file mode 100644
index 0000000..daf9486
--- /dev/null
+++ b/ui/resources/views/config/menu/files.blade.php
@@ -0,0 +1,4 @@
+
+ Archivos
+
+
diff --git a/ui/resources/views/cuentas/base.blade.php b/ui/resources/views/cuentas/base.blade.php
index c8d6d8d..6d20fd5 100644
--- a/ui/resources/views/cuentas/base.blade.php
+++ b/ui/resources/views/cuentas/base.blade.php
@@ -8,7 +8,7 @@
Cuentas
@endif
-
+
@yield('cuentas_content')
@endsection
diff --git a/ui/resources/views/cuentas/show.blade.php b/ui/resources/views/cuentas/show.blade.php
index 41037b6..632ff24 100644
--- a/ui/resources/views/cuentas/show.blade.php
+++ b/ui/resources/views/cuentas/show.blade.php
@@ -5,6 +5,26 @@
@endsection
@section('cuentas_content')
+
@@ -27,23 +47,16 @@
Saldo
- |
-
-
-
-
-
- Buscando los datos
-
-
- |
-
-
+
@@ -89,6 +102,7 @@
diff --git a/ui/resources/views/home.blade.php b/ui/resources/views/home.blade.php
index a96590e..dcfcde8 100644
--- a/ui/resources/views/home.blade.php
+++ b/ui/resources/views/home.blade.php
@@ -4,6 +4,8 @@
+
+
@endsection
@push('scripts')
diff --git a/ui/resources/views/layout/body/menu.blade.php b/ui/resources/views/layout/body/menu.blade.php
index fb6e1c3..80944d6 100644
--- a/ui/resources/views/layout/body/menu.blade.php
+++ b/ui/resources/views/layout/body/menu.blade.php
@@ -2,7 +2,6 @@
Inicio
@include('layout.body.menu.cuentas')
@include('layout.body.menu.categorias')
-
Importar
diff --git a/ui/resources/views/uploads/base.blade.php b/ui/resources/views/uploads/base.blade.php
new file mode 100644
index 0000000..9c0a995
--- /dev/null
+++ b/ui/resources/views/uploads/base.blade.php
@@ -0,0 +1,14 @@
+@extends('config.base')
+
+@section('config_content')
+
+
+ @yield('uploads_content')
+
+@endsection
diff --git a/ui/resources/views/uploads/list.blade.php b/ui/resources/views/uploads/list.blade.php
new file mode 100644
index 0000000..0e87110
--- /dev/null
+++ b/ui/resources/views/uploads/list.blade.php
@@ -0,0 +1,70 @@
+@extends('uploads.base')
+
+@section('uploads_content')
+
+
+
+@endsection
+
+@push('scripts')
+
+
+@endpush
diff --git a/ui/setup/settings/03_web.php b/ui/setup/settings/03_web.php
index 5e5d0d3..18dcb1a 100644
--- a/ui/setup/settings/03_web.php
+++ b/ui/setup/settings/03_web.php
@@ -22,5 +22,6 @@ return [
]);
$arr['api'] = $_ENV['API_URL'] ?? 'http://localhost:9001';
return (object) $arr;
- }
+ },
+ 'max_transacciones' => 100
];
diff --git a/ui/setup/setups/03_web.php b/ui/setup/setups/03_web.php
index 7ef8df1..0f5847a 100644
--- a/ui/setup/setups/03_web.php
+++ b/ui/setup/setups/03_web.php
@@ -2,15 +2,23 @@
use Psr\Container\ContainerInterface as Container;
return [
- Slim\Views\Blade::class => function(Container $c) {
- return new Slim\Views\Blade(
- $c->get('folders')->templates,
- $c->get('folders')->cache,
- null,
- [
- 'api_key' => $c->get('API_KEY'),
- 'urls' => $c->get('urls')
- ]
- );
- }
+ Slim\Views\Blade::class => function(Container $c) {
+ return new Slim\Views\Blade(
+ $c->get('folders')->templates,
+ $c->get('folders')->cache,
+ null,
+ [
+ 'api_key' => $c->get('API_KEY'),
+ 'urls' => $c->get('urls')
+ ]
+ );
+ },
+ GuzzleHttp\Client::class => function(Container $c) {
+ return new GuzzleHttp\Client([
+ 'base_uri' => 'http://api-proxy',
+ 'headers' => [
+ 'Authorization' => 'Bearer ' . $c->get('API_KEY')
+ ]
+ ]);
+ }
];