From 04478afa2fbe5c3fa62c5b48d035d734010715e7 Mon Sep 17 00:00:00 2001 From: Aldarien Date: Thu, 30 Nov 2023 18:40:15 -0300 Subject: [PATCH] Mejoras en Facturacion --- app/resources/routes/api/proyectos.php | 3 + app/resources/routes/api/ventas/unidades.php | 5 + app/resources/views/proyectos/show.blade.php | 20 +++ .../views/proyectos/terreno/edit.blade.php | 5 + .../views/ventas/facturacion/show.blade.php | 148 ++++++++++++++++-- app/resources/views/ventas/list.blade.php | 28 +++- .../views/ventas/show/escritura.blade.php | 4 + app/src/Controller/API/Proyectos.php | 15 ++ app/src/Controller/API/Ventas/Unidades.php | 15 ++ app/src/Controller/Proyectos.php | 2 +- app/src/Model/Proyecto/Terreno.php | 4 +- app/src/Repository/Proyecto.php | 47 +++++- app/src/Repository/Venta/Unidad.php | 35 +++++ app/src/Service/Proyecto.php | 3 +- 14 files changed, 310 insertions(+), 24 deletions(-) create mode 100644 app/resources/views/proyectos/terreno/edit.blade.php diff --git a/app/resources/routes/api/proyectos.php b/app/resources/routes/api/proyectos.php index 3d8cca7..0dd2495 100644 --- a/app/resources/routes/api/proyectos.php +++ b/app/resources/routes/api/proyectos.php @@ -17,4 +17,7 @@ $app->group('/proyecto/{proyecto_id}', function($app) { $app->get('/disponibles[/]', [Proyectos::class, 'disponibles']); $app->get('[/]', [Proyectos::class, 'unidades']); }); + $app->group('/terreno', function($app) { + $app->post('/edit[/]', [Proyectos::class, 'terreno']); + }); }); diff --git a/app/resources/routes/api/ventas/unidades.php b/app/resources/routes/api/ventas/unidades.php index 01630a7..693f963 100644 --- a/app/resources/routes/api/ventas/unidades.php +++ b/app/resources/routes/api/ventas/unidades.php @@ -4,3 +4,8 @@ use Incoviba\Controller\API\Ventas\Unidades; $app->group('/unidades', function($app) { $app->post('/disponibles', [Unidades::class, 'disponibles']); }); +$app->group('/unidad/{unidad_id}', function($app) { + $app->group('/prorrateo', function($app) { + $app->post('[/]', [Unidades::class, 'prorrateo']); + }); +}); diff --git a/app/resources/views/proyectos/show.blade.php b/app/resources/views/proyectos/show.blade.php index 835b783..568c0ca 100644 --- a/app/resources/views/proyectos/show.blade.php +++ b/app/resources/views/proyectos/show.blade.php @@ -42,6 +42,26 @@ + + Terreno + + + + + + + +
+ {{$format->number($proyecto->terreno->superficie, 2)}}m² + + {{$format->pesos($proyecto->terreno->valor)}} ({{$proyecto->terreno->fecha?->format('d-m-Y')}}) + + + + +
+ + Superficies diff --git a/app/resources/views/proyectos/terreno/edit.blade.php b/app/resources/views/proyectos/terreno/edit.blade.php new file mode 100644 index 0000000..d1164c2 --- /dev/null +++ b/app/resources/views/proyectos/terreno/edit.blade.php @@ -0,0 +1,5 @@ +@extends('layout.base') + +@push('page_content') + +@endpush diff --git a/app/resources/views/ventas/facturacion/show.blade.php b/app/resources/views/ventas/facturacion/show.blade.php index 9a9947a..68bfc92 100644 --- a/app/resources/views/ventas/facturacion/show.blade.php +++ b/app/resources/views/ventas/facturacion/show.blade.php @@ -13,6 +13,9 @@ {{$venta->propiedad()->summary()}} +
+ Valor Venta: {{$format->ufs($venta->valor)}} +
@@ -27,13 +30,37 @@ @foreach ($venta->propiedad()->unidades as $unidad)
-
+
UF
@endforeach
+
+ @foreach($venta->propiedad()->unidades as $unidad) +
+ @if ($unidad->prorrateo === 0.0) + +
+ +
%
+
+ @endif +
+ @endforeach +
+
+ @php $lastDic = new DateTimeImmutable((new DateTimeImmutable())->sub(new DateInterval('P1Y'))->format('31-12-Y')); @endphp + @if (!isset($venta->proyecto()->terreno->fecha) or $venta->proyecto()->terreno->fecha < $lastDic) +
+ +
+
$
+ +
+
+ @endif
@@ -44,7 +71,7 @@ {{mb_strtoupper($venta->proyecto()->inmobiliaria()->nombreCompleto())}}
GIRO:
- Dirección: + Dirección: {{$venta->proyecto()->direccion()->simple()}}
@@ -148,12 +175,14 @@ totales: {}, proporcion: 1, precio: {{$UF->transform($venta->currentEstado()->fecha, $venta->valor)}}, - terreno: {{$IPC->readjust($venta->proyecto()->terreno->valor, $venta->proyecto()->terreno->date, $venta->currentEstado()->fecha)}}, + terreno: {{(isset($venta->proyecto()->terreno->fecha) and $venta->proyecto()->terreno->fecha > $lastDic) ? + $IPC->readjust($venta->proyecto()->terreno->valor, $venta->proyecto()->terreno->fecha, $venta->currentEstado()->fecha) : 0}}, uf: {{$UF->get($venta->currentEstado()->fecha)}}, unidades: JSON.parse('{!! json_encode(array_map(function(Incoviba\Model\Venta\PropiedadUnidad $unidad) use ($venta, $UF, $format) { $precio = ($unidad->valor > 0) ? $unidad->valor : $unidad->precio($venta->currentEstado()->fecha)->valor; return [ - 'pu_id' => $unidad->pu_id, + 'id' => $unidad->id, + 'pid' => $unidad->pu_id, 'descripcion' => ucwords($unidad->proyectoTipoUnidad->tipoUnidad->descripcion) . ' ' . $unidad->descripcion, 'precio' => $precio, 'base' => $UF->transform($venta->currentEstado()->fecha, $precio), @@ -168,6 +197,8 @@ const percentFormatter = new Intl.NumberFormat('es-CL', {maximumFractionDigits: 5, minimumFractionDigits: 5}) let terreno = 0 let prorrateo = 0 + let totalUnidades = 0 + let precioUnidades = 0 let c = 1 const classes = [ '', @@ -178,6 +209,8 @@ 'right aligned' ] this.unidades.forEach(unidad => { + totalUnidades += unidad.base + precioUnidades += unidad.precio const descuento = this.terreno * unidad.prorrateo terreno += descuento prorrateo += unidad.prorrateo @@ -203,6 +236,13 @@ }) tbody.append(row) }) + $('#total_unidades') + .attr('class', 'ui compact segment ' + ((totalUnidades.toFixed(2) !== this.precio.toFixed(2)) ? 'inverted red' : 'inverted green')) + .html('Total Unidades: ' + ufFormatter.format(precioUnidades) + ' UF' + + ((totalUnidades.toFixed(2) !== this.precio.toFixed(2)) ? '; Diferencia: ' + ufFormatter.format({{$venta->valor}} - precioUnidades) + ' UF' : '')) + if (totalUnidades.toFixed(2) !== this.precio.toFixed(2)) { + this.highlight() + } const bruto = this.precio - terreno const base = bruto / 1.19 const iva = base * .19 @@ -210,15 +250,16 @@ const total = subtotal + terreno const totalUF = total / this.uf + const emptyTerreno = '
0
' const data = [ c, - 'Valor con Terreno ' + pesoFormatter.format((base + terreno) * this.proporcion) + ' - Menos valor terreno ' + pesoFormatter.format(-terreno * this.proporcion) + '
' + + 'Valor con Terreno ' + pesoFormatter.format((base + terreno) * this.proporcion) + ' - Menos valor terreno ' + ((terreno > 0) ? pesoFormatter.format(-terreno * this.proporcion) : emptyTerreno) + '
' + 'Base imponible ' + pesoFormatter.format(base * this.proporcion) + '
' + 'IVA ' + pesoFormatter.format(iva * this.proporcion) + '
' + 'SUBTOTAL ' + pesoFormatter.format(subtotal * this.proporcion) + '
' + - 'Mas valor terreno ' + pesoFormatter.format(terreno * this.proporcion) + '
' + + 'Mas valor terreno ' + ((terreno > 0) ? pesoFormatter.format(terreno * this.proporcion) : emptyTerreno) + '
' + 'TOTAL ' + pesoFormatter.format(total * this.proporcion) + ';' + ufFormatter.format(totalUF * this.proporcion) + ' UF

' + - 'Descuento Terreno: ' + percentFormatter.format(prorrateo * 100) + '%

' + + 'Descuento Terreno: ' + ((terreno > 0) ? percentFormatter.format(prorrateo * 100) : emptyTerreno) + '%

' + 'UF: ' + ufFormatter.format(this.uf), '1 UNID', pesoFormatter.format(terreno * this.proporcion), @@ -244,7 +285,8 @@ update: function() { return { price: (id, value) => { - const idx = this.unidades.findIndex(unidad => unidad.pu_id === id) + this.unhighlight() + const idx = this.unidades.findIndex(unidad => unidad.pid === id) if (idx === -1) { return } @@ -263,11 +305,59 @@ if (!json.edited) { return } - const idx = this.unidades.findIndex(unidad => unidad.pu_id === json.propiedad_unidad_id) + const idx = this.unidades.findIndex(unidad => unidad.pid === json.propiedad_unidad_id) this.unidades[idx].precio = parseInt(json.input.valor) this.unidades[idx].base = parseFloat(json.input.valor * this.unidades[idx].base / old_value) this.build() }) + }, + terreno: value => { + const url = '{{$urls->api}}/proyecto/{{$venta->proyecto()->id}}/terreno/edit' + const data = new FormData() + data.set('valor', value) + data.set('fecha', '{{$lastDic->format('Y-m-d')}}') + return fetchAPI(url, {method: 'post', body: data}).then(response => { + if (response.ok) { + return response.json() + } + }).then(json => { + if (!json.edited) { + return + } + this.terreno = parseInt(json.input.valor) + const data = new FormData() + data.set('start', '{{$lastDic->format('Y-m-d')}}') + data.set('end', '{{$venta->currentEstado()->fecha->sub(new DateInterval('P1M'))->format('Y-m-d')}}') + const url = '{{$urls->api}}/money/ipc' + return fetchAPI(url, {method: 'post', body: data}).then(response => { + if (response.ok) { + return response.json() + } + }).then(json => { + this.terreno *= (1 + parseFloat(json.ipc)) + this.build() + }) + }) + }, + prorrateo: (id, value) => { + if (parseFloat(value) === 0) { + return + } + const url = '{{$urls->api}}/ventas/unidad/' + id + '/prorrateo' + const data = new FormData() + data.set('prorrateo', value) + return fetchAPI(url, {method: 'post', body: data}).then(response => { + if (response.ok) { + return response.json() + } + }).then(json => { + if (!json.edited) { + return + } + const idx = this.unidades.findIndex(unidad => unidad.id === json.unidad_id) + this.unidades[idx].prorrateo = parseFloat(json.input.prorrateo) + this.build() + }) } } }, @@ -283,16 +373,46 @@ this.build() }) }, - prices: (class_name) => { + prices: class_name => { $(class_name).change(event => { const val = $(event.currentTarget).val() const id = $(event.currentTarget).data('id') this.update().price(id, val) }) + }, + prorrateo: class_name => { + $(class_name).change(event => { + const val = $(event.currentTarget).val() + const id = $(event.currentTarget).data('id') + this.update().prorrateo(id, val) + }) + }, + terreno: id => { + $(id).change(event => { + const val = $(event.currentTarget).val() + this.update().terreno(val).then(() => { + $(id).parent().parent().hide() + }) + }) } } }, - setup: function({form_id, tbody_id, input_id, prices_class, totales_ids}) { + highlight: function() { + const pid = this.unidades[0].pid + const input = $('#input' + pid) + input.addClass('error') + input.find('.label').addClass('red') + input.attr('data-content', 'Valor total no es igual a valor de venta') + input.popup() + }, + unhighlight: function() { + const pid = this.unidades[0].pid + const input = $('#input' + pid) + input.removeClass('error') + input.find('.label').removeClass('red') + input.removeAttr('data-content') + }, + setup: function({form_id, tbody_id, input_id, prices_class, prorrateo_class, terreno_id, totales_ids}) { $(form_id).submit(event => { event.preventDefault() return false @@ -302,12 +422,16 @@ this.proporcion = $(input_id).val() / 100 this.watch().proporcion(input_id) this.watch().prices(prices_class) + this.watch().prorrateo(prorrateo_class) + @if (!isset($venta->proyecto()->terreno->fecha) or $venta->proyecto()->terreno->fecha <= $lastDic) + this.watch().terreno(terreno_id) + @endif this.build() } } $(document).ready(() => { factura.setup({form_id: '#venta_form', tbody_id: '#unidades', input_id: '#proporcion', - prices_class: '.price', totales_ids: { + prices_class: '.price', prorrateo_class: '.prorrateo', terreno_id: '#terreno', totales_ids: { afecto: '#neto', exento: '#exento', iva: '#iva', diff --git a/app/resources/views/ventas/list.blade.php b/app/resources/views/ventas/list.blade.php index 0ff7a14..d4ebaed 100644 --- a/app/resources/views/ventas/list.blade.php +++ b/app/resources/views/ventas/list.blade.php @@ -112,15 +112,15 @@ } }).then(data => { if (data.total > 0) { + const progress = this.draw().progress(data.ventas.length) this.data.id = data.proyecto.id this.data.proyecto = data.proyecto.descripcion this.data.venta_ids = data.ventas const promises = [] data.ventas.forEach(venta_id => { - const promise = this.get().venta(venta_id) - /*promise.then(() => { - this.draw().ventas(true) - })*/ + const promise = this.get().venta(venta_id).then(() => { + progress.progress('increment') + }) promises.push(promise) }) Promise.all(promises).then(() => { @@ -185,6 +185,26 @@ parent.show() parent.find('.item.proyecto').click(this.actions().get) }, + progress: cantidad => { + const parent = $(this.ids.proyectos) + parent.html('') + const progress = $('
').addClass('ui active progress').append( + $('
').addClass('bar').append( + $('
').addClass('centered progress') + ) + ).append( + $('
').addClass('label').html('Cargando datos') + ) + progress.progress({ + total: cantidad, + label: 'ratio', + text: { + ratio: '{value} de {total} ({percent}%)' + } + }) + parent.append(progress) + return progress + }, ventas: (loading = false) => { const title = $(this.ids.title) const parent = $(this.ids.proyectos) diff --git a/app/resources/views/ventas/show/escritura.blade.php b/app/resources/views/ventas/show/escritura.blade.php index c8beaa4..98d2112 100644 --- a/app/resources/views/ventas/show/escritura.blade.php +++ b/app/resources/views/ventas/show/escritura.blade.php @@ -12,6 +12,10 @@ Firmar +
+ + Factura +
@else
diff --git a/app/src/Controller/API/Proyectos.php b/app/src/Controller/API/Proyectos.php index cff4441..616b6eb 100644 --- a/app/src/Controller/API/Proyectos.php +++ b/app/src/Controller/API/Proyectos.php @@ -140,4 +140,19 @@ class Proyectos } return $this->withJson($response, $output); } + public function terreno(ServerRequestInterface $request, ResponseInterface $response, Repository\Proyecto $proyectoRepository, int $proyecto_id): ResponseInterface + { + $body = $request->getParsedBody(); + $output = [ + 'input' => $body, + 'proyecto_id' => $proyecto_id, + 'edited' => false + ]; + try { + $proyecto = $proyectoRepository->fetchById($proyecto_id); + $proyectoRepository->editTerreno($proyecto, $body); + $output['edited'] = true; + } catch (EmptyResult) {} + return $this->withJson($response, $output); + } } diff --git a/app/src/Controller/API/Ventas/Unidades.php b/app/src/Controller/API/Ventas/Unidades.php index 1d29f81..aa0ced4 100644 --- a/app/src/Controller/API/Ventas/Unidades.php +++ b/app/src/Controller/API/Ventas/Unidades.php @@ -83,4 +83,19 @@ class Unidades } return $this->withJson($response, $output); } + public function prorrateo(ServerRequestInterface $request, ResponseInterface $response, Repository\Venta\Unidad $unidadRepository, int $unidad_id): ResponseInterface + { + $body = $request->getParsedBody(); + $output = [ + 'unidad_id' => $unidad_id, + 'input' => $body, + 'edited' => false + ]; + try { + $unidad = $unidadRepository->fetchById($unidad_id); + $unidadRepository->editProrrateo($unidad, $body); + $output['edited'] = true; + } catch (EmptyResult) {} + return $this->withJson($response, $output); + } } diff --git a/app/src/Controller/Proyectos.php b/app/src/Controller/Proyectos.php index 106de6b..80e7bae 100644 --- a/app/src/Controller/Proyectos.php +++ b/app/src/Controller/Proyectos.php @@ -55,7 +55,7 @@ class Proyectos { $redisKey = "proyecto:{$proyecto_id}"; try { - $proyecto = $proyectoService->getById($proyectoRepository->load((array) $this->fetchRedis($redisService, $redisKey))); + $proyecto = $proyectoService->getById($this->fetchRedis($redisService, $redisKey)->id); } catch (EmptyRedis) { $proyecto = $proyectoService->getById($proyecto_id); $this->saveRedis($redisService, $redisKey, $proyecto); diff --git a/app/src/Model/Proyecto/Terreno.php b/app/src/Model/Proyecto/Terreno.php index adcdaad..4f7a8a5 100644 --- a/app/src/Model/Proyecto/Terreno.php +++ b/app/src/Model/Proyecto/Terreno.php @@ -8,14 +8,14 @@ class Terreno implements JsonSerializable { public float $superficie; public float $valor; - public ?DateTimeInterface $date; + public ?DateTimeInterface $fecha; public function jsonSerialize(): mixed { return [ 'superficie' => $this->superficie, 'valor' => $this->valor, - 'date' => $this->date?->format('Y-m-d') + 'date' => $this->fecha?->format('Y-m-d') ]; } } diff --git a/app/src/Repository/Proyecto.php b/app/src/Repository/Proyecto.php index 6aac760..9d5bdbd 100644 --- a/app/src/Repository/Proyecto.php +++ b/app/src/Repository/Proyecto.php @@ -1,6 +1,8 @@ superficie = $data['superficie_terreno']; $terreno->valor = $data['valor_terreno']; - $terreno->date = null; + $terreno->fecha = null; if (isset($data['fecha_terreno']) and $data['fecha_terreno'] !== '') { - $terreno->date = new DateTimeImmutable($data['fecha_terreno']); + $terreno->fecha = new DateTimeImmutable($data['fecha_terreno']); } return $terreno; })) @@ -71,7 +73,7 @@ class Proyecto extends Ideal\Repository 'subterraneos'], $new_data); } - public function fetchById(int $id): Define\Model + public function fetchById(int $id): Model\Proyecto { $query = $this->connection->getQueryBuilder() ->select($this->columns()) @@ -81,7 +83,7 @@ class Proyecto extends Ideal\Repository return $this->fetchOne($query, [$id]); } - public function fetchByName(string $name): Define\Model + public function fetchByName(string $name): Model\Proyecto { $query = $this->connection->getQueryBuilder() ->select($this->columns()) @@ -116,6 +118,43 @@ class Proyecto extends Ideal\Repository ->order('a.descripcion'); return $this->fetchMany($query); } + public function editTerreno(Model\Proyecto $proyecto, array $data): Model\Proyecto + { + $fecha = new DateTimeImmutable($data['fecha']); + try { + $query = $this->connection->getQueryBuilder() + ->select('valor') + ->from('proyecto_terreno') + ->where('proyecto_id = ? AND fecha = ?'); + $result = $this->connection->execute($query, [$proyecto->id, $fecha->format('Y-m-d')])->fetch(PDO::FETCH_ASSOC); + if ($result === false) { + throw new Implement\Exception\EmptyResult($query); + } + if ($result['valor'] !== $data['valor']) { + $query = $this->connection->getQueryBuilder() + ->update('proyecto_terreno') + ->set('valor = ?') + ->where('proyecto_id = ? AND fecha = ?'); + $this->connection->execute($query, [$data['valor'], $proyecto->id, $fecha->format('Y-m-d')]); + $proyecto->terreno->valor = $data['valor']; + } + } catch (PDOException | Implement\Exception\EmptyResult) { + $query = $this->connection->getQueryBuilder() + ->insert() + ->into('proyecto_terreno') + ->columns(['proyecto_id', 'fecha', 'valor', 'tipo_moneda_id']) + ->values(['?', '?', '?', '1']); + try { + $this->connection->execute($query, [$proyecto->id, $fecha->format('Y-m-d'), $data['valor']]); + $proyecto->terreno->fecha = $fecha; + $proyecto->terreno->valor = $data['valor']; + } catch (PDOException $exception) { + error_log($exception); + throw new Implement\Exception\EmptyResult($query); + } + } + return $proyecto; + } /*public function fetchSuperficieVendido(int $proyecto_id): float { diff --git a/app/src/Repository/Venta/Unidad.php b/app/src/Repository/Venta/Unidad.php index 925425e..21fe4be 100644 --- a/app/src/Repository/Venta/Unidad.php +++ b/app/src/Repository/Venta/Unidad.php @@ -2,6 +2,7 @@ namespace Incoviba\Repository\Venta; use PDO; +use PDOException; use Incoviba\Common\Ideal; use Incoviba\Common\Define; use Incoviba\Common\Implement; @@ -38,6 +39,40 @@ class Unidad extends Ideal\Repository { return $this->update($model, ['subtipo', 'piso', 'descripcion', 'orientacion', 'pt'], $new_data); } + public function editProrrateo(Model\Venta\Unidad $model, array $new_data): Model\Venta\Unidad + { + try { + $query = $this->connection->getQueryBuilder() + ->select('prorrateo') + ->from('unidad_prorrateo') + ->where('unidad_id = ?'); + $result = $this->connection->execute($query, [$model->id])->fetch(PDO::FETCH_ASSOC); + if ($result === false) { + throw new Implement\Exception\EmptyResult($query); + } + if ($new_data['prorrateo'] !== $result['prorrateo']) { + $query = $this->connection->getQueryBuilder() + ->update('unidad_prorrateo') + ->set('prorrateo = ?') + ->where('unidad_id = ?'); + $this->connection->execute($query, [$new_data['prorrateo'], $model->id]); + $model->prorrateo = $new_data['prorrateo']; + } + } catch (PDOException | Implement\Exception\EmptyResult) { + $query = $this->connection->getQueryBuilder() + ->insert() + ->into('unidad_prorrateo') + ->columns(['unidad_id', 'prorrateo']) + ->values(['?', '?']); + try { + $this->connection->execute($query, [$model->id, $new_data['prorrateo']]); + $model->prorrateo = $new_data['prorrateo']; + } catch (PDOException) { + throw new Implement\Exception\EmptyResult($query); + } + } + return $model; + } public function fetchById(int $id): Model\Venta\Unidad { diff --git a/app/src/Service/Proyecto.php b/app/src/Service/Proyecto.php index 70b6332..7ac89db 100644 --- a/app/src/Service/Proyecto.php +++ b/app/src/Service/Proyecto.php @@ -27,7 +27,8 @@ class Proyecto { return $this->process($this->proyectoRepository->fetchByName($name)); } - public function process(Model\Proyecto $proyecto): Model\Proyecto + + protected function process(Model\Proyecto $proyecto): Model\Proyecto { $proyecto->addFactory('estados', (new Implement\Repository\Factory()) ->setCallable([$this->estadoProyecto, 'fetchByProyecto'])