diff --git a/app/resources/routes/api/ventas/facturacion.php b/app/resources/routes/api/ventas/facturacion.php index cae92a8..3c9d70d 100644 --- a/app/resources/routes/api/ventas/facturacion.php +++ b/app/resources/routes/api/ventas/facturacion.php @@ -3,4 +3,5 @@ use Incoviba\Controller\API\Ventas\Facturacion; $app->group('/facturacion', function($app) { $app->get('/proyecto/{proyecto_id}[/]', [Facturacion::class, 'proyecto']); + $app->post('/get[/]', [Facturacion::class, 'ventas']); }); diff --git a/app/resources/views/ventas/facturacion.blade.php b/app/resources/views/ventas/facturacion.blade.php index e0eac21..bb24aa8 100644 --- a/app/resources/views/ventas/facturacion.blade.php +++ b/app/resources/views/ventas/facturacion.blade.php @@ -60,25 +60,33 @@ return this.ufs[json.input.fecha] = json.uf }) }, - ipc: (end, start) => { + ipc: ({end, start}) => { const dateKey = [start.getFullYear(), (start.getMonth()+1), end.getFullYear(), (end.getMonth()+1)].join('-') if (typeof this.sent.ipc[dateKey] !== 'undefined') { return this.sent.ipc[dateKey] } + let mult = 1 + if (start > end) { + const tmp = structuredClone(end) + end = structuredClone(start) + start = tmp + mult = -1 + } const url = '{{$urls->api}}/money/ipc' const data = new FormData() - data.set('start', start.toISOString()) - data.set('end', end.toISOString()) + data.set('start', [start.getFullYear() + ((start.getMonth() > 0) ? 0 : -1), start.getMonth(), start.getDate()].join('-')) + data.set('end', [end.getFullYear() + ((end.getMonth() > 0) ? 0 : -1), end.getMonth(), end.getDate()].join('-')) const options = { method: 'post', body: data } return this.sent.ipc[dateKey] = fetchAPI(url, options).then(response => { - if (response.ok) { - return response.json() + if (!response) { + return } - }).then(json => { - return this.ipcs[json.date_string] = json.ipc + return response.json().then(json => { + return this.ipcs[dateKey] = json.ipc * mult + }) }) } } @@ -92,8 +100,7 @@ prorrateo precio - constructor({id, tipo, descripcion, prorrateo, precio}) - { + constructor({id, tipo, descripcion, prorrateo, precio}) { this.id = id this.tipo = tipo this.descripcion = descripcion @@ -111,44 +118,13 @@ unidades principal - constructor({id, precio, fecha, escritura}) { + constructor({id, precio, fecha, escritura, unidades, principal}) { this.id = id this.precio = precio - this.fecha = fecha - this.escritura = escritura - this.uf = 1 - this.ipc = 1 - this.unidades = [] - } - - get() { - return { - unidades: () => { - const url = '{{$urls->api}}/venta/' + this.id + '/unidades' - return fetchAPI(url).then(response => { - if (response.ok) { - return response.json() - } - }).then(json => { - json.unidades.forEach(unidad => { - const tipo = unidad.proyecto_tipo_unidad.tipo_unidad.descripcion - const data = { - id: unidad.id, - tipo: tipo.charAt(0).toUpperCase() + tipo.slice(1), - descripcion: unidad.descripcion, - prorrateo: unidad.prorrateo, - precio: 0 - } - if (unidad.current_precio !== null) { - data.precio = unidad.current_precio.valor - } - const u = new Unidad(data) - this.unidades.push(u) - }) - this.principal = this.unidades.filter(unidad => unidad.tipo === 'Departamento')[0] - }) - } - } + this.fecha = new Date(fecha) + this.escritura = new Date(escritura) + this.unidades = unidades + this.principal = principal } draw({tbody, valor_terreno}) { @@ -173,7 +149,7 @@ const dateFormatter = new Intl.DateTimeFormat('es-CL', {year: 'numeric', month: 'numeric', day: 'numeric'}) const pesosFormatter = new Intl.NumberFormat('es-CL', {style: 'currency', currency: 'CLP'}) const ufFormatter = new Intl.NumberFormat('es-CL', {minimumFractionDigits: 2, maximumFractionDigits: 2}) - const venta = this.unidades[0].descripcion + const venta = this.principal.descripcion const cantidad = this.unidades.length this.unidades.forEach(unidad => { const values = [ @@ -195,7 +171,8 @@ '' ] if (this.escritura !== null) { - const descuento = valor_terreno * (1 + this.ipc / 100) * unidad.prorrateo + const ipc = (this.ipc >= 0) ? (1 + this.ipc) : 1 / (1 + this.ipc) + const descuento = valor_terreno * ipc * unidad.prorrateo const precio_venta = unidad.precio * this.uf const precio_bruto = precio_venta - descuento const precio_neto = precio_bruto / 1.19 @@ -210,7 +187,7 @@ values[i++] = (iva / precio_venta * 100).toFixed(2) + '%' values[i++] = dateFormatter.format(this.escritura) values[i++] = ufFormatter.format(this.uf) - values[i++] = (this.ipc).toFixed(2) + '%' + values[i++] = ufFormatter.format(this.ipc >= 0 ? this.ipc * 100 : -this.ipc * 100) + '%' } const row = $('') values.forEach(value => { @@ -227,6 +204,10 @@ selected: 0, data: JSON.parse('{!! json_encode($proyectos) !!}'), sent: false, + queues: { + uf: {}, + ipc: {} + }, ufs: {}, ipcs: {}, table: null, @@ -243,89 +224,99 @@ if (!response) { return } - return response.json() - }).then(json => { - const idx = this.data.findIndex(proyecto => proyecto.id === json.proyecto_id) - const fecha_terreno = (typeof this.data[idx].terreno.date === 'undefined') ? new Date() : new Date(this.data[idx].terreno.date) - this.data[idx]['ventas'] = [] - const ventas = [] - const unidadesQueue = [] - const ufQueue = {} - const ipcQueue = {} - const chunkSize = 100 - const url = '{{$urls->api}}/ventas/get' - for (let i = 0; i < json.ventas.length; i += chunkSize) { - const chunk = json.ventas.slice(i, i + chunkSize).map(venta => venta.id) - const body = new FormData() - body.set('ventas', chunk) - const promise = fetchAPI(url, {method: 'post', body}).then(response => { - if (!response) { - return response - } - return response.json().then(json => { - json.ventas.forEach(venta => { - const data = { - id: venta.id, - precio: venta.valor, - fecha: new Date(venta.fecha), - escritura: new Date(venta.fecha) - } - if (['escriturando'].includes(venta.current_estado.tipo_estado_venta.descripcion)) { - data.escritura = new Date(venta.current_estado.fecha) - } - const v = new Venta(data) - if (v.escritura !== null) { - const dateString = v.escritura.toString() - if (!Object.hasOwn(ufQueue, dateString)) { - ufQueue[dateString] = [] - } - ufQueue[dateString].push(v.id) - if (!Object.hasOwn(ipcQueue, dateString)) { - ipcQueue[dateString] = [] - } - ipcQueue[dateString].push(v.id) - } - unidadesQueue.push(v.id) - this.data[idx].ventas.push(v) - - }) - }); - }) - ventas.push(promise) - } - Promise.all(ventas).then(() => { - const promises = [] - Object.entries(ufQueue).forEach(([dateString, ventas]) => { - const date = new Date(dateString) - promises.push(money.get().uf(date).then(uf => { - ventas.forEach(id => { - const vidx = this.data[idx].ventas.findIndex(venta => venta.id === id) - this.data[idx].ventas[vidx].uf = uf - }) - })) - }) - Object.entries(ipcQueue).forEach(([dateString, ventas]) => { - const date = new Date(dateString) - promises.push(money.get().ipc(date, fecha_terreno).then(ipc => { - ventas.forEach(id => { - const vidx = this.data[idx].ventas.findIndex(venta => venta.id === id) - this.data[idx].ventas[vidx].ipc = ipc - }) - })) - }) - for (let i = 0; i < unidadesQueue.length; i += chunkSize) { - const chunk = unidadesQueue.slice(i, i + chunkSize) - chunk.forEach(id => { - const vidx = this.data[idx].ventas.findIndex(venta => venta.id === id) - promises.push(this.data[idx].ventas[vidx].get().unidades()) - }) + return response.json().then(json => { + const idx = this.data.findIndex(proyecto => proyecto.id === json.proyecto_id) + this.data[idx]['ventas'] = [] + const ventas = [] + const chunkSize = 100 + for (let i = 0; i < json.ventas.length; i += chunkSize) { + const chunk = json.ventas.slice(i, i + chunkSize).map(venta => venta.id) + ventas.push(this.get().chunk({idx, chunk})) } - Promise.all(promises).then(() => { - this.draw().ventas(idx) - this.sent = false + Promise.all(ventas).then(() => { + const promises = [] + promises.push(...this.get().ufs(idx)) + promises.push(...this.get().ipcs(idx)) + Promise.all(promises).then(() => { + this.draw().ventas(idx) + this.sent = false + }) }) }) }) + }, + chunk: ({idx, chunk}) => { + const url = '{{$urls->api}}/ventas/facturacion/get' + const method = 'post' + const body = new FormData() + body.set('ventas', chunk) + + return fetchAPI(url, {method, body}).then(response => { + if (!response) { + return response + } + return response.json().then(json => { + json.ventas.forEach(venta => { + this.add().venta({proyecto_idx: idx, venta}) + }) + }); + }) + }, + ufs: idx => { + const promises = [] + Object.entries(this.queues.uf).forEach(([dateString, ventas]) => { + const date = new Date(dateString) + promises.push(money.get().uf(date).then(uf => { + ventas.forEach(id => { + const vidx = this.data[idx].ventas.findIndex(venta => venta.id === id) + this.data[idx].ventas[vidx].uf = uf + }) + })) + }) + return promises + }, + ipcs: idx => { + const fecha_terreno = (typeof this.data[idx].terreno.date === 'undefined') ? new Date() : new Date(this.data[idx].terreno.date) + const promises = [] + Object.entries(this.queues.ipc).forEach(([dateString, ventas]) => { + const date = new Date(dateString) + promises.push(money.get().ipc({end: date, start: fecha_terreno}).then(ipc => { + ventas.forEach(id => { + const vidx = this.data[idx].ventas.findIndex(venta => venta.id === id) + this.data[idx].ventas[vidx].ipc = ipc + }) + })) + }) + return promises + } + } + }, + add() { + return { + venta: ({proyecto_idx, venta}) => { + const v = new Venta(venta) + this.data[proyecto_idx].ventas.push(v) + if (v.escritura !== null) { + const dateString = v.escritura.toString() + this.register().uf({dateString, venta_id: v.id}) + this.register().ipc({dateString, venta_id: v.id}) + } + }, + } + }, + register() { + return { + uf: ({dateString, venta_id}) => { + if (!Object.hasOwn(this.queues.uf, dateString)) { + this.queues.uf[dateString] = [] + } + this.queues.uf[dateString].push(venta_id) + }, + ipc: ({dateString, venta_id}) => { + if (!Object.hasOwn(this.queues.ipc, dateString)) { + this.queues.ipc[dateString] = [] + } + this.queues.ipc[dateString].push(venta_id) } } }, diff --git a/app/resources/views/ventas/facturacion/show.blade.php b/app/resources/views/ventas/facturacion/show.blade.php index 1450208..a2e1b39 100644 --- a/app/resources/views/ventas/facturacion/show.blade.php +++ b/app/resources/views/ventas/facturacion/show.blade.php @@ -887,178 +887,6 @@ ].join("\n") } } - /*const output = [ - '
'+ - '
'+ - '
'+ - '
'+ - '
'+ - ''+proyecto.inmobiliaria.nombre.toUpperCase()+'
'+ - 'GIRO:
'+ - 'Dirección:'+proyecto.direccion+ - '
'+ - '
'+ - '
'+ - ''+ - 'RUT:'+proyecto.inmobiliaria.rut.toUpperCase()+'
'+ - 'FACTURA ELECTRÓNICA
'+ - 'N° #'+ - '
'+ - '
'+ - '
'+ - '
'+ - '
'+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - '
Señor(es)'+propietario.props.nombre+'RUT'+propietario.props.rut.toUpperCase()+'
GiroOtras Actividades ProfesionalesFecha Emisión'+formatters.date.format(propietario.props.fecha)+'
Dirección'+propietario.props.direccion+'Comuna'+propietario.props.comuna.toUpperCase()+'
'+ - '
'+ - '
'+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - '' - ] - const unidadesData = [] - - let c = 1 - const classes = [ - '', - '', - 'center aligned', - 'right aligned', - 'center aligned', - 'right aligned' - ] - const uf_mult = this.props.uf / uf - unidades.forEach(unidad => { - const descuento = parseFloat(proyecto.terreno) * parseFloat(unidad.prorrateo) * uf_mult - const bruto = parseFloat(unidad.base) * uf_mult - descuento - const neto = bruto / 1.19 - const data = [ - c ++, - unidad.descripcion + ' (UF ' + formatters.ufs.format(unidad.precio * this.props.proporcion * uf_mult) + ')', - '1 UNID', - formatters.pesos.format(neto * this.props.proporcion), - 'AF', - formatters.pesos.format(neto * this.props.proporcion) - ] - - const row = [''] - data.forEach((value, i) => { - const cell = [''+value+'') - row.push(cell.join('')) - }) - row.push('') - unidadesData.push(row.join('')) - }) - - const emptyTerreno = '
0
' - const data = [ - c, - 'Valor con Terreno ' + formatters.pesos.format((venta.base * uf_mult + venta.terreno * uf_mult) * this.props.proporcion) + ' - Menos valor terreno ' + ((venta.terreno > 0) ? formatters.pesos.format(-venta.terreno * uf_mult * this.props.proporcion) : emptyTerreno) + '
' + - 'Base imponible ' + formatters.pesos.format(venta.base * uf_mult * this.props.proporcion) + '
' + - 'IVA ' + formatters.pesos.format(venta.iva * uf_mult * this.props.proporcion) + '
' + - 'SUBTOTAL ' + formatters.pesos.format(venta.subtotal * uf_mult * this.props.proporcion) + '
' + - 'Mas valor terreno ' + ((venta.terreno > 0) ? formatters.pesos.format(venta.terreno * uf_mult * this.props.proporcion) : emptyTerreno) + '
' + - 'TOTAL ' + formatters.pesos.format(venta.total * uf_mult * this.props.proporcion) + ';' + formatters.ufs.format(venta.totalUF * this.props.proporcion) + ' UF

' + - 'Descuento Terreno: ' + ((venta.terreno > 0) ? formatters.percent.format(venta.prorrateo * 100) : emptyTerreno) + '%

' + - 'UF: ' + formatters.ufs.format(uf), - '1 UNID', - formatters.pesos.format(venta.terreno * uf_mult * this.props.proporcion), - 'EX', - formatters.pesos.format(venta.terreno * uf_mult * this.props.proporcion) - ] - - const row = [''] - data.forEach((value, i) => { - const cell = [''+value+'') - row.push(cell.join('')) - }) - unidadesData.push(row.join('')) - - output.push(''+unidadesData.join('')+'') - output.push( - ''+ - ''+ - ''+ - ''+ - ''+ - '
DETALLES
DescripciónCant/UnidadPrec. Unit.IndTotal
'+ - '
'+ - '
'+ - '
'+ - '
'+ - '
'+ - '
'+ - '
'+ - '
'+ - '
'+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - '
TOTALES
Monto Neto'+formatters.pesos.format(venta.neto * uf_mult * this.props.proporcion)+'
Monto Exento'+formatters.pesos.format(venta.exento * uf_mult * this.props.proporcion)+'
19% IVA'+formatters.pesos.format(venta.iva * uf_mult * this.props.proporcion)+'
Monto Total'+formatters.pesos.format(venta.total * uf_mult * this.props.proporcion)+'
'+ - '
'+ - '
'+ - '
'+ - '
' - ) - return output.join('')*/ } } diff --git a/app/src/Controller/API/Ventas/Facturacion.php b/app/src/Controller/API/Ventas/Facturacion.php index 759a426..e88e7ae 100644 --- a/app/src/Controller/API/Ventas/Facturacion.php +++ b/app/src/Controller/API/Ventas/Facturacion.php @@ -2,16 +2,15 @@ namespace Incoviba\Controller\API\Ventas; use DateTimeImmutable; -use Incoviba\Common\Implement\Exception\EmptyRedis; -use Incoviba\Common\Implement\Exception\EmptyResult; -use Incoviba\Controller\API\emptyBody; -use Incoviba\Controller\API\withJson; -use Incoviba\Controller\withRedis; -use Incoviba\Service; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Incoviba\Common\Implement\Exception\{EmptyRedis,EmptyResult}; +use Incoviba\Controller\API\{emptyBody,withJson}; +use Incoviba\Controller\withRedis; +use Incoviba\Service; +use Incoviba\Common\Ideal; -class Facturacion +class Facturacion extends Ideal\Service { use withJson, withRedis, emptyBody; @@ -34,4 +33,31 @@ class Facturacion } return $this->withJson($response, $output); } + public function ventas(ServerRequestInterface $request, ResponseInterface $response, Service\Redis $redisService, + Service\Venta $ventaService, Service\Proyecto\Terreno $terrenoService, Service\UF $ufService, + Service\IPC $ipcService): ResponseInterface + { + $input = $request->getParsedBody(); + $output = [ + 'input' => $input, + 'ventas' => [] + ]; + $ventas = explode(',', $input['ventas']); + foreach ($ventas as $venta_id) { + $redisKey = "ventas:facturacion:venta:{$venta_id}"; + try { + $venta = $this->fetchRedis($redisService, $redisKey); + $output['ventas'] []= $venta; + } catch (EmptyRedis) { + try { + $venta = $ventaService->getFacturacionById($venta_id); + $output['ventas'] []= $venta; + $this->saveRedis($redisService, $redisKey, $venta); + } catch (EmptyResult $exception) { + $this->logger->notice($exception); + } + } + } + return $this->withJson($response, $output); + } } diff --git a/app/src/Service/Venta.php b/app/src/Service/Venta.php index 36d9642..7cc2708 100644 --- a/app/src/Service/Venta.php +++ b/app/src/Service/Venta.php @@ -2,11 +2,12 @@ namespace Incoviba\Service; use DateTimeImmutable; +use DateInterval; +use Psr\Log\LoggerInterface; use Incoviba\Common\Ideal\Service; use Incoviba\Common\Implement; use Incoviba\Repository; use Incoviba\Model; -use Psr\Log\LoggerInterface; class Venta extends Service { @@ -27,6 +28,7 @@ class Venta extends Service protected Venta\Credito $creditoService, protected Venta\BonoPie $bonoPieService, protected Venta\Pago $pagoService, + protected Proyecto\Terreno $terrenoService, protected Money $moneyService ) { parent::__construct($logger); @@ -91,6 +93,33 @@ class Venta extends Service { return $this->process($this->ventaRepository->fetchByPie($pie_id)); } + public function getFacturacionById(int $venta_id): array + { + $venta = $this->getById($venta_id); + $escritura = (in_array($venta->currentEstado()->tipoEstadoVenta->descripcion, ['escriturando'])) ? $venta->currentEstado()->fecha : $venta->fecha; + $data = [ + 'id' => $venta->id, + 'fecha' => $venta->fecha->format('Y-m-d'), + 'valor' => $venta->valor, + 'escritura' => $escritura->format('Y-m-d'), + 'unidades' => [], + ]; + foreach ($venta->propiedad()->unidades as $unidad) { + $data['unidades'] []= [ + 'id' => $unidad->id, + 'tipo' => ucwords($unidad->proyectoTipoUnidad->tipoUnidad->descripcion), + 'descripcion' => $unidad->descripcion, + 'prorrateo' => $unidad->prorrateo, + 'precio' => (isset($unidad->currentPrecio)) ? $unidad->currentPrecio->valor : 0 + ]; + } + $principal = $venta->propiedad()->principal(); + $data['principal'] = [ + 'id' => $principal->id, + 'descripcion' => $principal->descripcion + ]; + return $data; + } protected function process(Model\Venta $venta): Model\Venta {