diff --git a/l10n_mx_facturae/__openerp__.py b/l10n_mx_facturae/__openerp__.py index d007d74f9a4c632a43d98bcb3e9dfee0415a5eb4..648897c065809d7028805d9a197b43136d68aaad 100644 --- a/l10n_mx_facturae/__openerp__.py +++ b/l10n_mx_facturae/__openerp__.py @@ -26,6 +26,7 @@ 'wizard/account_invoice_refund.xml', 'report/account_invoice.xml', 'report/account_voucher.xml', + 'data/ir_cron.xml', 'data/email_template.xml', 'data/ir_attachment_facturae_config.xml', ], diff --git a/l10n_mx_facturae/data/ir_cron.xml b/l10n_mx_facturae/data/ir_cron.xml new file mode 100644 index 0000000000000000000000000000000000000000..95322d884e17c7c2eba5343c5c3b3e454e39bd2f --- /dev/null +++ b/l10n_mx_facturae/data/ir_cron.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding='UTF-8'?> +<openerp> + <data noupdate="1"> + <record model="ir.cron" id="ir_cron_invoices_pending_to_cancel"> + <field name="name">Invoices pending to cancel</field> + <field name="interval_number">2</field> + <field name="interval_type">hours</field> + <field name="numbercall">-1</field> + <field name="model" eval="'account.invoice'"/> + <field name="function" eval="'cron_invoices_pending_to_cancel'"/> + <field name="args" eval="'(None,)'"/> + <field name="active" eval="False" /> + </record> + </data> +</openerp> + diff --git a/l10n_mx_facturae/migrations/2.8.0/post-migration.py b/l10n_mx_facturae/migrations/2.8.0/post-migration.py new file mode 100644 index 0000000000000000000000000000000000000000..b475eae4a37e5c1d01417628f41bc9be5f4ae9e4 --- /dev/null +++ b/l10n_mx_facturae/migrations/2.8.0/post-migration.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +import logging + +from openupgradelib import openupgrade as tools + +from openerp import SUPERUSER_ID + + +_logger = logging.getLogger(__name__) + + +def compute_cfdi_id(env): + """Update reference to `ir.attachment.facturae.mx` for invoices""" + ir_attachment_mx_obj = env['ir.attachment.facturae.mx'] + invoices = env['account.invoice'].search([('state', 'in', ['open', 'paid'])]) + _logger.info('Updating reference for cfdi on ' + str(len(invoices)) + ' invoices') + for invoice in invoices: + related_attachment = ir_attachment_mx_obj.search( + [ + ('res_id', '=', invoice.id), + ('type_attachment', '=', 'account.invoice'), + ('company_id', '=', invoice.company_id.id), + ('state', 'in', ['signed', 'done']), + ] + ) + if related_attachment: + invoice.cfdi_id = related_attachment + + +@tools.migrate(use_env=True, uid=SUPERUSER_ID) +def migrate(env, installed_version): + compute_cfdi_id(env) diff --git a/l10n_mx_facturae/models/account_invoice.py b/l10n_mx_facturae/models/account_invoice.py index 71c7a2a47c95422796c87b66ddcd4b79a7049fab..962282e008bc14051bb345ace3ca4b85a44288fb 100644 --- a/l10n_mx_facturae/models/account_invoice.py +++ b/l10n_mx_facturae/models/account_invoice.py @@ -23,7 +23,11 @@ _logger = logging.getLogger(__name__) class AccountInvoice(models.Model): - _inherit = 'account.invoice' + _name = 'account.invoice' + _inherit = [ + 'account.invoice', + 'base.cfdi', + ] @api.model def _default_address_issued_id(self): @@ -179,13 +183,6 @@ class AccountInvoice(models.Model): 'cfdi.use', 'CFDI use', readonly=True, states={'draft': [('readonly', False)]}, ) - cfdi_id = fields.Many2one( - 'ir.attachment.facturae.mx', 'CFDI', compute='_compute_cfdi_id', - ) - cfdi_folio_fiscal = fields.Char( - 'CFD-I Folio Fiscal', compute='_compute_cfdi_folio_fiscal', - help='Folio used in the electronic invoice', - ) cfdi_adenda_id = fields.Many2one( 'cfdi.adenda', 'Addendum', help='Select addendum node to use on invoice.', @@ -198,6 +195,18 @@ class AccountInvoice(models.Model): 'cfdi.relation.type', 'CFDI Relation type', ) + state = fields.Selection(selection_add=[('waiting', _('To cancel'))]) + + @api.model + def cron_invoices_pending_to_cancel(self): + invoices = self.env['account.invoice'].search( + [ + ('state', 'in', ['waiting']) + ] + ) + for invoice in invoices: + invoice.action_consult_cancellation_status() + @api.one @api.depends('state') def _compute_datetime(self): @@ -212,31 +221,12 @@ class AccountInvoice(models.Model): ) self.datetime = invoice_datetime - @api.one - def _compute_cfdi_id(self): - ir_attachment_mx_obj = self.env['ir.attachment.facturae.mx'] - related_attachment = ir_attachment_mx_obj.search( - [ - ('res_id', '=', self.id), - ('type_attachment', '=', 'account.invoice'), - ('company_id', '=', self.company_id.id), - ('state', 'in', ['signed', 'done']), - ], - ) - if related_attachment: - self.cfdi_id = related_attachment - - @api.one - @api.depends('state') - def _compute_cfdi_folio_fiscal(self): - self.cfdi_folio_fiscal = self.cfdi_id.uuid - def onchange_partner_id( self, cr, uid, ids, type, partner_id, date_invoice=False, payment_term=False, partner_bank_id=False, company_id=False, context=None, ): - # pylint: disable=too-many-arguments + # pylint: disable=too-many-arguments, redefined-builtin """Copy fields cfdi_use, payment_method_id and cfdi_adenda_id from selected partner """ @@ -279,37 +269,62 @@ class AccountInvoice(models.Model): @signals.invoice_validate.connect def sign_invoice_sat(self, cr, uid, ids, context=None): context = dict(context or {}) - ir_attach_obj = self.pool.get('ir.attachment.facturae.mx') for account_invoice in self.browse(cr, uid, ids, context=context): if account_invoice.journal_id.sign_sat: # Create new CFDI object for this invoice - cfdi_id = ir_attach_obj.create( - cr, uid, - { - 'name': account_invoice.fname_invoice, - 'res_id': account_invoice.id, - 'type_attachment': 'account.invoice', - 'company_id': account_invoice.company_id.id, - }, - context=context, - ) - cfdi = ir_attach_obj.browse( - cr, uid, cfdi_id, context=context, - ) - cfdi.action_validate() + account_invoice.create_cfdi() @api.multi - def action_cancel(self,): - res = super(AccountInvoice, self).action_cancel() + def action_cancel(self): + ''' + Extend `AccountInvoice.action_cancel()`; Cancels the CFDI related to the + invoice + ''' for account_invoice in self: # Maybe a third test could review state, but since # button cancel only is displayed in open state, we decided to not # used third test - if ( + account_invoice.check_if_can_cancel() + res = None + if account_invoice.cfdi_folio_fiscal is False: + res = super(AccountInvoice, self).action_cancel() + elif ( account_invoice.journal_id.sign_sat and account_invoice.cfdi_folio_fiscal ): - account_invoice.cfdi_id.action_cancel() + cancelacion = account_invoice.cancel_cfdi()[0] + if cancelacion is None: + account_invoice.write( + {'state': account_invoice.cfdi_state} + ) + elif cancelacion is False: + self.undo_waiting_state() + else: + res = super(AccountInvoice, self).action_cancel() + account_invoice.unlink_cfdi() + return res + + @api.multi + def undo_waiting_state(self): + """when cancel is negate, undo waiting state""" + self.ensure_one() + if self.state == 'waiting': + self.write({ + 'state': 'open' + }) + + @api.multi + def action_consult_cancellation_status(self): + for account_invoice in self: + res = None + status_cancelacion = account_invoice.consult_cfdi_cancellation_status() + if status_cancelacion is None: + pass + elif status_cancelacion is False: + self.undo_waiting_state() + else: + res = super(AccountInvoice, self).action_cancel() + account_invoice.unlink_cfdi() return res @staticmethod diff --git a/l10n_mx_facturae/models/account_voucher.py b/l10n_mx_facturae/models/account_voucher.py index 4849c4d5b81c80686e55ee766a6b9fd56878f11c..7e54dff529ff010708000a235e64998fef30e050 100644 --- a/l10n_mx_facturae/models/account_voucher.py +++ b/l10n_mx_facturae/models/account_voucher.py @@ -36,6 +36,7 @@ class AccountVoucher(models.Model): """Cancel CFDI for selected vouchers""" res = super(AccountVoucher, self).cancel_voucher() self.cancel_cfdi() + self.unlink_cfdi() return res @api.multi diff --git a/l10n_mx_facturae/tests/features/cancelacion_cfdi_33.feature b/l10n_mx_facturae/tests/features/cancelacion_cfdi_33.feature new file mode 100644 index 0000000000000000000000000000000000000000..0007bec28591fcc349c20a21f62402efefb2acab --- /dev/null +++ b/l10n_mx_facturae/tests/features/cancelacion_cfdi_33.feature @@ -0,0 +1,37 @@ +Feature: Cancelar CFDI's 3.3 que requiere aceptacion de cancelación + Como contador + A fin de cumplir con las reglas de la Resolucion Miscelanea Fiscal para 2017 + Quiero cancelar cfdi's + Una factura requiere aceptación de cancelación cuando su monto excede los + $5000.00, o cuando su emisión tiene mas de 72 horas, segun la regla 2.7.1.39 + de la resolución Miscelánea Fiscal para 2017 + +Scenario: solicitar cancelacion de Factura abierta con monto mayor a $5000.00 + Given factura en estado abierto con un monto de $5500.00 + When cancelo la factura + Then El estado de la Factura es 'Por cancelar' + And se notifica que la factura esta pendiente de cancelar + +Scenario: solicitar cancelacion de factura con mas de 72 horas abierta + Given una factura con mas de 72 horas abierta + When cancelo la factura + Then el estado de la factura es 'Por cancelar' + And se notifica que la factura esta pendiente de cancelar + +Scenario: contribuyente aprueba cancelacion de la factura + Given un factura en estado 'Por cancelar' + When el contribuyente aprueba la cancelacion + Then se cancela la factura + And se notifica que la factura se cancelo + +Scenario: contribuyente rechaza la cancelación de la factura + Given una factura en estado 'Por cancelar' + When el contribuyente rechaza la cancelación + Then el estado de la factura es 'abierta' + And se le notifica al usuario que la cancelacion fue rechazada + +Scenario: se aprueba cancelacion de la factura por plazo vencido + Given una factura en estado 'Por cancelar' + When se aprueba la cancelacion por plazo vencido + Then se cancela la factura + And se notifica que la factura se cancelo \ No newline at end of file diff --git a/l10n_mx_facturae/views/account_invoice.xml b/l10n_mx_facturae/views/account_invoice.xml index f3fa183d2de956103070952792d148e455674dc2..45f371a5482bf21546f83afd525ab7d18e218bc4 100644 --- a/l10n_mx_facturae/views/account_invoice.xml +++ b/l10n_mx_facturae/views/account_invoice.xml @@ -13,7 +13,7 @@ <field name="arch" type="xml"> <xpath expr="//sheet[@string='Invoice']/h1" position="after"> <h4 collspan="2"> - <field string="Fiscal Number" name="cfdi_folio_fiscal" + <field string="Fiscal Number" name="cfdi_id" placeholder="Fiscal Number" readonly="1"/> </h4> </xpath>