Coverage for invoices/models.py: 88%
123 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-06-29 13:45 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-06-29 13:45 -0600
1from django.db import models
2from django.core.files.uploadedfile import SimpleUploadedFile
3from app.models import RandomSlugModel, TimeStampedModel
4from app.facturapi import FacturapiOrganizationClient
7class FiscalRegime(RandomSlugModel):
8 """
9 Model for Fiscal Regime
10 """
12 description = models.CharField(max_length=128)
13 regime_key = models.CharField(max_length=3)
15 class Meta:
16 ordering = ["regime_key"]
19class InvoiceUse(RandomSlugModel):
20 """
21 Model for CFDI use
22 """
24 description = models.CharField(max_length=128)
25 use_key = models.CharField(max_length=4)
27 class Meta:
28 ordering = ["use_key"]
31class ProductInvoiceConfiguration(RandomSlugModel):
32 """
33 Model for product invoice configurations
34 """
36 organization = models.ForeignKey(
37 "organizations.Organization",
38 on_delete=models.PROTECT,
39 related_name="invoice_configurations",
40 )
41 product = models.OneToOneField(
42 "products.Product", on_delete=models.PROTECT, related_name="invoice_configuration", null=True
43 )
45 uses = models.ManyToManyField("invoices.InvoiceUse")
47 description = models.CharField(max_length=128)
48 product_key = models.CharField(max_length=8)
49 unit_key = models.CharField(max_length=8)
50 tax_included = models.BooleanField(default=True)
52 class Taxability(models.TextChoices):
53 NO_TAX = "01", "No tax"
54 TAX_DETAIL = "02", "Tax, detail"
55 TAX_NO_DETAIL = "03", "Tax, no detail"
57 taxability = models.CharField(max_length=2, choices=Taxability.choices, default=Taxability.NO_TAX)
59 for_membership = models.BooleanField(default=False)
60 for_event = models.BooleanField(default=False)
61 for_stock = models.BooleanField(default=False)
64class MembershipInvoiceConfiguration(RandomSlugModel):
65 """
66 Model for membership invoice configuration
67 """
69 membership = models.ForeignKey(
70 "memberships.Membership",
71 on_delete=models.PROTECT,
72 related_name="invoice_configurations",
73 )
74 tax_system = models.ForeignKey("invoices.FiscalRegime", on_delete=models.PROTECT)
76 legal_name = models.CharField(max_length=256)
77 email = models.EmailField()
78 tax_id = models.CharField(max_length=16)
79 zip_code = models.CharField(max_length=5)
80 default = models.BooleanField(default=False)
82 class Meta:
83 ordering = ["-default", "legal_name"]
86class InvoiceConfiguration(RandomSlugModel):
87 """
88 Model for invoice configuration copied from MembershipInvoiceConfiguration
89 """
91 tax_system = models.CharField(max_length=3)
92 legal_name = models.CharField(max_length=256)
93 email = models.EmailField()
94 tax_id = models.CharField(max_length=16)
95 zip_code = models.CharField(max_length=5)
97 class Meta:
98 ordering = ["legal_name"]
101class InvoiceProductConfiguration(RandomSlugModel):
102 """
103 Model for invoice product configurations copied from ProdcutInvoiceConfigurationsand
104 and applied for every product_charge on Invoice
105 """
107 invoice = models.ForeignKey("invoices.Invoice", on_delete=models.CASCADE, related_name="product_configurations")
108 product_charge = models.ForeignKey(
109 "accounting.ProductCharge", on_delete=models.PROTECT, related_name="invoice_configurations"
110 )
112 description = models.CharField(max_length=128)
113 product_key = models.CharField(max_length=8)
114 unit_key = models.CharField(max_length=8)
115 amount = models.DecimalField(max_digits=12, decimal_places=2)
116 tax_included = models.BooleanField(default=True)
118 class Taxability(models.TextChoices):
119 NO_TAX = "01", "No tax"
120 TAX_DETAIL = "02", "Tax, detail"
121 TAX_NO_DETAIL = "03", "Tax, no detail"
123 taxability = models.CharField(max_length=2, choices=Taxability.choices, default=Taxability.NO_TAX)
126class Invoice(RandomSlugModel, TimeStampedModel):
127 """
128 Model for invoice
129 """
131 organization = models.ForeignKey("organizations.Organization", on_delete=models.PROTECT, related_name="invoices")
132 membership = models.ForeignKey(
133 "memberships.Membership",
134 on_delete=models.PROTECT,
135 related_name="invoices",
136 )
137 product_charges = models.ManyToManyField(
138 "accounting.ProductCharge",
139 related_name="invoices",
140 )
141 invoice_configuration = models.ForeignKey(
142 "invoices.InvoiceConfiguration", on_delete=models.PROTECT, related_name="invoices", null=True
143 )
145 use = models.CharField(max_length=4, null=True)
147 pdf = models.FileField(upload_to="invoices/pdfs/", null=True)
148 xml = models.FileField(upload_to="invoices/xmls/", null=True)
149 facturapi_invoice_id = models.CharField(max_length=24, null=True)
150 sat_uuid = models.UUIDField(null=True)
151 folio_number = models.PositiveIntegerField(null=True)
153 class Status(models.TextChoices):
154 PENDING = "P", "Pendiente"
155 VALID = "V", "Valida"
156 CANCELED = "C", "Cancelada"
158 status = models.CharField(max_length=1, choices=Status.choices, default=Status.PENDING)
160 class CancellationStatus(models.TextChoices):
161 PENDING = "P", "Pendiente"
162 ACCEPTED = "A", "Aceptada"
163 REJECTED = "R", "Rechazada"
164 EXPIRED = "E", "Expirada"
166 cancellation_status = models.CharField(max_length=1, choices=CancellationStatus.choices, null=True, blank=True)
168 class CancellationMotive(models.TextChoices):
169 ERROR_WITH_RELATION = "01", "Comprobante emitido con errores con relación"
170 ERROR_WITHOUT_RELATION = "02", "Comprobante emitido con errores sin relación"
171 TRANSACTION_NOT_CONCLUDED = "03", "No se llevó a cabo la operación"
172 NOMINATIVE_INVOICE = "04", "Operación nominativa relacionada en la factura global"
174 cancellation_motive = models.CharField(max_length=2, choices=CancellationMotive.choices, null=True, blank=True)
176 @property
177 def total_amount(self):
178 return self.product_configurations.aggregate(total_amount=models.Sum("amount")).get("total_amount")
180 def set_folio_number(self):
181 facturapi = FacturapiOrganizationClient(self.organization.facturapi_secret_key)
182 folio_number = facturapi.Invoice.retrieve(self.facturapi_invoice_id).get("folio_number")
184 self.folio_number = folio_number
186 self.save()
188 def save_files(self):
189 assert self.facturapi_invoice_id is not None
191 facturapi = FacturapiOrganizationClient(self.organization.facturapi_secret_key)
193 response_xml = facturapi.Invoice.get_file(self.facturapi_invoice_id, "xml")
194 response_pdf = facturapi.Invoice.get_file(self.facturapi_invoice_id, "pdf")
196 xml_file = SimpleUploadedFile(f"{self.sat_uuid}.xml", response_xml, content_type="application/octet-stream")
197 pdf_file = SimpleUploadedFile(f"{self.sat_uuid}.pdf", response_pdf, content_type="application/octet-stream")
199 self.xml = xml_file
200 self.pdf = pdf_file
202 self.save()
204 def send_invoice_email(self, email: list = None):
205 facturapi = FacturapiOrganizationClient(self.organization.facturapi_secret_key)
206 if email:
207 return facturapi.Invoice.send_email(self.facturapi_invoice_id, email)
208 else:
209 return facturapi.Invoice.send_email(self.facturapi_invoice_id, self.invoice_configuration.email)
211 class Meta:
212 ordering = ["-created_at"]
215class InvoiceExport(RandomSlugModel, TimeStampedModel):
216 """
217 Model for Invoice export
218 """
220 organization = models.ForeignKey("organizations.Organization", on_delete=models.CASCADE)
222 from_date = models.DateField()
223 to_date = models.DateField()
224 status = models.CharField(max_length=1, choices=Invoice.Status.choices, default=Invoice.Status.VALID)
225 cancellation_status = models.CharField(
226 max_length=1, choices=Invoice.CancellationStatus.choices, null=True, blank=True
227 )
229 export_file = models.FileField(upload_to="exports/", blank=True, null=True)
231 class Meta:
232 ordering = ["-created_at"]