Files
2026-09-07 18:44:32 -03:00

386 lines
22 KiB
Python

import re
import sys
part1 = """
1: from django.db import models
2: from django.contrib.auth.models import User
3: from decimal import Decimal
4: import uuid
5:
6: # Enums
7: class CurrencyEnum(models.TextChoices):
8: CLP = 'CLP', 'Pesos Chilenos'
9: USD = 'USD', 'Dólares Estadounidenses'
10:
11: class TaxTypeEnum(models.TextChoices):
12: EXENTO = 'EXENTO', 'Exento / No Afecto'
13: IVA_19 = 'IVA_19', 'Afecto a IVA (19%)'
14: HONORARIOS = 'HONORARIOS_13_75', 'Boleta de Honorarios (13.75%)'
15:
16: class ExpenseTypeEnum(models.TextChoices):
17: COSTO_DIRECTO = 'COSTO_DIRECTO_PROYECTO', 'Costo Directo Proyecto'
18: GASTO_OPERACIONAL = 'GASTO_OPERACIONAL_OVERHEAD', 'Gasto Operacional Overhead'
19:
20: class ApprovalStatusEnum(models.TextChoices):
21: EN_REVISION = 'EN_REVISION_DOCUMENTAL', 'En Revisión Documental (Paso 1)'
22: SOLICITADO_FINANZAS = 'SOLICITADO_FINANZAS', 'Solicitado a Finanzas (Paso 2)'
23: LIBERADO_PAGADO = 'LIBERADO_PAGADO', 'Pago Liberado (Paso 3)'
24: OBSERVADO_RECHAZADO = 'OBSERVADO_RECHAZADO', 'Observado / Rechazado'
25:
26: class ProjectStatusEnum(models.TextChoices):
27: BORRADOR = 'BORRADOR', 'Borrador / En Diseño'
28: ACTIVO = 'ACTIVO', 'Activo'
29: PAUSADA = 'PAUSADA', 'En Pausa'
30: TERMINADO = 'TERMINADO', 'Terminado'
31:
32: class VehicleOwnershipEnum(models.TextChoices):
33: ARRENDADO = 'ARRENDADO', 'Arrendado (4x4, etc.)'
34: PROPIO = 'PROPIO', 'Propio (Reembolso Kilometraje)'
35: BUS = 'BUS', 'Bus'
36: UBER_TAXI = 'UBER_TAXI', 'Uber/Taxi'
37:
38: class ExpenseReportStatusEnum(models.TextChoices):
39: BORRADOR = 'BORRADOR', 'Borrador'
40: ENVIADO = 'ENVIADO', 'Enviado para Aprobación'
41: APROBADO = 'APROBADO', 'Aprobado'
42: REEMBOLSADO = 'REEMBOLSADO', 'Reembolsado / Pagado'
43: OBSERVADO = 'OBSERVADO', 'Observado / Rechazado'
44:
45:
46: class Project(models.Model):
47: code = models.CharField(max_length=50, unique=True, verbose_name="Código", default='PROY-000')
48: name = models.CharField(max_length=200, verbose_name="Nombre del Proyecto/Centro")
49: description = models.TextField(blank=True, verbose_name="Descripción")
50:
51: manager = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='managed_projects', verbose_name="Encargado / Director")
52: parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children', verbose_name="Portafolio / Proyecto Padre")
53:
54: assigned_budget_clp = models.DecimalField(max_digits=15, decimal_places=2, default=0, verbose_name="Presupuesto Asignado (CLP)")
55: assigned_budget_usd = models.DecimalField(max_digits=15, decimal_places=2, default=0, verbose_name="Presupuesto Asignado (USD)")
56: actual_cost_clp = models.DecimalField(max_digits=15, decimal_places=2, default=0, verbose_name="Costo Real Acumulado (CLP)")
57:
58: payment_method = models.CharField(max_length=150, default="Contrato por Fases / Hitos", verbose_name="Forma de Pago")
59: total_income_clp = models.DecimalField(max_digits=15, decimal_places=2, default=0, verbose_name="Ingresos Totales (CLP)")
60:
61: type = models.CharField(max_length=50, choices=ExpenseTypeEnum.choices, default=ExpenseTypeEnum.COSTO_DIRECTO, verbose_name="Tipo de Centro")
62: status = models.CharField(max_length=50, choices=ProjectStatusEnum.choices, default=ProjectStatusEnum.ACTIVO, verbose_name="Estado de Proyecto")
63: category = models.ForeignKey('Category', on_delete=models.SET_NULL, null=True, blank=True, related_name='projects', verbose_name="Categoría del Proyecto")
64:
65: created_at = models.DateTimeField(auto_now_add=True, verbose_name="Fecha de Creación")
66: updated_at = models.DateTimeField(auto_now=True, verbose_name="Última Modificación")
67: created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='created_projects', verbose_name="Creado por")
68:
69: def __str__(self):
70: return f"{self.code} - {self.name}"
71:
72: @property
73: def total_expenses_clp(self):
74: return self.expenses.aggregate(total=models.Sum('amount_clp'))['total'] or Decimal('0.00')
75:
76: @property
77: def balance_clp(self):
78: return self.total_income_clp - self.total_expenses_clp
79:
80: @property
81: def budget_balance_clp(self):
82: return self.assigned_budget_clp - self.total_expenses_clp
83:
84: class Meta:
85: verbose_name = "Proyecto / Centro de Costo"
86: verbose_name_plural = "Proyectos y Centros"
87:
88:
89: class ProjectPhase(models.Model):
90: project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='phases', verbose_name="Proyecto")
91: phase_number = models.IntegerField(default=1, verbose_name="N° Fase / Hito")
92: name = models.CharField(max_length=150, verbose_name="Nombre de la Fase / Hito")
93: description = models.TextField(blank=True, verbose_name="Descripción de Entregables")
94: planned_amount_clp = models.DecimalField(max_digits=15, decimal_places=2, default=0, verbose_name="Monto Planificado / Pactado (CLP)")
95: income_amount_clp = models.DecimalField(max_digits=15, decimal_places=2, default=0, verbose_name="Monto Ingresado / Cobrado (CLP)")
96: expense_amount_clp = models.DecimalField(max_digits=15, decimal_places=2, default=0, verbose_name="Monto Egresado / Gastado (CLP)")
97: due_date = models.DateField(null=True, blank=True, verbose_name="Fecha Pactada")
98: is_completed = models.BooleanField(default=False, verbose_name="Fase Completada")
99:
100: created_at = models.DateTimeField(auto_now_add=True)
101: updated_at = models.DateTimeField(auto_now=True)
102:
103: class Meta:
104: ordering = ['phase_number']
105: verbose_name = "Fase / Hito de Pago de Proyecto"
106: verbose_name_plural = "Fases / Hitos de Pago de Proyecto"
107:
108: @property
109: def balance_clp(self):
110: return self.income_amount_clp - self.expense_amount_clp
111:
112: def __str__(self):
113: return f"{self.project.code} - Fase {self.phase_number}: {self.name}"
114:
115:
116: class Category(models.Model):
117: code = models.CharField(max_length=10, unique=True, verbose_name="Código de Categoría")
118: name = models.CharField(max_length=100, verbose_name="Nombre de Categoría")
119: description = models.TextField(blank=True, verbose_name="Descripción")
120: parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children', verbose_name="Categoría Padre")
121:
122: def __str__(self):
123: return f"{self.code}: {self.name}"
124:
125: class Meta:
126: verbose_name = "Categoría"
127: verbose_name_plural = "Categorías"
128:
129:
130: class Vendor(models.Model):
131: rut = models.CharField(max_length=15, unique=True, verbose_name="RUT Proveedor")
132: name = models.CharField(max_length=200, verbose_name="Razón Social / Nombre")
133: business_line = models.CharField(max_length=200, blank=True, null=True, verbose_name="Giro Comercial")
134:
135: bank = models.CharField(max_length=100, blank=True, null=True, verbose_name="Banco")
136: account_type = models.CharField(max_length=50, blank=True, null=True, verbose_name="Tipo de Cuenta")
137: account_number = models.CharField(max_length=50, blank=True, null=True, verbose_name="Número de Cuenta")
138: email = models.EmailField(blank=True, null=True, verbose_name="Correo de Pago/Facturación")
139:
140: payment_terms = models.IntegerField(default=30, verbose_name="Plazo de Pago (Días)")
141:
142: created_at = models.DateTimeField(auto_now_add=True)
143: updated_at = models.DateTimeField(auto_now=True)
144:
145: def __str__(self):
146: return f"{self.rut} - {self.name}"
147:
148: class Meta:
149: verbose_name = "Proveedor"
150: verbose_name_plural = "Proveedores"
151: ordering = ['name']
152:
153:
154: class ExpenseReport(models.Model):
155: report_code = models.CharField(max_length=50, unique=True, verbose_name="Código Rendición", default="EXP-REP-001")
156: title = models.CharField(max_length=250, verbose_name="Título del Informe / Viaje")
157: user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='expense_reports', verbose_name="Responsable Rendidor")
158: project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='expense_reports', verbose_name="Proyecto Imputado")
159:
160: start_date = models.DateField(verbose_name="Fecha de Inicio")
161: end_date = models.DateField(verbose_name="Fecha de Término")
162:
163: purpose = models.TextField(verbose_name="Propósito / Justificación de la Comisión")
164: status = models.CharField(max_length=30, choices=ExpenseReportStatusEnum.choices, default=ExpenseReportStatusEnum.BORRADOR, verbose_name="Estado Rendición")
165:
166: approved_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='approved_expense_reports', verbose_name="Aprobado por")
167: approved_at = models.DateTimeField(null=True, blank=True, verbose_name="Fecha de Aprobación")
168:
169: notes = models.TextField(blank=True, verbose_name="Observaciones / Comentarios de Cierre")
170: created_at = models.DateTimeField(auto_now_add=True, verbose_name="Fecha Creación")
171: updated_at = models.DateTimeField(auto_now=True, verbose_name="Última Modificación")
172:
173: def __str__(self):
174: return f"{self.report_code} - {self.title} ({self.user.username})"
175:
176: @property
177: def total_amount(self):
178: return sum(exp.total_amount_clp for exp in self.expenses.all())
179:
180: @property
181: def total_neto(self):
182: return sum(exp.amount_clp for exp in self.expenses.all())
183:
184: @property
185: def expenses_count(self):
186: return self.expenses.count()
187:
188: class Meta:
189: verbose_name = "Informe de Rendición de Gastos"
190: verbose_name_plural = "Informes de Rendición de Gastos"
191: ordering = ['-created_at']
192:
193:
194: class Expense(models.Model):
195: expense_code = models.CharField(max_length=50, unique=True, default=uuid.uuid4, verbose_name="Código Transacción")
196: report = models.ForeignKey(ExpenseReport, on_delete=models.SET_NULL, null=True, blank=True, related_name='expenses', verbose_name="Informe de Rendición (Opcional)")
197: project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='expenses', verbose_name="Proyecto")
198: category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True, related_name='expenses', verbose_name="Subcategoría")
199:
200: document_number = models.CharField(max_length=50, blank=True, null=True, verbose_name="N° Folio Boleta / Factura")
201:
202: amount_clp = models.DecimalField(max_digits=15, decimal_places=2, verbose_name="Monto Neto (CLP)")
203: tax_type = models.CharField(max_length=20, choices=TaxTypeEnum.choices, default=TaxTypeEnum.IVA_19, verbose_name="Tipo de Impuesto")
204: tax_amount_clp = models.DecimalField(max_digits=15, decimal_places=2, default=0, verbose_name="Monto Impuesto (CLP)")
205: total_amount_clp = models.DecimalField(max_digits=15, decimal_places=2, default=0, verbose_name="Monto Total (CLP)")
206:
207: amount_usd = models.DecimalField(max_digits=15, decimal_places=2, default=0, verbose_name="Monto Total (USD)")
208: currency = models.CharField(max_length=10, choices=CurrencyEnum.choices, default=CurrencyEnum.CLP, verbose_name="Moneda Original")
209: exchange_rate = models.DecimalField(max_digits=10, decimal_places=2, default=950.0, verbose_name="Tipo de Cambio")
210:
211: description = models.TextField(verbose_name="Detalle de Imputación")
212: date = models.DateField(verbose_name="Fecha del Gasto")
213:
214: vendor = models.ForeignKey(Vendor, on_delete=models.SET_NULL, null=True, blank=True, related_name='expenses', verbose_name="Proveedor / Emisor")
215:
216: region = models.CharField(max_length=100, blank=True, null=True, verbose_name="Región (Chile)")
217: comuna = models.CharField(max_length=100, blank=True, null=True, verbose_name="Comuna")
218: vehicle_ownership = models.CharField(max_length=50, blank=True, null=True, choices=VehicleOwnershipEnum.choices, verbose_name="Modalidad / Vehículo")
219:
220: is_reimbursement = models.BooleanField(default=False, verbose_name="¿Es Rendición / Reembolso de Gastos?")
221:
222: approval_status = models.CharField(max_length=50, choices=ApprovalStatusEnum.choices, default=ApprovalStatusEnum.EN_REVISION, verbose_name="Estado de Aprobación")
223:
224: user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='expenses_created', verbose_name="Registrado por")
225: responsible_person = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='expenses_responsible', verbose_name="Responsable Solicitante / Rendidor")
226: elevated_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='expenses_elevated', verbose_name="Elevado por (Paso 2)")
227: paid_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='expenses_paid', verbose_name="Pagado por (Paso 3)")
228:
229: rejection_reason = models.TextField(blank=True, null=True, verbose_name="Motivo de Observación/Rechazo")
230: paid_at = models.DateTimeField(null=True, blank=True, verbose_name="Fecha de Pago")
231:
232: created_at = models.DateTimeField(auto_now_add=True)
233: updated_at = models.DateTimeField(auto_now=True)
234:
235: def save(self, *args, **kwargs):
236: if not self.amount_usd or self.amount_usd == Decimal('0'):
237: rate = Decimal(str(self.exchange_rate or '950.0'))
238: if self.amount_clp:
239: amount_dec = Decimal(str(self.amount_clp))
240: self.amount_usd = (amount_dec / rate).quantize(Decimal('0.01'))
241: if not self.total_amount_clp or self.total_amount_clp == Decimal('0'):
242: amount_clp = Decimal(str(self.amount_clp or '0'))
243: tax_clp = Decimal(str(self.tax_amount_clp or '0'))
244: self.total_amount_clp = amount_clp + tax_clp
245: super().save(*args, **kwargs)
246:
247: def __str__(self):
248: return f"{self.expense_code} - {self.amount_clp} CLP ({self.date})"
249:
250: def is_vehicle_category(self):
251: if self.category:
252: return self.category.code == '3101' or 'combustible' in self.category.name.lower()
253: return False
254:
255: class Meta:
256: verbose_name = "Gasto"
257: verbose_name_plural = "Gastos"
258: ordering = ['-date', '-created_at']
259:
260:
261: class VehicleLog(models.Model):
262: expense = models.OneToOneField(Expense, on_delete=models.CASCADE, related_name='vehicle_log', null=True, blank=True, verbose_name="Gasto Asociado")
263: project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='vehicle_logs', verbose_name="Proyecto")
264:
265: license_plate = models.CharField(max_length=20, verbose_name="Patente Vehículo")
266: driver_name = models.CharField(max_length=150, verbose_name="Nombre Conductor")
267: driver_rut = models.CharField(max_length=20, blank=True, verbose_name="RUT Conductor")
268:
269: odometer_start = models.DecimalField(max_digits=10, decimal_places=1, default=0, verbose_name="Odómetro Inicial (KM)")
270: odometer_end = models.DecimalField(max_digits=10, decimal_places=1, default=0, verbose_name="Odómetro Final (KM)")
271: km_traveled = models.DecimalField(max_digits=10, decimal_places=1, default=0, verbose_name="KM Recorridos")
272:
273: fuel_type = models.CharField(max_length=50, default="Diésel", verbose_name="Tipo Combustible", blank=True)
274: fuel_liters = models.DecimalField(max_digits=8, decimal_places=2, default=0, blank=True, verbose_name="Litros Cargados")
275:
276: route_origin = models.CharField(max_length=150, verbose_name="Origen", default="Oficina / Base")
277: route_destination = models.CharField(max_length=150, verbose_name="Destino / Faena")
278:
279: log_date = models.DateField(verbose_name="Fecha de Registro")
280: notes = models.TextField(blank=True, verbose_name="Observaciones de Ruta")
281:
282: created_at = models.DateTimeField(auto_now_add=True)
283:
284: def save(self, *args, **kwargs):
285: if self.odometer_end and self.odometer_start and self.odometer_end >= self.odometer_start:
286: self.km_traveled = self.odometer_end - self.odometer_start
287: super().save(*args, **kwargs)
288:
289: def __str__(self):
290: return f"{self.license_plate} - {self.driver_name} ({self.km_traveled} KM)"
291:
292: class Meta:
293: verbose_name = "Bitácora de Vehículo"
294: verbose_name_plural = "Bitácoras de Vehículos"
295: ordering = ['-log_date']
296:
297:
298: class Document(models.Model):
299: expense = models.ForeignKey(Expense, on_delete=models.CASCADE, related_name='documents', verbose_name="Gasto Relacionado")
300: file = models.FileField(upload_to='evidences/', verbose_name="Archivo Adjunto")
301: file_type = models.CharField(max_length=50, blank=True, verbose_name="Tipo de Archivo")
302: title = models.CharField(max_length=200, blank=True, verbose_name="Título del Documento")
303: uploaded_at = models.DateTimeField(auto_now_add=True, verbose_name="Fecha de Subida")
304: uploaded_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, verbose_name="Subido por")
305:
306: def __str__(self):
307: return f"Evidencia para {self.expense.expense_code}"
308:
309: @property
310: def is_image(self):
311: ext = self.file.name.lower().split('.')[-1] if '.' in self.file.name else ''
312: return ext in ['jpg', 'jpeg', 'png', 'gif', 'webp']
313:
314: @property
315: def is_pdf(self):
316: return self.file.name.lower().endswith('.pdf')
317:
318: class Meta:
319: verbose_name = "Documento Evidencia"
320: verbose_name_plural = "Documentos Evidencias"
321:
322:
323: class UserProfile(models.Model):
324: user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile', verbose_name="Usuario")
325: rut = models.CharField(max_length=20, blank=True, null=True, verbose_name="RUT")
326: job_title = models.CharField(max_length=150, blank=True, null=True, verbose_name="Cargo")
327: description = models.TextField(blank=True, verbose_name="Descripción del Responsable")
328: specialties = models.ManyToManyField('Category', blank=True, related_name='specialists', verbose_name="Especialidades / Habilidades")
329:
330: created_at = models.DateTimeField(auto_now_add=True, verbose_name="Fecha de Registro")
331: updated_at = models.DateTimeField(auto_now=True, verbose_name="Última Modificación")
332:
333: def __str__(self):
334: return f"Perfil de {self.user.username}"
335:
336: class Meta:
337: verbose_name = "Perfil de Usuario"
338: verbose_name_plural = "Perfiles de Usuario"
339:
340:
341: class AuditLog(models.Model):
342: user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Usuario")
343: action = models.CharField(max_length=100, verbose_name="Acción")
344: entity_name = models.CharField(max_length=100, blank=True, null=True, verbose_name="Entidad (Tabla)")
345: entity_id = models.CharField(max_length=100, blank=True, null=True, verbose_name="ID Entidad")
346: field_changed = models.CharField(max_length=100, blank=True, null=True, verbose_name="Campo Modificado")
347: old_value = models.TextField(blank=True, null=True, verbose_name="Valor Anterior")
348: new_value = models.TextField(blank=True, null=True, verbose_name="Valor Nuevo")
349:
350: description = models.TextField(verbose_name="Detalle del Cambio")
351: timestamp = models.DateTimeField(auto_now_add=True, verbose_name="Fecha y Hora")
352:
353: def __str__(self):
354: return f"{self.timestamp} - {self.user.username if self.user else 'Sistema'}: {self.action}"
355:
356: class Meta:
357: ordering = ['-timestamp']
358: verbose_name = "Log de Auditoría (SAP CDHDR/CDPOS)"
359: verbose_name_plural = "Logs de Auditoría"
360:
361:
362: class SystemSetting(models.Model):
363: global_budget = models.DecimalField(max_digits=15, decimal_places=2, default=25000000.00, verbose_name="Presupuesto General I+D+i")
364: usd_exchange_rate = models.DecimalField(max_digits=10, decimal_places=2, default=950.0, verbose_name="Tasa USD/CLP por Defecto")
365: iva_rate = models.DecimalField(max_digits=5, decimal_places=2, default=19.00, verbose_name="Tasa de IVA (% Dinámico SII)")
366:
367: def __str__(self):
368: return "Configuración Global EFINTEL"
369:
370: class Meta:
371: verbose_name = "Configuración del Sistema"
372: verbose_name_plural = "Configuraciones del Sistema"
373: """
out_lines = []
for line in part1.split("\n"):
if re.match(r'^\d+:\s', line):
out_lines.append(line.split(": ", 1)[1])
with open('expenses/models.py', 'w', encoding='utf-8') as f:
f.write("\n".join(out_lines))