Coverage for invoices/models.py: 88%

123 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1from django.db import models 

2from django.core.files.uploadedfile import SimpleUploadedFile 

3from app.models import RandomSlugModel, TimeStampedModel 

4from app.facturapi import FacturapiOrganizationClient 

5 

6 

7class FiscalRegime(RandomSlugModel): 

8 """ 

9 Model for Fiscal Regime 

10 """ 

11 

12 description = models.CharField(max_length=128) 

13 regime_key = models.CharField(max_length=3) 

14 

15 class Meta: 

16 ordering = ["regime_key"] 

17 

18 

19class InvoiceUse(RandomSlugModel): 

20 """ 

21 Model for CFDI use 

22 """ 

23 

24 description = models.CharField(max_length=128) 

25 use_key = models.CharField(max_length=4) 

26 

27 class Meta: 

28 ordering = ["use_key"] 

29 

30 

31class ProductInvoiceConfiguration(RandomSlugModel): 

32 """ 

33 Model for product invoice configurations 

34 """ 

35 

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 ) 

44 

45 uses = models.ManyToManyField("invoices.InvoiceUse") 

46 

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) 

51 

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" 

56 

57 taxability = models.CharField(max_length=2, choices=Taxability.choices, default=Taxability.NO_TAX) 

58 

59 for_membership = models.BooleanField(default=False) 

60 for_event = models.BooleanField(default=False) 

61 for_stock = models.BooleanField(default=False) 

62 

63 

64class MembershipInvoiceConfiguration(RandomSlugModel): 

65 """ 

66 Model for membership invoice configuration 

67 """ 

68 

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) 

75 

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) 

81 

82 class Meta: 

83 ordering = ["-default", "legal_name"] 

84 

85 

86class InvoiceConfiguration(RandomSlugModel): 

87 """ 

88 Model for invoice configuration copied from MembershipInvoiceConfiguration 

89 """ 

90 

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) 

96 

97 class Meta: 

98 ordering = ["legal_name"] 

99 

100 

101class InvoiceProductConfiguration(RandomSlugModel): 

102 """ 

103 Model for invoice product configurations copied from ProdcutInvoiceConfigurationsand 

104 and applied for every product_charge on Invoice 

105 """ 

106 

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 ) 

111 

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) 

117 

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" 

122 

123 taxability = models.CharField(max_length=2, choices=Taxability.choices, default=Taxability.NO_TAX) 

124 

125 

126class Invoice(RandomSlugModel, TimeStampedModel): 

127 """ 

128 Model for invoice 

129 """ 

130 

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 ) 

144 

145 use = models.CharField(max_length=4, null=True) 

146 

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) 

152 

153 class Status(models.TextChoices): 

154 PENDING = "P", "Pendiente" 

155 VALID = "V", "Valida" 

156 CANCELED = "C", "Cancelada" 

157 

158 status = models.CharField(max_length=1, choices=Status.choices, default=Status.PENDING) 

159 

160 class CancellationStatus(models.TextChoices): 

161 PENDING = "P", "Pendiente" 

162 ACCEPTED = "A", "Aceptada" 

163 REJECTED = "R", "Rechazada" 

164 EXPIRED = "E", "Expirada" 

165 

166 cancellation_status = models.CharField(max_length=1, choices=CancellationStatus.choices, null=True, blank=True) 

167 

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" 

173 

174 cancellation_motive = models.CharField(max_length=2, choices=CancellationMotive.choices, null=True, blank=True) 

175 

176 @property 

177 def total_amount(self): 

178 return self.product_configurations.aggregate(total_amount=models.Sum("amount")).get("total_amount") 

179 

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") 

183 

184 self.folio_number = folio_number 

185 

186 self.save() 

187 

188 def save_files(self): 

189 assert self.facturapi_invoice_id is not None 

190 

191 facturapi = FacturapiOrganizationClient(self.organization.facturapi_secret_key) 

192 

193 response_xml = facturapi.Invoice.get_file(self.facturapi_invoice_id, "xml") 

194 response_pdf = facturapi.Invoice.get_file(self.facturapi_invoice_id, "pdf") 

195 

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") 

198 

199 self.xml = xml_file 

200 self.pdf = pdf_file 

201 

202 self.save() 

203 

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) 

210 

211 class Meta: 

212 ordering = ["-created_at"] 

213 

214 

215class InvoiceExport(RandomSlugModel, TimeStampedModel): 

216 """ 

217 Model for Invoice export 

218 """ 

219 

220 organization = models.ForeignKey("organizations.Organization", on_delete=models.CASCADE) 

221 

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 ) 

228 

229 export_file = models.FileField(upload_to="exports/", blank=True, null=True) 

230 

231 class Meta: 

232 ordering = ["-created_at"]