From 4ba2ba7ff563c21c4c8d6b1fb2954c27fada32cb Mon Sep 17 00:00:00 2001 From: Maxime Chambreuil Date: Fri, 12 Apr 2024 12:34:24 -0600 Subject: [PATCH] [DEL] l10n_mx_cfdi --- l10n_mx_cfdi/__init__.py | 3 - l10n_mx_cfdi/__manifest__.py | 57 -- l10n_mx_cfdi/controllers/__init__.py | 1 - l10n_mx_cfdi/controllers/controllers.py | 20 - l10n_mx_cfdi/data/cfdi_publico_en_general.xml | 15 - l10n_mx_cfdi/data/paper_format.xml | 9 - l10n_mx_cfdi/demo/demo.xml | 30 - l10n_mx_cfdi/models/__init__.py | 20 - l10n_mx_cfdi/models/account_journal.py | 13 - l10n_mx_cfdi/models/account_move.py | 877 ------------------ l10n_mx_cfdi/models/account_move_reversal.py | 35 - .../models/account_partial_reconcile.py | 70 -- l10n_mx_cfdi/models/account_payment.py | 204 ---- .../models/account_payment_register.py | 38 - l10n_mx_cfdi/models/account_tax.py | 21 - l10n_mx_cfdi/models/cfdi_document.py | 526 ----------- l10n_mx_cfdi/models/cfdi_issuer.py | 141 --- l10n_mx_cfdi/models/cfdi_series.py | 17 - l10n_mx_cfdi/models/cfdi_service.py | 193 ---- l10n_mx_cfdi/models/cfdi_service_topup.py | 48 - l10n_mx_cfdi/models/mail_template.py | 47 - l10n_mx_cfdi/models/partner.py | 19 - l10n_mx_cfdi/models/product.py | 16 - l10n_mx_cfdi/models/res_company.py | 21 - l10n_mx_cfdi/models/res_config_settings.py | 12 - l10n_mx_cfdi/readme.md | 7 - l10n_mx_cfdi/reports/report_cfdi_blocks.xml | 274 ------ .../reports/report_external_layouts.xml | 437 --------- l10n_mx_cfdi/reports/report_invoice.xml | 149 --- l10n_mx_cfdi/reports/report_payment.xml | 144 --- l10n_mx_cfdi/security/ir.model.access.csv | 14 - .../security/l10n_mx_cfdi_security.xml | 56 -- l10n_mx_cfdi/views/cfdi_document_view.xml | 161 ---- .../views/cfdi_documents_issued_view.xml | 17 - l10n_mx_cfdi/views/cfdi_issuer_view.xml | 113 --- l10n_mx_cfdi/views/cfdi_menu.xml | 8 - l10n_mx_cfdi/views/cfdi_series_view.xml | 50 - l10n_mx_cfdi/views/cfdi_service_view.xml | 55 -- .../account_view_account_payment_form.xml | 47 - ...unt_view_account_payment_register_form.xml | 12 - .../views/inherit/account_view_move_form.xml | 145 --- .../inherit/account_view_out_invoice_tree.xml | 37 - .../views/inherit/base_view_partner_form.xml | 18 - .../product_product_template_form_view.xml | 17 - .../inherit/res_config_settings_views.xml | 31 - l10n_mx_cfdi/wizard/__init__.py | 3 - .../wizard/account_invoice_send_views.xml | 15 - .../wizard/create_cfdi_publico_en_general.py | 168 ---- .../wizard/create_cfdi_publico_en_general.xml | 64 -- l10n_mx_cfdi/wizard/document_cancel.py | 75 -- l10n_mx_cfdi/wizard/document_cancel_form.xml | 44 - l10n_mx_cfdi/wizard/download_cfdi_files.py | 81 -- .../wizard/download_cfdi_files_wizard.xml | 55 -- setup/l10n_mx_cfdi/odoo/addons/l10n_mx_cfdi | 1 - setup/l10n_mx_cfdi/setup.py | 6 - 55 files changed, 4757 deletions(-) delete mode 100644 l10n_mx_cfdi/__init__.py delete mode 100644 l10n_mx_cfdi/__manifest__.py delete mode 100644 l10n_mx_cfdi/controllers/__init__.py delete mode 100644 l10n_mx_cfdi/controllers/controllers.py delete mode 100644 l10n_mx_cfdi/data/cfdi_publico_en_general.xml delete mode 100644 l10n_mx_cfdi/data/paper_format.xml delete mode 100644 l10n_mx_cfdi/demo/demo.xml delete mode 100644 l10n_mx_cfdi/models/__init__.py delete mode 100644 l10n_mx_cfdi/models/account_journal.py delete mode 100644 l10n_mx_cfdi/models/account_move.py delete mode 100644 l10n_mx_cfdi/models/account_move_reversal.py delete mode 100644 l10n_mx_cfdi/models/account_partial_reconcile.py delete mode 100644 l10n_mx_cfdi/models/account_payment.py delete mode 100644 l10n_mx_cfdi/models/account_payment_register.py delete mode 100644 l10n_mx_cfdi/models/account_tax.py delete mode 100644 l10n_mx_cfdi/models/cfdi_document.py delete mode 100644 l10n_mx_cfdi/models/cfdi_issuer.py delete mode 100644 l10n_mx_cfdi/models/cfdi_series.py delete mode 100644 l10n_mx_cfdi/models/cfdi_service.py delete mode 100644 l10n_mx_cfdi/models/cfdi_service_topup.py delete mode 100644 l10n_mx_cfdi/models/mail_template.py delete mode 100644 l10n_mx_cfdi/models/partner.py delete mode 100644 l10n_mx_cfdi/models/product.py delete mode 100644 l10n_mx_cfdi/models/res_company.py delete mode 100644 l10n_mx_cfdi/models/res_config_settings.py delete mode 100644 l10n_mx_cfdi/readme.md delete mode 100644 l10n_mx_cfdi/reports/report_cfdi_blocks.xml delete mode 100644 l10n_mx_cfdi/reports/report_external_layouts.xml delete mode 100644 l10n_mx_cfdi/reports/report_invoice.xml delete mode 100644 l10n_mx_cfdi/reports/report_payment.xml delete mode 100644 l10n_mx_cfdi/security/ir.model.access.csv delete mode 100644 l10n_mx_cfdi/security/l10n_mx_cfdi_security.xml delete mode 100644 l10n_mx_cfdi/views/cfdi_document_view.xml delete mode 100644 l10n_mx_cfdi/views/cfdi_documents_issued_view.xml delete mode 100644 l10n_mx_cfdi/views/cfdi_issuer_view.xml delete mode 100644 l10n_mx_cfdi/views/cfdi_menu.xml delete mode 100644 l10n_mx_cfdi/views/cfdi_series_view.xml delete mode 100644 l10n_mx_cfdi/views/cfdi_service_view.xml delete mode 100644 l10n_mx_cfdi/views/inherit/account_view_account_payment_form.xml delete mode 100644 l10n_mx_cfdi/views/inherit/account_view_account_payment_register_form.xml delete mode 100644 l10n_mx_cfdi/views/inherit/account_view_move_form.xml delete mode 100644 l10n_mx_cfdi/views/inherit/account_view_out_invoice_tree.xml delete mode 100644 l10n_mx_cfdi/views/inherit/base_view_partner_form.xml delete mode 100644 l10n_mx_cfdi/views/inherit/product_product_template_form_view.xml delete mode 100644 l10n_mx_cfdi/views/inherit/res_config_settings_views.xml delete mode 100644 l10n_mx_cfdi/wizard/__init__.py delete mode 100644 l10n_mx_cfdi/wizard/account_invoice_send_views.xml delete mode 100644 l10n_mx_cfdi/wizard/create_cfdi_publico_en_general.py delete mode 100644 l10n_mx_cfdi/wizard/create_cfdi_publico_en_general.xml delete mode 100644 l10n_mx_cfdi/wizard/document_cancel.py delete mode 100644 l10n_mx_cfdi/wizard/document_cancel_form.xml delete mode 100644 l10n_mx_cfdi/wizard/download_cfdi_files.py delete mode 100644 l10n_mx_cfdi/wizard/download_cfdi_files_wizard.xml delete mode 120000 setup/l10n_mx_cfdi/odoo/addons/l10n_mx_cfdi delete mode 100644 setup/l10n_mx_cfdi/setup.py diff --git a/l10n_mx_cfdi/__init__.py b/l10n_mx_cfdi/__init__.py deleted file mode 100644 index e4f4917..0000000 --- a/l10n_mx_cfdi/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from . import controllers -from . import models -from . import wizard diff --git a/l10n_mx_cfdi/__manifest__.py b/l10n_mx_cfdi/__manifest__.py deleted file mode 100644 index 99cf55b..0000000 --- a/l10n_mx_cfdi/__manifest__.py +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "CFDI Mexico - Facturación", - "summary": """ - Allow generating CFDI (Comprobante Fiscal Digital por Internet) for Mexico.""", - "description": """ - Allow generating CFDI (Comprobante Fiscal Digital por Internet) for Mexico. - """, - "author": "Alexis López Zubieta (Auge TEC)", - "website": "https://github.com/OCA/l10n-mexico", - "license": "LGPL-3", - # Categories can be used to filter modules in modules listing - # Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml - # for the full list - "category": "Accounting", - "version": "15.0.1.0.4", - # any module necessary for this one to work correctly - "depends": ["base", "account", "l10n_mx", "l10n_mx_catalogs"], - # always loaded - "data": [ - # security rules - "security/ir.model.access.csv", - "security/l10n_mx_cfdi_security.xml", - # presets - "data/cfdi_publico_en_general.xml", # references l10n_mx_cfdi.regimen_fiscal therefore must be loaded after it - "data/paper_format.xml", # references l10n_mx_cfdi.regimen_fiscal therefore must be loaded after it - # inherited views - "views/inherit/account_view_move_form.xml", - "views/inherit/account_view_out_invoice_tree.xml", - "views/inherit/base_view_partner_form.xml", - "views/inherit/product_product_template_form_view.xml", - "views/inherit/account_view_account_payment_register_form.xml", - "views/inherit/account_view_account_payment_form.xml", - "views/inherit/res_config_settings_views.xml", - # new views - "views/cfdi_menu.xml", - "views/cfdi_series_view.xml", - "views/cfdi_issuer_view.xml", - "views/cfdi_service_view.xml", - "views/cfdi_document_view.xml", - "views/cfdi_documents_issued_view.xml", - "wizard/document_cancel_form.xml", - "wizard/create_cfdi_publico_en_general.xml", - "wizard/account_invoice_send_views.xml", - "wizard/download_cfdi_files_wizard.xml", - "reports/report_cfdi_blocks.xml", - "reports/report_external_layouts.xml", - "reports/report_invoice.xml", - "reports/report_payment.xml", - ], - # only loaded in demonstration mode - "demo": [ - "demo/demo.xml", - ], - "external_dependencies": { - "python": ["facturama"], - }, -} diff --git a/l10n_mx_cfdi/controllers/__init__.py b/l10n_mx_cfdi/controllers/__init__.py deleted file mode 100644 index e046e49..0000000 --- a/l10n_mx_cfdi/controllers/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import controllers diff --git a/l10n_mx_cfdi/controllers/controllers.py b/l10n_mx_cfdi/controllers/controllers.py deleted file mode 100644 index 516060b..0000000 --- a/l10n_mx_cfdi/controllers/controllers.py +++ /dev/null @@ -1,20 +0,0 @@ -# from odoo import http - - -# class InvoiceMxCfdi(http.Controller): -# @http.route('/l10n_mx_cfdi/l10n_mx_cfdi', auth='public') -# def index(self, **kw): -# return "Hello, world" - -# @http.route('/l10n_mx_cfdi/l10n_mx_cfdi/objects', auth='public') -# def list(self, **kw): -# return http.request.render('l10n_mx_cfdi.listing', { -# 'root': '/l10n_mx_cfdi/l10n_mx_cfdi', -# 'objects': http.request.env['l10n_mx_cfdi.l10n_mx_cfdi'].search([]), -# }) - -# @http.route('/l10n_mx_cfdi/l10n_mx_cfdi/objects/', auth='public') -# def object(self, obj, **kw): -# return http.request.render('l10n_mx_cfdi.object', { -# 'object': obj -# }) diff --git a/l10n_mx_cfdi/data/cfdi_publico_en_general.xml b/l10n_mx_cfdi/data/cfdi_publico_en_general.xml deleted file mode 100644 index ba36d32..0000000 --- a/l10n_mx_cfdi/data/cfdi_publico_en_general.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - PUBLICO EN GENERAL - - XAXX010101000 - - - - - - - diff --git a/l10n_mx_cfdi/data/paper_format.xml b/l10n_mx_cfdi/data/paper_format.xml deleted file mode 100644 index b2eb96c..0000000 --- a/l10n_mx_cfdi/data/paper_format.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - 60 - 55 - - - diff --git a/l10n_mx_cfdi/demo/demo.xml b/l10n_mx_cfdi/demo/demo.xml deleted file mode 100644 index e2db613..0000000 --- a/l10n_mx_cfdi/demo/demo.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - diff --git a/l10n_mx_cfdi/models/__init__.py b/l10n_mx_cfdi/models/__init__.py deleted file mode 100644 index ca6f4f5..0000000 --- a/l10n_mx_cfdi/models/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# model extensions -from . import partner -from . import product -from . import account_move -from . import account_tax -from . import account_journal -from . import account_partial_reconcile -from . import account_move_reversal -from . import account_payment -from . import mail_template -from . import res_company -from . import res_config_settings - -# new models -from . import account_payment_register -from . import cfdi_document -from . import cfdi_issuer -from . import cfdi_series -from . import cfdi_service -from . import cfdi_service_topup diff --git a/l10n_mx_cfdi/models/account_journal.py b/l10n_mx_cfdi/models/account_journal.py deleted file mode 100644 index 303e7d7..0000000 --- a/l10n_mx_cfdi/models/account_journal.py +++ /dev/null @@ -1,13 +0,0 @@ -from odoo import fields, models - - -class AccountJournal(models.Model): - _inherit = "account.journal" - - cfdi_cert_ids = fields.Many2many( - comodel_name="l10n_mx_cfdi.document", - string="CFDI", - help="Send CFDI invoices", - readonly=False, - store=True, - ) diff --git a/l10n_mx_cfdi/models/account_move.py b/l10n_mx_cfdi/models/account_move.py deleted file mode 100644 index f82605e..0000000 --- a/l10n_mx_cfdi/models/account_move.py +++ /dev/null @@ -1,877 +0,0 @@ -import base64 -from datetime import datetime, timedelta - -from lxml import etree - -from odoo import _, api, fields, models -from odoo.exceptions import UserError, ValidationError -from odoo.tools import float_is_zero, float_round, json_float_round - - -class AccountMove(models.Model): - """ - Integration with the Mexican CFDI 4.0 system for electronic invoices - """ - - _inherit = "account.move" - - cfdi_document_id = fields.Many2one( - "l10n_mx_cfdi.document", - string="CFDI", - readonly=True, - copy=False, - compute="_compute_cfdi_document_id", - store=True, - ) - - cfdi_document_name = fields.Char( - string="Folio CFDI", readonly=True, related="cfdi_document_id.name", store=True - ) - cfdi_document_state = fields.Selection( - string="CFDI Status", readonly=True, related="cfdi_document_id.state" - ) - - related_cert_ids = fields.Many2many( - "l10n_mx_cfdi.document", string="Documentos", readonly=True, copy=False - ) - - # Invoice CFDI required fields - cfdi_required = fields.Boolean(string="Requiere CFDI", default=False) - - issuer_id = fields.Many2one( - "l10n_mx_cfdi.issuer", string="Emisor", domain=[("registered", "=", True)] - ) - receiver_id = fields.Many2one("res.partner", string="Receptor", readonly=True) - - cfdi_use_id = fields.Many2one("l10n_mx_catalogs.c_uso_cfdi", string="Uso de CFDI") - payment_method_id = fields.Many2one( - "l10n_mx_catalogs.c_metodo_pago", string="Método de pago" - ) - payment_form_id = fields.Many2one( - "l10n_mx_catalogs.c_forma_pago", string="Forma de pago" - ) - - cfdi_posted = fields.Boolean( - string="Requiere CFDI", compute="_compute_cfdi_posted", store=True - ) - cfdi_data_in_attachments = fields.Boolean( - string="CFDI data in attachments", compute="_compute_cfdi_data_in_attachments" - ) - - l10n_mx_cfdi_auto = fields.Boolean( - string="CFDI Automatico", related="company_id.l10n_mx_cfdi_auto", readonly=True - ) - l10n_mx_cfdi_enabled = fields.Boolean( - string="CFDI Habilitado", - related="company_id.l10n_mx_cfdi_enabled", - readonly=True, - ) - - @api.depends("cfdi_document_id") - def _compute_cfdi_data_in_attachments(self): - # remove 'bin_size' from the context to allow data to be read - self = self.with_context(bin_size=False) - for move in self: - move.cfdi_data_in_attachments = False - - # get xml attachments - xml_attachments = self.env["ir.attachment"].search( - [ - ("res_model", "=", "account.move"), - ("res_id", "=", self.id), - ("mimetype", "=", "application/xml"), - ] - ) - - for attachment in xml_attachments: - xml = base64.b64decode(attachment.datas) - if b"cfdi:Comprobante" in xml: - move.cfdi_data_in_attachments = True - - @api.model - def default_get(self, field_names): - defaults_dict = super().default_get(field_names) - defaults_dict["receiver_id"] = defaults_dict.get("partner_id") - if self.partner_id and not self.receiver_id: - self.receiver_id = self.partner_id - self.cfdi_use_id = self.partner_id.cfdi_use_id - self.payment_method_id = self.partner_id.payment_method_id - self.payment_form_id = self.partner_id.payment_form_id - - # set issuer if there is only one choice - issuers = self.env["l10n_mx_cfdi.issuer"].search([("registered", "=", True)]) - if len(issuers) == 1: - defaults_dict["issuer_id"] = issuers[0].id - - return defaults_dict - - @api.onchange("partner_id") - def _update_receiver(self): - """ - Update the receiver_id field - """ - for move in self: - move.receiver_id = move.partner_id - move._update_cfdi_data() - - @api.onchange("receiver_id") - def _update_cfdi_data(self): - """ - Update the CFDI data when the receiver_id changes - """ - for move in self: - if move.receiver_id: - move.cfdi_use_id = move.receiver_id.cfdi_use_id - move.payment_method_id = move.receiver_id.payment_method_id - move.payment_form_id = move.receiver_id.payment_form_id - - @api.depends("related_cert_ids") - def _compute_cfdi_document_id(self): - for move in self: - # remove current reference - move.cfdi_document_id = False - - # get the last CFDI - if move.move_type in ("in_invoice", "out_invoice"): - move.cfdi_document_id = move.related_cert_ids.filtered( - lambda x: x.type == "I" and x.state == "published" - ) - - if move.move_type == "out_refund": - move.cfdi_document_id = move.related_cert_ids.filtered( - lambda x: x.type == "E" and x.state == "published" - ) - - if move.move_type == "in_payment": - move.cfdi_document_id = move.related_cert_ids.filtered( - lambda x: x.type == "P" and x.state == "published" - ) - - @api.depends("related_cert_ids") - def _compute_cfdi_posted(self): - for move in self: - if move.cfdi_document_id and move.cfdi_document_id.state == "published": - move.cfdi_posted = True - else: - move.cfdi_posted = False - - def action_post(self): - """ - Override the action_post method to create the CFDI - """ - - res = super(AccountMove, self).action_post() - - if self.l10n_mx_cfdi_auto: - # Create the CFDIs if required - for move in self: - if ( - move.move_type == "out_invoice" - and move.cfdi_required - and move.cfdi_document_id.state != "published" - ): - move.create_invoice_cfdi() - - return res - - def create_invoice_cfdi(self): - """ - Create the CFDI - """ - self.ensure_one() - - self._validate_invoice_cfdi_required_fields() - - cert = self.env["l10n_mx_cfdi.document"].create( - { - "type": "I", - "issuer_id": self.issuer_id.id, - "receiver_id": self.receiver_id.id, - "related_invoice_id": self.id, - } - ) - - try: - cfdi_data = self._gather_invoice_cfdi_data() - cert.publish(cfdi_data) - - self.update( - { - "related_cert_ids": [(4, cert.id)], - } - ) - - except Exception as e: - cert.unlink() - raise e - - def _validate_invoice_cfdi_required_fields(self): - """ - Validate the CFDI required fields - """ - self.ensure_one() - err_msg = "" - - # validate issuer - if not self.issuer_id: - err_msg += "- No se ha definido el emisor\n" - - # validate partner data - if not self.receiver_id.vat: - err_msg += "- No se ha definido el RFC del receptor\n" - - if not self.receiver_id.tax_regime: - err_msg += "- No se ha definido el régimen fiscal del receptor\n" - - if not self.receiver_id.zip and self.receiver_id.vat != "XAXX010101000": - err_msg += "- No se ha definido el código postal del receptor\n" - - if not self.cfdi_use_id: - err_msg += "- No se ha definido el uso del CFDI\n" - - if not self.payment_method_id: - err_msg += "- No se ha definido el método de pago\n" - - if not self.payment_form_id: - err_msg += "- No se ha definido la forma de pago\n" - - err_msg += self.validate_invoice_items_for_cfdi_generation() - - if err_msg: - raise ValidationError("No se puede generar el CFDI:\n" + err_msg) - - def _gather_invoice_cfdi_data(self): - cfdi_data = { - "Currency": self.company_currency_id.name, - "ExpeditionPlace": self.issuer_id.zip, - "Date": self._format_cfdi_date_str(self.invoice_date), - "CfdiType": "I", - "PaymentForm": self.payment_form_id.code, - "PaymentMethod": self.payment_method_id.code, - "Receiver": { - "Name": self.receiver_id.name, - "Rfc": self.receiver_id.vat, - "CfdiUse": self.cfdi_use_id.code, - "FiscalRegime": self.receiver_id.tax_regime.code, - "TaxZipCode": self.receiver_id.zip, - }, - "Items": self.gather_invoice_cfdi_items_data(), - } - - self._add_global_information_to_cfdi_if_required(cfdi_data) - - return cfdi_data - - def _format_cfdi_date_str(self, document_date): - """ - Format the date to be used in the CFDI - - This method will add the time to the document_date to make it - compatible with the CFDI format. Then will format the date to - ISO 8601 format. - - """ - fixed_tz_recordset = self.with_context({"tz": self.env.user.tz}) - now_utc = fields.datetime.now() - now_utc_tz = fields.Datetime.context_timestamp(fixed_tz_recordset, now_utc) - - # add 2h if there is a difference larger than 24h between - # this is a workaround to avoid issues with the PAC when - # signing a CFDI with a date in the past - if (now_utc_tz.date() - document_date).days > 1: - # add 2h to now_utc_tz - now_utc_tz = now_utc_tz + timedelta(hours=2) - - # add time info to invoice_date - document_date = datetime.combine(document_date, now_utc_tz.time()) - - # invoice_date to ISO 8601 format - document_date_str = document_date.strftime("%Y-%m-%dT%H:%M:%S") - return document_date_str - - def gather_invoice_cfdi_items_data(self): - """ - Gather the data for the CFDI items - """ - self.ensure_one() - - cfdi_items_data = [] - for line in self.line_ids: - if line.exclude_from_invoice_tab or not line.product_id: - continue - - cfdi_item_data = line._gater_cfdi_item_data() - cfdi_items_data.append(cfdi_item_data) - - return cfdi_items_data - - def gater_invoice_cfdi_item_data(self, line): - """Gather the data for a CFDI item. - :param line: The invoice line - :return: The CFDI item data - """ - - cfdi_item_data = line._gater_cfdi_item_data() - - return cfdi_item_data - - def validate_invoice_items_for_cfdi_generation(self): - err_msg = "" - # validate invoice items - for line in self.line_ids: - if line.exclude_from_invoice_tab or not line.product_id: - continue - - if not line.product_id.l10n_mx_cfdi_product_code_id: - err_msg += ( - "- No se ha definido el código de producto para el producto %s\n" - % line.product_id.name - ) - - if not line.product_id.l10n_mx_cfdi_product_measurement_unit_id: - err_msg += ( - "- No se ha definido la unidad de medida para el producto %s\n" - % line.product_id.name - ) - - return err_msg - - @api.model - def _gather_invoice_cfdi_item_taxes_data(self, line, discount): - """Gather the taxes data for a CFDI item.""" - - price_unit_wo_discount = line.price_unit - discount - - taxes = [] - for tax_id in line.tax_ids: - computed_tax = tax_id.compute_all( - price_unit_wo_discount, - quantity=line.quantity, - currency=line.currency_id, - ) - tax_rate = ( - tax_id.amount / 100 - if tax_id.amount_type == "percent" - else tax_id.amount - ) - tax_total = ( - computed_tax["taxes"][0]["amount"] if computed_tax["taxes"] else 0 - ) - taxes.append( - { - "Name": tax_id.extract_l10n_mx_tax_code(), - "Rate": tax_rate, - "IsRetention": tax_id.extract_is_retention(), - "Base": computed_tax["total_excluded"], - "Total": tax_total, - } - ) - return taxes - - def prepare_invoice_cfdi_total_taxes(self): - self.ensure_one() - - total_taxes = {} - for line in self.line_ids: - if line.tax_line_id: - tax_id = line.tax_line_id - tax_code = tax_id.extract_l10n_mx_tax_code() - if not tax_code: - raise UserError( - _("The tax code for tax %s is not defined.") - % line.tax_ids[0].name - ) - - tax_rate = ( - tax_id.amount / 100 - if tax_id.amount_type == "percent" - else tax_id.amount - ) - - if tax_code in total_taxes: - total_taxes[tax_code]["Base"] += line.tax_base_amount - total_taxes[tax_code]["Total"] += line.price_total - else: - total_taxes[tax_code] = { - "Name": tax_code, - "Rate": tax_rate, - "IsRetention": tax_id.extract_is_retention(), - "Base": line.tax_base_amount, - "Total": line.price_total, - } - - # prepare float values to be serialized as JSON - for k, v in total_taxes.items(): - v["Base"] = json_float_round(v["Base"], 2) - v["Total"] = json_float_round(v["Total"], 2) - - return list(total_taxes.values()) - - def button_draft(self): - for rec in self: - if rec.l10n_mx_cfdi_auto: - published_related_cfdi = rec.related_cert_ids.filtered_domain( - [("state", "=", "published")] - ) - if len(published_related_cfdi) > 0 and rec.move_type != "in_invoice": - # show CFDI cancel dialog - return ( - rec.env.ref("l10n_mx_cfdi.document_cancel_action") - .sudo() - .read()[0] - ) - - return super(AccountMove, self).button_draft() - - def create_refund_cfdi(self): - """ - Create CFDI of type 'E' (Egreso). - """ - for refund in self: - items_data = self.gather_invoice_cfdi_items_data() - - receivables = refund.line_ids.filtered( - lambda l: l.account_id.user_type_id.type == "receivable" - ) - partial_reconcile = self.env["account.partial.reconcile"].search( - [("debit_move_id", "in", receivables.ids)] - ) - partial_reconcile |= ( - receivables.matched_debit_ids + receivables.matched_credit_ids - ) - - move_lines = ( - partial_reconcile.credit_move_id + partial_reconcile.debit_move_id - ) - - related_cfdis = move_lines.move_id.related_cert_ids.filtered_domain( - [ - ("state", "=", "published"), - ("type", "=", "I"), - ] - ) - - cfdi_data = { - "NameId": "2", - "ExpeditionPlace": refund.issuer_id.zip, - "Date": self._format_cfdi_date_str(self.invoice_date), - "PaymentForm": refund.payment_form_id.code, - "PaymentMethod": refund.payment_method_id.code, - "Receiver": { - "Name": refund.partner_id.name, - "Rfc": refund.partner_id.vat, - "CfdiUse": refund.cfdi_use_id.code, - "FiscalRegime": refund.partner_id.tax_regime.code, - "TaxZipCode": refund.partner_id.zip, - }, - "Items": items_data, - "Relations": { - "Type": "01", - "Cfdis": [ - {"Uuid": related_cfdi.uuid} for related_cfdi in related_cfdis - ], - }, - } - - refund_cfdi = self.env["l10n_mx_cfdi.document"].create( - { - "type": "E", - "issuer_id": refund.issuer_id.id, - "receiver_id": refund.receiver_id.id, - "related_invoice_id": refund.id, - } - ) - - self._add_global_information_to_cfdi_if_required(cfdi_data) - - # register relations - refund_cfdi.update( - { - "related_document_ids": [ - ( - 0, - 0, - { - "source_id": refund_cfdi.id, - "target_id": related_cfdi.id, - "relation_type_id": self.env.ref( - "l10n_mx_catalogs.c_tipo_relacion_1" - ).id, - }, - ) - for related_cfdi in related_cfdis - ] - } - ) - - try: - refund_cfdi.publish(cfdi_data) - - refund.update( - { - "related_cert_ids": [(4, refund_cfdi.id)], - } - ) - - for cfdi in related_cfdis: - if cfdi.related_invoice_id: - cfdi.related_invoice_id.related_cert_ids |= refund_cfdi - - except Exception as e: - refund_cfdi.unlink() - raise e - - def _add_global_information_to_cfdi_if_required(self, cfdi_data): - if self.receiver_id.vat == "XAXX010101000": - currentDateTime = datetime.now() - - cfdi_data["GlobalInformation"] = { - "Periodicity": "01", # Daily periodicity - "Months": str(currentDateTime.month).rjust(2, "0"), - "Year": currentDateTime.year, - } - - cfdi_data["Receiver"]["TaxZipCode"] = self.issuer_id.zip - cfdi_data["Receiver"]["FiscalRegime"] = "616" - - @api.returns("self", lambda value: value.id) - def copy(self, default=None): - # avoid copying the related cfdis - default = (default or {}).update( - { - "related_cert_ids": [(6, 0, [])], - } - ) - - return super(AccountMove, self).copy(default) - - def _get_name_invoice_report(self): - self.ensure_one() - if self.company_id.account_fiscal_country_id.code == "MX": - return "l10n_mx_cfdi.report_invoice_document" - - return super()._get_name_invoice_report() - - def action_load_from_attachment(self): - self.ensure_one() - - # find xml attachment - xml_attachment = self.env["ir.attachment"].search( - [ - ("res_model", "=", "account.move"), - ("res_id", "=", self.id), - ("mimetype", "=", "application/xml"), - ], - limit=1, - ) - - if not xml_attachment: - raise UserError(_("No XML attachment found for this invoice.")) - - # decode attachment - xml = base64.b64decode(xml_attachment.datas) - - cfdi = self._parse_cfdi_xml(xml) - cfdi.xml_file = xml - - self.related_cert_ids |= cfdi - - def _parse_cfdi_xml(self, xml): - # parse CFDI XML - root = etree.fromstring(xml) - namespaces = root.nsmap - - # add tfd namespace - namespaces["tfd"] = "http://www.sat.gob.mx/TimbreFiscalDigital" - - cfdi_data = { - "type": root.attrib["TipoDeComprobante"], - "serie": root.attrib.get("Serie", ""), - "folio": root.attrib.get("Folio", ""), - "state": "published", - "related_invoice_id": self.id, - } - - # get uuid - timbre_fiscal = root.find( - "./cfdi:Complemento/tfd:TimbreFiscalDigital", namespaces - ) - cfdi_data["uuid"] = timbre_fiscal.attrib["UUID"] - - issuer_id = self._resolve_issuer_from_xml(namespaces, root) - cfdi_data["issuer_id"] = issuer_id.id - self.issuer_id = issuer_id - - receiver_id, cfdi_use = self._resolve_receiver_data_from_xml(namespaces, root) - cfdi_data["receiver_id"] = receiver_id.id - cfdi_use_model = self.env["l10n_mx_catalogs.c_uso_cfdi"] - cfdi_use = cfdi_use_model.search([("code", "=", cfdi_use)], limit=1) - - self.receiver_id = receiver_id - self.cfdi_use_id = cfdi_use - - # create or update cfdi document - cfdi_document_model = self.env["l10n_mx_cfdi.document"] - document = cfdi_document_model.search( - [("uuid", "=", cfdi_data["uuid"])], limit=1 - ) - if document: - document.write(cfdi_data) - else: - document = cfdi_document_model.create(cfdi_data) - - self.cfdi_document_id = document - self.cfdi_required = True - - # resolve payment form - payment_form_model = self.env["l10n_mx_catalogs.c_forma_pago"] - payment_form_code = root.attrib["FormaPago"] - self.payment_form_id = payment_form_model.search( - [("code", "=", payment_form_code)], limit=1 - ) - - # resolve payment method - payment_method_model = self.env["l10n_mx_catalogs.c_metodo_pago"] - payment_method_code = root.attrib["MetodoPago"] - self.payment_method_id = payment_method_model.search( - [("code", "=", payment_method_code)], limit=1 - ) - - return document - - def _resolve_receiver_data_from_xml(self, namespaces, root): - # get receiver - receiver = root.find("cfdi:Receptor", namespaces) - receiver_id = self.env["res.partner"].search( - [("vat", "=", receiver.attrib["Rfc"])], limit=1 - ) - if not receiver_id: - raise UserError( - "No se encontró el receptor del certificado. RFC: %s" - % receiver.attrib["Rfc"] - ) - - cfdi_use = receiver.attrib["UsoCFDI"] - return receiver_id, cfdi_use - - def _resolve_issuer_from_xml(self, namespaces, root): - # get issuer - issuer = root.find("cfdi:Emisor", namespaces) - issuer_id = self.env["l10n_mx_cfdi.issuer"].search( - [("vat", "=", issuer.attrib["Rfc"])] - ) - if not issuer_id: - # find partner - partner_id = self.env["res.partner"].search( - [("vat", "=", issuer.attrib["Rfc"])] - ) - if not partner_id: - raise UserError( - "No se encontró el partner del emisor del certificado. RFC: %s" - % issuer.attrib["Rfc"] - ) - - # create issuer - issuer_id = self.env["l10n_mx_cfdi.issuer"].create( - { - "partner_id": partner_id.id, - } - ) - return issuer_id - - def action_generate_cfdi(self): - self.ensure_one() - - if self.cfdi_document_id.state == "published": - raise UserError("El CFDI ya ha sido publicado") - - if self.move_type == "out_invoice": - self.create_invoice_cfdi() - - if self.move_type == "out_refund": - # create credit note CFDI if required - if self.amount_residual != 0: - raise UserError( - "No se puede generar un CFDI de nota de " - "crédito con saldo pendiente" - ) - - self.create_refund_cfdi() - - -class AccountMoveLine(models.Model): - _inherit = "account.move.line" - - cfdi_price_unit = fields.Monetary( - string="Precio Unitario sin Impuestos", - compute="_compute_cfdi_fields", - store=True, - ) - - cfdi_subtotal = fields.Monetary( - string="Subtotal", - compute="_compute_cfdi_fields", - store=True, - ) - - cfdi_discount = fields.Monetary( - string="Descuento", - compute="_compute_cfdi_fields", - store=True, - ) - - @api.depends( - "product_id", - "price_unit", - "quantity", - "discount", - ) - def _compute_cfdi_fields(self): - for line in self: - line._gater_cfdi_item_data() - - def _gater_cfdi_item_data(self): - self.ensure_one() - - res = {} - currency = self.currency_id.sudo() - - # use product price decimal precision to round the price calculations - price_decimal_precision = self.env.ref("product.decimal_price").sudo().digits - - # Compute 'Subtotal'. - line_discount_price_unit = self.price_unit - - if hasattr(self, "discount_fixed"): - line_discount_price_unit -= self.discount_fixed - - line_discount_price_unit = line_discount_price_unit * ( - 1 - self.discount / 100.0 - ) - # round the price unit to the currency precision to prevent - # differences between the invoice totals and the CFDI total - line_discount_price_unit = float_round( - line_discount_price_unit, precision_digits=price_decimal_precision - ) - - subtotal = self.quantity * line_discount_price_unit - - # keep track of taxes included in price to subtract them later - # from the unit price as the CFDI specification doesn't support - # then - taxes_included = 0 - - cfdi_taxes = [] - if self.tax_ids: - # Compute taxes and adjust 'Subtotal' and 'Total' - taxes = self.tax_ids._origin.with_context(force_sign=1) - taxes_res = taxes.compute_all( - line_discount_price_unit, - quantity=self.quantity, - currency=self.currency_id, - product=self.product_id, - partner=self.partner_id, - is_refund=self.move_id.move_type in ("out_refund", "in_refund"), - ) - res["Subtotal"] = taxes_res["total_excluded"] - res["Total"] = taxes_res["total_included"] - - for computed_tax in taxes_res["taxes"]: - tax_id = self.env["account.tax"].browse(computed_tax["id"]) - tax_rate = ( - tax_id.amount / 100 - if tax_id.amount_type == "percent" - else tax_id.amount - ) - is_retention = tax_id.extract_is_retention() - tax_rate = json_float_round(tax_rate, precision_digits=6) - tax_total = json_float_round( - computed_tax["amount"], precision_digits=currency.decimal_places - ) - tax_base = json_float_round( - taxes_res["total_excluded"], - precision_digits=currency.decimal_places, - ) - - # sat expects retention taxes to be positive but odoo uses negative values - if is_retention: - tax_rate *= -1 - tax_total *= -1 - - cfdi_taxes.append( - { - "Name": tax_id.extract_l10n_mx_tax_code(), - "Rate": tax_rate, - "IsRetention": is_retention, - "Base": tax_base, - "Total": tax_total, - } - ) - - if tax_id.price_include: - taxes_included += tax_total - - if cfdi_taxes: - res.update( - { - "Taxes": cfdi_taxes, - "TaxObject": "02", # 'Si objeto de impuesto' - } - ) - else: - res["Total"] = res["Subtotal"] = subtotal - res["TaxObject"] = "01" - - if self.product_id.default_code: - res["IdentificationNumber"] = self.product_id.default_code - unit_included_taxes = taxes_included / (self.quantity or 1) - line_discount_price_unit -= unit_included_taxes - res.update( - { - "Quantity": self.quantity, - "ProductCode": self.product_id.l10n_mx_cfdi_product_code_id.code, - "Description": self.name, - "UnitCode": self.product_id.l10n_mx_cfdi_product_measurement_unit_id.code, - } - ) - - self._round_values_to_currency_precision(res) - - # compute discount - expected_subtotal_wo_discount = line_discount_price_unit * self.quantity - discount = ( - (self.price_unit * self.quantity) - - expected_subtotal_wo_discount - - taxes_included - ) - if float_is_zero(discount, precision_digits=currency.decimal_places): - # ignore a difference below the currency precision - discount = 0 - - res["Discount"] = discount - res["Subtotal"] += discount - - # recompute the unit price from the subtotal to avoid rounding - res["UnitPrice"] = res["Subtotal"] / (self.quantity or 1) - - self._round_values_to_currency_precision(res) - - # store the values to be used in the report - self.cfdi_subtotal = res["Subtotal"] - self.cfdi_discount = res["Discount"] - self.cfdi_price_unit = res["UnitPrice"] - - return res - - def _round_values_to_currency_precision(self, res, skip=None): - currency_decimal_places = self.currency_id.decimal_places - - # Round all values to the currency precision - for k, v in res.items(): - if skip and k in skip: - continue - - if isinstance(v, float): - res[k] = json_float_round(v, precision_digits=currency_decimal_places) - else: - res[k] = v diff --git a/l10n_mx_cfdi/models/account_move_reversal.py b/l10n_mx_cfdi/models/account_move_reversal.py deleted file mode 100644 index 23987a8..0000000 --- a/l10n_mx_cfdi/models/account_move_reversal.py +++ /dev/null @@ -1,35 +0,0 @@ -from odoo import models - - -class AccountMoveReversal(models.TransientModel): - _inherit = "account.move.reversal" - - def _prepare_default_reversal(self, move): - """Add CFDI required fields to the reversal if the original move is a CFDI""" - - res = super(AccountMoveReversal, self)._prepare_default_reversal(move) - - if move.cfdi_required: - data = { - "cfdi_required": True, - "payment_method_id": self.env["l10n_mx_catalogs.c_metodo_pago"] - .search([("code", "=", "PUE")]) - .id, - "cfdi_use_id": self.env["l10n_mx_catalogs.c_uso_cfdi"] - .search([("code", "=", "G02")]) - .id, - "issuer_id": move.issuer_id.id, - "related_cert_ids": [(6, 0, [])], - } - - # set the right cfdi use for operations with public - if move.partner_id.vat == "XAXX010101000": - data["cfdi_use_id"] = ( - self.env["l10n_mx_catalogs.c_uso_cfdi"] - .search([("code", "=", "S01")]) - .id - ) - - res.update(data) - - return res diff --git a/l10n_mx_cfdi/models/account_partial_reconcile.py b/l10n_mx_cfdi/models/account_partial_reconcile.py deleted file mode 100644 index f325e57..0000000 --- a/l10n_mx_cfdi/models/account_partial_reconcile.py +++ /dev/null @@ -1,70 +0,0 @@ -from odoo import models - - -class AccountPartialReconcile(models.Model): - _inherit = "account.partial.reconcile" - - def create(self, vals_list): - """Create Payments and Credit Note CFDI if required""" - - res = super().create(vals_list) - - if self.env.company.l10n_mx_cfdi_auto: - move_line_ids = res.debit_move_id | res.credit_move_id - - for move in move_line_ids.move_id: - if move.move_type == "entry": - # create payment CFDI if required - payment = move.payment_id - payment_requires_cfdi = any( - invoice.cfdi_required - and invoice.payment_method_id.code == "PPD" - for invoice in payment.reconciled_invoice_ids - ) - - if ( - payment.payment_type == "inbound" - and payment.is_reconciled - and payment_requires_cfdi - ): - payment.create_payment_cfdi() - - if move.move_type == "out_refund": - # create credit note CFDI if required - existent_cfdi = move.related_cert_ids.filtered_domain( - [("type", "=", "E"), ("state", "=", "published")] - ) - - if ( - move.amount_residual == 0 - and move.cfdi_required - and not existent_cfdi - ): - move.create_refund_cfdi() - - return res - - def unlink(self): - """Cancel related Payments CFDI if any""" - - move_line_ids = self.debit_move_id | self.credit_move_id - res = super().unlink() - - if self.env.company.l10n_mx_cfdi_auto: - for move in move_line_ids.move_id: - if move.move_type == "entry": - payment = move.payment_id - related_cfdi = payment.related_cert_ids.filtered_domain( - [("type", "=", "P"), ("state", "=", "published")] - ) - if related_cfdi and not payment.is_reconciled: - payment.cancel_payment_cfdi() - - if move.move_type == "out_refund": - for cfdi in move.related_cert_ids: - if cfdi.state == "published" and cfdi.type == "E": - cfdi.cancel( - "02" - ) # cancel reason: 'Comprobantes emitidos con errores sin relación' - - return res diff --git a/l10n_mx_cfdi/models/account_payment.py b/l10n_mx_cfdi/models/account_payment.py deleted file mode 100644 index c06281c..0000000 --- a/l10n_mx_cfdi/models/account_payment.py +++ /dev/null @@ -1,204 +0,0 @@ -from datetime import datetime - -from odoo import models -from odoo.exceptions import ValidationError -from odoo.tools import json_float_round - - -class AccountPayment(models.Model): - _inherit = "account.payment" - - def action_generate_cfdi(self): - self.ensure_one() - - if self.cfdi_document_id: - raise ValidationError("El pago ya tiene un CFDI asociado.") - - if not self.is_reconciled: - raise ValidationError("El pago no está totalmente reconciliado.") - - if self.payment_type == "inbound": - self.create_payment_cfdi() - - def create_payment_cfdi(self): - """ - Create CFDI of type payment ('P') matching the invoice payments if they are required. - """ - - self.ensure_one() - - # assert move type is inbound payment - if self.move_type != "entry" or self.payment_type != "inbound": - raise ValidationError("Solo se pueden crear pagos de tipo entrada.") - - # check if the payment is fully reconciled - if not self.is_reconciled: - raise ValidationError("El pago no está totalmente reconciliado") - - payment_data = self.prepare_payment_cfdi() - - # get invoices issuer - issuer = self.reconciled_invoice_ids.issuer_id - issuer.ensure_one() - - # get invoices receiver - receiver = self.reconciled_invoice_ids.receiver_id - if not receiver: - # resolve receiver from the invoice CFDI for legacy invoices - receiver = self.reconciled_invoice_ids.related_cert_ids.filtered_domain( - [("type", "=", "I"), ("state", "=", "published")] - ).mapped("receiver_id")[0] - - receiver.ensure_one() - - payment_cfdi = self.env["l10n_mx_cfdi.document"].create( - { - "type": "P", - "issuer_id": issuer.id, - "receiver_id": receiver.id, - "related_payment_id": self.id, - } - ) - - # establecer uso de CFDI a Pagos (CP01) - self.cfdi_use_id = self.env.ref("l10n_mx_catalogs.c_uso_cfdi_CP01").id - - try: - cfdi_data = { - "ExpeditionPlace": issuer.zip, - "Receiver": { - "Name": receiver.name, - "Rfc": receiver.vat, - "CfdiUse": self.cfdi_use_id.code, - "FiscalRegime": receiver.tax_regime.code, - "TaxZipCode": receiver.zip, - }, - "Complemento": {"Payments": [payment_data]}, - } - - if receiver.vat == "XAXX010101000": - currentDateTime = datetime.now() - - cfdi_data["GlobalInformation"] = { - "Periodicity": "01", # Daily periodicity - "Months": str(currentDateTime.month).rjust(2, "0"), - "Year": currentDateTime.year, - } - - cfdi_data["Receiver"]["TaxZipCode"] = issuer.zip - cfdi_data["Receiver"]["FiscalRegime"] = "616" - - payment_cfdi.publish(cfdi_data) - - self.update( - { - "related_cert_ids": [(4, payment_cfdi.id)], - "cfdi_document_id": payment_cfdi.id, - } - ) - - for invoice in self.reconciled_invoice_ids: - invoice.related_cert_ids |= payment_cfdi - - except Exception as e: - payment_cfdi.unlink() - raise e - - def prepare_payment_cfdi(self): - self.ensure_one() - - related_documents_data = [] - - for invoice in self.reconciled_invoice_ids: - if not invoice.cfdi_document_id: - raise ValidationError( - 'Error al emitir CFDI tipo Comprobante de Pago. La factura "%s" no tiene CFDI' - % invoice.name - ) - - # get related cfdi of type 'Ingreso' - existent_invoice_cfdi = invoice.related_cert_ids.filtered_domain( - [("type", "=", "I"), ("state", "=", "published")] - ) - existent_invoice_cfdi.ensure_one() - - # get related CFDIs of type 'Pago' - existent_payments_cfdi = invoice.related_cert_ids.filtered_domain( - [("type", "=", "P"), ("state", "=", "published")] - ) - - # initialize related document data - related_document_data = { - "Uuid": existent_invoice_cfdi.uuid, - "Folio": existent_invoice_cfdi.name, - "PartialityNumber": len(existent_payments_cfdi) + 1, - "PaymentMethod": "PUE", - "AmountPaid": 0, - "PreviousBalanceAmount": invoice.amount_residual, - } - - # add amounts from matched credit lines - for credit in invoice.line_ids.matched_credit_ids: - # add line amount if it comes from the current payment - if credit.credit_move_id.move_id == self.move_id: - related_document_data["AmountPaid"] += credit.amount - related_document_data["PreviousBalanceAmount"] += credit.amount - - # add tax data - tax_data = self._compute_taxes(related_document_data["AmountPaid"], invoice) - if tax_data: - related_document_data["TaxObject"] = "02" - related_document_data["Taxes"] = tax_data - else: - related_document_data["TaxObject"] = "01" - - # round monetary fields - for field in ["AmountPaid", "PreviousBalanceAmount"]: - related_document_data[field] = json_float_round( - related_document_data[field], 2 - ) - - # add related document data to the list - related_documents_data.append(related_document_data) - - payment_date = self.move_id._format_cfdi_date_str(self.date) - payment_data = { - "Date": payment_date, - "PaymentForm": self.move_id.payment_form_id.code, - "Amount": json_float_round(self.amount, 2), - "RelatedDocuments": related_documents_data, - } - return payment_data - - def _compute_taxes(self, amount_paid, invoice): - total_taxes = invoice.prepare_invoice_cfdi_total_taxes() - payment_taxes = [] - - # skip if there are no taxes - if not total_taxes: - return payment_taxes - - # compute taxes base (amount_paid = rate * base) so ( base = amount_paid / rate ) - total_rate = sum(float(tax["Rate"] + 1) for tax in total_taxes) - base = amount_paid / total_rate - for tax in total_taxes: - payment_taxes.append( - { - "Name": tax["Name"], - "Rate": tax["Rate"], - "IsRetention": tax["IsRetention"], - "Base": json_float_round(base, 2), - "Total": json_float_round(base * float(tax["Rate"]), 2), - } - ) - - return payment_taxes - - def cancel_payment_cfdi(self): - self.ensure_one() - - for cfdi in self.related_cert_ids: - if cfdi.state == "published": - cfdi.cancel( - "02" - ) # cancel reason: 'Comprobantes emitidos con errores sin relación' diff --git a/l10n_mx_cfdi/models/account_payment_register.py b/l10n_mx_cfdi/models/account_payment_register.py deleted file mode 100644 index e49424e..0000000 --- a/l10n_mx_cfdi/models/account_payment_register.py +++ /dev/null @@ -1,38 +0,0 @@ -from odoo import fields, models -from odoo.exceptions import UserError - - -class AccountPaymentRegister(models.TransientModel): - _inherit = "account.payment.register" - - payment_form_id = fields.Many2one( - "l10n_mx_catalogs.c_forma_pago", string="Forma de Pago", required=True - ) - - def _init_payments(self, to_process, edit_mode=False): - """ - Add payment for id to payments creation data - """ - for entry in to_process: - entry["create_vals"].update( - { - "payment_form_id": self.payment_form_id.id, - } - ) - - return super(AccountPaymentRegister, self)._init_payments(to_process, edit_mode) - - def _create_payments(self): - # Prevent partial payments on invoices with cfdi and payment method different of 'PPD' - if self.payment_difference > 0: - related_invoices = self.line_ids.move_id - if any( - invoice.cfdi_required and invoice.payment_method_id.code != "PPD" - for invoice in related_invoices - ): - raise UserError( - "No se puede registrar un pago parcial sobre una factura con CFDI " - "y método de pago diferente a PPD" - ) - - return super(AccountPaymentRegister, self)._create_payments() diff --git a/l10n_mx_cfdi/models/account_tax.py b/l10n_mx_cfdi/models/account_tax.py deleted file mode 100644 index 933b9ac..0000000 --- a/l10n_mx_cfdi/models/account_tax.py +++ /dev/null @@ -1,21 +0,0 @@ -from odoo import models -from odoo.exceptions import UserError - - -class AccountTax(models.Model): - _inherit = "account.tax" - - def extract_l10n_mx_tax_code(self): - self.ensure_one() - if "ISR" in self.name: - return "ISR" - elif "IVA" in self.name: - return "IVA" - elif "IEPS" in self.name: - return "IEPS" - - raise UserError("No se pudo extraer el código de impuesto de %s" % self.name) - - def extract_is_retention(self): - self.ensure_one() - return "RET" in self.name diff --git a/l10n_mx_cfdi/models/cfdi_document.py b/l10n_mx_cfdi/models/cfdi_document.py deleted file mode 100644 index e84ea75..0000000 --- a/l10n_mx_cfdi/models/cfdi_document.py +++ /dev/null @@ -1,526 +0,0 @@ -import base64 -import json -import re -from io import BytesIO - -import qrcode -from dateutil import parser - -from odoo import _, api, fields, models -from odoo.exceptions import UserError - - -class Document(models.Model): - _name = "l10n_mx_cfdi.document" - _description = "CFDI document" - - ### - # Certificate fields - ### - type = fields.Selection( - [ - ("I", "Ingreso"), - ("E", "Egreso"), - ("P", "Pago"), - ("T", "Traslado"), - ], - string="Tipo", - readonly=True, - ) - - version = fields.Char(string="Version", default="4.0") - - serie = fields.Char(string="Serie") - folio = fields.Char(string="Folio") - name = fields.Char( - string="Nombre", readonly=True, compute="_compute_name", store=True - ) - - uuid = fields.Char(string="UUID", readonly=True, help="UUID asignado por el SAT") - - issuer_id = fields.Many2one( - "l10n_mx_cfdi.issuer", - string="Emisor", - required=True, - domain=[("registered", "=", True)], - ) - - receiver_id = fields.Many2one("res.partner", string="Receptor", required=True) - - tracking_id = fields.Char(string="ID del documento en el API", readonly=True) - - pdf_file = fields.Binary(string="Archivo PDF", attachment=True, readonly=True) - xml_file = fields.Binary(string="Archivo XML", attachment=True, readonly=True) - - related_invoice_id = fields.Many2one( - "account.move", string="Factura relacionada", readonly=True - ) - related_payment_id = fields.Many2one( - "account.payment", string="Pago relacionado", readonly=True - ) - is_global_note = fields.Boolean(string="Nota global", readonly=True, default=False) - - ### - # Auxiliary fields - ### - state = fields.Selection( - [ - ("draft", "Borrador"), - ("pending", "Pendiente"), - ("published", "Publicada"), - ("pending_cancel", "Cancelación pendiente"), - ("canceled", "Cancelada"), - ("unknown", "Desconocido"), - ], - default="draft", - string="Estado", - readonly=True, - ) - - pdf_filename = fields.Char(string="Nombre del archivo PDF", readonly=True) - xml_filename = fields.Char(string="Nombre del archivo XML", readonly=True) - - cert_data_json = fields.Char(readonly=True) - - company_id = fields.Many2one( - "res.company", required=True, default=lambda self: self.env.company - ) - - standalone = fields.Boolean( - string="Independiente", - compute="_compute_standalone", - store=True, - help="Si está marcado, el certificado no esta relacionado " - "a otros documentos del sistema", - ) - - cancellation_request_proof_file = fields.Binary( - string="Acuse de solicitud de cancelación", attachment=True, readonly=True - ) - cancellation_request_proof_filename = fields.Char( - string="Nombre del archivo de acuse de solicitud de cancelación", readonly=True - ) - - # used to download the files on demand - files_in_cache = fields.Boolean( - readonly=True, compute="download_files_if_needed", store=False - ) - - issuing_datetime = fields.Datetime( - string="Fecha de emisión", readonly=True, compute="_load_json_data" - ) - cert_number = fields.Char( - string="Número de certificado", readonly=True, compute="_load_json_data" - ) - original_string = fields.Char( - string="Cadena original", readonly=True, compute="_load_json_data" - ) - cfdi_signature = fields.Char( - string="Firma del CFDI", readonly=True, compute="_load_json_data" - ) - sat_signature = fields.Char( - string="Firma del SAT", readonly=True, compute="_load_json_data" - ) - sat_cert_number = fields.Char( - string="Número de certificado del SAT", readonly=True, compute="_load_json_data" - ) - rfc_prov_certif = fields.Char( - string="RFC del proveedor de certificación", - readonly=True, - compute="_load_json_data", - ) - signing_date = fields.Datetime( - string="Fecha de timbrado", readonly=True, compute="_load_json_data" - ) - related_document_ids = fields.One2many( - "l10n_mx_cfdi.document_relation", "source_id", string="Documentos relacionados" - ) - tax_codes = fields.Char( - string="Código de impuesto", readonly=True, compute="_load_json_data" - ) - - verification_url = fields.Char( - string="URL de verificación", readonly=True, compute="_load_json_data" - ) - verification_qr_code = fields.Binary( - string="Código QR de Verificación", readonly=True, compute="_load_json_data" - ) - - # utility fields - l10n_mx_cfdi_auto = fields.Boolean( - string="CFDI Automatico", related="company_id.l10n_mx_cfdi_auto", readonly=True - ) - - l10n_mx_cfdi_enabled = fields.Boolean( - string="CFDI Habilitado", - related="company_id.l10n_mx_cfdi_enabled", - readonly=True, - ) - - @api.depends("cert_data_json") - def _load_json_data(self): - for rec in self: - if rec.cert_data_json: - data = json.loads(rec.cert_data_json) - rec.issuing_datetime = parser.parse(data["Date"]) - rec.cert_number = data["CertNumber"] - rec.original_string = data["OriginalString"] - rec.cfdi_signature = data["Complement"]["TaxStamp"]["CfdiSign"] - rec.sat_signature = data["Complement"]["TaxStamp"]["SatSign"] - rec.sat_cert_number = data["Complement"]["TaxStamp"]["SatCertNumber"] - rec.rfc_prov_certif = data["Complement"]["TaxStamp"]["RfcProvCertif"] - rec.signing_date = parser.parse(data["Complement"]["TaxStamp"]["Date"]) - rec.verification_url = self._generate_verification_url( - rec.uuid, - rec.issuer_id.vat, - rec.receiver_id.vat, - data["Total"], - rec.cfdi_signature[-8:], - ) - rec.verification_qr_code = self._generate_qr_code( - rec.verification_url.encode("utf-8") - ) - rec.tax_codes = self._load_tax_code_from_json_data(data) - - @api.model - def _load_tax_code_from_json_data(self, data): - taxes = set() - if "Taxes" in data: - for tax in data["Taxes"]: - if tax["Name"] == "ISR": - taxes.add("001") - if tax["Name"] == "IVA": - taxes.add("002") - if tax["Name"] == "IEPS": - taxes.add("003") - - return ",".join(taxes) - - @api.depends("pdf_file", "xml_file") - def _generate_verification_url( - self, uuid, issuer_cfdi, receiver_cfdi, total, sign_extract - ): - url = ( - f"https://verificacfdi.facturaelectronica.sat.gob.mx/default.aspx?" - f"id={uuid}&" - f"re={issuer_cfdi}&" - f"rr={receiver_cfdi}&" - f"tt={total}&" - f"fe={sign_extract}" - ) - return url - - def _generate_qr_code(self, data): - qr = qrcode.QRCode( - version=1, - error_correction=qrcode.constants.ERROR_CORRECT_Q, - box_size=4, - border=0, - ) - - qr.add_data(data) - img = qr.make_image(fit=True) - temp = BytesIO() - img.save(temp, format="PNG") - qr_image = base64.b64encode(temp.getvalue()) - return qr_image - - ### - # Computed fields generation functions - ### - - @api.depends("tracking_id") - def download_files_if_needed(self): - for entry in self: - if entry.tracking_id: - if not entry.pdf_file: - report, resource_ids = self._resolve_report() - - if report: - # force the report to be rendered to work around a bug in _render_qweb_pdf - report = report.with_context({"force_report_rendering": True}) - doc_data, doc_format = report._render_qweb_pdf(resource_ids) - # in some scenarios, the report is not generated, so we need to check if the file is empty - if doc_data: - result = base64.b64encode(doc_data) - entry.pdf_file = result - - if not entry.pdf_file: - # fallback to the provider's PDF - res = entry.issuer_id.service_id.sudo().get_cfdi_pdf( - entry.tracking_id - ) - entry.pdf_file = res["Content"] - - # set filename - entry.pdf_filename = "%s.pdf" % entry.name - - if not entry.xml_file: - res = entry.issuer_id.service_id.sudo().get_cfdi_xml( - entry.tracking_id - ) - entry.xml_file = res["Content"].encode("utf-8") - entry.xml_filename = "%s.xml" % entry.name - - entry.files_in_cache = True - else: - entry.files_in_cache = False - - def _resolve_report(self): - """Returns the report and the resource ids to be used to generate the PDF file.""" - - report = None - resource_ids = [] - - if self.type in ("I", "E") and self.related_invoice_id: - report = self.env.ref("account.account_invoices") - resource_ids = [self.related_invoice_id.id] - - if self.type == "P" and self.related_payment_id: - report = self.env.ref("account.action_report_payment_receipt") - resource_ids = [self.related_payment_id.id] - - return report, resource_ids - - @api.depends("serie", "folio") - def _compute_name(self): - for entry in self: - if entry.serie: - entry.name = "%s-%s" % (entry.serie, entry.folio) - else: - entry.name = "%s" % entry.folio - - @api.depends("type") - def _compute_standalone(self): - # only documents of type 'T' are considered standalone - for entry in self: - entry.standalone = entry.type == "T" - - ### - # Model methods - ### - - def create(self, vals_list): - # Set values to serie and folio from sequence if not provided - - # check if vals_list is a list of dictionaries - if isinstance(vals_list, dict): - vals_list = [vals_list] - - for vals in vals_list: - if "serie" not in vals or "folio" not in vals: - issuer = self._resolve_issuer_on_create(vals) - if ( - issuer.use_origin_document_sequence - and vals.get("type", False) != "T" - and vals.get("is_global_note", False) is False - ): - self._set_serie_and_folio_from_document_sequence(vals) - else: - self._set_serie_and_folio_from_cfdi_sequence(vals) - - # Create certificate - return super().create(vals_list) - - def _resolve_issuer_on_create(self, vals): - issuer_id = vals.get("issuer_id", False) - if not issuer_id: - raise UserError(_("Issuer is required to generate a new document.")) - - return self.env["l10n_mx_cfdi.issuer"].browse(issuer_id) - - def _set_serie_and_folio_from_cfdi_sequence(self, vals): - sequence_id = self.get_sequence_for_cfdi_type(vals) - - vals["serie"] = sequence_id.prefix - vals["folio"] = sequence_id.number_next - - sequence_id.next_by_id(sequence_id.id) - - def _set_serie_and_folio_from_document_sequence(self, vals): - serie = "" - folio = "" - document_name = "" - - if "related_invoice_id" in vals: - invoice = self.env["account.move"].browse(vals["related_invoice_id"]) - document_name = invoice.name - - if "related_payment_id" in vals: - payment = self.env["account.payment"].browse(vals["related_payment_id"]) - document_name = payment.name - - if not document_name: - raise UserError(_("Unable to determine the origin document name.")) - - # extract numeric postfix from invoice name using regex - match = re.search(r"\d+$", document_name) - if match: - folio = match.group() - serie = document_name[: -len(match.group())] - else: - raise UserError(_("Invoice name does not contain a numeric postfix.")) - - # remove non-alphanumeric characters from serie - serie = re.sub(r"\W+", "", serie) - - vals["serie"] = serie - vals["folio"] = folio - - @api.model - def get_sequence_for_cfdi_type(self, vals_list): - issuer_id = self.env["l10n_mx_cfdi.issuer"].browse(vals_list["issuer_id"]) - - if vals_list["type"] == "I": - return issuer_id.invoice_sequence_id - elif vals_list["type"] == "E": - return issuer_id.refund_sequence_id - elif vals_list["type"] == "P": - return issuer_id.payment_sequence_id - elif vals_list["type"] == "T": - return issuer_id.transfer_sequence_id - else: - raise UserError("Tipo de certificado desconocido") - - def cancel(self, reason: str, replacement=None, simulate=False): - self.ensure_one() - - if self.state != "published": - return - - if not simulate: - res = self.issuer_id.service_id.sudo().cancel_cfdi( - self.tracking_id, reason, replacement - ) - if ( - res["Status"] == "canceled" - or res["Status"] == "acepted" - or res["Status"] == "expired" - ): - self.state = "canceled" - self.pdf_file = False - self.xml_file = False - elif res["Status"] == "pending": - self.state = "pending_cancel" - elif res["Status"] == "rejected": - self.state = "published" - else: - raise UserError("Error al cancelar el certificado: %s" % res["Message"]) - else: - self.state = "canceled" - - def publish(self, cfdi_data): - self.ensure_one() - - if "Serie" not in cfdi_data: - cfdi_data["Serie"] = self.serie - - if "Folio" not in cfdi_data: - cfdi_data["Folio"] = self.folio - - if "CfdiType" not in cfdi_data: - cfdi_data["CfdiType"] = self.type - - if "Issuer" not in cfdi_data: - cfdi_data["Issuer"] = { - "Name": self.issuer_id.fiscal_name - if hasattr(self.issuer_id, "fiscal_name") - else self.issuer_id.name, - "Rfc": self.issuer_id.vat, - "FiscalRegime": self.issuer_id.tax_regime.code, - } - - if "LogoUrl" not in cfdi_data and self.issuer_id.logo_url: - cfdi_data["LogoUrl"] = self.issuer_id.logo_url - - for entry in self: - if entry.state != "draft": - raise UserError("El certificado no está en borrador") - - # check if there are no other published certificates with the same serie and folio - similar_certificates_count = self.search( - [ - ("serie", "=", entry.serie), - ("folio", "=", entry.folio), - ("state", "=", "published"), - ], - count=True, - ) - - if similar_certificates_count > 0: - raise UserError( - "Ya existe un certificado publicado con la serie y folio indicados" - ) - - # use sudo to allow users to publish certificates - res = entry.issuer_id.service_id.sudo().create_cfdi(cfdi_data) - - # store result for later usage - self.cert_data_json = json.dumps(res) - - if res["Status"] == "active": - self.uuid = res["Complement"]["TaxStamp"]["Uuid"] - self.tracking_id = res["Id"] - self.state = "published" - else: - raise UserError("Error al publicar el certificado: %s" % res["Message"]) - - def action_cancel(self): - self.ensure_one() - - return { - "name": "Cancelar certificado", - "type": "ir.actions.act_window", - "view_mode": "form", - "res_model": "l10n_mx_cfdi.document_cancel", - "target": "new", - "context": {"default_certificate_ids": [(6, 0, [self.id])]}, - } - - def action_check_status(self): - self.ensure_one() - - service = self.issuer_id.service_id.sudo() - amount_total = 0 - if self.related_invoice_id: - amount_total = self.related_invoice_id.amount_total - - if self.related_payment_id: - amount_total = self.related_payment_id.amount - - status = service.check_cfdi_status( - self.uuid, self.issuer_id.vat, self.receiver_id.vat, amount_total - ) - - if self.state != status: - self.state = status - - def action_get_cancellation_request_proof(self): - self.ensure_one() - - # check that the certificate is canceled - if self.state != "canceled": - raise UserError("El certificado no está cancelado") - - service = self.issuer_id.service_id.sudo() - - file = service.get_cancellation_request_proof(self.tracking_id) - self.cancellation_request_proof_file = file - self.cancellation_request_proof_filename = ( - "Solicitud de cancelación %s.pdf" % self.name - ) - - -class DocumentRelation(models.Model): - _name = "l10n_mx_cfdi.document_relation" - _description = "Describe a relation between two CFDIs" - - relation_type_id = fields.Many2one( - "l10n_mx_catalogs.c_tipo_relacion", required=True - ) - source_id = fields.Many2one( - "l10n_mx_cfdi.document", required=True, ondelete="cascade" - ) - target_id = fields.Many2one( - "l10n_mx_cfdi.document", required=True, ondelete="cascade" - ) diff --git a/l10n_mx_cfdi/models/cfdi_issuer.py b/l10n_mx_cfdi/models/cfdi_issuer.py deleted file mode 100644 index 3d03034..0000000 --- a/l10n_mx_cfdi/models/cfdi_issuer.py +++ /dev/null @@ -1,141 +0,0 @@ -import logging - -from odoo import api, fields, models -from odoo.exceptions import UserError - -_logger = logging.getLogger(__name__) - - -class CFDIIssuer(models.Model): - """Holds the CFDI issuer information""" - - _name = "l10n_mx_cfdi.issuer" - _description = "Emisor" - - # Embed partner fields - partner_id = fields.Many2one( - "res.partner", delegate=True, ondelete="cascade", required=True - ) - - logo_url = fields.Char(string="URL del logo") - fiscal_name = fields.Char(string="Razón Social", help="Razón Social del Emisor") - certificate_file = fields.Binary( - string="Certificate File", groups="account.group_account_manager" - ) - key_file = fields.Binary(string="Key File", groups="account.group_account_manager") - key_password = fields.Char( - string="Password", groups="account.group_account_manager" - ) - service_id = fields.Many2one("l10n_mx_cfdi.cfdi_service", string="Servicio") - - registered = fields.Boolean(string="Registered", store=True) - company_id = fields.Many2one("res.company", default=lambda self: self.env.company) - - use_origin_document_sequence = fields.Boolean( - string="Usar Serie de Origen", - help="Usar serie del documento de origen para los CFDI generados", - ) - - invoice_sequence_id = fields.Many2one( - "l10n_mx_cfdi.series", - string="Serie Ingresos", - default=lambda self: self._create_default_cfdi_sequence("Ingresos"), - ) - refund_sequence_id = fields.Many2one( - "l10n_mx_cfdi.series", - string="Serie Egresos", - default=lambda self: self._create_default_cfdi_sequence("Egresos"), - ) - transfer_sequence_id = fields.Many2one( - "l10n_mx_cfdi.series", - string="Serie Traslados", - default=lambda self: self._create_default_cfdi_sequence("Traslados"), - ) - payment_sequence_id = fields.Many2one( - "l10n_mx_cfdi.series", - string="Serie Pagos", - default=lambda self: self._create_default_cfdi_sequence("Pagos"), - ) - - @api.model - def default_get(self, fields_list): - # set country to Mexico - res = super(CFDIIssuer, self).default_get(fields_list) - res["country_id"] = self.env.ref("base.mx").id - - return res - - @api.model - def _slugify(self, string): - # slugify string - return string.lower().replace(" ", "_") - - @api.model - def _create_default_cfdi_sequence(self, name): - # format a unique sequence code for the company - sequence_code = "l10n_mx_cfdi.sequence.{}.{}".format( - self._slugify(self.env.company.name), self._slugify(name) - ) - - existent_sequence = self.env["l10n_mx_cfdi.series"].search( - [("code", "=", sequence_code)] - ) - if existent_sequence: - return existent_sequence - else: - return ( - self.env["l10n_mx_cfdi.series"] - .sudo() - .create( - { - "name": "Folios CFDI %s" % name, - "implementation": "no_gap", - "number_increment": 1, - "number_next_actual": 0, - "prefix": name[0], - "company_id": self.env.user.company_id.id, - "code": sequence_code, - } - ) - ) - - def register_issuer(self): - """Registers the certificate in the SAT""" - - self.ensure_one() - if not self.vat: - raise UserError("No se ha configurado el RFC del Emisor") - - if not self.certificate_file or not self.key_file or not self.key_password: - raise UserError("No se ha configurado el Certificado Digital") - - if not self.service_id: - raise UserError("No se ha definido el Servicio de facturación a utilizar") - - try: - self.service_id.register_csd( - self.vat, self.certificate_file, self.key_file, self.key_password - ) - self.registered = True - except Exception as e: - self.registered = False - _logger.warning(e) - raise UserError("No se pudo registrar el Certificado") - - def unregister_issuer(self): - """Unregisters the certificate in the SAT""" - - self.ensure_one() - if not self.certificate_file or not self.key_file or not self.key_password: - self.registered = False - return - - if not self.service_id: - raise UserError("No se ha definido el Servicio de facturación a utilizar") - - try: - self.service_id.unregister_csd(self.vat) - self.registered = False - except Exception as e: - _logger.warning(e) - raise UserError("No se pudo desregistrar el Certificado") diff --git a/l10n_mx_cfdi/models/cfdi_series.py b/l10n_mx_cfdi/models/cfdi_series.py deleted file mode 100644 index 5c2f790..0000000 --- a/l10n_mx_cfdi/models/cfdi_series.py +++ /dev/null @@ -1,17 +0,0 @@ -from odoo import api, fields, models - - -class CFDISeries(models.Model): - _name = "l10n_mx_cfdi.series" - _inherit = ["ir.sequence"] - _description = "CFDI Series" - - code = fields.Char(string="Code", copy=False) - - # override create method to set the code - @api.model - def create(self, vals): - if not vals.get("implementation"): - vals["implementation"] = "no_gap" - - return super(CFDISeries, self).create(vals) diff --git a/l10n_mx_cfdi/models/cfdi_service.py b/l10n_mx_cfdi/models/cfdi_service.py deleted file mode 100644 index e62ac12..0000000 --- a/l10n_mx_cfdi/models/cfdi_service.py +++ /dev/null @@ -1,193 +0,0 @@ -import json -import logging - -import facturama - -from odoo import api, fields, models -from odoo.exceptions import UserError - -_logger = logging.getLogger(__name__) - - -class CFDIService(models.Model): - _name = "l10n_mx_cfdi.cfdi_service" - _description = "CFDI Service Settings" - - name = fields.Char(string="Nombre", required=True) - company_ids = fields.Many2many( - "res.company", string="Compañías", default=lambda self: self.env.company - ) - - user = fields.Char(string="User", required=True, groups="base.group_system") - password = fields.Char(string="Password", required=True, groups="base.group_system") - sandbox_mode = fields.Boolean( - string="Modo Pruebas", default=False, groups="base.group_system" - ) - - topup_ids = fields.One2many( - "l10n_mx_cfdi.cfdi_service.topup", "service_id", string="Recargas" - ) - stamps_used = fields.Integer( - string="Folios Usados", readonly=True, compute="_compute_stamps_used" - ) - - def _compute_stamps_used(self): - for record in self: - if self.usage_sequence_id: - record.stamps_used = self.usage_sequence_id.number_next_actual - 1 - else: - record.stamps_used = 0 - - stamps_available = fields.Integer( - string="Folios Disponibles", readonly=True, compute="_compute_stamps_available" - ) - - def _compute_stamps_available(self): - for record in self: - total_stamps_acquired = sum(record.topup_ids.mapped("stamp_number")) - record.stamps_available = total_stamps_acquired - record.stamps_used - - def _create_usage_sequence(self): - return self.env["ir.sequence"].create( - { - "name": "CFDI Folios Usados", - "implementation": "no_gap", - "number_increment": 1, - "number_next_actual": 0, - "company_id": self.env.user.company_id.id, - "code": "l10n_mx_cfdi.cfdi_service.usage", - } - ) - - usage_sequence_id = fields.Many2one( - "ir.sequence", - string="Secuencia de uso de Folios", - default=_create_usage_sequence, - ondelete="cascade", - ) - - @api.ondelete(at_uninstall=False) - def _unlink_related_usage_sequence(self): - self.usage_sequence_id.unlink() - - def _get_client(self): - self.ensure_one() - - facturama.sandbox = True if self.sandbox_mode else False - facturama._credentials = (self.user, self.password) - return facturama - - def register_csd( - self, rfc: str, cert_base64: bytes, key_base64: bytes, key_password: str - ): - self.ensure_one() - - client = self._get_client() - try: - client.csdsMultiEmisor.build_http_request( - "post", - "csds", - { - "Rfc": str(rfc).upper(), - "Certificate": cert_base64.decode("utf-8"), - "PrivateKey": key_base64.decode("utf-8"), - "PrivateKeyPassword": key_password, - }, - version=2, - ) - except facturama.MalformedRequestError as e: - error_message = str(e.error_json["Message"]) + "\n" - if "ModelState" in e.error_json: - model_state = e.error_json["ModelState"] - for entry in model_state: - error_message += str(model_state[entry]) + "\n" - - raise UserError(error_message) - - def unregister_csd(self, rfc): - self.ensure_one() - - client = self._get_client() - client.csdsMultiEmisor.delete(rfc) - - def get_csd_status(self, rfc: str): - self.ensure_one() - - client = self._get_client() - return client.csdsMultiEmisor.get_by_rfc(rfc) - - def create_cfdi(self, cfdi_data: dict): - self.ensure_one() - if not self.stamps_available: - raise UserError( - "No hay folios disponibles para emitir CFDIs.\n" - "Comuníquese con el administrador del sistema." - ) - - client = self._get_client() - try: - res = client.CfdiMultiEmisor.build_http_request( - "post", "cfdis", cfdi_data, version=6 - ) - if "Id" in res: - _logger.info("CFDI creado: %s", res["Id"]) - self.usage_sequence_id.next_by_id() - return res - except facturama.MalformedRequestError as e: - error_message = ( - "Se produjo un error el crear el CFDI: %s\n" % e.error_json["Message"] - ) - if "ModelState" in e.error_json: - model_state = e.error_json["ModelState"] - error_message += json.dumps(model_state, indent=4) + "\n" - - raise UserError(error_message) - except facturama.ApiError as e: - logging.error(e) - raise UserError("Ocurrió un error con el servicio de facturación.\n") - - def get_cfdi_pdf(self, cfdi_id: str): - client = self._get_client() - return client.CfdiMultiEmisor.get_by_file("pdf", "IssuedLite", cfdi_id) - - def get_cfdi_xml(self, cfdi_id: str): - client = self._get_client() - return client.CfdiMultiEmisor.get_by_file("xml", "IssuedLite", cfdi_id) - - def cancel_cfdi(self, cfdi_id: str, reason, uuid_replacement): - client = self._get_client() - _logger.info("Cancelando CFDI %s", cfdi_id) - return client.CfdiMultiEmisor.delete(cfdi_id, reason, uuid_replacement) - - def get_cancellation_request_proof(self, cfdi_id: str): - client = self._get_client() - res = client.CfdiMultiEmisor.build_http_request( - "get", f"acuse/pdf/issuedLite/{cfdi_id}" - ) - return res["Content"] - - def check_cfdi_status(self, uudi, issuer_rfc, receiver_rfc, amount_total): - client = self._get_client() - _logger.info("Consultando estado de CFDI %s", uudi) - res = client.CfdiMultiEmisor.build_http_request( - "get", - "cfdi/status", - params={ - "uuid": uudi, - "issuerRfc": issuer_rfc, - "receiverRfc": receiver_rfc, - "total": amount_total, - }, - ) - status = res["Status"] - _logger.info("Estado de CFDI %s: %s", uudi, status) - - # mapping of status - if status == "Vigente": - return "published" - - if status == "Cancelado": - return "cancelled" - - if status == "No Encontrado": - return "unknown" diff --git a/l10n_mx_cfdi/models/cfdi_service_topup.py b/l10n_mx_cfdi/models/cfdi_service_topup.py deleted file mode 100644 index 128378b..0000000 --- a/l10n_mx_cfdi/models/cfdi_service_topup.py +++ /dev/null @@ -1,48 +0,0 @@ -from odoo import api, fields, models - - -class CFDIServiceTopUp(models.Model): - _name = "l10n_mx_cfdi.cfdi_service.topup" - _description = "CFDI Service Top Up" - - topup_date = fields.Datetime( - string="Fecha de Adquisición", default=fields.Datetime.now() - ) - stamp_number = fields.Integer(string="Cantidad de Folios", required=True) - stamp_price = fields.Monetary( - string="Precio por Folio", required=True, currency_field="currency_id" - ) - total = fields.Monetary( - string="Precio Total", - compute="_compute_total", - store=True, - currency_field="currency_id", - ) - - service_id = fields.Many2one( - "l10n_mx_cfdi.cfdi_service", - string="Servicio", - required=True, - ondelete="cascade", - ) - partner_id = fields.Many2one( - "res.partner", - string="Contacto", - required=True, - readonly=True, - ondelete="restrict", - default=lambda self: self.env.user.partner_id, - ) - - currency_id = fields.Many2one( - "res.currency", - string="Moneda", - required=True, - readonly=True, - default=lambda self: self.env.company.currency_id, - ) - - @api.depends("stamp_number", "stamp_price") - def _compute_total(self): - for record in self: - record.total = record.stamp_number * record.stamp_price diff --git a/l10n_mx_cfdi/models/mail_template.py b/l10n_mx_cfdi/models/mail_template.py deleted file mode 100644 index 3647a89..0000000 --- a/l10n_mx_cfdi/models/mail_template.py +++ /dev/null @@ -1,47 +0,0 @@ -from odoo import models - - -class MailTemplate(models.Model): - _inherit = "mail.template" - - def generate_email(self, res_ids, fields): - """Override to add the CFDI attachment to the email.""" - - res = super().generate_email(res_ids, fields) - - multi_mode = True - if isinstance(res_ids, int): - res_ids = [res_ids] - multi_mode = False - - if self.model not in ["account.move", "account.payment"]: - return res - - records = self.env[self.model].browse(res_ids) - for record in records: - record_data = res[record.id] if multi_mode else res - related_cfdi_documents = record.related_cert_ids - - # existent documents will be replaced by their cfdi counterpart - if related_cfdi_documents: - record_data["attachments"] = [] - - for doc in related_cfdi_documents: - if doc.state == "published": - doc.download_files_if_needed() - - record_data["attachments"].append( - ( - doc.pdf_filename, - doc.pdf_file, - ) - ) - - record_data["attachments"].append( - ( - doc.xml_filename, - doc.xml_file, - ) - ) - - return res diff --git a/l10n_mx_cfdi/models/partner.py b/l10n_mx_cfdi/models/partner.py deleted file mode 100644 index f83f424..0000000 --- a/l10n_mx_cfdi/models/partner.py +++ /dev/null @@ -1,19 +0,0 @@ -from odoo import fields, models - - -class Partner(models.Model): - _inherit = "res.partner" - - # Add a new field to the res.partner model, called tax_regime with SAT tax regime - # source https://www.cfdi.org.mx/catalogos-de-cfdi/regimen-fiscal/ - tax_regime = fields.Many2one( - "l10n_mx_catalogs.c_regimen_fiscal", string="Régimen Fiscal" - ) - - cfdi_use_id = fields.Many2one("l10n_mx_catalogs.c_uso_cfdi", string="Uso de CFDI") - payment_method_id = fields.Many2one( - "l10n_mx_catalogs.c_metodo_pago", string="Método de pago" - ) - payment_form_id = fields.Many2one( - "l10n_mx_catalogs.c_forma_pago", string="Forma de pago" - ) diff --git a/l10n_mx_cfdi/models/product.py b/l10n_mx_cfdi/models/product.py deleted file mode 100644 index ce178de..0000000 --- a/l10n_mx_cfdi/models/product.py +++ /dev/null @@ -1,16 +0,0 @@ -from odoo import fields, models - - -class ProductTemplate(models.Model): - # Add CFDI required product code and measure unit to product template - # reference: https://www.cfdi.org.mx/catalogos-de-cfdi/productos-y-servicios/ - - _inherit = "product.template" - - l10n_mx_cfdi_product_code_id = fields.Many2one( - "l10n_mx_catalogs.c_clave_prod_serv", string="Código de Producto" - ) - - l10n_mx_cfdi_product_measurement_unit_id = fields.Many2one( - "l10n_mx_catalogs.c_clave_unidad", string="Unidad de Medida" - ) diff --git a/l10n_mx_cfdi/models/res_company.py b/l10n_mx_cfdi/models/res_company.py deleted file mode 100644 index bea0589..0000000 --- a/l10n_mx_cfdi/models/res_company.py +++ /dev/null @@ -1,21 +0,0 @@ -from odoo import fields, models - - -class ResCompany(models.Model): - _inherit = "res.company" - - l10n_mx_cfdi_auto = fields.Boolean( - "Create CFDI on post", - default=True, - help="Enable to automatically sign CFDI when validating invoices.", - ) - - l10n_mx_cfdi_enabled = fields.Boolean( - "Enable CFDI", - compute="_compute_l10n_mx_cfdi_enabled", - help="Enable CFDI for this company.", - ) - - def _compute_l10n_mx_cfdi_enabled(self): - for company in self: - company.l10n_mx_cfdi_enabled = company.country_id == self.env.ref("base.mx") diff --git a/l10n_mx_cfdi/models/res_config_settings.py b/l10n_mx_cfdi/models/res_config_settings.py deleted file mode 100644 index cd408ff..0000000 --- a/l10n_mx_cfdi/models/res_config_settings.py +++ /dev/null @@ -1,12 +0,0 @@ -from odoo import fields, models - - -class ResConfigSettings(models.TransientModel): - _inherit = "res.config.settings" - - l10n_mx_cfdi_auto = fields.Boolean( - "Create CFDI on post", - related="company_id.l10n_mx_cfdi_auto", - help="Enable to automatically sign CFDI when validating invoices.", - readonly=False, - ) diff --git a/l10n_mx_cfdi/readme.md b/l10n_mx_cfdi/readme.md deleted file mode 100644 index 0e18fcb..0000000 --- a/l10n_mx_cfdi/readme.md +++ /dev/null @@ -1,7 +0,0 @@ -# MX CFDI - -Modulo de generación de Certificados Digitales por Internet (CFDI) para Odoo - -# Sellos de Prueba - -https://apisandbox.facturama.mx/guias/conocimientos/sellos-digitales-pruebas diff --git a/l10n_mx_cfdi/reports/report_cfdi_blocks.xml b/l10n_mx_cfdi/reports/report_cfdi_blocks.xml deleted file mode 100644 index d3a106d..0000000 --- a/l10n_mx_cfdi/reports/report_cfdi_blocks.xml +++ /dev/null @@ -1,274 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/l10n_mx_cfdi/reports/report_external_layouts.xml b/l10n_mx_cfdi/reports/report_external_layouts.xml deleted file mode 100644 index 42dad4b..0000000 --- a/l10n_mx_cfdi/reports/report_external_layouts.xml +++ /dev/null @@ -1,437 +0,0 @@ - - - - - - - - - - - - diff --git a/l10n_mx_cfdi/reports/report_invoice.xml b/l10n_mx_cfdi/reports/report_invoice.xml deleted file mode 100644 index 65eb994..0000000 --- a/l10n_mx_cfdi/reports/report_invoice.xml +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - - - - - - - diff --git a/l10n_mx_cfdi/reports/report_payment.xml b/l10n_mx_cfdi/reports/report_payment.xml deleted file mode 100644 index 1ea801e..0000000 --- a/l10n_mx_cfdi/reports/report_payment.xml +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - - - diff --git a/l10n_mx_cfdi/security/ir.model.access.csv b/l10n_mx_cfdi/security/ir.model.access.csv deleted file mode 100644 index f32f112..0000000 --- a/l10n_mx_cfdi/security/ir.model.access.csv +++ /dev/null @@ -1,14 +0,0 @@ -id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_l10n_mx_cfdi_issuer,access_l10n_mx_cfdi_issuer,model_l10n_mx_cfdi_issuer,base.group_user,1,0,0,0 -manage_l10n_mx_cfdi_issuer,manage_l10n_mx_cfdi_issuer,model_l10n_mx_cfdi_issuer,account.group_account_manager,1,1,1,1 -access_l10n_mx_cfdi_cfdi_service,access_l10n_mx_cfdi_cfdi_service,l10n_mx_cfdi.model_l10n_mx_cfdi_cfdi_service,base.group_user,1,0,0,0 -manage_l10n_mx_cfdi_cfdi_service,manage_l10n_mx_cfdi_cfdi_service,l10n_mx_cfdi.model_l10n_mx_cfdi_cfdi_service,base.group_system,1,1,1,1 -l10n_mx_cfdi.access_l10n_mx_cfdi_cfdi_service_topup,access_l10n_mx_cfdi_cfdi_service_topup,l10n_mx_cfdi.model_l10n_mx_cfdi_cfdi_service_topup,base.group_user,1,0,0,0 -l10n_mx_cfdi.admin_l10n_mx_cfdi_cfdi_service_topup,admin_l10n_mx_cfdi_cfdi_service_topup,l10n_mx_cfdi.model_l10n_mx_cfdi_cfdi_service_topup,base.group_system,1,1,1,1 -access_l10n_mx_cfdi_document,Acceso a certificados 4.0,model_l10n_mx_cfdi_document,base.group_user,1,1,1,1 -cancel_l10n_mx_cfdi_document,Cancelar a certificados,model_l10n_mx_cfdi_document_cancel,base.group_user,1,1,1,1 -l10n_mx_cfdi.access_l10n_mx_cfdi_document_relation,access_l10n_mx_cfdi_document_relation,l10n_mx_cfdi.model_l10n_mx_cfdi_document_relation,base.group_user,1,1,1,1 -l10n_mx_cfdi.access_l10n_mx_cfdi_generic_invoice_create,access_l10n_mx_cfdi_generic_invoice_create,l10n_mx_cfdi.model_l10n_mx_cfdi_generic_invoice_create,base.group_user,1,1,1,1 -l10n_mx_cfdi.access_l10n_mx_cfdi_series,access_l10n_mx_cfdi_series,l10n_mx_cfdi.model_l10n_mx_cfdi_series,base.group_user,1,0,0,0 -l10n_mx_cfdi.manage_l10n_mx_cfdi_series,manage_l10n_mx_cfdi_series,l10n_mx_cfdi.model_l10n_mx_cfdi_series,account.group_account_manager,1,1,1,1 -l10n_mx_cfdi.access_l10n_mx_cfdi_download_cfdi_files_wizard,access_l10n_mx_cfdi_download_cfdi_files_wizard,l10n_mx_cfdi.model_l10n_mx_cfdi_download_cfdi_files_wizard,base.group_user,1,1,1,1 diff --git a/l10n_mx_cfdi/security/l10n_mx_cfdi_security.xml b/l10n_mx_cfdi/security/l10n_mx_cfdi_security.xml deleted file mode 100644 index eb20f09..0000000 --- a/l10n_mx_cfdi/security/l10n_mx_cfdi_security.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - Document CFDI Admin Access - - [(1,'=',1)] - - - - - - Document CFDI Company Access - - ['|',('company_id', '=', False),('company_id', 'in', company_ids)] - - - - - - - Issuer CFDI Admin Access - - [(1,'=',1)] - - - - - - Issuer CFDI Company Access - - ['|',('company_id', '=', False),('company_id', 'in', company_ids)] - - - - - - CFDI Service Admin Access - - [(1,'=',1)] - - - - - CFDI Service Company Access - - ['|',('company_ids', '=', False),('company_ids', 'in', company_ids)] - - - - diff --git a/l10n_mx_cfdi/views/cfdi_document_view.xml b/l10n_mx_cfdi/views/cfdi_document_view.xml deleted file mode 100644 index 1d1e728..0000000 --- a/l10n_mx_cfdi/views/cfdi_document_view.xml +++ /dev/null @@ -1,161 +0,0 @@ - - - Documentos CFDI - l10n_mx_cfdi.document - - - - - - - - - - - - - - - - - - - - - - - - Documentos CFDI - l10n_mx_cfdi.document - - - - - - - - - - - - - - - - - - Documentos CFDI - l10n_mx_cfdi.document - -
- - - - - - - - - - - - - - - - - -
- - -
-
-
Documentos Relacionados
- - - - - - -
-
- - -
-
-
-
-
-
diff --git a/l10n_mx_cfdi/views/cfdi_documents_issued_view.xml b/l10n_mx_cfdi/views/cfdi_documents_issued_view.xml deleted file mode 100644 index 23fc06a..0000000 --- a/l10n_mx_cfdi/views/cfdi_documents_issued_view.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - CFDI Emitidos - l10n_mx_cfdi.document - tree,form - [('type','in',['I', 'E', 'P'])] - {'search_default_filter_published': 1} - - - - diff --git a/l10n_mx_cfdi/views/cfdi_issuer_view.xml b/l10n_mx_cfdi/views/cfdi_issuer_view.xml deleted file mode 100644 index c64b536..0000000 --- a/l10n_mx_cfdi/views/cfdi_issuer_view.xml +++ /dev/null @@ -1,113 +0,0 @@ - - - - Emisor de CFDIs - l10n_mx_cfdi.issuer - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - Emisores - l10n_mx_cfdi.issuer - tree,form - - - - -
diff --git a/l10n_mx_cfdi/views/cfdi_menu.xml b/l10n_mx_cfdi/views/cfdi_menu.xml deleted file mode 100644 index 37cacb8..0000000 --- a/l10n_mx_cfdi/views/cfdi_menu.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/l10n_mx_cfdi/views/cfdi_series_view.xml b/l10n_mx_cfdi/views/cfdi_series_view.xml deleted file mode 100644 index 3612b03..0000000 --- a/l10n_mx_cfdi/views/cfdi_series_view.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - Serie CFDI - l10n_mx_cfdi.series - -
- - - - - - - - - -
-
-
- - - Serie CFDI - l10n_mx_cfdi.series - - - - - - - - - - - - Serie CFDI - l10n_mx_cfdi.series - tree,form - - - - - -
-
diff --git a/l10n_mx_cfdi/views/cfdi_service_view.xml b/l10n_mx_cfdi/views/cfdi_service_view.xml deleted file mode 100644 index 7e8796d..0000000 --- a/l10n_mx_cfdi/views/cfdi_service_view.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - Configuración del Servicio CFDI - l10n_mx_cfdi.cfdi_service - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
- - - - Configuración de Servicio - l10n_mx_cfdi.cfdi_service - tree,form - - - -
diff --git a/l10n_mx_cfdi/views/inherit/account_view_account_payment_form.xml b/l10n_mx_cfdi/views/inherit/account_view_account_payment_form.xml deleted file mode 100644 index bef3935..0000000 --- a/l10n_mx_cfdi/views/inherit/account_view_account_payment_form.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - Account Payment: Payment Form - account.payment - - - - - - - - - - - - - diff --git a/l10n_mx_cfdi/views/inherit/account_view_account_payment_register_form.xml b/l10n_mx_cfdi/views/inherit/account_view_account_payment_register_form.xml deleted file mode 100644 index 187c307..0000000 --- a/l10n_mx_cfdi/views/inherit/account_view_account_payment_register_form.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - Account Move Payment: CFDI generation - account.payment.register - - - - - - - - diff --git a/l10n_mx_cfdi/views/inherit/account_view_move_form.xml b/l10n_mx_cfdi/views/inherit/account_view_move_form.xml deleted file mode 100644 index 883f5b0..0000000 --- a/l10n_mx_cfdi/views/inherit/account_view_move_form.xml +++ /dev/null @@ -1,145 +0,0 @@ - - - Account Move: CFDI generation - account.move - - - - Enviar - - - Enviar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/l10n_mx_cfdi/views/inherit/account_view_out_invoice_tree.xml b/l10n_mx_cfdi/views/inherit/account_view_out_invoice_tree.xml deleted file mode 100644 index e6706fb..0000000 --- a/l10n_mx_cfdi/views/inherit/account_view_out_invoice_tree.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - account.out.invoice.tree.cfdi - account.move - - - - - - - - - - - account.invoice.filter.cfdi - account.move - - - - - - - - - diff --git a/l10n_mx_cfdi/views/inherit/base_view_partner_form.xml b/l10n_mx_cfdi/views/inherit/base_view_partner_form.xml deleted file mode 100644 index fdf2c7c..0000000 --- a/l10n_mx_cfdi/views/inherit/base_view_partner_form.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - Partner: tax regime field - res.partner - - - - 1 - - - - - - - - - - diff --git a/l10n_mx_cfdi/views/inherit/product_product_template_form_view.xml b/l10n_mx_cfdi/views/inherit/product_product_template_form_view.xml deleted file mode 100644 index 619847f..0000000 --- a/l10n_mx_cfdi/views/inherit/product_product_template_form_view.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - Product Template: CFDI code and measurement unit fields - product.template - - - - - - - - - - - diff --git a/l10n_mx_cfdi/views/inherit/res_config_settings_views.xml b/l10n_mx_cfdi/views/inherit/res_config_settings_views.xml deleted file mode 100644 index ac0acc5..0000000 --- a/l10n_mx_cfdi/views/inherit/res_config_settings_views.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - res.config.settings.view.form.inherit.l10n.mx - res.config.settings - - - - 1 - - -
-
- - -
-
-
-
-
-
-
-
diff --git a/l10n_mx_cfdi/wizard/__init__.py b/l10n_mx_cfdi/wizard/__init__.py deleted file mode 100644 index 4d0db3a..0000000 --- a/l10n_mx_cfdi/wizard/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from . import document_cancel -from . import create_cfdi_publico_en_general -from . import download_cfdi_files diff --git a/l10n_mx_cfdi/wizard/account_invoice_send_views.xml b/l10n_mx_cfdi/wizard/account_invoice_send_views.xml deleted file mode 100644 index b40ed44..0000000 --- a/l10n_mx_cfdi/wizard/account_invoice_send_views.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - account.invoice.send.no_print - account.invoice.send - - -
- 1 -
-
- 1 -
-
-
-
diff --git a/l10n_mx_cfdi/wizard/create_cfdi_publico_en_general.py b/l10n_mx_cfdi/wizard/create_cfdi_publico_en_general.py deleted file mode 100644 index 361e7be..0000000 --- a/l10n_mx_cfdi/wizard/create_cfdi_publico_en_general.py +++ /dev/null @@ -1,168 +0,0 @@ -from odoo import api, fields, models -from odoo.exceptions import ValidationError -from odoo.tools.safe_eval import datetime - - -class CFDIGenericInvoiceCreate(models.TransientModel): - _name = "l10n_mx_cfdi.generic_invoice_create" - _description = "Create a Generic CFDI Invoice" - - periodicity_id = fields.Many2one( - "l10n_mx_catalogs.c_periodicidad", string="Periodicidad", required=True - ) - - meses_id = fields.Many2one("l10n_mx_catalogs.c_meses", string="Mes", required=True) - - year = fields.Text(string="Año", required=True) - - issuer_id = fields.Many2one( - "l10n_mx_cfdi.issuer", - string="Emisor", - domain=[("registered", "=", True)], - required=True, - ) - - payment_method_id = fields.Many2one( - "l10n_mx_catalogs.c_metodo_pago", string="Metodo de Pago", readonly=True - ) - payment_form_id = fields.Many2one( - "l10n_mx_catalogs.c_forma_pago", string="Forma de Pago" - ) - - fiscal_regime_id = fields.Many2one( - "l10n_mx_catalogs.c_regimen_fiscal", - string="Régimen Fiscal", - required=True, - compute="_compute_fiscal_regime_id", - readonly=True, - ) - - cfdi_use_id = fields.Many2one( - "l10n_mx_catalogs.c_uso_cfdi", - string="Uso de CFDI", - required=True, - readonly=True, - ) - move_ids = fields.Many2many("account.move", string="Facturas", required=True) - - date = fields.Date(string="Fecha", required=True, default=fields.Date.context_today) - - @api.depends("periodicity_id") - def _compute_fiscal_regime_id(self): - for record in self: - if record.periodicity_id.code == "05": - record.fiscal_regime_id = self.env.ref( - "l10n_mx_catalogs.c_regimen_fiscal_621" - ) - else: - record.fiscal_regime_id = self.env.ref( - "l10n_mx_catalogs.c_regimen_fiscal_616" - ) - - @api.model - def default_get(self, field_names): - defaults_dict = super().default_get(field_names) - context = self.env.context - - if context["active_model"] == "account.move": - related_invoice_objs = self.env["account.move"].browse( - context["active_ids"] - ) - for invoice in related_invoice_objs: - self._validate_invoice(invoice) - - defaults_dict.update({"move_ids": related_invoice_objs}) - - currentDateTime = datetime.datetime.now() - defaults_dict.update( - { - "year": currentDateTime.strftime("%Y"), - "cfdi_use_id": self.env.ref("l10n_mx_catalogs.c_uso_cfdi_S01").id, - "payment_method_id": self.env.ref( - "l10n_mx_catalogs.c_metodo_pago_PUE" - ).id, - "payment_form_id": self.env.ref("l10n_mx_catalogs.c_forma_pago_01").id, - } - ) - return defaults_dict - - @api.constrains("move_ids") - def _validate_included_invoices(self): - for record in self: - for invoice in record.move_ids: - self._validate_invoice(invoice) - - @api.model - def _validate_invoice(self, invoice): - invoice.ensure_one() - - if invoice.state != "posted": - raise ValidationError( - 'La factura "%s" factura que no este confirmada' % invoice.name - ) - - related_cfdi = invoice.related_cert_ids.filtered_domain( - [("state", "=", "published")] - ) - if related_cfdi: - raise ValidationError( - "La factura '%s' ya tiene un CFDI publicado" % invoice.name - ) - - err_msg = invoice.validate_invoice_items_for_cfdi_generation() - if err_msg: - raise ValidationError(err_msg) - - def create_cfdi(self): - """Emit CFDI 'Al Público en General'""" - - self.ensure_one() - receiver = self.env.ref( - "l10n_mx_cfdi.l10n_mx_cfdi_res_partner_publico_en_general" - ) - cert = self.env["l10n_mx_cfdi.document"].create( - { - "type": "I", - "issuer_id": self.issuer_id.id, - "receiver_id": receiver.id, - "is_global_note": True, - } - ) - - try: - all_items_data = [] - for invoice in self.move_ids: - items_data = invoice.gather_invoice_cfdi_items_data() - all_items_data.extend(items_data) - currency = self.move_ids[0].currency_id - - cfdi_data = { - "Currency": currency[0].name, - "ExpeditionPlace": self.issuer_id.zip, - "CfdiType": "I", - "Date": self.move_ids._format_cfdi_date_str(self.date), - "PaymentForm": self.payment_form_id.code, - "PaymentMethod": self.payment_method_id.code, - "GlobalInformation": { - "Periodicity": self.periodicity_id.code, - "Months": self.meses_id.code, - "Year": self.year, - }, - "Receiver": { - "Name": receiver.name, - "Rfc": receiver.vat, - "CfdiUse": self.cfdi_use_id.code, - "FiscalRegime": receiver.tax_regime.code, - "TaxZipCode": self.issuer_id.zip, - }, - "Items": all_items_data, - } - - cert.publish(cfdi_data) - - for invoice in self.move_ids: - invoice.related_cert_ids = [(4, cert.id)] - - except Exception as e: - cert.unlink() - raise e diff --git a/l10n_mx_cfdi/wizard/create_cfdi_publico_en_general.xml b/l10n_mx_cfdi/wizard/create_cfdi_publico_en_general.xml deleted file mode 100644 index 0c72c28..0000000 --- a/l10n_mx_cfdi/wizard/create_cfdi_publico_en_general.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - Crear CFDI al Público en General - l10n_mx_cfdi.generic_invoice_create - -
- - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - Crear CFDI al Público en General - l10n_mx_cfdi.generic_invoice_create - form - new - - list - -
diff --git a/l10n_mx_cfdi/wizard/document_cancel.py b/l10n_mx_cfdi/wizard/document_cancel.py deleted file mode 100644 index acaf737..0000000 --- a/l10n_mx_cfdi/wizard/document_cancel.py +++ /dev/null @@ -1,75 +0,0 @@ -from odoo import api, fields, models - - -class CertificateCancel(models.TransientModel): - _name = "l10n_mx_cfdi.document_cancel" - _description = "Certificate Cancel" - - certificate_ids = fields.Many2many( - "l10n_mx_cfdi.document", string="Certificados", required=True - ) - cancel_reason_id = fields.Many2one( - "l10n_mx_catalogs.c_motivo_cancelacion", string="Razón", required=True - ) - replacement_certificate_id = fields.Many2one( - "l10n_mx_cfdi.document", string="Reemplazo" - ) - single_cancel = fields.Boolean(default=True) - - related_invoices = fields.Many2many("account.move", string="Facturas Relacionadas") - - requires_replacement = fields.Boolean( - compute="_compute_requires_replacement", store=False - ) - simulate_operation = fields.Boolean( - default=False, - help="Simulate the cancel operation without sending the request to the SAT", - groups="base.group_system", - ) - - @api.depends("cancel_reason_id") - def _compute_requires_replacement(self): - for record in self: - record.requires_replacement = record.cancel_reason_id.code == "01" - - @api.model - def default_get(self, field_names): - defaults_dict = super().default_get(field_names) - context = self.env.context - - if context["active_model"] == "account.move": - related_invoice_objs = self.env["account.move"].browse( - context["active_ids"] - ) - defaults_dict.update( - { - "certificate_ids": related_invoice_objs.related_cert_ids.filtered_domain( - [("state", "=", "published")] - ), - "related_invoices": related_invoice_objs, - "cancel_reason_id": self.env.ref( - "l10n_mx_catalogs.c_motivo_cancelacion_02" - ).id, - } - ) - - return defaults_dict - - def cancel_certificate(self): - for record in self: - for certificate in record.certificate_ids: - if certificate.state == "published": - certificate.cancel( - record.cancel_reason_id.code, - record.replacement_certificate_id, - record.simulate_operation, - ) - - for invoice in record.certificate_ids.related_invoice_id: - invoice._compute_cfdi_document_id() - - if self.env.company.l10n_mx_cfdi_auto: - invoice.button_draft() - - for payment in record.certificate_ids.related_payment_id: - payment.move_id._compute_cfdi_document_id() diff --git a/l10n_mx_cfdi/wizard/document_cancel_form.xml b/l10n_mx_cfdi/wizard/document_cancel_form.xml deleted file mode 100644 index 795d685..0000000 --- a/l10n_mx_cfdi/wizard/document_cancel_form.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - Cancelar CFDI - l10n_mx_cfdi.document_cancel - -
- - - - - - - - - - - - - -
-
-
-
-
- - - Cancelar CFDI - l10n_mx_cfdi.document_cancel - form - new - -
diff --git a/l10n_mx_cfdi/wizard/download_cfdi_files.py b/l10n_mx_cfdi/wizard/download_cfdi_files.py deleted file mode 100644 index 768dc6e..0000000 --- a/l10n_mx_cfdi/wizard/download_cfdi_files.py +++ /dev/null @@ -1,81 +0,0 @@ -import base64 -import io -import zipfile - -from odoo import api, fields, models - - -class DownloadCFDIFilesWizard(models.TransientModel): - _name = "l10n_mx_cfdi.download_cfdi_files_wizard" - _description = "Create ZIP file containing selected invoices CFDI" - - invoice_ids = fields.Many2many("account.move", string="Facturas", required=True) - cfdi_document_ids = fields.Many2many( - "l10n_mx_cfdi.document", - string="CFDI Documents", - required=True, - relation="l10n_mx_cfdi_download_cfdi_files_wizard_rel", - ) - - zip_file = fields.Many2one( - "ir.attachment", string="Zip File", readonly=True, ondelete="cascade" - ) - - @api.model - def default_get(self, field_names): - defaults_dict = super().default_get(field_names) - context = self.env.context - - if context["active_model"] == "account.move": - related_invoice_objs = self.env["account.move"].browse( - context["active_ids"] - ) - defaults_dict.update( - { - "invoice_ids": related_invoice_objs, - "cfdi_document_ids": related_invoice_objs.mapped( - "cfdi_document_id" - ), - } - ) - - return defaults_dict - - def _create_zip_file(self): - # prepare zip file - stream = io.BytesIO() - zip_archive = zipfile.ZipFile(stream, "w") - - # add docs to zip file - for cfdi_doc in self.cfdi_document_ids: - if cfdi_doc: - cfdi_doc.download_files_if_needed() - - zip_archive.writestr( - cfdi_doc.pdf_filename, base64.b64decode(cfdi_doc.pdf_file) - ) - zip_archive.writestr( - cfdi_doc.xml_filename, base64.b64decode(cfdi_doc.xml_file) - ) - - zip_archive.close() - - bytes_of_zipfile = stream.getvalue() - - # create attachment - self.zip_file = self.env["ir.attachment"].create( - { - "name": "cfdis.zip", - "datas": base64.b64encode(bytes_of_zipfile), - "type": "binary", - } - ) - - def action_download_zip(self): - self._create_zip_file() - - return { - "type": "ir.actions.act_url", - "url": "/web/content/%s?download=true" % self.zip_file.id, - "target": "self", - } diff --git a/l10n_mx_cfdi/wizard/download_cfdi_files_wizard.xml b/l10n_mx_cfdi/wizard/download_cfdi_files_wizard.xml deleted file mode 100644 index c79b6ba..0000000 --- a/l10n_mx_cfdi/wizard/download_cfdi_files_wizard.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - Descargar CFDIs - l10n_mx_cfdi.download_cfdi_files_wizard - -
-

CFDIs Encontrados

- - - - - - - - - - - -
-
- -
-
- - - Descargar CFDIs - l10n_mx_cfdi.download_cfdi_files_wizard - form - new - - list - -
-
diff --git a/setup/l10n_mx_cfdi/odoo/addons/l10n_mx_cfdi b/setup/l10n_mx_cfdi/odoo/addons/l10n_mx_cfdi deleted file mode 120000 index 837a116..0000000 --- a/setup/l10n_mx_cfdi/odoo/addons/l10n_mx_cfdi +++ /dev/null @@ -1 +0,0 @@ -../../../../l10n_mx_cfdi \ No newline at end of file diff --git a/setup/l10n_mx_cfdi/setup.py b/setup/l10n_mx_cfdi/setup.py deleted file mode 100644 index 28c57bb..0000000 --- a/setup/l10n_mx_cfdi/setup.py +++ /dev/null @@ -1,6 +0,0 @@ -import setuptools - -setuptools.setup( - setup_requires=['setuptools-odoo'], - odoo_addon=True, -)