Files
oficial/app/resources/views/ventas/facturacion.blade.php
2024-05-16 21:23:24 -04:00

533 lines
23 KiB
PHP

@extends('layout.base')
@push('page_styles')
<style>
#data {
margin-left: 1rem;
margin-right: 1rem;
}
</style>
@endpush
@section('page_content')
<div class="ui container">
<h2 class="ui header">Matriz Facturación</h2>
<h4 class="ui dividing header">
<div class="ui two column grid">
<div id="list_title" class="column">Proyectos</div>
<div class="right aligned column">
<div class="ui tiny icon buttons">
<button id="up_button" class="ui button">
<i class="up arrow icon"></i>
</button>
<button id="refresh_button" class="ui button">
<i class="refresh icon"></i>
</button>
</div>
</div>
</div>
</h4>
<div id="proyectos" class="ui link selection list">
@foreach ($proyectos as $proyecto)
<div class="item" data-proyecto="{{$proyecto->id}}">{{$proyecto->descripcion}}</div>
@endforeach
</div>
</div>
<div id="data"></div>
@endsection
@include('layout.head.styles.datatables')
@include('layout.body.scripts.datatables')
@push('page_scripts')
<script>
const money = {
data: {
ufs: {},
ipcs: {}
},
ufs: {},
ipcs: {},
sent: {
uf: {},
ipc: {}
},
queue: {
size: 0,
array: () => {
const array = []
Object.entries(money.queue).forEach(([type, dates]) => {
if (['uf', 'ipc'].includes(type) === false) {
return
}
if (type === 'uf') {
Object.entries(dates).forEach(([dateString, ventas]) => {
const temp = []
ventas.forEach(venta => {
temp.push(venta)
})
array.push({
type,
date: dateString,
ventas: temp
})
})
return
}
Object.entries(dates).forEach(([dateString, data]) => {
const temp = []
data.ventas.forEach(venta => {
temp.push(venta)
})
array.push({
type,
start: data.start,
end: data.end,
ventas: temp
})
})
})
return array
}
},
enqueue() {
return {
uf: ({date, venta_id}) => {
const type = 'uf'
const data = {
date: [date.getFullYear(), date.getMonth()+1, date.getDate()].join('-'),
}
if (!(type in this.queue)) {
this.queue[type] = {}
}
if (!(data.date in this.queue[type])) {
this.queue[type][data.date] = []
}
if (this.queue[type][data.date].includes(data)) {
return
}
this.queue[type][data.date].push(venta_id)
this.queue['size'] ++
},
ipc: ({start, end, venta_id}) => {
const type = 'ipc'
let mult = 1
if (start > end) {
mult = -1
let temp = end
end = start
start = temp
}
const data = {
start: [start.getFullYear(), start.getMonth()+1, start.getDate()].join('-'),
end: [end.getFullYear(), end.getMonth()+1, end.getDate()].join('-'),
}
if (!(type in this.queue)) {
this.queue[type] = {}
}
if (!([data.start, data.end].join('|') in this.queue[type])) {
this.queue[type][[data.start, data.end].join('|')] = {
start: data.start,
end: data.end,
ventas: []
}
}
if (this.queue[type][[data.start, data.end].join('|')].ventas.includes(data)) {
return
}
this.queue[type][[data.start, data.end].join('|')].ventas.push({venta_id, mult})
this.queue['size'] ++
},
}
},
get() {
return {
many: () => {
const url = '{{$urls->api}}/money/many'
const method = 'post'
const chunkSize = 100
const promises = []
for (let i = 0; i < this.queue.size; i += chunkSize) {
const chunk = this.queue.array().slice(i, i + chunkSize)
const missing = chunk.filter(data => {
if (data.type === 'uf') {
return !(data.date in this.data.ufs)
}
if (data.type === 'ipc') {
const dateKey = [data.start, data.end].join('|')
return !(dateKey in this.data.ipcs)
}
})
if (missing.length === 0) {
continue
}
const body = new FormData()
body.set('dates', JSON.stringify(missing))
const options = {
method,
body
}
promises.push(fetchAPI(url, options).then(response => {
if (!response) {
return
}
return response.json().then(json => {
json.values.forEach(data => {
if (data.type === 'uf') {
this.data.ufs[data.date] = data.value
}
if (data.type === 'ipc') {
const dateKey = [data.start, data.end].join('|')
this.data.ipcs[dateKey] = data.value
}
})
return json.values
})
}))
}
return Promise.all(promises)
}
}
}
}
class Unidad {
id
tipo
descripcion
prorrateo
precio
constructor({id, tipo, descripcion, prorrateo, precio}) {
this.id = id
this.tipo = tipo
this.descripcion = descripcion
this.prorrateo = prorrateo
this.precio = precio
}
}
class Venta {
id
precio
fecha
escritura
uf
ipc
unidades
principal
constructor({id, precio, fecha, escritura, unidades, principal}) {
this.id = id
this.precio = precio
this.fecha = new Date(fecha)
this.escritura = new Date(escritura)
this.unidades = unidades
this.principal = principal
}
draw({tbody, valor_terreno}) {
/**
'Venta',
'Unidades',
'Tipo',
'Unidad',
'Fecha Promesa',
'Valor Promesa',
'Prorrateo',
'Descuento',
'Precio Venta',
'Precio Bruto',
'IVA',
'Precio Neto',
'% IVA/Neto',
'Fecha Escritura',
'UF',
'IPC'
*/
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.principal.descripcion
const cantidad = this.unidades.length
this.unidades.forEach(unidad => {
const valor = (unidad.valor > 0) ? unidad.valor : unidad.precio
const values = [
'<a href="{{$urls->base}}/ventas/factura/' + this.id + '">' + venta + '</a>',
cantidad,
unidad.tipo,
unidad.descripcion,
dateFormatter.format(this.fecha),
ufFormatter.format(valor) + ' UF',
unidad.prorrateo,
'',
'',
'',
'',
'',
'',
'',
'',
''
]
if (this.escritura !== null) {
const ipc = (this.ipc >= 0) ? (1 + this.ipc) : 1 / (1 + this.ipc)
const descuento = valor_terreno * ipc * unidad.prorrateo
const precio_venta = valor * this.uf
const precio_bruto = precio_venta - descuento
const precio_neto = precio_bruto / 1.19
const iva = precio_bruto - precio_neto
let i = 7
values[i++] = (!isNaN(descuento)) ? pesosFormatter.format(descuento) : '<div class="ui active inline loader"></div>'
values[i++] = (!isNaN(precio_venta)) ? pesosFormatter.format(precio_venta) : ''
values[i++] = (!isNaN(precio_bruto)) ? pesosFormatter.format(precio_bruto) : ''
values[i++] = (!isNaN(iva)) ? pesosFormatter.format(iva) : ''
values[i++] = (!isNaN(precio_neto)) ? pesosFormatter.format(precio_neto) : ''
values[i++] = (!isNaN(iva) && !isNaN(precio_venta)) ? (iva / precio_venta * 100).toFixed(2) + '%' : ''
values[i++] = dateFormatter.format(this.escritura)
values[i++] = (!isNaN(this.uf)) ? ufFormatter.format(this.uf) : ''
values[i++] = (!isNaN(this.ipc)) ? (ufFormatter.format(this.ipc >= 0 ? this.ipc * 100 : -this.ipc * 100) + '%') : ''
}
const row = $('<tr></tr>')
values.forEach(value => {
row.append(
$('<td></td>').html(value)
)
})
tbody.append(row)
})
}
}
const proyectos = {
ids: {},
selected: 0,
data: JSON.parse('{!! json_encode($proyectos) !!}'),
sent: false,
queues: {
uf: {},
ipc: {}
},
ufs: {},
ipcs: {},
table: null,
get: function() {
return {
ventas: () => {
if (this.sent) {
return
}
this.sent = true
const url = '{{$urls->api}}/ventas/facturacion/proyecto/' + this.selected
this.draw().loading()
return fetchAPI(url).then(response => {
if (!response) {
return
}
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(ventas).then(() => {
this.draw().ventas(idx)
this.sent = false
money.get().many().then(response => {
response.forEach(dates => {
dates.forEach(values => {
values.ventas.forEach(venta => {
let vidx = 0
if (values.type === 'uf') {
vidx = this.data[idx].ventas.findIndex(v => v.id === venta)
this.data[idx].ventas[vidx].uf = values.value
return
}
if (values.type === 'ipc') {
vidx = this.data[idx].ventas.findIndex(v => v.id === venta.venta_id)
this.data[idx].ventas[vidx].ipc = values.value * venta.mult
}
})
})
})
this.draw().ventas(idx)
})
})
})
})
},
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})
})
});
})
},
}
},
add() {
return {
venta: ({proyecto_idx, venta}) => {
const v = new Venta(venta)
this.data[proyecto_idx].ventas.push(v)
const fecha_terreno = (typeof this.data[proyecto_idx].terreno.fecha === 'undefined') ? new Date() : new Date(this.data[proyecto_idx].terreno.fecha)
if (v.escritura !== null) {
money.enqueue().uf({date: v.escritura, venta_id: v.id})
money.enqueue().ipc({start: v.escritura, end: fecha_terreno, 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)
}
}
},
draw: function() {
return {
proyectos: () => {
$(this.ids.title).html('Proyectos')
if (this.table !== null) {
this.table.clear()
this.table.destroy()
$(this.ids.data).html('')
}
$(this.ids.data).hide()
$(this.ids.proyectos).find('.item').css('cursor', 'pointer')
$(this.ids.proyectos).show()
},
ventas: proyecto_idx => {
$(this.ids.title).html(this.data[proyecto_idx].descripcion)
const parent = $(this.ids.data)
if (this.table !== null) {
this.table.clear()
this.table.destroy()
this.table = null
parent.html('')
}
const table = $('<table></table>').addClass('ui compact table')
table.append(this.draw().thead())
table.append(this.draw().tbody(proyecto_idx))
parent.show()
parent.html(table)
$(this.ids.proyectos).hide()
if (this.table === null) {
const dtD = structuredClone(datatables_defaults)
dtD['pageLength'] = 100
dtD['order'] = [[0, 'asc']]
this.table = table.DataTable(dtD)
}
},
thead: () => {
const thead = $('<thead></thead>')
const columns = [
'Venta',
'Unidades',
'Tipo',
'Unidad',
'Fecha Promesa',
'Valor Promesa',
'Prorrateo',
'Descuento',
'Precio Venta',
'Precio Bruto',
'IVA',
'Precio Neto',
'% IVA/Neto',
'Fecha Escritura',
'UF',
'IPC'
]
const row = $('<tr></tr>')
columns.forEach(title => {
row.append(
$('<th></th>').html(title)
)
})
thead.append(row)
return thead
},
tbody: proyecto_idx => {
const tbody = $('<tbody></tbody>')
this.data[proyecto_idx].ventas.forEach(venta => {
venta.draw({tbody, valor_terreno: this.data[proyecto_idx].terreno.valor})
})
return tbody
},
loading: () => {
$(this.ids.proyectos).find('.item').css('cursor', 'wait')
}
}
},
actions: function() {
return {
up: event => {
this.draw().proyectos()
},
refresh: event => {
const list = $(this.ids.proyectos)
if (list.is(':hidden')) {
const table = $(this.ids.table)
table.hide()
if (this.table !== null) {
this.table.clear().destroy()
this.table = null
}
this.get().ventas()
} else {
this.draw().proyectos()
}
}
}
},
setup: function({ids}) {
this.ids = ids
$(this.ids.data).hide()
$(this.ids.proyectos).find('.item').click(event => {
this.selected = $(event.currentTarget).data('proyecto')
this.get().ventas()
})
$(this.ids.buttons.up).click(this.actions().up)
$(this.ids.buttons.refresh).click(this.actions().refresh)
}
}
$(document).ready(() => {
proyectos.setup({ids: {
title: '#list_title',
proyectos: '#proyectos',
data: '#data',
buttons: {
up: '#up_button',
refresh: '#refresh_button'
}
}
})
})
</script>
@endpush