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