account_cfdi.py 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831
  1. from odoo import api, fields, models, _, Command
  2. from odoo.exceptions import ValidationError
  3. from collections import OrderedDict
  4. from datetime import date
  5. from os.path import basename
  6. from zipfile import ZipFile
  7. from tempfile import TemporaryDirectory
  8. import xmltodict
  9. import base64
  10. import logging
  11. import os.path
  12. _logger = logging.getLogger(__name__)
  13. class AccountCFDI(models.Model):
  14. _name = 'account.cfdi'
  15. _inherit = ['mail.thread', 'mail.activity.mixin']
  16. _description = 'Complemento CFDI'
  17. code = fields.Char(string="Código")
  18. name = fields.Char(string="Referencia")
  19. uuid = fields.Char(string="UUID")
  20. certificate = fields.Char(string="Certificado")
  21. certificate_number = fields.Char(string="Nro. de certificado")
  22. serie = fields.Char(string="Serie")
  23. folio = fields.Char(string="Folio")
  24. stamp = fields.Char(string="Sello")
  25. version = fields.Char(string="Versión")
  26. payment_condition = fields.Char(string="Condiciones de pago")
  27. currency = fields.Char(string="Moneda")
  28. payment_method = fields.Char(string="Forma de pago")
  29. location = fields.Char(string="Lugar de expedición")
  30. observations = fields.Text(string='Notas')
  31. attachment_id = fields.Many2one(comodel_name="ir.attachment", string="Archivo adjunto")
  32. pdf_id = fields.Many2one(comodel_name="ir.attachment", string="Representación impresa")
  33. company_id = fields.Many2one(comodel_name="res.company", string="Empresa", default=lambda self: self.env.company)
  34. emitter_id = fields.Many2one(comodel_name="res.partner", string="Emisor")
  35. receiver_id = fields.Many2one(comodel_name="res.partner", string="Receptor")
  36. move_id = fields.Many2one(comodel_name="account.move", string="Movimiento contable", copy=False)
  37. payable_account_id = fields.Many2one(comodel_name='account.account', string='Cuenta contable a pagar', tracking=True)
  38. account_id = fields.Many2one(comodel_name='account.account', string='Cuenta contable gasto', tracking=True)
  39. account_analytic_account_id = fields.Many2one(comodel_name='account.analytic.account', string='Cuenta analítica')
  40. fiscal_position_id = fields.Many2one(comodel_name='account.fiscal.position', string='Posición fiscal')
  41. journal_id = fields.Many2one(comodel_name='account.journal', string='Diario', tracking=True)
  42. tax_isr_id = fields.Many2one(comodel_name='account.tax', string='Retención ISR')
  43. tax_iva_id = fields.Many2one(comodel_name='account.tax', string='Retención IVA')
  44. analytic_distribution = fields.Json(string="Distribución analítica")
  45. analytic_precision = fields.Integer(string="Precisión analítica", store=False, default=lambda self: self.env['decimal.precision'].precision_get("Percentage Analytic"))
  46. concept_ids = fields.One2many("account.cfdi.line", "cfdi_id", string="Conceptos")
  47. tax_ids = fields.One2many('account.cfdi.tax', 'cfdi_id', string='Impuestos')
  48. date = fields.Date(string="Fecha")
  49. subtotal = fields.Float(string="Subtotal", copy=False)
  50. total = fields.Float(string="Total", copy=False)
  51. tax_total = fields.Float(string="Impuestos", compute="compute_tax_total", store=True)
  52. payment_type = fields.Selection(selection=[('PPD', 'PPD'), ('PUE', 'PUE')], string='Método de pago', readonly=True)
  53. cfdi_type = fields.Selection(string="Tipo de comprobante", selection=[
  54. ('I', 'Facturas de clientes'),
  55. ('SI', 'Facturas de proveedor'),
  56. ('E', 'Notas de crédito cliente'),
  57. ('SE', 'Notas de crédito proveedor'),
  58. ('P', 'REP de clientes'),
  59. ('SP', 'REP de proveedores'),
  60. ('N', 'Nóminas de empleados'),
  61. ('SN', 'Nómina propia'),
  62. ('T', 'Factura de traslado cliente'),
  63. ('ST', 'Factura de traslado proveedor')
  64. ], index=True, copy=False)
  65. state = fields.Selection(string="Estado", selection=[
  66. ('draft', 'Borrador'),
  67. ('done', 'Procesada'),
  68. ('cancel', 'Anulado')
  69. ], copy=False, default='draft', tracking=True)
  70. sat_state = fields.Selection(string="Estado SAT", selection=[
  71. ("valid", "Valido"),
  72. ("not_found", "No Encontrado"),
  73. ("undefined", "No sincronizado Aún"),
  74. ("none", "Estado no definido"),
  75. ("cancelled", "Cancelado")
  76. ], copy=False, tracking=True, default="valid")
  77. #Datos de addenda
  78. delivery_number = fields.Char(string="No. Entrega")
  79. invoice_qty = fields.Integer(string="Unidades facturadas")
  80. @api.depends("subtotal","total")
  81. def compute_tax_total(self):
  82. for rec in self:
  83. rec.tax_total = rec.total - rec.subtotal
  84. # ---------------------------------------------------Metodos de creación---------------------------------------------------------
  85. @api.model
  86. def create(self, vals_list):
  87. self = self.with_context(skip_invoice_sync=True, check_move_validity=False)
  88. res = super().create(vals_list)
  89. for cfdi in res.filtered(lambda move: move.move_id):
  90. cfdi.move_id.update({
  91. "cfdi_id": cfdi.id
  92. })
  93. return res
  94. def name_get(self):
  95. result = []
  96. for record in self:
  97. folio = f"[{record.serie}-{record.folio}] - " if record.serie and record.folio else f"[{record.serie}] - " if record.serie and not record.folio else f"[{record.folio}] - " if not record.serie and record.folio else ""
  98. name = f"{folio}" + record.uuid + ' - $' + str(record.total)
  99. result.append((record.id, name))
  100. return result
  101. # Creación de CFDIS
  102. def create_cfdis(self, attachment_data):
  103. self = self.with_context(skip_invoice_sync=True, check_move_validity=False)
  104. cfdi_list = []
  105. cfdi_ids = self.env["account.cfdi"]
  106. uuids = []
  107. for data in attachment_data:
  108. # Obtener la información para crear el cfdi
  109. data_xml = data.get("xml")
  110. xml_data = self.get_cfdi_data(data_xml.get("datas"))
  111. if xml_data.get("Comprobante"):
  112. cfdi_type = self.get_cfdi_type(xml_data)
  113. uuid = self.validation_cfdi(xml_data, cfdi_type)
  114. # Evitar UUID repetidos ya que el SAT manda en ocasiones el mismo XML dos veces
  115. if uuid and uuid not in uuids:
  116. uuids.append(uuid)
  117. emiiter_partner_id, recipient_partner_id = self.get_cfdi_partners(xml_data)
  118. move_id = self.validate_cfdi_move(xml_data, uuid, cfdi_type, emiiter_partner_id)
  119. cfdi_data = self.add_cfdi_data(xml_data, uuid, emiiter_partner_id, recipient_partner_id, move_id, cfdi_type, data)
  120. cfdi_data = self.get_cfdi_lines(xml_data, cfdi_data, cfdi_type, emiiter_partner_id, recipient_partner_id)
  121. # cfdi_data = self.get_payment_tax(cfdi_type, xml_data, cfdi_data)
  122. cfdi_list.append(cfdi_data)
  123. else:
  124. continue
  125. if cfdi_list:
  126. cfdi_ids = self.env["account.cfdi"].sudo().create(cfdi_list)
  127. return cfdi_ids
  128. # Obtener la información del xml
  129. def get_cfdi_data(self, xml_file):
  130. file_content = base64.b64decode(xml_file)
  131. if b'xmlns:schemaLocation' in file_content and b'xsi:schemaLocation' not in file_content:
  132. file_content = file_content.replace(b'xmlns:schemaLocation', b'xsi:schemaLocation')
  133. file_content = file_content.replace(b'cfdi:', b'')
  134. file_content = file_content.replace(b'tfd:', b'')
  135. try:
  136. xml_data = xmltodict.parse(file_content)
  137. return xml_data
  138. except Exception as e:
  139. _logger.info(e)
  140. return dict()
  141. # Obtener el tipo de comprobante que es el CFDI
  142. def get_cfdi_type(self, xml_data):
  143. cfdi_type = xml_data['Comprobante']['@TipoDeComprobante'] if '@TipoDeComprobante' in xml_data['Comprobante'] else 'I'
  144. if cfdi_type in ['I', 'E', 'P']:
  145. if xml_data['Comprobante']['Emisor']['@Rfc'] != self.env.company.vat:
  146. cfdi_type = 'S' + cfdi_type
  147. return cfdi_type
  148. # Validaciones antes de la creación del CFDI
  149. def validation_cfdi(self, xml_data, cfdi_type):
  150. if '@UUID' in xml_data['Comprobante']['Complemento']['TimbreFiscalDigital']:
  151. uuid = xml_data['Comprobante']['Complemento']['TimbreFiscalDigital']['@UUID']
  152. cfdi_id = self.env['account.cfdi'].sudo().search([('uuid', '=', uuid)], limit=1)
  153. if cfdi_id:
  154. _logger.info(f"El CFDI con UUID {uuid}, ya existe en la base de datos, se omitirá en el proceso.")
  155. return False
  156. # Evitar que se suban xml que no pertenecen a la empresa
  157. if "S" in cfdi_type:
  158. partner_rfc = xml_data['Comprobante']['Receptor']['@Rfc']
  159. else:
  160. partner_rfc = xml_data['Comprobante']['Emisor']['@Rfc']
  161. if partner_rfc != self.env.company.vat:
  162. return False
  163. else:
  164. return False
  165. return uuid
  166. # Buscar el asiento contable de odoo relacionado si es que existe
  167. def validate_cfdi_move(self, xml_data, uuid, cfdi_type, emitter_partner_id):
  168. move_id = self.env["account.move"]
  169. delivery_number, invoice_qty = self.get_addenda_data(xml_data)
  170. if cfdi_type in ["I", "E"]:
  171. move_id = move_id.sudo().search(
  172. [("move_type", "in", ["out_invoice", "out_refund"]), ("l10n_mx_edi_cfdi_uuid", "=", uuid),
  173. ("state", "=", "posted"), ("cfdi_id", "=", False)], limit=1)
  174. elif cfdi_type in ["SI", "SE"]:
  175. invoice_date = xml_data['Comprobante']['@Fecha'] if '@Fecha' in xml_data['Comprobante'] else ""
  176. folio = xml_data['Comprobante']['@Folio'] if '@Folio' in xml_data['Comprobante'] else ''
  177. move_id = self.env['account.move'].sudo().search(
  178. [('partner_id.vat', '=', emitter_partner_id.vat), ('ref', '=', folio),
  179. ('move_type', 'in', ['in_invoice', 'in_refund']), ("state", "=", "posted"), ("cfdi_id", "=", False),
  180. ("invoice_date", "=", invoice_date[:10])], limit=1)
  181. if not move_id:
  182. if '@Serie' in xml_data['Comprobante']:
  183. folio = xml_data['Comprobante']['@Serie'] + folio
  184. move_id = self.env['account.move'].sudo().search(
  185. [('partner_id.vat', '=', emitter_partner_id.vat), ('ref', '=', folio),
  186. ('move_type', 'in', ['in_invoice', 'in_refund']), ("cfdi_id", "=", False),
  187. ("state", "=", "posted"), ("invoice_date", "=", invoice_date[:10])], limit=1)
  188. if not move_id:
  189. move_id = self.env['account.move'].sudo().search(
  190. [('partner_id.vat', '=', emitter_partner_id.vat),
  191. ('move_type', 'in', ['in_invoice', 'in_refund']), ("state", "=", "posted"),
  192. ("cfdi_id", "=", False),
  193. ("invoice_date", "=", invoice_date[:10])], limit=1)
  194. if not move_id and delivery_number:
  195. move_id = self.env['account.move'].sudo().search(
  196. [('partner_id.vat', '=', emitter_partner_id.vat),
  197. ('move_type', 'in', ['in_invoice', 'in_refund']), ("state", "=", "posted"),
  198. ("cfdi_id", "=", False), ("x_delivery_number","!=",False),
  199. ("x_delivery_number", "=", delivery_number)], limit=1)
  200. if not move_id:
  201. amount_untaxed = xml_data['Comprobante']['@SubTotal'] if '@SubTotal' in xml_data['Comprobante'] else 0
  202. invoice_date = xml_data['Comprobante']['@Fecha'] if '@Fecha' in xml_data['Comprobante'] else ""
  203. move_id = self.env['account.move'].sudo().search(
  204. [('partner_id.vat', '=', emitter_partner_id.vat), ('move_type', 'in', ['in_invoice']),
  205. ("cfdi_id", "=", False), ("state", "=", "posted"), ("amount_untaxed", "=", amount_untaxed),
  206. ("invoice_date", "=", invoice_date[:10])], limit=1)
  207. return move_id
  208. # Preparar la informacion del CFDI
  209. def add_cfdi_data(self, xml_data, uuid, emitter_partner_id, recipient_partner_id, move_id, cfdi_type, attachment_data):
  210. journal_id, account_id = self.get_cfdi_journal_id(cfdi_type, emitter_partner_id, recipient_partner_id)
  211. partner_id = recipient_partner_id if cfdi_type in ["I", "E"] else emitter_partner_id
  212. payable_account_id = self.get_payable_cfdi_account_id(cfdi_type, partner_id)
  213. attachment_id = self.env["ir.attachment"].sudo().create(attachment_data.get("xml"))
  214. pdf_id = self.env["ir.attachment"].sudo().create(attachment_data.get("pdf")) if attachment_data.get("pdf") else False
  215. delivery_number, invoice_qty = self.get_addenda_data(xml_data)
  216. data = {
  217. "attachment_id": attachment_id.id,
  218. "pdf_id": pdf_id.id if pdf_id else False,
  219. "code": uuid,
  220. "uuid": uuid,
  221. "certificate": xml_data['Comprobante']['@Certificado'] if '@Certificado' in xml_data['Comprobante'] else '',
  222. "date": xml_data['Comprobante']['@Fecha'] if '@Fecha' in xml_data['Comprobante'] else '',
  223. "folio": xml_data['Comprobante']['@Folio'] if '@Folio' in xml_data['Comprobante'] else '',
  224. "payment_method": xml_data['Comprobante']['@FormaPago'] if '@FormaPago' in xml_data['Comprobante'] else '',
  225. "location": xml_data['Comprobante']['@LugarExpedicion'] if '@LugarExpedicion' in xml_data['Comprobante'] else '',
  226. "payment_type": xml_data['Comprobante']['@MetodoPago'] if '@MetodoPago' in xml_data['Comprobante'] else '',
  227. "currency": xml_data['Comprobante']['@Moneda'] if '@Moneda' in xml_data['Comprobante'] else '',
  228. "certificate_number": xml_data['Comprobante']['@NoCertificado'] if '@NoCertificado' in xml_data['Comprobante'] else '',
  229. "stamp": xml_data['Comprobante']['@Sello'] if '@Sello' in xml_data['Comprobante'] else '',
  230. "serie": xml_data['Comprobante']['@Serie'] if '@Serie' in xml_data['Comprobante'] else '',
  231. "subtotal": xml_data['Comprobante']['@SubTotal'] if '@SubTotal' in xml_data['Comprobante'] else '',
  232. "cfdi_type": cfdi_type,
  233. "total": xml_data['Comprobante']['@Total'] if '@Total' in xml_data['Comprobante'] else '',
  234. "version": xml_data['Comprobante']['@Version'] if '@Version' in xml_data['Comprobante'] else '',
  235. "payment_condition": xml_data['Comprobante']['@CondicionesDePago'] if '@CondicionesDePago' in xml_data['Comprobante'] else '',
  236. "emitter_id": emitter_partner_id.id,
  237. "receiver_id": recipient_partner_id.id,
  238. "move_id": move_id.id if move_id else False,
  239. "state": "draft" if not move_id else "done",
  240. "journal_id": journal_id.id if journal_id else False,
  241. "account_id": account_id.id if account_id else False,
  242. "fiscal_position_id": partner_id.property_account_position_id.id if partner_id.property_account_position_id else False,
  243. "tax_iva_id": partner_id.x_tax_iva_id.id if partner_id.x_tax_iva_id else False,
  244. "tax_isr_id": partner_id.x_tax_isr_id.id if partner_id.x_tax_isr_id else False,
  245. "payable_account_id": payable_account_id.id if payable_account_id else False,
  246. }
  247. data["name"] = data["serie"] + data["folio"] if data.get("serie") and data.get("folio") else data["serie"] if data.get("serie") else data.get("folio")
  248. data["delivery_number"] = delivery_number
  249. data["invoice_qty"] = invoice_qty
  250. return data
  251. # Se obtienen las lineas de CFDI
  252. def get_cfdi_lines(self, xml_data, cfdi_data, cfdi_type, emitter_partner_id, recipient_partner_id):
  253. if type(xml_data['Comprobante']['Conceptos']['Concepto']) is list:
  254. lines = xml_data['Comprobante']['Conceptos']['Concepto']
  255. elif type(xml_data['Comprobante']['Conceptos']['Concepto']) is OrderedDict:
  256. lines = xml_data['Comprobante']['Conceptos'].items()
  257. else:
  258. lines = [xml_data['Comprobante']['Conceptos']['Concepto']]
  259. i = 1
  260. data_list = []
  261. for line_value in lines:
  262. if type(xml_data['Comprobante']['Conceptos']['Concepto']) is list:
  263. line = line_value
  264. elif type(xml_data['Comprobante']['Conceptos']['Concepto']) is OrderedDict:
  265. line = line_value[1]
  266. else:
  267. line = line_value
  268. if float(line['@Importe']) >= 0:
  269. uom_id = False
  270. if '@ClaveUnidad' in line:
  271. uom_unspsc_id = self.env['product.unspsc.code'].sudo().search([('code', '=', line['@ClaveUnidad'])])
  272. if uom_unspsc_id:
  273. uom_id = self.env['uom.uom'].sudo().search([('unspsc_code_id', '=', uom_unspsc_id.id)], limit=1)
  274. if uom_id:
  275. unidad_id = uom_id.id
  276. else:
  277. unidad_id = False
  278. product_category_id = False
  279. if '@ClaveProdServ' in line:
  280. unspsc_product_category_id = self.env['product.unspsc.code'].sudo().search([('code', '=', line['@ClaveProdServ'])])
  281. if unspsc_product_category_id:
  282. product_category_id = unspsc_product_category_id.id
  283. else:
  284. product_category_id = False
  285. data_line = {
  286. 'sequence': i,
  287. 'code_cfdi': cfdi_data.get("code"),
  288. 'date': cfdi_data.get("date"),
  289. 'folio': cfdi_data.get("folio"),
  290. 'payment_method': cfdi_data.get("payment_method"),
  291. 'location': cfdi_data.get("location"),
  292. 'payment_type': cfdi_data.get("payment_type"),
  293. 'currency': cfdi_data.get("currency"),
  294. 'certificate_number': cfdi_data.get("certificate_number"),
  295. 'stamp': cfdi_data.get("stamp"),
  296. 'serie': cfdi_data.get("serie"),
  297. 'subtotal': cfdi_data.get("subtotal"),
  298. 'cfdi_type': cfdi_data.get("cfdi_type"),
  299. 'total': cfdi_data.get("total"),
  300. 'version': cfdi_data.get("version"),
  301. 'emitter_id': cfdi_data.get("emitter_id"),
  302. 'receiver_id': cfdi_data.get("receiver_id"),
  303. 'product_code': line['@ClaveProdServ'] if '@ClaveProdServ' in line else '',
  304. 'no_identification': line['@NoIdentificacion'] if '@NoIdentificacion' in line else '',
  305. 'quantity': float(line['@Cantidad']),
  306. 'uom_code': line['@ClaveUnidad'] if '@ClaveUnidad' in line else '',
  307. 'uom': line['@Unidad'] if '@Unidad' in line else '',
  308. 'description': line['@Descripcion'],
  309. 'discount': float(line['@Descuento']) if '@Descuento' in line else 0,
  310. 'unit_price': float(line['@ValorUnitario']),
  311. 'uom_id': unidad_id,
  312. 'unspsc_product_category_id': product_category_id,
  313. 'amount': float(line['@Importe']),
  314. }
  315. data_line = self.search_cfdi_product(line, cfdi_type, data_line, emitter_partner_id, recipient_partner_id)
  316. data_line = self.get_cfdi_tax_lines(data_line, line, cfdi_type, recipient_partner_id)
  317. data_list.append(Command.create(data_line))
  318. i += 1
  319. if data_list:
  320. cfdi_data["concept_ids"] = data_list
  321. return cfdi_data
  322. #Obtener intereses de pago
  323. def get_payment_tax(self, cfdi_type, xml_data, cfdi_data):
  324. if cfdi_type in ['P', 'SP']:
  325. payment_tax_list = []
  326. payments_list = xml_data['Comprobante']['Complemento']['pago20:Pagos']['pago20:Pago']
  327. payments_list = self.get_data_iterable(payments_list)
  328. if payments_list:
  329. for payment_list in payments_list:
  330. payment_date = payment_list["@FechaPago"][:10],
  331. payments = payment_list["pago20:DoctoRelacionado"]
  332. payments = self.get_data_iterable(payments)
  333. if payments:
  334. for payment in payments:
  335. if payment.get("@ObjetoImpDR") and payment.get("@ObjetoImpDR") == '02':
  336. payment_taxes = payment["pago20:ImpuestosDR"]["pago20:TrasladosDR"]["pago20:TrasladoDR"]
  337. payment_taxes = self.get_data_iterable(payment_taxes)
  338. if payment_taxes:
  339. for payment_tax in payment_taxes:
  340. payment_tax_data = {
  341. "name": payment["@IdDocumento"],
  342. "serie": payment.get("@Serie"),
  343. "folio": payment.get("@Folio"),
  344. "currency": payment["@MonedaDR"],
  345. "currency_rate": payment["@EquivalenciaDR"],
  346. "paid_amount": payment["@ImpPagado"],
  347. "previous_balance": payment["@ImpSaldoAnt"],
  348. "current_balance": payment["@ImpSaldoInsoluto"],
  349. "subject_tax": payment["@ObjetoImpDR"],
  350. "payment_date": payment_date[0],
  351. "tax_amount": payment_tax.get("@ImporteDR"),
  352. "base_amount": payment_tax["@BaseDR"],
  353. "type_tax": payment_tax["@ImpuestoDR"],
  354. "base_tax": float(payment_tax["@TasaOCuotaDR"]) * 100 if payment_tax.get("@TasaOCuotaDR") else 0,
  355. "exempt_tax": True if payment_tax.get("@TipoFactorDR") == 'Exento' else False,
  356. }
  357. payment_tax_list.append(Command.create(payment_tax_data))
  358. if payment_tax_list:
  359. cfdi_data["tax_paymnent_ids"] = payment_tax_list
  360. return cfdi_data
  361. def get_addenda_data(self, xml_data):
  362. try:
  363. addenda = xml_data["Comprobante"]["Addenda"]
  364. addenda_header = addenda["customized"]["NEW_ERA"]["Cabecera"]
  365. addenda_footer = addenda["customized"]["NEW_ERA"]["DatosPie"]
  366. return addenda_header["@DL_VBLEN"], addenda_footer["@SUMQTYEA"]
  367. except:
  368. return False, False
  369. #Obtener el producto del cfdi si existe
  370. def search_cfdi_product(self, line, cfdi_type, data_line, emitter_partner_id, recipient_partner_id):
  371. partner_id = recipient_partner_id if cfdi_type in ["I", "E"] else emitter_partner_id
  372. product_tmpl_id = self.env['product.template']
  373. concept_id = self.env['account.cfdi.line']
  374. account_line_id = self.env['account.move.line']
  375. # Buscar el producto para poder relacionarlo a la linea del concepto
  376. if '@NoIdentificacion' in line:
  377. # Si el comprobante es una factura de proveedor o una nota de cliente del proveedor
  378. if cfdi_type in ['SI', 'SE']:
  379. # Se busca si es que se cuenta con una lista de precios a proveedor que identifique el producto mediante el codigo del producto
  380. product_supplier_id = self.env['product.supplierinfo'].sudo().search([('partner_id', '=', partner_id.id), ('product_code', '=', line['@NoIdentificacion'])], limit=1)
  381. if product_supplier_id:
  382. product_tmpl_id = product_supplier_id.product_tmpl_id
  383. if not product_tmpl_id and cfdi_type in ["I", "E"]:
  384. product_tmpl_id = self.env['product.template'].sudo().search([('default_code', '=', line['@NoIdentificacion'])], limit=1)
  385. # Identificar si se tiene registro de algun producto que coincida exactamente con la descripción del concepto
  386. if not product_tmpl_id and '@Descripcion' in line:
  387. if cfdi_type in ['SI', 'SE']:
  388. product_supplier_id = self.env['product.supplierinfo'].sudo().search([('partner_id', '=', partner_id.id), ('product_name', '=', line['@Descripcion'])], limit=1)
  389. if product_supplier_id:
  390. product_tmpl_id = product_supplier_id.product_tmpl_id
  391. elif cfdi_type in ['I', 'E']:
  392. product_tmpl_id = self.env['product.template'].sudo().search(['|', ("name", "=", line['@Descripcion']), ('default_code', '=', line['@Descripcion'])], limit=1)
  393. # Se busca el producto si ya ha habido lineas de producto con el mismo emisor y misma clave de producto o descripcion
  394. if not product_tmpl_id and '@ClaveProdServ' in line and partner_id:
  395. concept_id = self.env['account.cfdi.line'].sudo().search([("product_tmpl_id", "!=", False), ("emitter_id.id", "=", partner_id.id if cfdi_type in ["SI", "SE"] else emitter_partner_id.id), ("product_code", "=", line['@ClaveProdServ']), ("company_id.id", "=", self.env.company.id)], limit=1)
  396. if concept_id:
  397. product_tmpl_id = concept_id.product_tmpl_id
  398. else:
  399. account_line_id = self.env["account.move.line"].sudo().search([("product_id", "!=", False), ("product_id.unspsc_code_id.code", "=", line['@ClaveProdServ']), ("partner_id.id", "=", partner_id.id)], limit=1)
  400. if account_line_id:
  401. product_tmpl_id = account_line_id.product_id.product_tmpl_id
  402. # Identificar si existen lineas de conceptos que coincidan con la misma descripción y del mismo emisor
  403. if not product_tmpl_id and '@Descripcion' in line and partner_id:
  404. concept_id = self.env['account.cfdi.line'].sudo().search([("product_tmpl_id", "!=", False), ("emitter_id.id", "=", partner_id.id if cfdi_type in ["SI", "SE"] else emitter_partner_id.id), ("description", "=", line['@Descripcion']), ("company_id.id", "=", self.env.company.id)], limit=1)
  405. if concept_id:
  406. product_tmpl_id = concept_id.product_tmpl_id
  407. if not product_tmpl_id and partner_id and partner_id.x_product_tmpl_id:
  408. product_tmpl_id = partner_id.x_product_tmpl_id
  409. if product_tmpl_id:
  410. product_id = self.env['product.product'].sudo().search([('product_tmpl_id', '=', product_tmpl_id.id)], limit=1)
  411. data_line["product_id"] = product_id.id if product_id else False
  412. data_line["product_tmpl_id"] = product_tmpl_id.id
  413. categ_id = product_tmpl_id.categ_id
  414. if cfdi_type in ["SI", "SE"]:
  415. account_id = product_tmpl_id.property_account_expense_id if product_tmpl_id.property_account_expense_id else categ_id.property_account_expense_categ_id if categ_id else False
  416. elif cfdi_type in ["I", "E"]:
  417. account_id = product_tmpl_id.property_account_income_id if product_tmpl_id.property_account_income_id else categ_id.property_account_income_categ_id if categ_id else False
  418. data_line["account_id"] = concept_id.account_id.id if concept_id and concept_id.account_id else account_line_id.account_id.id if account_line_id else account_id.id if account_id else False
  419. return data_line
  420. #Obtener lineas de impuestos
  421. def get_cfdi_tax_lines(self, data_line, line, cfdi_type, recipient_partner_id):
  422. tax_list = []
  423. if 'Impuestos' in line:
  424. j = 1
  425. if 'Traslados' in line['Impuestos']:
  426. if type(line['Impuestos']['Traslados']['Traslado']) is list:
  427. impuestos = line['Impuestos']['Traslados']['Traslado']
  428. elif type(line['Impuestos']['Traslados']['Traslado']) is OrderedDict:
  429. impuestos = line['Impuestos']['Traslados'].items()
  430. else:
  431. impuestos = [line['Impuestos']['Traslados']['Traslado']]
  432. for value_tax in impuestos:
  433. if type(line['Impuestos']['Traslados']['Traslado']) is list:
  434. impuesto = value_tax
  435. elif type(line['Impuestos']['Traslados']['Traslado']) is OrderedDict:
  436. impuesto = value_tax[1]
  437. else:
  438. impuesto = value_tax
  439. if str(impuesto['@TipoFactor']).strip().upper() == 'EXENTO':
  440. tasa_o_cuota = 0
  441. importe = 0
  442. else:
  443. tasa_o_cuota = float(impuesto['@TasaOCuota'])
  444. importe = float(impuesto['@Importe'])
  445. tax_data = {
  446. 'sequence': j,
  447. 'base': float(impuesto['@Base']),
  448. 'code': impuesto['@Impuesto'],
  449. 'factor_type': impuesto['@TipoFactor'],
  450. 'rate': tasa_o_cuota,
  451. 'amount': importe,
  452. 'tax_type': 'traslado'
  453. }
  454. j = j + 1
  455. amount = tasa_o_cuota * 100
  456. tax_domain = [('amount', '=', amount), ('company_id', '=', self.env.company.id)]
  457. t_id = self.env["account.tax"]
  458. if tax_data.get("impuesto") == '002':
  459. tax_domain.append(("name", "ilike", "iva"))
  460. elif tax_data.get("impuesto") == '003':
  461. tax_domain.append(("name", "ilike", "ieps"))
  462. elif tax_data.get("impuesto") == '001':
  463. tax_domain.append(("name", "ilike", "isr"))
  464. if cfdi_type in ['I', 'E']:
  465. tax_domain.append(('type_tax_use', '=', 'sale'))
  466. t_id = self.env['account.tax'].sudo().search(tax_domain, limit=1)
  467. elif cfdi_type in ['SI', 'SE']:
  468. tax_domain.append(('type_tax_use', '=', 'purchase'))
  469. t_id = self.env['account.tax'].sudo().search(tax_domain, limit=1)
  470. if t_id:
  471. tax_data["tax_id"] = t_id.id
  472. tax_list.append(Command.create(tax_data))
  473. if 'Retenciones' in line['Impuestos']:
  474. if type(line['Impuestos']['Retenciones']['Retencion']) is list:
  475. retenciones = line['Impuestos']['Retenciones']['Retencion']
  476. elif type(line['Impuestos']['Retenciones']['Retencion']) is OrderedDict:
  477. retenciones = line['Impuestos']['Retenciones'].items()
  478. else:
  479. retenciones = [line['Impuestos']['Retenciones']['Retencion']]
  480. for value_tax in retenciones:
  481. if type(line['Impuestos']['Retenciones']['Retencion']) is list:
  482. retencion = value_tax
  483. elif type(line['Impuestos']['Retenciones']['Retencion']) is OrderedDict:
  484. retencion = value_tax[1]
  485. else:
  486. retencion = value_tax
  487. tasa_o_cuota = float(retencion['@TasaOCuota'])
  488. importe = float(retencion['@Importe'])
  489. tax_data = {
  490. 'sequence': j,
  491. 'base': float(retencion['@Base']),
  492. 'code': retencion['@Impuesto'],
  493. 'factor_type': retencion['@TipoFactor'],
  494. 'rate': tasa_o_cuota,
  495. 'amount': importe,
  496. 'tax_type': 'retencion'
  497. }
  498. j = j + 1
  499. amount = round(-(tasa_o_cuota * 100), 2)
  500. tax_domain = [('amount', '=', amount), ('company_id', '=', self.env.company.id)]
  501. t_id = self.env['account.tax']
  502. if tax_data.get("impuesto") == '002':
  503. tax_domain.append(("name", "ilike", "iva"))
  504. elif tax_data.get("impuesto") == '003':
  505. tax_domain.append(("name", "ilike", "ieps"))
  506. elif tax_data.get("impuesto") == '001':
  507. tax_domain.append(("name", "ilike", "isr"))
  508. if cfdi_type in ['I', 'E']:
  509. tax_domain.append(('type_tax_use', '=', 'sale'))
  510. t_id = self.env['account.tax'].sudo().search(tax_domain, limit=1)
  511. elif cfdi_type in ['SI', 'SE']:
  512. tax_domain.append(('type_tax_use', '=', 'purchase'))
  513. t_id = self.env['account.tax'].sudo().search(tax_domain, limit=1)
  514. if t_id:
  515. tax_data["tax_id"] = t_id.id
  516. if not t_id and cfdi_type in ['SI', 'SE']:
  517. if tax_data.get("impuesto") == '001':
  518. if recipient_partner_id.tax_isr_id:
  519. tax_data["tax_id"] = recipient_partner_id.tax_isr_id.id
  520. elif tax_data.get("impuesto") == '002':
  521. if recipient_partner_id.tax_iva_id:
  522. tax_data["tax_id"] = recipient_partner_id.tax_iva_id.id
  523. tax_list.append(Command.create(tax_data))
  524. if tax_list:
  525. data_line["tax_ids"] = tax_list
  526. return data_line
  527. # Obteniendo el diario contable dependiendo del tipo de comprobante
  528. def get_cfdi_journal_id(self, cfdi_type, emitter_partner_id, recipient_partner_id):
  529. journal_id = self.env['account.journal'].sudo().search([('x_cfdi_type', '=', cfdi_type), ("company_id.id", "=", self.env.company.id)], limit=1)
  530. # Buscamos si ya hubo un CFDI al mismo cliente y receptor y del mismo tipo y que tenga un diario colocado
  531. if not journal_id:
  532. cfdi_id = self.env["account.cfdi"].sudo().search(
  533. [("emitter_id.id", "=", emitter_partner_id.id),
  534. ("receiver_id.id", "=", recipient_partner_id.id), ("cfdi_type", "=", cfdi_type),
  535. ("journal_id", "!=", False)], limit=1)
  536. journal_id = cfdi_id.journal_id if cfdi_id else False
  537. account_id = journal_id.default_account_id if journal_id and journal_id.default_account_id else False
  538. if not account_id:
  539. partner_id = recipient_partner_id if cfdi_type in ["I", "E"] else emitter_partner_id
  540. account_id = self.get_expense_cfdi_account_id(cfdi_type, partner_id)
  541. return journal_id, account_id
  542. # Obtener la cuenta de gastos
  543. def get_expense_cfdi_account_id(self, cfdi_type, partner_id):
  544. if partner_id:
  545. expense_account_id = partner_id.x_account_expense_id
  546. # Buscamos un CFDI parecido para obtener la cuenta por pagar
  547. if not expense_account_id and cfdi_type in ['I', 'E']:
  548. cfdi_id = self.env["account.cfdi"].sudo().search(
  549. [("receiver_id.id", "=", partner_id.id), ("cfdi_type", "=", cfdi_type),
  550. ("account_id", "!=", False)], limit=1)
  551. expense_account_id = cfdi_id.payable_account_id if cfdi_id else False
  552. elif not expense_account_id and cfdi_type in ['SI', 'SE']:
  553. cfdi_id = self.env["account.cfdi"].sudo().search(
  554. [("emitter_id.id", "=", partner_id.id), ("cfdi_type", "=", cfdi_type),
  555. ("account_id", "!=", False)], limit=1)
  556. expense_account_id = cfdi_id.payable_account_id if cfdi_id else False
  557. return expense_account_id
  558. # Obtener cuenta por pagar dependiendo del tipo de comprobante
  559. def get_payable_cfdi_account_id(self, cfdi_type, partner_id):
  560. # Se busca si se tiene configurado la cuenta por pagar en la cuenta sino se busca por el contacto y por ultimo se busca un cfdi con el mismo contacto y tipo
  561. payable_account_id = self.env['account.account'].sudo().search([('x_cfdi_type', '=', cfdi_type), ("company_ids.id", "=", self.env.company.id)], limit=1)
  562. if not payable_account_id and partner_id:
  563. payable_account_id = partner_id.property_account_payable_id
  564. # Buscamos un CFDI parecido para obtener la cuenta por pagar
  565. if not payable_account_id and cfdi_type in ['I', 'E']:
  566. cfdi_id = self.env["account.cfdi"].sudo().search(
  567. [("receiver_id.id", "=", partner_id.id), ("cfdi_type", "=", cfdi_type),
  568. ("payable_account_id", "!=", False)], limit=1)
  569. payable_account_id = cfdi_id.payable_account_id if cfdi_id else False
  570. elif not payable_account_id and cfdi_type in ['SI', 'SE']:
  571. cfdi_id = self.env["account.cfdi"].sudo().search(
  572. [("emitter_id.id", "=", partner_id.id), ("cfdi_type", "=", cfdi_type),
  573. ("payable_account_id", "!=", False)], limit=1)
  574. payable_account_id = cfdi_id.payable_account_id if cfdi_id else False
  575. return payable_account_id
  576. # Obtener el emisor y receptor del cfdi
  577. def get_cfdi_partners(self, xml_data):
  578. emitter_id = self.env['res.partner'].sudo().search([('vat', '=', xml_data['Comprobante']['Emisor']['@Rfc'])], limit=1)
  579. receiver_id = self.env['res.partner'].sudo().search([('vat', '=', xml_data['Comprobante']['Receptor']['@Rfc'])], limit=1)
  580. if not emitter_id:
  581. emitter_id = self.create_cfdi_partner(xml_data, "Emisor")
  582. if not receiver_id:
  583. receiver_id = self.create_cfdi_partner(xml_data, "Receptor")
  584. return emitter_id, receiver_id
  585. # Crear los contactos del CFDI si es necesario
  586. def create_cfdi_partner(self, xml_data, partner_type):
  587. data = {
  588. 'name': xml_data['Comprobante'][partner_type]['@Nombre'],
  589. 'vat': xml_data['Comprobante'][partner_type]['@Rfc'],
  590. 'l10n_mx_edi_fiscal_regime': xml_data['Comprobante'][partner_type][
  591. '@RegimenFiscal'] if partner_type == "Emisor" else xml_data['Comprobante'][partner_type][
  592. '@RegimenFiscalReceptor'],
  593. 'country_id': self.env.company.country_id.id,
  594. 'company_type': 'company'
  595. }
  596. partner_id = self.env["res.partner"].create(data)
  597. return partner_id
  598. # --------------------------------------------------------------------------------------------------------------------
  599. # ---------------------------------------------------Metodos de descarga masiva---------------------------------------------------------
  600. def download_massive_pdf_zip(self):
  601. return self.download_massive_zip("PDF Masivos.zip", "pdf_id")
  602. def download_massive_xml_zip(self):
  603. return self.download_massive_zip("XML Masivos.zip", "attachment_id")
  604. def download_massive_zip(self, filename, field_name):
  605. self = self.with_user(1)
  606. zip_file = self.env['ir.attachment'].sudo().search([('name', '=', filename)], limit=1)
  607. if zip_file:
  608. zip_file.sudo().unlink()
  609. # Funcion para decodificar el archivo
  610. def isBase64_decodestring(s):
  611. try:
  612. decode_archive = base64.decodebytes(s)
  613. return decode_archive
  614. except Exception as e:
  615. raise ValidationError('Error:', + str(e))
  616. tempdir_file = TemporaryDirectory()
  617. location_tempdir = tempdir_file.name
  618. # Creando ruta dinamica para poder guardar el archivo zip
  619. date_act = date.today()
  620. file_name = 'DescargaMasiva(Fecha de descarga' + " - " + str(date_act) + ")"
  621. file_name_zip = file_name + ".zip"
  622. zipfilepath = os.path.join(location_tempdir, file_name_zip)
  623. path_files = os.path.join(location_tempdir)
  624. # Creando zip
  625. for file in self.mapped(field_name):
  626. object_name = file.name
  627. ruta_ob = object_name
  628. object_handle = open(os.path.join(location_tempdir, ruta_ob), "wb")
  629. object_handle.write(isBase64_decodestring(file.datas))
  630. object_handle.close()
  631. with ZipFile(zipfilepath, 'w') as zip_obj:
  632. for file in os.listdir(path_files):
  633. file_path = os.path.join(path_files, file)
  634. if file_path != zipfilepath:
  635. zip_obj.write(file_path, basename(file_path))
  636. with open(zipfilepath, 'rb') as file_data:
  637. bytes_content = file_data.read()
  638. encoded = base64.b64encode(bytes_content)
  639. data = {
  640. 'name': filename,
  641. 'type': 'binary',
  642. 'datas': encoded,
  643. 'company_id': self.env.company.id
  644. }
  645. attachment = self.env['ir.attachment'].sudo().create(data)
  646. return self.download_zip(file_name_zip, attachment.id)
  647. def download_zip(self, filename, id_file):
  648. path = "/web/binary/download_document?"
  649. model = "ir.attachment"
  650. url = path + "model={}&id={}&filename={}".format(model, id_file, filename)
  651. return {
  652. 'type': 'ir.actions.act_url',
  653. 'url': url,
  654. 'target': 'self',
  655. }
  656. # ---------------------------------------------------------------------------------------------------------------------
  657. # ---------------------------------------------------Metodos de conciliación---------------------------------------------------------
  658. def action_done(self):
  659. self = self.with_user(1)
  660. invoice_list = []
  661. folio_names = []
  662. for rec in self.filtered(lambda cfdi: not cfdi.move_id and cfdi.attachment_id and cfdi.cfdi_type in ["SI", "I","SE"]):
  663. cfdi_type = rec.cfdi_type
  664. partner_id = rec.emitter_id if cfdi_type in ["SI",'SE','SP'] else rec.receiver_id
  665. folio = f"{rec.serie}-{rec.folio}" if rec.serie and rec.folio else f"{rec.serie}" if rec.serie and not rec.folio else f"{rec.folio}" if not rec.serie and rec.folio else ''
  666. move_type = {
  667. "SI": "in_invoice",
  668. "I": "out_invoice",
  669. "SE": "in_refund",
  670. "E": "out_refund",
  671. }
  672. invoice_data = {
  673. 'move_type': move_type.get(cfdi_type),
  674. 'partner_id': partner_id.id,
  675. 'date': rec.date,
  676. 'invoice_date': rec.date,
  677. 'invoice_date_due': rec.date,
  678. 'fiscal_position_id': partner_id.property_account_position_id.id if partner_id.property_account_position_id else rec.fiscal_position_id.id,
  679. 'ref': folio,
  680. 'amount_total_signed': rec.total,
  681. 'amount_total': rec.total,
  682. 'journal_id': rec.journal_id.id,
  683. 'company_id': rec.company_id.id,
  684. 'cfdi_id': rec.id,
  685. 'x_uuid': rec.uuid,
  686. "x_delivery_number": rec.delivery_number,
  687. "x_invoice_qty": rec.invoice_qty
  688. }
  689. if folio != '' and not self.env["account.move"].sudo().search([("name", "=", folio), ("state", "=", "posted")], limit=1) and folio not in folio_names and cfdi_type == "I":
  690. invoice_data["name"] = folio
  691. folio_names.append(folio)
  692. i = 1
  693. line_list = []
  694. for line in rec.concept_ids:
  695. if float(line.amount) > 0:
  696. data_line = {
  697. 'sequence': i,
  698. 'name': line.description,
  699. 'quantity': float(line.quantity),
  700. 'product_uom_id': line.uom_id.id,
  701. 'discount': (float(line.discount) * 100) / float(line.amount),
  702. 'price_unit': float(line.unit_price),
  703. 'tax_ids': line.mapped("tax_ids.tax_id").ids,
  704. 'account_id': line.account_id.id if line.account_id else rec.account_id.id if rec.account_id else False,
  705. 'analytic_distribution': line.analytic_distribution if line.analytic_distribution else rec.analytic_distribution,
  706. 'partner_id': partner_id.id,
  707. }
  708. if line.product_tmpl_id:
  709. product_id = self.env['product.product'].sudo().search([('product_tmpl_id', '=', line.product_tmpl_id.id)], limit=1)
  710. data_line["product_id"] = product_id.id
  711. line_list.append(Command.create(data_line))
  712. i = i + 1
  713. invoice_data["invoice_line_ids"] = line_list
  714. invoice_list.append(invoice_data)
  715. invoice_ids = self.env['account.move'].with_context(check_move_validity=False).sudo().create(invoice_list)
  716. for invoice_id in invoice_ids:
  717. attachment_id = invoice_id.cfdi_id.attachment_id
  718. invoice_id.cfdi_id.write({
  719. "move_id": invoice_id.id,
  720. "state": "done"
  721. })
  722. attachment_id.write({
  723. 'res_model': 'account.move',
  724. 'res_id': invoice_id.id,
  725. })
  726. if invoice_id.move_type == "out_invoice":
  727. if attachment_id:
  728. id_edi_format = self.env['account.edi.format'].sudo().search([('name', '=', 'CFDI (4.0)')], limit=1)
  729. if not invoice_id.edi_document_ids:
  730. if id_edi_format:
  731. create_edi = self.env['account.edi.document'].sudo().create(
  732. {'edi_format_id': id_edi_format.id, 'attachment_id': attachment_id.id, 'state': 'sent',
  733. 'move_id': invoice_id.id, 'error': False})
  734. invoice_id.write({'edi_document_ids': [(6, False, [create_edi.id])], 'l10n_mx_edi_cfdi_uuid': invoice_id.l10n_mx_edi_cfdi_uuid_cusom})
  735. # Se colocaria la factura como timbrada
  736. elif id_edi_format and invoice_id.edi_document_ids:
  737. edi_format_id = invoice_id.edi_document_ids.filtered(
  738. lambda edi: edi.edi_format_id.id == id_edi_format.id)
  739. if edi_format_id:
  740. edi_format_id["attachment_id"] = attachment_id.id
  741. edi_format_id["error"] = False
  742. edi_format_id["state"] = "sent"
  743. else:
  744. data = {
  745. 'move_id': invoice_id.id,
  746. 'attachment_id': attachment_id.id,
  747. 'state': 'sent',
  748. 'error': False,
  749. 'edi_format_id': id_edi_format.id
  750. }
  751. self.env["account.edi.document"].sudo().create([data])
  752. invoice_id.write({'l10n_mx_edi_cfdi_uuid': invoice_id.l10n_mx_edi_cfdi_uuid_cusom})
  753. invoice_id.write({
  754. "state": "posted"
  755. })
  756. return {
  757. "name": _("Facturas"),
  758. "view_mode": "list,form",
  759. "res_model": "account.move",
  760. "type": "ir.actions.act_window",
  761. "target": "current",
  762. "domain": [('id', 'in', invoice_ids.ids)]
  763. }
  764. def action_unlink_move(self):
  765. for rec in self:
  766. if rec.move_id:
  767. rec.attachment_id.write({
  768. 'res_model': 'account.cfdi',
  769. 'res_id': rec.id,
  770. })
  771. rec.write({
  772. "state": "draft",
  773. "move_id": False
  774. })
  775. rec.move_id.write({
  776. 'cfdi_id': False,
  777. 'x_uuid': False
  778. })