allow to have more than one Facturae deffinition per party. Moving the

fields from party model to address. For the company move from party to
company model.

Update the way that the certificate to sign xml is used, based with the way
that the certificats are all load the same way.
This commit is contained in:
Bernat Brunet 2023-08-04 17:22:55 +02:00
parent a235054bc4
commit a376c04bce
16 changed files with 422 additions and 157 deletions

View File

@ -1,10 +1,7 @@
# The COPYRIGHT file at the top level of this repository contains the full # The COPYRIGHT file at the top level of this repository contains the full
# copyright notices and license terms. # copyright notices and license terms.
from trytond.pool import Pool from trytond.pool import Pool
from . import account from . import account, invoice, party, company, payment_type
from . import invoice
from . import party
from . import payment_type
from .invoice import FACTURAE_SCHEMA_VERSION from .invoice import FACTURAE_SCHEMA_VERSION
__all__ = [FACTURAE_SCHEMA_VERSION] __all__ = [FACTURAE_SCHEMA_VERSION]
@ -20,8 +17,9 @@ def register():
invoice.InvoiceLine, invoice.InvoiceLine,
invoice.CreditInvoiceStart, invoice.CreditInvoiceStart,
invoice.GenerateFacturaeStart, invoice.GenerateFacturaeStart,
party.Party, party.Address,
payment_type.PaymentType, payment_type.PaymentType,
company.Company,
module='account_invoice_facturae', type_='model') module='account_invoice_facturae', type_='model')
Pool.register( Pool.register(
invoice.CreditInvoice, invoice.CreditInvoice,

View File

@ -77,7 +77,7 @@ class ConfigurationFacturae(ModelSQL, CompanyValueMixin):
facturae_certificate = fields.Many2One('certificate', "Factura-e Certificate", facturae_certificate = fields.Many2One('certificate', "Factura-e Certificate",
help='Certificate to sign Factura-e') help='Certificate to sign Factura-e')
facturae_service = fields.Selection([ facturae_service = fields.Selection([
(None, ''), (None, 'Only Generate Facturae'),
], "Factura-e Service") ], "Factura-e Service")
@staticmethod @staticmethod

24
company.py Normal file
View File

@ -0,0 +1,24 @@
# The COPYRIGHT file at the top level of this repository contains the full
# copyright notices and license terms.
from trytond.model import fields
from trytond.pool import PoolMeta
class Company(metaclass=PoolMeta):
__name__ = 'company.company'
facturae_person_type = fields.Selection([
(None, ''),
('J', 'Legal Entity'),
('F', 'Individual'),
], 'Person Type', sort=False)
facturae_residence_type = fields.Selection([
(None, ''),
('R', 'Resident in Spain'),
('U', 'Resident in other EU country'),
('E', 'Foreigner'),
], 'Residence Type', sort=False)
oficina_contable = fields.Char('Oficina contable')
organo_gestor = fields.Char('Organo gestor')
unidad_tramitadora = fields.Char('Unidad tramitadora')
organo_proponente = fields.Char('Organo proponente')

12
company.xml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0"?>
<!-- The COPYRIGHT file at the top level of this repository contains the full
copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="company_view_form">
<field name="model">company.company</field>
<field name="inherit" ref="company.company_view_form"/>
<field name="name">company_form</field>
</record>
</data>
</tryton>

View File

@ -14,9 +14,6 @@ from decimal import Decimal
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from lxml import etree from lxml import etree
from operator import attrgetter from operator import attrgetter
from tempfile import NamedTemporaryFile
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.serialization import pkcs12
from trytond.model import ModelView, fields from trytond.model import ModelView, fields
from trytond.pool import Pool, PoolMeta from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval from trytond.pyson import Bool, Eval
@ -25,6 +22,8 @@ from trytond.wizard import Wizard, StateView, StateTransition, Button
from trytond import backend from trytond import backend
from trytond.i18n import gettext from trytond.i18n import gettext
from trytond.exceptions import UserError from trytond.exceptions import UserError
from trytond.modules.certificate_manager.certificate_manager import (
ENCODING_DER)
FACTURAE_SCHEMA_VERSION = '3.2.2' FACTURAE_SCHEMA_VERSION = '3.2.2'
@ -319,13 +318,13 @@ class Invoice(metaclass=PoolMeta):
"Missing some tax in invoice %s" % self.id) "Missing some tax in invoice %s" % self.id)
for field in FACe_REQUIRED_FIELDS: for field in FACe_REQUIRED_FIELDS:
for party in [self.party, self.company.party]: if (not getattr(self.invoice_address, field)
if not getattr(party, field): or not getattr(self.company, field)):
raise UserError(gettext( raise UserError(gettext(
'account_invoice_facturae.party_facturae_fields', 'account_invoice_facturae.party_facturae_fields',
party=party.rec_name, party=party.rec_name,
invoice=self.rec_name, invoice=self.rec_name,
field=field)) field=field))
if (not self.company.party.tax_identifier if (not self.company.party.tax_identifier
or len(self.company.party.tax_identifier.code) < 3 or len(self.company.party.tax_identifier.code) < 3
or len(self.company.party.tax_identifier.code) > 30): or len(self.company.party.tax_identifier.code) > 30):
@ -351,7 +350,7 @@ class Invoice(metaclass=PoolMeta):
'account_invoice_facturae.party_vat_identifier', 'account_invoice_facturae.party_vat_identifier',
party=self.party.rec_name, party=self.party.rec_name,
invoice=self.rec_name)) invoice=self.rec_name))
if (self.party.facturae_person_type == 'F' if (self.invoice_address.facturae_person_type == 'F'
and len(self.party.name.split(' ', 2)) < 2): and len(self.party.name.split(' ', 2)) < 2):
raise UserError(gettext( raise UserError(gettext(
'account_invoice_facturae.party_name_surname', 'account_invoice_facturae.party_name_surname',
@ -475,7 +474,8 @@ class Invoice(metaclass=PoolMeta):
""" """
Inspired by https://github.com/pedrobaeza/l10n-spain/blob/d01d049934db55130471e284012be7c860d987eb/l10n_es_facturae/wizard/create_facturae.py Inspired by https://github.com/pedrobaeza/l10n-spain/blob/d01d049934db55130471e284012be7c860d987eb/l10n_es_facturae/wizard/create_facturae.py
""" """
Configuration = Pool().get('account.configuration') pool = Pool()
Configuration = pool.get('account.configuration')
if not certificate: if not certificate:
certificate = Configuration(1).facturae_certificate certificate = Configuration(1).facturae_certificate
@ -483,34 +483,14 @@ class Invoice(metaclass=PoolMeta):
raise UserError(gettext( raise UserError(gettext(
'account_invoice_facturae.msg_missing_certificate')) 'account_invoice_facturae.msg_missing_certificate'))
certificate_facturae = certificate.pem_certificate
certificate_password = certificate.certificate_password
logger = logging.getLogger('account_invoice_facturae') logger = logging.getLogger('account_invoice_facturae')
with NamedTemporaryFile(suffix='.xml', delete=False) as unsigned_file: def _sign_file(cert, request):
unsigned_file.write(xml_string) key = cert.load_pem_key()
pem = cert.load_pem_certificate()
with NamedTemporaryFile(suffix='.pfx', delete=False) as cert_file:
cert_file.write(certificate_facturae)
def _sign_file(cert, password, request):
# get key and certificates from PCK12 file
try:
(
private_key,
certificate,
additional_certificates,
) = pkcs12.load_key_and_certificates(cert, password)
except ValueError as e:
logger.warning("Error load_key_and_certificates file",
exc_info=True)
raise UserError(gettext(
'account_invoice_facturae.msg_certificate_error',
error=str(e)))
# DER is an ASN.1 encoding type # DER is an ASN.1 encoding type
crt = certificate.public_bytes(serialization.Encoding.DER) crt = pem.public_bytes(ENCODING_DER)
# Set variables values # Set variables values
rand_min = 1 rand_min = 1
@ -574,10 +554,9 @@ class Invoice(metaclass=PoolMeta):
# Set the certificate values # Set the certificate values
ctx = xmlsig.SignatureContext() ctx = xmlsig.SignatureContext()
ctx.private_key = private_key ctx.private_key = key
ctx.x509 = certificate ctx.x509 = pem
ctx.ca_certificates = additional_certificates ctx.public_key = pem.public_key()
ctx.public_key = certificate.public_key()
# Set the footer validation # Set the footer validation
object_node = etree.SubElement( object_node = etree.SubElement(
@ -632,11 +611,11 @@ class Invoice(metaclass=PoolMeta):
etree.SubElement( etree.SubElement(
issuer_serial, etree.QName(xmlsig.constants.DSigNs, issuer_serial, etree.QName(xmlsig.constants.DSigNs,
"X509IssuerName") "X509IssuerName")
).text = xmlsig.utils.get_rdns_name(certificate.issuer.rdns) ).text = xmlsig.utils.get_rdns_name(pem.issuer.rdns)
etree.SubElement( etree.SubElement(
issuer_serial, etree.QName(xmlsig.constants.DSigNs, issuer_serial, etree.QName(xmlsig.constants.DSigNs,
"X509SerialNumber") "X509SerialNumber")
).text = str(certificate.serial_number) ).text = str(pem.serial_number)
signature_policy_identifier = etree.SubElement( signature_policy_identifier = etree.SubElement(
signed_signature_properties, signed_signature_properties,
@ -710,11 +689,7 @@ class Invoice(metaclass=PoolMeta):
return etree.tostring(root, xml_declaration=True, encoding="UTF-8") return etree.tostring(root, xml_declaration=True, encoding="UTF-8")
signed_file_content = _sign_file( signed_file_content = _sign_file(certificate, xml_string)
certificate_facturae,
certificate_password.encode(),
xml_string,
)
logger.info("Factura-e for invoice %s (%s) generated and signed", logger.info("Factura-e for invoice %s (%s) generated and signed",
self.rec_name, self.id) self.rec_name, self.id)
@ -750,8 +725,8 @@ class InvoiceLine(metaclass=PoolMeta):
@property @property
def facturae_item_description(self): def facturae_item_description(self):
return ( return (
(self.description and self.description[:2500]) (self.description and self.description)
or (self.product and self.product.rec_name[:2500]) or (self.product and self.product.rec_name)
or '#'+str(self.id) or '#'+str(self.id)
) )
@ -760,6 +735,34 @@ class InvoiceLine(metaclass=PoolMeta):
# TODO Issuer/ReceiverTransactionDate (sale, contract...) # TODO Issuer/ReceiverTransactionDate (sale, contract...)
return '' return ''
@property
def facturae_start_date(self):
try:
pool = Pool()
Consumption = pool.get('contract.consumption')
except:
Consumption = None
if (self.origin and Consumption
and isinstance(self.origin, Consumption)):
return self.origin.start_date
return None
@property
def facturae_end_date(self):
try:
pool = Pool()
Consumption = pool.get('contract.consumption')
except:
Consumption = None
if (self.origin and Consumption
and isinstance(self.origin, Consumption)):
return self.origin.end_date
elif self.facturae_start_date:
return self.facturae_start_date
return None
@property @property
def taxes_outputs(self): def taxes_outputs(self):
"""Return list of 'impuestos repecutidos'""" """Return list of 'impuestos repecutidos'"""
@ -813,17 +816,10 @@ class GenerateFacturaeStart(ModelView):
'Generate Factura-e file - Start' 'Generate Factura-e file - Start'
__name__ = 'account.invoice.generate_facturae.start' __name__ = 'account.invoice.generate_facturae.start'
service = fields.Selection([ service = fields.Selection([
(None, ''), (None, 'Only generate facturae'),
], 'Service') ], 'Factura-e Service')
certificate_facturae = fields.Many2One('certificate', certificate = fields.Many2One('certificate',
'Certificate Factura-e', 'Factura-e Certificate', depends=['service'])
states={
'invisible': ~Bool(Eval('service')),
}, depends=['service'])
@staticmethod
def default_service():
return None
class GenerateFacturae(Wizard): class GenerateFacturae(Wizard):
@ -836,11 +832,26 @@ class GenerateFacturae(Wizard):
]) ])
generate = StateTransition() generate = StateTransition()
def default_start(self, fields):
pool = Pool()
Configuration = pool.get('account.configuration')
default = {
'service': None,
'certificate': None,
}
config = Configuration(1)
if config.facturae_certificate:
default['certificate'] = config.facturae_certificate.id
return default
def transition_generate(self): def transition_generate(self):
Invoice = Pool().get('account.invoice') Invoice = Pool().get('account.invoice')
invoices = Invoice.browse(Transaction().context['active_ids']) invoices = Invoice.browse(Transaction().context['active_ids'])
for invoice in invoices: for invoice in invoices:
invoice.generate_facturae(certificate=self.start.certificate_facturae, invoice.generate_facturae(certificate=self.start.certificate,
service=self.start.service) service=self.start.service)
return 'end' return 'end'

View File

@ -38,17 +38,25 @@ msgctxt "field:account.invoice.credit.start,rectificative_reason_code:"
msgid "Rectificative Reason Code" msgid "Rectificative Reason Code"
msgstr "Codi motiu rectificació" msgstr "Codi motiu rectificació"
msgctxt "field:account.invoice.generate_facturae.start,certificate_facturae:" msgctxt "field:account.invoice.generate_facturae.start,service:"
msgid "Certificate Factura-e" msgid "Factura-e Service"
msgstr "Certificat" msgstr "Servei Factura-e"
msgctxt "field:account.invoice.generate_facturae.start,certificate:"
msgid "Factura-e Certificate"
msgstr "Certificat Factura-e"
msgctxt "field:account.invoice.generate_facturae.start,certificate_password:" msgctxt "field:account.invoice.generate_facturae.start,certificate_password:"
msgid "Certificate Password" msgid "Certificate Password"
msgstr "Contrasenya certificat" msgstr "Contrasenya certificat"
msgctxt "field:account.invoice.generate_facturae.start,service:" msgctxt "field:account.configuration,facturae_certificate:"
msgid "Service" msgid "Factura-e Certificate"
msgstr "Servei" msgstr "Certificat Factura-e"
msgctxt "field:account.configuration,facturae_service:"
msgid "Factura-e Service"
msgstr "Servei Factura-e"
msgctxt "field:account.payment.type,facturae_type:" msgctxt "field:account.payment.type,facturae_type:"
msgid "Factura-e Type" msgid "Factura-e Type"
@ -58,31 +66,55 @@ msgctxt "field:account.tax,report_type:"
msgid "Report Type" msgid "Report Type"
msgstr "Tipus informe" msgstr "Tipus informe"
msgctxt "field:company.company,facturae_person_type:"
msgid "Person Type"
msgstr "Tipus persona"
msgctxt "field:company.company,facturae_residence_type:"
msgid "Residence Type"
msgstr "Tipus residència"
msgctxt "field:company.company,oficina_contable:"
msgid "Oficina contable"
msgstr "Oficina contable"
msgctxt "field:company.company,organo_gestor:"
msgid "Organo gestor"
msgstr "Organo gestor"
msgctxt "field:company.company,organo_proponente:"
msgid "Organo proponente"
msgstr "Organo proponente"
msgctxt "field:company.company,unidad_tramitadora:"
msgid "Unidad tramitadora"
msgstr "Unidad tramitadora"
msgctxt "field:account.tax.template,report_type:" msgctxt "field:account.tax.template,report_type:"
msgid "Report Type" msgid "Report Type"
msgstr "Tipus informe" msgstr "Tipus informe"
msgctxt "field:party.party,facturae_person_type:" msgctxt "field:party.address,facturae_person_type:"
msgid "Person Type" msgid "Person Type"
msgstr "Tipus persona" msgstr "Tipus persona"
msgctxt "field:party.party,facturae_residence_type:" msgctxt "field:party.address,facturae_residence_type:"
msgid "Residence Type" msgid "Residence Type"
msgstr "Tipus residència" msgstr "Tipus residència"
msgctxt "field:party.party,oficina_contable:" msgctxt "field:party.address,oficina_contable:"
msgid "Oficina contable" msgid "Oficina contable"
msgstr "Oficina contable" msgstr "Oficina contable"
msgctxt "field:party.party,organo_gestor:" msgctxt "field:party.address,organo_gestor:"
msgid "Organo gestor" msgid "Organo gestor"
msgstr "Organo gestor" msgstr "Organo gestor"
msgctxt "field:party.party,organo_proponente:" msgctxt "field:party.address,organo_proponente:"
msgid "Organo proponente" msgid "Organo proponente"
msgstr "Organo proponente" msgstr "Organo proponente"
msgctxt "field:party.party,unidad_tramitadora:" msgctxt "field:party.address,unidad_tramitadora:"
msgid "Unidad tramitadora" msgid "Unidad tramitadora"
msgstr "Unidad tramitadora" msgstr "Unidad tramitadora"
@ -753,31 +785,43 @@ msgctxt "selection:account.tax.template,report_type:"
msgid "Value-Added Tax" msgid "Value-Added Tax"
msgstr "" msgstr ""
msgctxt "selection:party.party,facturae_person_type:" msgctxt "selection:company.company,facturae_person_type:"
msgid ""
msgstr " "
msgctxt "selection:party.party,facturae_person_type:"
msgid "Individual" msgid "Individual"
msgstr "Persona física" msgstr "Persona física"
msgctxt "selection:party.party,facturae_person_type:" msgctxt "selection:company.company,facturae_person_type:"
msgid "Legal Entity" msgid "Legal Entity"
msgstr "Persona jurídica" msgstr "Persona jurídica"
msgctxt "selection:party.party,facturae_residence_type:" msgctxt "selection:company.company,facturae_residence_type:"
msgid ""
msgstr " "
msgctxt "selection:party.party,facturae_residence_type:"
msgid "Foreigner" msgid "Foreigner"
msgstr "Estranger (Fora Unió Europea)" msgstr "Estranger (Fora Unió Europea)"
msgctxt "selection:party.party,facturae_residence_type:" msgctxt "selection:company.company,facturae_residence_type:"
msgid "Resident in Spain" msgid "Resident in Spain"
msgstr "Resident (a Espanya)" msgstr "Resident (a Espanya)"
msgctxt "selection:party.party,facturae_residence_type:" msgctxt "selection:company.company,facturae_residence_type:"
msgid "Resident in other EU country"
msgstr "Resident a la Unió Europea (excepte Espanya)"
msgctxt "selection:party.address,facturae_person_type:"
msgid "Individual"
msgstr "Persona física"
msgctxt "selection:party.address,facturae_person_type:"
msgid "Legal Entity"
msgstr "Persona jurídica"
msgctxt "selection:party.address,facturae_residence_type:"
msgid "Foreigner"
msgstr "Estranger (Fora Unió Europea)"
msgctxt "selection:party.address,facturae_residence_type:"
msgid "Resident in Spain"
msgstr "Resident (a Espanya)"
msgctxt "selection:party.address,facturae_residence_type:"
msgid "Resident in other EU country" msgid "Resident in other EU country"
msgstr "Resident a la Unió Europea (excepte Espanya)" msgstr "Resident a la Unió Europea (excepte Espanya)"
@ -807,7 +851,11 @@ msgctxt "view:account.tax:"
msgid "Factura-e" msgid "Factura-e"
msgstr "Factura-e" msgstr "Factura-e"
msgctxt "view:party.party:" msgctxt "view:company.company:"
msgid "Factura-e"
msgstr "Factura-e"
msgctxt "view:party.address:"
msgid "Factura-e" msgid "Factura-e"
msgstr "Factura-e" msgstr "Factura-e"
@ -818,3 +866,11 @@ msgstr "Cancel·la"
msgctxt "wizard_button:account.invoice.generate_facturae,start,generate:" msgctxt "wizard_button:account.invoice.generate_facturae,start,generate:"
msgid "Generate" msgid "Generate"
msgstr "Genera" msgstr "Genera"
msgctxt "selection:account.configuration.facturae,facturae_service:"
msgid "Only Generate Facturae"
msgstr "Només genera la Factura-e"
msgctxt "selection:account.invoice.generate_facturae.start,service:"
msgid "Only Generate Facturae"
msgstr "Només genera la Factura-e"

View File

@ -38,17 +38,25 @@ msgctxt "field:account.invoice.credit.start,rectificative_reason_code:"
msgid "Rectificative Reason Code" msgid "Rectificative Reason Code"
msgstr "Código motivo rectificación" msgstr "Código motivo rectificación"
msgctxt "field:account.invoice.generate_facturae.start,certificate_facturae:"
msgid "Certificate Factura-e"
msgstr "Certificado Factura-e"
msgctxt "field:account.invoice.generate_facturae.start,certificate_password:" msgctxt "field:account.invoice.generate_facturae.start,certificate_password:"
msgid "Certificate Password" msgid "Certificate Password"
msgstr "Contraseña certificado" msgstr "Contraseña certificado"
msgctxt "field:account.invoice.generate_facturae.start,service:" msgctxt "field:account.invoice.generate_facturae.start,service:"
msgid "Service" msgid "Factura-e Service"
msgstr "Servicio" msgstr "Servicio Factura-e"
msgctxt "field:account.invoice.generate_facturae.start,certificate:"
msgid "Factura-e Certificate"
msgstr "Certificado Factura-e"
msgctxt "field:account.configuration,facturae_certificate:"
msgid "Factura-e Certificate"
msgstr "Certificado Factura-e"
msgctxt "field:account.configuration,facturae_service:"
msgid "Factura-e Service"
msgstr "Servicio Factura-e"
msgctxt "field:account.payment.type,facturae_type:" msgctxt "field:account.payment.type,facturae_type:"
msgid "Factura-e Type" msgid "Factura-e Type"
@ -62,27 +70,51 @@ msgctxt "field:account.tax.template,report_type:"
msgid "Report Type" msgid "Report Type"
msgstr "Tipo informe" msgstr "Tipo informe"
msgctxt "field:party.party,facturae_person_type:" msgctxt "field:company.company,facturae_person_type:"
msgid "Person Type" msgid "Person Type"
msgstr "Tipo persona" msgstr "Tipo persona"
msgctxt "field:party.party,facturae_residence_type:" msgctxt "field:company.company,facturae_residence_type:"
msgid "Residence Type" msgid "Residence Type"
msgstr "Tipo residencia" msgstr "Tipo residencia"
msgctxt "field:party.party,oficina_contable:" msgctxt "field:company.company,oficina_contable:"
msgid "Oficina contable" msgid "Oficina contable"
msgstr "Oficina contable" msgstr "Oficina contable"
msgctxt "field:party.party,organo_gestor:" msgctxt "field:company.company,organo_gestor:"
msgid "Organo gestor" msgid "Organo gestor"
msgstr "Organo gestor" msgstr "Organo gestor"
msgctxt "field:party.party,organo_proponente:" msgctxt "field:company.company,organo_proponente:"
msgid "Organo proponente" msgid "Organo proponente"
msgstr "Organo proponente" msgstr "Organo proponente"
msgctxt "field:party.party,unidad_tramitadora:" msgctxt "field:company.company,unidad_tramitadora:"
msgid "Unidad tramitadora"
msgstr "Unidad tramitadora"
msgctxt "field:party.address,facturae_person_type:"
msgid "Person Type"
msgstr "Tipo persona"
msgctxt "field:party.address,facturae_residence_type:"
msgid "Residence Type"
msgstr "Tipo residencia"
msgctxt "field:party.address,oficina_contable:"
msgid "Oficina contable"
msgstr "Oficina contable"
msgctxt "field:party.address,organo_gestor:"
msgid "Organo gestor"
msgstr "Organo gestor"
msgctxt "field:party.address,organo_proponente:"
msgid "Organo proponente"
msgstr "Organo proponente"
msgctxt "field:party.address,unidad_tramitadora:"
msgid "Unidad tramitadora" msgid "Unidad tramitadora"
msgstr "Unidad tramitadora" msgstr "Unidad tramitadora"
@ -751,23 +783,43 @@ msgctxt "selection:account.tax.template,report_type:"
msgid "Value-Added Tax" msgid "Value-Added Tax"
msgstr "" msgstr ""
msgctxt "selection:party.party,facturae_person_type:" msgctxt "selection:company.company,facturae_person_type:"
msgid "Individual" msgid "Individual"
msgstr "Persona física" msgstr "Persona física"
msgctxt "selection:party.party,facturae_person_type:" msgctxt "selection:company.company,facturae_person_type:"
msgid "Legal Entity" msgid "Legal Entity"
msgstr "Persona jurídica" msgstr "Persona jurídica"
msgctxt "selection:party.party,facturae_residence_type:" msgctxt "selection:company.company,facturae_residence_type:"
msgid "Foreigner" msgid "Foreigner"
msgstr "Extranjero (Fuera Unión Europea)" msgstr "Extranjero (Fuera Unión Europea)"
msgctxt "selection:party.party,facturae_residence_type:" msgctxt "selection:company.company,facturae_residence_type:"
msgid "Resident in Spain" msgid "Resident in Spain"
msgstr "Residente (en España)" msgstr "Residente (en España)"
msgctxt "selection:party.party,facturae_residence_type:" msgctxt "selection:company.company,facturae_residence_type:"
msgid "Resident in other EU country"
msgstr "Residente en la Unión Europea (excepto España)"
msgctxt "selection:party.address,facturae_person_type:"
msgid "Individual"
msgstr "Persona física"
msgctxt "selection:party.address,facturae_person_type:"
msgid "Legal Entity"
msgstr "Persona jurídica"
msgctxt "selection:party.address,facturae_residence_type:"
msgid "Foreigner"
msgstr "Extranjero (Fuera Unión Europea)"
msgctxt "selection:party.address,facturae_residence_type:"
msgid "Resident in Spain"
msgstr "Residente (en España)"
msgctxt "selection:party.address,facturae_residence_type:"
msgid "Resident in other EU country" msgid "Resident in other EU country"
msgstr "Residente en la Unión Europea (excepto España)" msgstr "Residente en la Unión Europea (excepto España)"
@ -793,7 +845,11 @@ msgctxt "view:account.tax:"
msgid "Factura-e" msgid "Factura-e"
msgstr "Factura-e" msgstr "Factura-e"
msgctxt "view:party.party:" msgctxt "view:company.company:"
msgid "Factura-e"
msgstr "Factura-e"
msgctxt "view:party.address:"
msgid "Factura-e" msgid "Factura-e"
msgstr "Factura-e" msgstr "Factura-e"
@ -804,3 +860,11 @@ msgstr "Cancelar"
msgctxt "wizard_button:account.invoice.generate_facturae,start,generate:" msgctxt "wizard_button:account.invoice.generate_facturae,start,generate:"
msgid "Generate" msgid "Generate"
msgstr "Generar" msgstr "Generar"
msgctxt "selection:account.configuration.facturae,service:"
msgid "Only Generate Facturae"
msgstr "Solo genera la Factura-e"
msgctxt "selection:account.invoice.generate_facturae.start,service:"
msgid "Only Generate Facturae"
msgstr "Solo genera la Factura-e"

View File

@ -1,13 +1,13 @@
# The COPYRIGHT file at the top level of this repository contains the full # The COPYRIGHT file at the top level of this repository contains the full
# copyright notices and license terms. # copyright notices and license terms.
from trytond.model import fields from trytond.model import fields
from trytond.pool import PoolMeta from trytond.pool import Pool, PoolMeta
from trytond.transaction import Transaction
__all__ = ['Party']
class Party(metaclass=PoolMeta): class Address(metaclass=PoolMeta):
__name__ = 'party.party' __name__ = 'party.address'
facturae_person_type = fields.Selection([ facturae_person_type = fields.Selection([
(None, ''), (None, ''),
('J', 'Legal Entity'), ('J', 'Legal Entity'),
@ -23,3 +23,80 @@ class Party(metaclass=PoolMeta):
organo_gestor = fields.Char('Organo gestor') organo_gestor = fields.Char('Organo gestor')
unidad_tramitadora = fields.Char('Unidad tramitadora') unidad_tramitadora = fields.Char('Unidad tramitadora')
organo_proponente = fields.Char('Organo proponente') organo_proponente = fields.Char('Organo proponente')
@classmethod
def __register__(cls, module_name):
pool = Pool()
Party = pool.get('party.party')
address_table = cls.__table__()
party_table = Party.__table__()
party = Party.__table_handler__()
cursor = Transaction().connection.cursor()
super().__register__(module_name)
# Migrat facturae fields from party to invoice addresses or in case of
# missing the fist address.
if party.column_exist('facturae_person_type'):
cursor.execute(*party_table.select(
party_table.id,
where=party_table.facturae_person_type != None))
addresses_update = []
for party_id in cursor.fetchall():
cursor.execute(*address_table.select(
address_table.id, address_table.invoice,
where=address_table.party.in_(party_id)))
addresses = cursor.fetchall()
addresses = {a[0]: a[1] for a in addresses}
if not addresses:
continue
if len(addresses) == 1:
addresses_update.append(next(iter(addresses)))
else:
for address, invoice in addresses.items():
if invoice:
addresses_update.append(address)
if not any(addresses.values()):
addresses_update.extend(addresses)
for address_update in addresses_update:
cursor.execute(*address_table.select(
address_table.party,
where=address_table.id == address_update))
party_id = cursor.fetchone()
cursor.execute(*party_table.select(
party_table.facturae_person_type,
party_table.facturae_residence_type,
party_table.oficina_contable,
party_table.organo_gestor,
party_table.unidad_tramitadora,
party_table.organo_proponente,
where=party_table.id == party_id))
party_values = cursor.fetchone()
cursor.execute(*address_table.update(
columns=[
address_table.facturae_person_type,
address_table.facturae_residence_type,
address_table.oficina_contable,
address_table.organo_gestor,
address_table.unidad_tramitadora,
address_table.organo_proponente
],
values=[
party_values[0],
party_values[1],
party_values[2],
party_values[3],
party_values[4],
party_values[5],
],
where=address_table.id == address_update))
party.drop_column('facturae_person_type')
party.drop_column('facturae_residence_type')
party.drop_column('oficina_contable')
party.drop_column('organo_gestor')
party.drop_column('unidad_tramitadora')
party.drop_column('organo_proponente')

View File

@ -3,10 +3,10 @@
copyright notices and license terms. --> copyright notices and license terms. -->
<tryton> <tryton>
<data> <data>
<record model="ir.ui.view" id="party_view_form"> <record model="ir.ui.view" id="address_view_form">
<field name="model">party.party</field> <field name="model">party.address</field>
<field name="inherit" ref="party.party_view_form"/> <field name="inherit" ref="party.address_view_form"/>
<field name="name">party_form</field> <field name="name">address_form</field>
</record> </record>
</data> </data>
</tryton> </tryton>

View File

@ -38,20 +38,20 @@
<Parties> <Parties>
<SellerParty> <SellerParty>
<TaxIdentification> <TaxIdentification>
<PersonTypeCode>{{ invoice.company.party.facturae_person_type }}</PersonTypeCode> <PersonTypeCode>{{ invoice.company.facturae_person_type }}</PersonTypeCode>
<ResidenceTypeCode>{{ invoice.company.party.facturae_residence_type }}</ResidenceTypeCode> <ResidenceTypeCode>{{ invoice.company.facturae_residence_type }}</ResidenceTypeCode>
<TaxIdentificationNumber>{{ invoice.company.party.tax_identifier.code[:30] }}</TaxIdentificationNumber> <TaxIdentificationNumber>{{ invoice.company.party.tax_identifier.code[:30] }}</TaxIdentificationNumber>
</TaxIdentification> </TaxIdentification>
{# Optional. It could be the ID or the code #} {# Optional. It could be the ID or the code #}
{% if invoice.company.party.code and invoice.company.party.code | length < 10 %} {% if invoice.company.party.code and invoice.company.party.code | length < 10 %}
<PartyIdentification>{{ invoice.company.party.code|int or invoice.company.party.id }}</PartyIdentification> <PartyIdentification>{{ invoice.company.party.code|int or invoice.company.party.id }}</PartyIdentification>
{% endif %} {% endif %}
{% if invoice.company.party.oficina_contable or invoice.company.party.organo_gestor or invoice.company.party.unidad_tramitadora or invoice.company.party.organo_proponente %} {% if invoice.company.oficina_contable or invoice.company.organo_gestor or invoice.company.unidad_tramitadora or invoice.company.organo_proponente %}
<AdministrativeCentres> <AdministrativeCentres>
{% if invoice.company.party.oficina_contable %}{{ administrative_center(invoice.company.party.oficina_contable, '01', invoice.company.party) }}{% endif %} {% if invoice.company.oficina_contable %}{{ administrative_center(invoice.company.oficina_contable, '01', invoice.company.facturae_person_type, invoice.company.party.address_get('invoice')) }}{% endif %}
{% if invoice.company.party.organo_gestor %}{{ administrative_center(invoice.company.party.organo_gestor, '02', invoice.company.party) }}{% endif %} {% if invoice.company.organo_gestor %}{{ administrative_center(invoice.company.organo_gestor, '02', invoice.company.facturae_person_type, invoice.company.party.address_get('invoice')) }}{% endif %}
{% if invoice.company.party.unidad_tramitadora %}{{ administrative_center(invoice.company.party.unidad_tramitadora, '03', invoice.company.party) }}{% endif %} {% if invoice.company.unidad_tramitadora %}{{ administrative_center(invoice.company.unidad_tramitadora, '03', invoice.company.facturae_person_type, invoice.company.party.address_get('invoice')) }}{% endif %}
{% if invoice.company.party.organo_proponente %}{{ administrative_center(invoice.company.party.organo_proponente, '04', invoice.company.party) }}{% endif %} {% if invoice.company.organo_proponente %}{{ administrative_center(invoice.company.organo_proponente, '04', invoice.company.facturae_person_type, invoice.company.party.address_get('invoice')) }}{% endif %}
</AdministrativeCentres> </AdministrativeCentres>
{% endif %} {% endif %}
<LegalEntity> <LegalEntity>
@ -73,23 +73,23 @@
<BuyerParty> <BuyerParty>
<TaxIdentification> <TaxIdentification>
<PersonTypeCode>{{ invoice.party.facturae_person_type }}</PersonTypeCode> <PersonTypeCode>{{ invoice.invoice_address.facturae_person_type }}</PersonTypeCode>
<ResidenceTypeCode>{{ invoice.party.facturae_residence_type }}</ResidenceTypeCode> <ResidenceTypeCode>{{ invoice.invoice_address.facturae_residence_type }}</ResidenceTypeCode>
<TaxIdentificationNumber>{{ invoice.party.tax_identifier.code[:30] }}</TaxIdentificationNumber> <TaxIdentificationNumber>{{ invoice.party.tax_identifier.code[:30] }}</TaxIdentificationNumber>
</TaxIdentification> </TaxIdentification>
{# Optional. It could be the ID or the code #} {# Optional. It could be the ID or the code #}
{% if invoice.party.code and invoice.party.code | length < 10 %} {% if invoice.party.code and invoice.party.code | length < 10 %}
<PartyIdentification>{{ invoice.party.code|int or invoice.party.id }}</PartyIdentification> <PartyIdentification>{{ invoice.party.code|int or invoice.party.id }}</PartyIdentification>
{% endif %} {% endif %}
{% if invoice.party.oficina_contable or invoice.party.organo_gestor or invoice.party.unidad_tramitadora or invoice.party.organo_proponente %} {% if invoice.invoice_address.oficina_contable or invoice.invoice_address.organo_gestor or invoice.invoice_address.unidad_tramitadora or invoice.invoice_address.organo_proponente %}
<AdministrativeCentres> <AdministrativeCentres>
{% if invoice.party.oficina_contable %}{{ administrative_center(invoice.party.oficina_contable, '01', invoice.party) }}{% endif %} {% if invoice.invoice_address.oficina_contable %}{{ administrative_center(invoice.invoice_address.oficina_contable, '01', invoice.invoice_address.facturae_person_type, invoice.invoice_address) }}{% endif %}
{% if invoice.party.organo_gestor %}{{ administrative_center(invoice.party.organo_gestor, '02', invoice.party) }}{% endif %} {% if invoice.invoice_address.organo_gestor %}{{ administrative_center(invoice.invoice_address.organo_gestor, '02', invoice.invoice_address.facturae_person_type, invoice.invoice_address) }}{% endif %}
{% if invoice.party.unidad_tramitadora %}{{ administrative_center(invoice.party.unidad_tramitadora, '03', invoice.party) }}{% endif %} {% if invoice.invoice_address.unidad_tramitadora %}{{ administrative_center(invoice.invoice_address.unidad_tramitadora, '03', invoice.invoice_address.facturae_person_type, invoice.invoice_address) }}{% endif %}
{% if invoice.party.organo_proponente %}{{ administrative_center(invoice.party.organo_proponente, '04', invoice.party) }}{% endif %} {% if invoice.invoice_address.organo_proponente %}{{ administrative_center(invoice.invoice_address.organo_proponente, '04', invoice.invoice_address.facturae_person_type, invoice.invoice_address) }}{% endif %}
</AdministrativeCentres> </AdministrativeCentres>
{% endif %} {% endif %}
{% if invoice.party.facturae_person_type == 'J' %} {% if invoice.invoice_address.facturae_person_type == 'J' %}
<LegalEntity> <LegalEntity>
<CorporateName>{{ invoice.party.name and invoice.party.name[:80] or invoice.party.code[:80] }}</CorporateName> <CorporateName>{{ invoice.party.name and invoice.party.name[:80] or invoice.party.code[:80] }}</CorporateName>
{% if invoice.party.trade_name %} {% if invoice.party.trade_name %}
@ -162,7 +162,7 @@
{% if invoice.currency != euro %} {% if invoice.currency != euro %}
<ExchangeRateDetails> <ExchangeRateDetails>
<ExchangeRate>{{ Invoice.double_up_to_eight(exchange_rate) }}</ExchangeRate> <ExchangeRate>{{ Invoice.double_up_to_eight(exchange_rate) }}</ExchangeRate>
<ExchangeRateDate>{{ exchange_rate_date }}</ExchangeRateDate> <ExchangeRateDate>{% if exchange_rate_date %}{{ exchange_rate_date.isoformat() }}{% endif %}</ExchangeRateDate>
</ExchangeRateDetails> </ExchangeRateDetails>
{% endif %} {% endif %}
<TaxCurrencyCode>EUR</TaxCurrencyCode> <TaxCurrencyCode>EUR</TaxCurrencyCode>
@ -255,7 +255,7 @@
- Extensions - Extensions
#} #}
<ReceiverTransactionReference>{{ line.facturae_receiver_transaction_reference }}</ReceiverTransactionReference> <ReceiverTransactionReference>{{ line.facturae_receiver_transaction_reference }}</ReceiverTransactionReference>
<ItemDescription>{{ line.facturae_item_description }}</ItemDescription> <ItemDescription>{{ line.facturae_item_description[:2500] }}</ItemDescription>
<Quantity>{{ line.quantity }}</Quantity> <Quantity>{{ line.quantity }}</Quantity>
<UnitOfMeasure>{{ UOM_CODE2TYPE.get(line.unit.symbol, '05') if line.unit else '05' }}</UnitOfMeasure> <UnitOfMeasure>{{ UOM_CODE2TYPE.get(line.unit.symbol, '05') if line.unit else '05' }}</UnitOfMeasure>
<UnitPriceWithoutTax>{{ Invoice.double_up_to_eight(line.unit_price) }}</UnitPriceWithoutTax> <UnitPriceWithoutTax>{{ Invoice.double_up_to_eight(line.unit_price) }}</UnitPriceWithoutTax>
@ -321,10 +321,10 @@
</Tax> </Tax>
{% endfor %} {% endfor %}
</TaxesOutputs> </TaxesOutputs>
{% if line.origin and line.origin.contract %} {% if line.facturae_start_date %}
<LineItemPeriod> <LineItemPeriod>
<StartDate>{{ line.origin.start_date }}</StartDate> <StartDate>{{ line.facturae_start_date.isoformat() }}</StartDate>
<EndDate>{{ line.origin.end_date }}</EndDate> <EndDate>{{ line.facturae_end_date.isoformat() }}</EndDate>
</LineItemPeriod> </LineItemPeriod>
{% endif %} {% endif %}
{% if line.taxes_additional_line_item_information or line.note %} {% if line.taxes_additional_line_item_information or line.note %}

View File

@ -1,19 +1,19 @@
{% macro administrative_center(centre_code, role_type_code, party) %} {% macro administrative_center(centre_code, role_type_code, person_type, addrs) %}
<AdministrativeCentre> <AdministrativeCentre>
<CentreCode>{{ centre_code }}</CentreCode> <CentreCode>{{ centre_code }}</CentreCode>
<RoleTypeCode>{{ role_type_code }}</RoleTypeCode> <RoleTypeCode>{{ role_type_code }}</RoleTypeCode>
<Name>{% if party.facturae_person_type == 'J' %}{{ party.name and party.name[:40] }}{% else %}{{ party.name and party.name.split(' ', 2)[0][:40] or party.code[:40] }}{% endif %}</Name> <Name>{% if person_type == 'J' %}{{ addrs.party.name and addrs.party.name[:40] }}{% else %}{{ addrs.party.name and addrs.party.name.split(' ', 2)[0][:40] or addrs.party.code[:40] }}{% endif %}</Name>
{% if party.facturae_person_type == 'F' %} {% if person_type == 'F' %}
<FirstSurname>{{ party.name and party.name.split(' ', 2)[1][:40] }}</FirstSurname> <FirstSurname>{{ addrs.party.name and addrs.party.name.split(' ', 2)[1][:40] }}</FirstSurname>
{% endif %} {% endif %}
{% if party.facturae_person_type == 'F' and party.name.split(' ') | length > 2 %} {% if person_type == 'F' and addrs.party.name.split(' ') | length > 2 %}
<SecondSurname>{{ party.name and party.name.split(' ', 2)[2][:40] }}</SecondSurname> <SecondSurname>{{ addrs.party.name and addrs.party.name.split(' ', 2)[2][:40] }}</SecondSurname>
{% endif %} {% endif %}
{% if party.addresses %}{{ address(party.addresses[0]) }}{% endif %} {% if addrs %}{{ address(addrs) }}{% endif %}
{% if party.contact_mechanisms %}{{ contact(party) }}{% endif %} {% if addrs.party.contact_mechanisms %}{{ contact(addrs.party) }}{% endif %}
<PhysicalGLN/> <PhysicalGLN/>
<LogicalOperationalPoint/> <LogicalOperationalPoint/>
<CentreDescription>{{ party.name and party.name.split(' ', 2)[0][:40] or party.code[:40] }}</CentreDescription> <CentreDescription>{{ addrs.party.name and addrs.party.name.split(' ', 2)[0][:40] or addrs.party.code[:40] }}</CentreDescription>
</AdministrativeCentre> </AdministrativeCentre>
{% endmacro %} {% endmacro %}

View File

@ -58,8 +58,8 @@ class AccountInvoiceFacturaeTestCase(CompanyTestMixin, ModuleTestCase):
company.header = 'Report Header' company.header = 'Report Header'
company.party.name = 'Seller' company.party.name = 'Seller'
company.party.identifiers = [tax_identifier] company.party.identifiers = [tax_identifier]
company.party.facturae_person_type = 'J' company.facturae_person_type = 'J'
company.party.facturae_residence_type = 'R' company.facturae_residence_type = 'R'
company.party.save() company.party.save()
company.save() company.save()
@ -108,8 +108,6 @@ class AccountInvoiceFacturaeTestCase(CompanyTestMixin, ModuleTestCase):
company_address.country = country company_address.country = country
company_address.save() company_address.save()
party = Party(name='Buyer') party = Party(name='Buyer')
party.facturae_person_type = 'J'
party.facturae_residence_type = 'R'
tax_identifier = PartyIdentifier() tax_identifier = PartyIdentifier()
tax_identifier.type = 'eu_vat' tax_identifier.type = 'eu_vat'
tax_identifier.code = 'BE0897290877' tax_identifier.code = 'BE0897290877'
@ -123,6 +121,8 @@ class AccountInvoiceFacturaeTestCase(CompanyTestMixin, ModuleTestCase):
'postal_code': '08201', 'postal_code': '08201',
'subdivision': subdivision.id, 'subdivision': subdivision.id,
'country': country.id, 'country': country.id,
'address.facturae_person_type': 'J',
'address.facturae_residence_type': 'R',
} }
address, = Address.create([address_dict]) address, = Address.create([address_dict])

View File

@ -8,10 +8,12 @@ depends:
extras_depend: extras_depend:
account_bank account_bank
account_es account_es
contract
xml: xml:
account.xml account.xml
account_es.xml
company.xml
invoice.xml invoice.xml
party.xml party.xml
payment_type.xml payment_type.xml
account_es.xml
message.xml message.xml

View File

@ -2,7 +2,7 @@
<!-- The COPYRIGHT file at the top level of this repository contains the full <!-- The COPYRIGHT file at the top level of this repository contains the full
copyright notices and license terms. --> copyright notices and license terms. -->
<data> <data>
<xpath expr="/form/notebook/page[@id='accounting']" position="after"> <xpath expr="/form/notebook" position="inside">
<page id="facturae" string="Factura-e"> <page id="facturae" string="Factura-e">
<label name="facturae_person_type"/> <label name="facturae_person_type"/>
<field name="facturae_person_type"/> <field name="facturae_person_type"/>

21
view/company_form.xml Normal file
View File

@ -0,0 +1,21 @@
<?xml version="1.0"?>
<!-- The COPYRIGHT file at the top level of this repository contains the full
copyright notices and license terms. -->
<data>
<xpath expr="/form/notebook" position="inside">
<page id="facturae" string="Factura-e">
<label name="facturae_person_type"/>
<field name="facturae_person_type"/>
<label name="facturae_residence_type"/>
<field name="facturae_residence_type"/>
<label name="oficina_contable"/>
<field name="oficina_contable"/>
<label name="organo_gestor"/>
<field name="organo_gestor"/>
<label name="unidad_tramitadora"/>
<field name="unidad_tramitadora"/>
<label name="organo_proponente"/>
<field name="organo_proponente"/>
</page>
</xpath>
</data>

View File

@ -10,7 +10,7 @@
yalign="0.0" xalign="0.0" xexpand="1"/> yalign="0.0" xalign="0.0" xexpand="1"/>
<label name="service"/> <label name="service"/>
<field name="service"/> <field name="service"/>
<label name="certificate_facturae"/> <label name="certificate"/>
<field name="certificate_facturae"/> <field name="certificate"/>
</group> </group>
</form> </form>