Módulo l10n_latam_invoice_document

Gustavo Orrillo
- 01/08/2022 - 6 min. de lectura

El módulo l10n_latam_invoice_document es un módulo que permite asignar a las facturas de venta/compra un documento de facturación. Como bien lo explica el __manifest__.py, algunos paises en Latinoamérica (Argentina, Chile, Uruguay por ejemplo) que requieren que sus facturas de venta y compra pertenezcan a un tipo de documento (por ejemplo en Argentina la Factura A). Este módulo crea la infraestructura necesaria para que las localizaciones puedan agregar dicha funcionalidad.

Modelos

 El modelo res.company ejecuta un truco muy común en la programación orientada a objetos. Cuando uno está en una clase padre, y quiere que en las clases hijo se ejecute un método particular; invoca ese método en la clase padre retornando Falso o Error. Lo que uno quiere de esta manera es que si o si en las clases hijas se ejecute dicho método.

def _localization_use_documents(self):
    """ This method is to be inherited by localizations and return True if localization use documents """
    self.ensure_one()
    return False

En este caso, se espera que cada localización defina el método _localization_use_documents en el modelo res.company.

El modelo account.chart.template extiende el método _prepare_all_journals y le agrega a cada journal de compras o de ventas el indicador "Usa documentos".

Se crea el modelo l10n_latam.document.type. Este objeto es el que permite clasificar las facturas. Por ejemplo, tenemos un campo llamado internal_type que indica si el documento es nota de crédito, nota de débito (otro invento de gestión argentino) o factura. El prefijo que se usará en cada documento, el nombre de documento que se usará en los reportes, y el código. También tiene una secuencia (que se usa en Argentina pero solo temporariamente, ya que la numeración es mantenida por AFIP) y un campo activo.

También de forma interesante, el modelo l10n_latam.document.type reescribe como se muestra el campo display_name agregando como prefijo el código al nombre del nombre. Y se sobre-escribe la búsqueda de nombres para agregarle el campo código al domain de la búsqueda. Un día de estos vamos a hablar más sobre como se utilizan estas funciones. 

def name_get(self):
    result = []
    for rec in self:
        name = rec.name
        if rec.code:
            name = '(%s) %s' % (rec.code, name)
        result.append((rec.id, name))
    return result
@api.model def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None):
    args = args or []
    if operator == 'ilike' and not (name or '').strip():
        domain = []
    else:
        domain = ['|', ('name', 'ilike', name), ('code', 'ilike', name)]
    return self._search(expression.AND([domain, args]), limit=limit, access_rights_uid=name_get_uid)

El modelo account.move hace unas cuantas cosas. Una muy interesante es agregar un atributo al account.move, el cual indica si su numeración es manual si el documento es una compra:

def _is_manual_document_number(self):
    return self.journal_id.type == 'purchase'

Tiene una función que computa el nombre si el documento es de ventas y no tiene nombre. Tambien computa y graba el campo document_number basado en el campo name (algo que nosotros sobreescribimos en el módulo l10n_afipws_fe). 

Después cuando se postea el documento, chequea que no se posteen documentos de tipo recibo (tipos de documento que no sabía que existían):

def _post(self, soft=True):
    for rec in self.filtered(lambda x: x.l10n_latam_use_documents and (not x.name or x.name == '/')):
        if rec.move_type in ('in_receipt', 'out_receipt'):
            raise UserError(_('We do not accept the usage of document types on receipts yet. '))
    return super()._post(soft)

También se chequea que el documento cuando se confirma posea un tipo de documento cuando el asiento contable es una factura de ventas/compra y además su diario es del tipo de usa documentos. Lo que es interesante es que trabaja con un recordset, y en lugar de iterar sobre el recordset utiliza de forma intensiva el método filtered

@api.constrains('state', 'l10n_latam_document_type_id')
def _check_l10n_latam_documents(self):
    """ This constraint checks that if a invoice is posted and does not have a document type configured will raise
        an error. This only applies to invoices related to journals that has the "Use Documents" set as True.
        And if the document type is set then check if the invoice number has been set, because a posted invoice
        without a document number is not valid in the case that the related journals has "Use Docuemnts" set as True """
        validated_invoices = self.filtered(lambda x: x.l10n_latam_use_documents and x.state == 'posted')
        without_doc_type = validated_invoices.filtered(lambda x: not x.l10n_latam_document_type_id)
        if without_doc_type:
            raise ValidationError(_(
                'The journal require a document type but not document type has been selected on invoices %s.',
                without_doc_type.ids
                ))
        without_number = validated_invoices.filtered(
            lambda x: not x.l10n_latam_document_number and x.l10n_latam_manual_document_number)
        if without_number:
            raise ValidationError(_(
                'Please set the document number on the following invoices %s.',
                without_number.ids
            ))

Y también hace varios controles; por ejemplo que el account.move sea de un tipo de acuerdo al tipo de documento del diario. Y que la factura de compra tenga un número de factura único por proveedor. Y por último se agrega un método que debe ser extendido en otras partes, ya que realiza el filtro de los tipos de documento disponibles al momento de crearse un account.move (por ejemplo, que a un cliente consumidor final se le haga una factura C)

def _get_l10n_latam_documents_domain(self):
    self.ensure_one()
    if self.move_type in ['out_refund', 'in_refund']:
        internal_types = ['credit_note']
    else:
        internal_types = ['invoice', 'debit_note']
    return [('internal_type', 'in', internal_types), ('country_id', '=', self.company_id.account_fiscal_country_id.id)]


@api.depends('journal_id', 'partner_id', 'company_id', 'move_type')
def _compute_l10n_latam_available_document_types(self)
    self.l10n_latam_available_document_type_ids = False
    for rec in self.filtered(lambda x: x.journal_id and x.l10n_latam_use_documents and x.partner_id):
        rec.l10n_latam_available_document_type_ids = self.env['l10n_latam.document.type'].search(rec._get_l10n_latam_documents_domain())

Como pueden ver, se debe extender el método _get_l10n_latam_documents_domain.

El modelo account.move.line agrega el campo de tipo de documento por medio de un campo related. Es interesante porque es almacenado (algo que no sabía hasta ahora).

l10n_latam_document_type_id = fields.Many2one(related='move_id.l10n_latam_document_type_id', auto_join=True, store=True, index=True)

Vistas 

En los diarios oculta el campo código de país. Y luego agrega el indicador de si se usa documentos en el diario. Y se agrega el menú para los tipos de documentos, junto con su acción, tree y formulario.

En los asientos contables, se agrega primero en los filtros de las facturas y asientos contables el tipo de documento. Y en la vista tipo formulario agrega de modo invisible tres campos; 

<field name="l10n_latam_available_document_type_ids" invisible="1"/>
<field name="l10n_latam_use_documents" invisible="1"/>
<field name="l10n_latam_manual_document_number" invisible="1"/>

Estos le indican al asiento contable si se usan documentos en el asiento, si el asiento tiene números de asiento manuales y ademas los tipos de documento permitidos para la letra del asiento. 

<field name="l10n_latam_document_type_id"
    attrs="{'invisible': [('l10n_latam_use_documents', '=', False)], 'required': [('l10n_latam_use_documents', '=', True)],     'readonly': [('posted_before', '=', True)]}"
    domain="[('id', 'in', l10n_latam_available_document_type_ids)]" options="{'no_open': True, 'no_create': True}"/>
<field name="l10n_latam_document_number"
    attrs="{'invisible': ['|', ('l10n_latam_use_documents', '=', False),                 ('l10n_latam_manual_document_number', '=', False),                 '|', '|', ('l10n_latam_use_documents', '=', False), ('highest_name', '!=', False), ('state', '!=', 'draft')],             'required': [('l10n_latam_use_documents', '=', True), '|',             ('l10n_latam_manual_document_number', '=', True), ('highest_name', '=', False)],     'readonly': [('posted_before', '=', True), ('state', '!=', 'draft')]}"/>

Y se extienden los diferentes templates de facturas (a los layouts de los header y footer para ser mas exactos) la llamada a los métodos custom_header y custom_footer.

Wizard

Para el wizard se asegura que al momento de crearse una nota de crédito, el documento relacionado sea una factura de tipo de documento correcto.

Reportes

Agrega el campo de tipo de documento al análisis de facturas (modelo account.invoice.report). Extiende el modelo account.invoice.report y le agrega el campo l10n_latam_document_type_id. Y agrega dicho campo a la sentencia SELECT de SQL

def _select(self):
    return super()._select() + ", move.l10n_latam_document_type_id as l10n_latam_document_type_id"

Y se agregan dichos campos a la búsqueda de account.invoice.report

<record model="ir.ui.view" id="view_account_invoice_report_search">
    <field name="name">account.invoice.report.search</field>
    <field name="model">account.invoice.report</field>
    <field name="inherit_id" ref="account.view_account_invoice_report_search"/>
    <field name="arch" type="xml">
        <search>
            <field name="l10n_latam_document_type_id"/>
        </search>
        <filter name="user" position="after">
            <filter domain="[]" string="Document Type" name="l10n_latam_document_type" context="{'group_by':'l10n_latam_document_type_id'}"/>
        </filter>
        </field>
</record>

Seguridad

Se permite que el modelo l10n_latam_document_type sea leido por todos los usuarios, y modificado por un manager.

Acerca de:

Gustavo Orrillo

Apasionado de la programación, implementa Odoo para distintos tipos de negocios desde el año 2010. En Moldeo Interactive es Socio fundador y Programador; además de escribir en el Blog sobre distintos temas relacionados a los desarrollos que realiza.