19 Commits

Author SHA1 Message Date
560fb356fa Move to today in cuentas.show 2022-04-02 23:32:27 -04:00
10e5383a3e FIX: modalToAdd in cuentas.show 2022-04-02 23:58:57 -03:00
bc5338b1f1 FIX: consolidado not changed with tipocambio 2022-04-02 23:40:53 -03:00
4db8161c70 FIX: withJson requires 2nd parameter 2022-03-26 20:55:57 -03:00
d94f076946 Script structure and searchable cuentas 2022-03-25 20:54:02 -03:00
8009433958 FIX: update buttons 2022-03-25 17:52:21 -03:00
d7b406ef25 FIX: double transform value 2022-03-25 17:45:58 -03:00
cddf768b1e FIX: show cuentas missing moneda 2022-03-25 17:39:12 -03:00
8b86560973 Error catching in UI 2022-03-25 16:36:58 -03:00
4ea5c6de3e Env files 2022-03-25 16:21:38 -03:00
91cc28e681 Merge branch 'feature/commands' into develop 2022-03-25 16:18:38 -03:00
ef01ab3c55 Migrations 2022-03-25 16:18:09 -03:00
e41ffd505b Migrations fixed 2022-03-25 16:16:31 -03:00
2986806137 Merge branch 'feature/commands' into develop 2022-03-25 15:04:53 -03:00
f9076d3bac Error catching in API 2022-03-25 15:04:42 -03:00
ee3133da72 Consolidar command 2022-03-25 15:04:10 -03:00
dbad283e14 Command queue 2022-03-25 15:03:49 -03:00
e3737aba27 Consolidar 2022-03-25 10:11:02 -03:00
fcc84ac09c Console application 2022-03-25 10:10:43 -03:00
56 changed files with 997 additions and 121 deletions

1
.console.env.sample Normal file
View File

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

5
.db.env.sample Normal file
View File

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

View File

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

View File

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

View File

@ -121,7 +121,7 @@ class Cuentas {
$c = $transaccion->credito();
}
$service->get($transaccion->fecha(), $c->moneda()->id);
$arr['valor'] = $c->moneda()->cambiar($transaccion->fecha(), $transaccion->valor);
$arr['valor'] = $transaccion->valor;
$arr['valorFormateado'] = $cuenta->moneda()->format($arr['valor']);
}
}
@ -136,21 +136,6 @@ class Cuentas {
$transacciones = $cuenta->transacciones($limit, $start);
if (count($transacciones) > 0) {
foreach ($transacciones as &$transaccion) {
/*$arr = $transaccion->toArray();
if ($cuenta->moneda()->codigo === 'CLP') {
if ($transaccion->debito()->moneda()->codigo !== 'CLP' or $transaccion->credito()->moneda()->codigo !== 'CLP') {
if ($transaccion->debito()->moneda()->codigo !== 'CLP') {
$c = $transaccion->debito();
} else {
$c = $transaccion->credito();
}
$service->get($transaccion->fecha(), $c->moneda()->id);
$arr['valor'] = $c->moneda()->cambiar($transaccion->fecha(), $transaccion->valor);
$arr['valorFormateado'] = $cuenta->moneda()->format($arr['valor']);
}
}
$arr['debito']['categoria'] = $transaccion->debito()->categoria()->toArray();
$arr['credito']['categoria'] = $transaccion->credito()->categoria()->toArray();*/
$transaccion = $this->transaccionToArray($service, $cuenta, $transaccion);
}
}
@ -170,21 +155,6 @@ class Cuentas {
$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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@ -1,9 +1,7 @@
<?php
namespace Contabilidad;
use DateTimeInterface;
use Carbon\Carbon;
use PhpParser\Node\Expr\AssignOp\Mod;
use ProVM\Common\Alias\Model;
use Contabilidad\Common\Service\TiposCambios as Service;
@ -54,7 +52,43 @@ class Cuenta extends Model {
}
return $this->abonos;
}
protected $consolidados;
public function consolidados() {
if ($this->consolidados === null) {
$this->consolidados = $this->parentOf(Consolidado::class, [Model::CHILD_KEY => 'cuenta_id']);
}
return $this->consolidados;
}
public function hasConsolidados(): bool {
$t = Carbon::now();
return (bool) Model::factory(Consolidado::class)
->whereEqual('cuenta_id', $this->id)
->count('id');
}
public function hasConsolidadosPending(): bool {
$t = Carbon::now();
return !(bool) Model::factory(Consolidado::class)
->whereEqual('cuenta_id', $this->id)
->whereGte('fecha', $t->copy()->subMonthNoOverflow()->startOfMonth()->format('Y-m-d'))
->orderByDesc('fecha')
->count('id');
}
public function hasTransacciones(): bool {
return (bool) Model::factory(Transaccion::class)
->select('transacciones.*')
->join('cuentas', 'cuentas.id = transacciones.debito_id OR cuentas.id = transacciones.credito_id')
->whereEqual('cuentas.id', $this->id)
->count('transacciones.id');
}
protected $transacciones;
protected function parseTransaccion(Transaccion $transaccion) {
$transaccion->setFactory($this->factory);
if ($transaccion->debito_id === $this->id) {
$transaccion->valor = - $transaccion->valor;
}
$transaccion->valor = $transaccion->transformar($this->moneda());
return $transaccion;
}
public function transacciones($limit = null, $start = 0) {
$transacciones = Model::factory(Transaccion::class)
->select('transacciones.*')
@ -67,10 +101,7 @@ class Cuenta extends Model {
}
$transacciones = $transacciones->findMany();
foreach ($transacciones as &$transaccion) {
$transaccion->setFactory($this->factory);
if ($transaccion->debito_id === $this->id) {
$transaccion->valor = - $transaccion->valor;
}
$transaccion = $this->parseTransaccion($transaccion);
}
return $transacciones;
}
@ -87,30 +118,24 @@ class Cuenta extends Model {
$transacciones = $transacciones->findMany();
foreach ($transacciones as &$transaccion) {
$transaccion->setFactory($this->factory);
if ($transaccion->desde_id === $this->id) {
$transaccion->valor = - $transaccion->valor;
}
$transaccion = $this->parseTransaccion($transaccion);
}
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;
$consolidados = $this->consolidados();
if ($consolidados === null) {
return 0;
}
return $cargos - $abonos;
$saldo = 0;
foreach ($consolidados as $consolidado) {
if ($consolidado->fecha() >= $date) {
continue;
}
$saldo += $consolidado->saldo();
}
return $saldo;
}
protected $saldo;
public function saldo(Service $service = null, $in_clp = false) {

View File

@ -22,19 +22,22 @@ class Moneda extends Model {
$this->sufijo
]));
}
public function cambio(\DateTime $fecha) {
public function cambio(\DateTime $fecha, Moneda $moneda = null) {
if ($moneda === null) {
$moneda = $this->factory->find(Moneda::class)->one(1);
}
$cambio = $this->factory->find(TipoCambio::class)
->where([['desde_id', $this->id], ['hasta_id', 1], ['fecha', $fecha->format('Y-m-d H:i:s')]])
->where([['desde_id', $this->id], ['hasta_id', $moneda->id], ['fecha', $fecha->format('Y-m-d H:i:s')]])
->one();
if ($cambio === null) {
$cambio = $this->factory->find(TipoCambio::class)
->where([['hasta_id', $this->id], ['desde_id', 1], ['fecha', $fecha->format('Y-m-d H:i:s')]])
->where([['hasta_id', $this->id], ['desde_id', $moneda->id], ['fecha', $fecha->format('Y-m-d H:i:s')]])
->one();
}
return $cambio;
}
public function cambiar(\DateTime $fecha, float $valor) {
$cambio = $this->cambio($fecha);
public function cambiar(\DateTime $fecha, float $valor, Moneda $moneda = null) {
$cambio = $this->cambio($fecha, $moneda);
if (!$cambio) {
return $valor;
}

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

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

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

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

View File

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

12
console/Dockerfile Normal file
View File

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

7
console/bin/console Normal file
View File

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

View File

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

View File

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

View File

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

25
console/composer.json Normal file
View File

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

24
console/crontab Normal file
View File

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

4
console/php.ini Normal file
View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
class Transaccion {
constructor({id, debito_id, credito_id, fecha, glosa, detalle, valor, debito, credito, fechaFormateada, valorFormateado, format}) {
constructor({id, debito_id, credito_id, fecha, glosa, detalle, valor, debito, credito, fechaFormateada, valorFormateado, format, moneda_id}) {
this.id = id
this.debito_id = debito_id
this.credito_id = credito_id
@ -95,6 +95,7 @@ const transacciones = {
buttons: {
prev: '#prev_button',
left: '#left_button',
today: '#today_button',
right: '#right_button',
next: '#next_button'
},
@ -102,6 +103,7 @@ const transacciones = {
cuenta: null,
transacciones: [],
cuentas: [],
monedas: [],
date: new Date(),
saldo: 0,
acumulation: 0,
@ -208,6 +210,17 @@ const transacciones = {
select.append(
$('<option></option>').attr('value', el.id).html(el.nombre + ' (' + el.categoria.nombre + ')')
)
select.dropdown()
})
})
this.get().monedas().then(() => {
const select = this.modal.find("[name='moneda']")
$.each(this.monedas, (i, el) => {
const option = $('<option></option>').attr('value', el.id).html(el.denominacion)
if (el.id === '1') {
option.attr('selected', 'selected')
}
select.append(option)
})
})
})
@ -216,6 +229,11 @@ const transacciones = {
return sendGet(_urls.api + '/cuenta/' + this.cuentas[idx].id + '/categoria').then((data) => {
this.cuentas[idx].categoria = data.categoria
})
},
monedas: () => {
return sendGet(_urls.api + '/monedas').then((response) => {
this.monedas = response.monedas
})
}
}
},
@ -238,7 +256,7 @@ const transacciones = {
const parent = $(this.id)
parent.html('')
$.each(this.transacciones, (i, el) => {
this.saldo = this.saldo + parseInt(el.valor.valor) * ((el.isIncrement()) ? 1 : -1)
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}))
})
},
@ -272,17 +290,11 @@ const transacciones = {
return {
show: () => {
this.modal.find('form').trigger('reset')
modalToAdd(this.modal)
this.modal.modal('show')
},
exec: () => {
const fecha = $("[name='fecha']").val()
const data1 = JSON.stringify({
desde_id: $("[name='moneda']").val(),
hasta_id: 1,
fecha: fecha,
valor: $("[name='cambio']").val()
})
sendPost(_urls.api + '/tipos/cambios/add', data1)
const valor = $("[name='valor']").val()
const cuenta = $("[name='cuenta']").val()
@ -292,7 +304,8 @@ const transacciones = {
fecha: fecha,
glosa: $("[name='glosa']").val(),
detalle: $("[name='detalle']").val(),
valor: (valor < 0) ? -valor : valor
valor: (valor < 0) ? -valor : valor,
moneda_id: $("[name='moneda']").val()
})
return sendPost(_urls.api + '/transacciones/add', data).then(() => {
this.modal.modal('hide')
@ -308,13 +321,15 @@ const transacciones = {
const glosa = $("[name='glosa']").val()
const detalle = $("[name='detalle']").val()
const valor = $("[name='valor']").val()
const moneda_id = $("[name='moneda']").val()
const data = JSON.stringify({
debito_id: (valor < 0) ? this.cuenta_id : cuenta,
credito_id: (valor < 0) ? cuenta : this.cuenta_id,
fecha,
glosa,
detalle,
valor: (valor < 0) ? -valor : valor
valor: (valor < 0) ? -valor : valor,
moneda_id
})
return sendPut(_urls.api + '/transaccion/' + id + '/edit', data).then(() => {
this.modal.modal('hide')
@ -328,6 +343,11 @@ const transacciones = {
this.checkButtons()
$(this.mes).calendar('set date', this.date)
},
today: function() {
this.date = new Date()
this.checkButtons()
$(this.mes).calendar('set date', this.date)
},
changeYear: function(dif) {
let d = this.date
d.setFullYear(this.date.getFullYear() + dif)
@ -342,6 +362,11 @@ const transacciones = {
} else {
$(this.buttons.right).removeClass('disabled')
}
if (this.date.toDateString() === f.toDateString()) {
$(this.buttons.today).addClass('disabled')
} else {
$(this.buttons.today).removeClass('disabled')
}
if (this.date.getFullYear() === f.getFullYear()) {
$(this.buttons.next).addClass('disabled')
} else {
@ -350,6 +375,7 @@ const transacciones = {
},
refresh: function () {
this.get().transacciones()
this.checkButtons()
},
build: function() {
return {
@ -407,6 +433,9 @@ const transacciones = {
})
},
buttons: () => {
$(this.buttons.today).click((e) => {
this.today()
})
$(this.buttons.right).click((e) => {
this.changeMonth(1)
})

View File

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

View File

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

View File

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

View File

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

View File

@ -18,6 +18,9 @@
<div class="ui icon button" id="left_button">
<i class="left angle icon"></i>
</div>
<div class="ui icon disabled button" id="today_button">
<i class="calendar day icon"></i>
</div>
<div class="ui icon disabled button" id="right_button">
<i class="right angle icon"></i>
</div>
@ -75,7 +78,7 @@
</div>
<div class="field">
<label>Cuenta</label>
<select name="cuenta"></select>
<select name="cuenta" class="ui searchable dropdown"></select>
</div>
<div class="field">
<label>Glosa</label>
@ -85,9 +88,15 @@
<label>Detalle</label>
<input type="text" name="detalle" />
</div>
<div class="field">
<label>Valor</label>
<input type="text" name="valor" />
<div class="fields">
<div class="field">
<label>Valor</label>
<input type="text" name="valor" />
</div>
<div class="field">
<lable>Moneda</lable>
<select name="moneda"></select>
</div>
</div>
<button class="ui icon button">
<i class="plus icon"></i>
@ -98,7 +107,7 @@
@endsection
@push('scripts')
<script type="text/javascript" src="{{$urls->scripts}}/cuentas.show.js"></script>
<script type="text/javascript" src="{{$urls->scripts}}/cuentas/show.js"></script>
<script type="text/javascript">
$(document).ready(() => {
transacciones.cuenta_id = {{$cuenta_id}}

View File

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

View File

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