Sign factura-e by xmlsign and amounts with two digits

#045097
This commit is contained in:
Raimon Esteve 2021-09-13 17:09:07 +02:00
parent 6a4677c3a6
commit d374361a82
34 changed files with 231 additions and 368 deletions

View File

@ -56,6 +56,7 @@ class TaxTemplate(metaclass=PoolMeta):
return res
class Tax(metaclass=PoolMeta):
__name__ = 'account.tax'

View File

@ -1,17 +1,21 @@
# -*- coding: utf-8 -*-
# The COPYRIGHT file at the top level of this repository contains the full
# copyright notices and license terms.
import glob
import logging
import os
import re
import base64
import random
import xmlsig
import hashlib
import datetime
from decimal import Decimal
from jinja2 import Environment, FileSystemLoader
from lxml import etree
from operator import attrgetter
from subprocess import Popen, PIPE
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.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval
@ -151,6 +155,16 @@ class Invoice(metaclass=PoolMeta):
}
})
@classmethod
def copy(cls, invoices, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.setdefault('invoice_facturae', None)
default.setdefault('rectificative_reason_code', None)
return super(Invoice, cls).copy(invoices, default=default)
def get_credited_invoices(self, name):
pool = Pool()
InvoiceLine = pool.get('account.invoice.line')
@ -308,7 +322,7 @@ class Invoice(metaclass=PoolMeta):
'account_invoice_facturae.invoice_address_fields',
invoice=self.rec_name))
euro, = Currency.search([('code', '=', 'EUR')])
euro, = Currency.search([('code', '=', 'EUR')], limit=1)
if self.currency != euro:
assert (euro.rate == Decimal(1)
or self.currency.rate == Decimal(1)), (
@ -436,52 +450,184 @@ class Invoice(metaclass=PoolMeta):
cert_file.write(self.company.facturae_certificate)
cert_file.close()
signed_file = NamedTemporaryFile(suffix='.xsig', delete=False)
def _sign_file(cert, password, request):
(
private_key,
certificate,
additional_certificates,
) = pkcs12.load_key_and_certificates(cert, password)
# key = private_key.private_bytes(
# encoding=serialization.Encoding.PEM,
# format=serialization.PrivateFormat.PKCS8,
# encryption_algorithm=serialization.NoEncryption(),
# )
# DER is an ASN.1 encoding type
crt = certificate.public_bytes(serialization.Encoding.DER)
env = {}
env.update(os.environ)
libs = os.path.join(module_path(), 'java', 'lib', '*.jar')
env['CLASSPATH'] = ':'.join(glob.glob(libs))
rand_min = 1
rand_max = 99999
signature_id = "Signature%05d" % random.randint(rand_min, rand_max)
signed_properties_id = (
signature_id
+ "-SignedProperties%05d" % random.randint(rand_min, rand_max)
)
key_info_id = "KeyInfo%05d" % random.randint(rand_min, rand_max)
reference_id = "Reference%05d" % random.randint(rand_min, rand_max)
object_id = "Object%05d" % random.randint(rand_min, rand_max)
etsi = "http://uri.etsi.org/01903/v1.3.2#"
sig_policy_identifier = (
"http://www.facturae.es/"
"politica_de_firma_formato_facturae/"
"politica_de_firma_formato_facturae_v3_1"
".pdf"
)
sig_policy_hash_value = "Ohixl6upD6av8N7pEvDABhEL6hM="
root = etree.fromstring(request)
sign = xmlsig.template.create(
c14n_method=xmlsig.constants.TransformInclC14N,
sign_method=xmlsig.constants.TransformRsaSha1,
name=signature_id,
ns="ds",
)
key_info = xmlsig.template.ensure_key_info(sign, name=key_info_id)
x509_data = xmlsig.template.add_x509_data(key_info)
xmlsig.template.x509_data_add_certificate(x509_data)
xmlsig.template.add_key_value(key_info)
# TODO: implement Signer with python
# http://www.pyopenssl.org/en/stable/api/crypto.html#OpenSSL.crypto.load_pkcs12
signature_command = [
'java',
'-Djava.awt.headless=true',
'com.nantic.facturae.Signer',
'0',
unsigned_file.name,
signed_file.name,
'facturae31',
cert_file.name,
certificate_password
]
signature_process = Popen(signature_command,
stdout=PIPE,
stderr=PIPE,
env=env,
cwd=os.path.join(module_path(), 'java'))
output, err = signature_process.communicate()
rc = signature_process.returncode
if rc != 0:
logger.warning('Error %s signing invoice "%s" with command '
'"%s <password>": %s %s', rc, self.id,
signature_command[:-1], output, err)
raise UserError(gettext(
'account_invoice_factura.error_signing',
invoice=self.rec_name,
process_output=output))
xmlsig.template.add_reference(
sign,
xmlsig.constants.TransformSha1,
uri="#" + signed_properties_id,
uri_type="http://uri.etsi.org/01903#SignedProperties",
)
xmlsig.template.add_reference(
sign, xmlsig.constants.TransformSha1, uri="#" + key_info_id
)
ref = xmlsig.template.add_reference(
sign, xmlsig.constants.TransformSha1, name=reference_id, uri=""
)
xmlsig.template.add_transform(ref, xmlsig.constants.TransformEnveloped)
object_node = etree.SubElement(
sign,
etree.QName(xmlsig.constants.DSigNs, "Object"),
nsmap={"etsi": etsi},
attrib={xmlsig.constants.ID_ATTR: object_id},
)
qualifying_properties = etree.SubElement(
object_node,
etree.QName(etsi, "QualifyingProperties"),
attrib={"Target": "#" + signature_id},
)
signed_properties = etree.SubElement(
qualifying_properties,
etree.QName(etsi, "SignedProperties"),
attrib={xmlsig.constants.ID_ATTR: signed_properties_id},
)
signed_signature_properties = etree.SubElement(
signed_properties, etree.QName(etsi, "SignedSignatureProperties")
)
now = datetime.datetime.now()
etree.SubElement(
signed_signature_properties, etree.QName(etsi, "SigningTime")
).text = now.isoformat()
signing_certificate = etree.SubElement(
signed_signature_properties, etree.QName(etsi, "SigningCertificate")
)
signing_certificate_cert = etree.SubElement(
signing_certificate, etree.QName(etsi, "Cert")
)
cert_digest = etree.SubElement(
signing_certificate_cert, etree.QName(etsi, "CertDigest")
)
etree.SubElement(
cert_digest,
etree.QName(xmlsig.constants.DSigNs, "DigestMethod"),
attrib={"Algorithm": "http://www.w3.org/2000/09/xmldsig#sha1"},
)
hash_cert = hashlib.sha1(crt)
etree.SubElement(
cert_digest, etree.QName(xmlsig.constants.DSigNs, "DigestValue")
).text = base64.b64encode(hash_cert.digest())
issuer_serial = etree.SubElement(
signing_certificate_cert, etree.QName(etsi, "IssuerSerial")
)
etree.SubElement(
issuer_serial, etree.QName(xmlsig.constants.DSigNs, "X509IssuerName")
).text = xmlsig.utils.get_rdns_name(certificate.issuer.rdns)
etree.SubElement(
issuer_serial, etree.QName(xmlsig.constants.DSigNs, "X509SerialNumber")
).text = str(certificate.serial_number)
signature_policy_identifier = etree.SubElement(
signed_signature_properties,
etree.QName(etsi, "SignaturePolicyIdentifier"),
)
signature_policy_id = etree.SubElement(
signature_policy_identifier, etree.QName(etsi, "SignaturePolicyId")
)
sig_policy_id = etree.SubElement(
signature_policy_id, etree.QName(etsi, "SigPolicyId")
)
etree.SubElement(
sig_policy_id, etree.QName(etsi, "Identifier")
).text = sig_policy_identifier
etree.SubElement(
sig_policy_id, etree.QName(etsi, "Description")
).text = "Política de Firma FacturaE v3.1"
sig_policy_hash = etree.SubElement(
signature_policy_id, etree.QName(etsi, "SigPolicyHash")
)
etree.SubElement(
sig_policy_hash,
etree.QName(xmlsig.constants.DSigNs, "DigestMethod"),
attrib={"Algorithm": "http://www.w3.org/2000/09/xmldsig#sha1"},
)
hash_value = sig_policy_hash_value
etree.SubElement(
sig_policy_hash, etree.QName(xmlsig.constants.DSigNs, "DigestValue")
).text = hash_value
signer_role = etree.SubElement(
signed_signature_properties, etree.QName(etsi, "SignerRole")
)
claimed_roles = etree.SubElement(
signer_role, etree.QName(etsi, "ClaimedRoles")
)
etree.SubElement(
claimed_roles, etree.QName(etsi, "ClaimedRole")
).text = "supplier"
signed_data_object_properties = etree.SubElement(
signed_properties, etree.QName(etsi, "SignedDataObjectProperties")
)
data_object_format = etree.SubElement(
signed_data_object_properties,
etree.QName(etsi, "DataObjectFormat"),
attrib={"ObjectReference": "#" + reference_id},
)
etree.SubElement(
data_object_format, etree.QName(etsi, "Description")
).text = "Factura"
etree.SubElement(
data_object_format, etree.QName(etsi, "MimeType")
).text = "text/xml"
ctx = xmlsig.SignatureContext()
ctx.x509 = certificate
ctx.public_key = certificate.public_key()
ctx.private_key = private_key
root.append(sign)
ctx.sign(sign)
return etree.tostring(root, xml_declaration=True, encoding="UTF-8")
signed_file_content = _sign_file(
self.company.facturae_certificate,
certificate_password.encode(),
xml_string,
)
logger.info("Factura-e for invoice %s (%s) generated and signed",
self.rec_name, self.id)
signed_file_content = signed_file.read()
signed_file.close()
os.unlink(unsigned_file.name)
os.unlink(cert_file.name)
os.unlink(signed_file.name)
return signed_file_content

View File

@ -1,51 +0,0 @@
package com.nantic.facturae;
import com.nantic.facturae.xmlsign.Sign;
public class Signer {
public static void main (String [] args) {
if (args.length < 1) {
System.out.println("Missing operation param");
System.exit(-1);
}
int op = Integer.parseInt(args[0]);
switch (op) {
case 0: {
if (args.length < 6) {
System.out.println("Missing expected params: 0 " +
"<file-to-sign.xml> <target-file.xsig> <policy> " +
"<PKCS12-certificate.p12> <password>");
System.exit(-1);
}
Sign s = new Sign();
s.FirmaXADES_EPES(args[1], args[2], args[3], args[4], args[5]);
break;
}
// case 1: {
// Verify v = new Verify();
// if (v.validarFichero(args[1])) break;
// System.out.println("La firma no es valida.");
// System.exit(2001);
// break;
// }
// case 2: {
// PDFSign pdfsign = new PDFSign();
// float[] dim = new float[]{Float.parseFloat(args[8]), Float.parseFloat(args[9]), Float.parseFloat(args[10]), Float.parseFloat(args[11])};
// try {
// pdfsign.FirmaPDF(args[1], args[2], args[3], args[4], args[5], args[6], args[7], dim[0], dim[1], dim[2], dim[3]);
// }
// catch (Exception e) {
// e.printStackTrace();
// System.exit(3001);
// }
// break;
// }
default: {
System.out.println("Unexpected operation");
System.exit(9001);
}
}
System.exit(0);
}
}

View File

@ -1,18 +0,0 @@
package com.nantic.facturae.xmlsign;
import java.security.cert.X509Certificate;
import es.mityc.javasign.pkstore.IPassStoreKS;
public class PassStoreKS implements IPassStoreKS {
private transient String password;
public PassStoreKS(String pass) {
this.password = new String(pass);
}
public char[] getPassword(X509Certificate certificate, String alias) {
return this.password.toCharArray();
}
}

View File

@ -1,151 +0,0 @@
package com.nantic.facturae.xmlsign;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.URI;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
import es.mityc.firmaJava.libreria.utilidades.UtilidadTratarNodo;
import es.mityc.firmaJava.libreria.xades.DataToSign;
import es.mityc.javasign.EnumFormatoFirma;
import es.mityc.firmaJava.libreria.xades.FirmaXML;
import es.mityc.firmaJava.libreria.xades.XAdESSchemas;
import es.mityc.firmaJava.libreria.xades.elementos.xades.ObjectIdentifier;
import es.mityc.javasign.pkstore.CertStoreException;
import es.mityc.javasign.pkstore.IPassStoreKS;
import es.mityc.javasign.pkstore.keystore.KSStore;
import es.mityc.javasign.xml.refs.AbstractObjectToSign;
import es.mityc.javasign.xml.refs.AllXMLToSign;
import es.mityc.javasign.xml.refs.ObjectToSign;
import com.nantic.facturae.xmlsign.PassStoreKS;
public class Sign extends FirmaXML {
public void FirmaXADES_EPES(String xmlOrigen, String xmlDestino, String policy, String certificado, String password) {
KSStore storeManager = null;
try {
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(new FileInputStream(certificado), password.toCharArray());
storeManager = new KSStore(ks, (IPassStoreKS)new PassStoreKS(password));
}
catch (KeyStoreException ex) {
System.out.println("Error KeyStoreException...");
System.exit(1001);
}
catch (NoSuchAlgorithmException ex) {
System.out.println("Error NoSuchAlgorithmException...");
System.exit(1002);
}
catch (CertificateException ex) {
System.out.println("Error CertificateException...");
System.exit(1003);
}
catch (IOException ex) {
System.out.println("Error IOException... Provably the supplied password is incorrect");
System.exit(1004);
}
List certs = null;
try {
certs = storeManager.getSignCertificates();
}
catch (CertStoreException ex) {
System.out.println("Error al obtener los certificados");
System.exit(1005);
}
if (certs == null || certs.size() == 0) {
System.out.println("No hay certificados");
System.exit(1006);
}
X509Certificate certificate = (X509Certificate)certs.get(0);
PrivateKey privateKey = null;
try {
privateKey = storeManager.getPrivateKey(certificate);
}
catch (CertStoreException e) {
System.out.println("Error al acceder al almac\u00e9n");
System.exit(1007);
}
Provider provider = storeManager.getProvider(certificate);
DataToSign dataToSign = this.createDataToSign(xmlOrigen, policy);
Object[] res = null;
try {
res = this.signFile(certificate, dataToSign, privateKey, provider);
}
catch (Exception ex) {
System.out.println("Error!!!");
System.exit(1008);
}
try {
UtilidadTratarNodo.saveDocumentToOutputStream((Document)((Document)res[0]), (OutputStream)new FileOutputStream(xmlDestino), (boolean)true);
}
catch (FileNotFoundException e) {
System.out.println("Error!");
System.exit(1009);
}
System.out.println("\u00a1XML de origen firmado correctamente!.");
}
private DataToSign createDataToSign(String xmlOrigen, String policy) {
DataToSign dataToSign = new DataToSign();
dataToSign.setXadesFormat(EnumFormatoFirma.XAdES_BES);
dataToSign.setEsquema(XAdESSchemas.XAdES_132);
dataToSign.setPolicyKey(policy);
dataToSign.setAddPolicy(true);
dataToSign.setXMLEncoding("UTF-8");
dataToSign.setEnveloped(true);
dataToSign.addObject(new ObjectToSign((AbstractObjectToSign)new AllXMLToSign(), "Documento de ejemplo", null, "text/xml", null));
Document docToSign = this.getDocument(xmlOrigen);
dataToSign.setDocument(docToSign);
return dataToSign;
}
private Document getDocument(String resource) {
Document doc = null;
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
try {
doc = dbf.newDocumentBuilder().parse(new BufferedInputStream(new FileInputStream(resource)));
}
catch (ParserConfigurationException ex) {
System.err.println("Error al parsear el documento");
ex.printStackTrace();
System.exit(-1);
}
catch (SAXException ex) {
System.err.println("Error al parsear el documento");
ex.printStackTrace();
System.exit(-1);
}
catch (IOException ex) {
System.err.println("Error al parsear el documento");
ex.printStackTrace();
System.exit(-1);
}
catch (IllegalArgumentException ex) {
System.err.println("Error al parsear el documento");
ex.printStackTrace();
System.exit(-1);
}
return doc;
}
}

View File

@ -1,41 +0,0 @@
#!/bin/bash
GENERATED_JAR="nantic-facturae-0.1.jar"
rm lib/$GENERATED_JAR
if [ -z "$JAVA_HOME" ]; then
directories="/usr/lib/jvm/java-7-openjdk-amd64/bin /usr/lib/j2sdk1.6-sun /usr/lib/j2sdk1.5-sun"
for d in $directories; do
if [ -d "$d" ]; then
export JAVA_HOME="$d"
fi
done
fi
# echo "JAVA_HOME=$JAVA_HOME"
# export PATH="$JAVA_HOME"/bin:/bin:/usr/bin
export CLASSPATH=$(ls -1 lib/* | grep jar$ | awk '{printf "%s:", $1}')
# echo "Class-Path: $(ls -1 lib/* | grep jar$ | awk '{printf "%s:", $1}')" > Manifest.txt
FILES=$(find com -iname "*.java")
echo "Compiling com.nantic.facturae"
javac -Xlint:deprecation $FILES || exit
# echo "Main-Class: com.nantic.facturae.Signer" > Manifest.txt
# echo "Class-Path:" >> Manifest.txt
# for jarfile in `ls -1 lib/*.jar`
# do
# echo " $jarfile" >> Manifest.txt
# done
# echo "" >> Manifest.txt
#
# jar cvfm nantic-facturae-0.1.jar Manifest.txt com
jar cvf $GENERATED_JAR com
mv $GENERATED_JAR lib/$GENERATED_JAR
# echo "Executing java com.nantic.facturae.Sign"
export CLASSPATH="lib/$GENERATED_JAR"":$CLASSPATH"
java com.nantic.facturae.Signer

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -10,21 +10,21 @@
<BatchIdentifier>{{ ('%s%s' % (invoice.company.party.tax_identifier.code, invoice.number))[:70] }}</BatchIdentifier>
<InvoicesCount>1</InvoicesCount>
<TotalInvoicesAmount>
<TotalAmount>{{ invoice.total_amount }}</TotalAmount>
<TotalAmount>{{ Currency.compute(invoice.currency, invoice.total_amount, euro) }}</TotalAmount>
{% if invoice.currency != euro %}
<EquivalentInEuros>{{ Currency.compute(invoice.currency, invoice.total_amount, euro) }}</EquivalentInEuros>
{% endif %}
</TotalInvoicesAmount>
<TotalOutstandingAmount>
{# TODO: it must to get amount_to_pay? #}
<TotalAmount>{{ invoice.total_amount }}</TotalAmount>
<TotalAmount>{{ Currency.compute(invoice.currency, invoice.total_amount, euro) }}</TotalAmount>
{% if invoice.currency != euro %}
<EquivalentInEuros>{{ Currency.compute(invoice.currency, invoice.total_amount, euro) }}</EquivalentInEuros>
{% endif %}
</TotalOutstandingAmount>
<TotalExecutableAmount>
{# TODO: it must to get amount_to_pay? #}
<TotalAmount>{{ invoice.total_amount }}</TotalAmount>
<TotalAmount>{{ Currency.compute(invoice.currency, invoice.total_amount, euro) }}</TotalAmount>
{% if invoice.currency != euro %}
<EquivalentInEuros>{{ Currency.compute(invoice.currency, invoice.total_amount, euro) }}</EquivalentInEuros>
{% endif %}
@ -161,7 +161,7 @@
<InvoiceCurrencyCode>{{ invoice.currency.code.upper() }}</InvoiceCurrencyCode>
{% if invoice.currency != euro %}
<ExchangeRateDetails>
<ExchangeRate>{{ exchange_rate }}</ExchangeRate>
<ExchangeRate>{{ Currency.compute(invoice.currency, exchange_rate, euro) }}</ExchangeRate>
<ExchangeRateDate>{{ exchange_rate_date }}</ExchangeRateDate>
</ExchangeRateDetails>
{% endif %}
@ -172,15 +172,15 @@
{% for invoice_tax in invoice.taxes_outputs %}
<Tax>
<TaxTypeCode>{{ invoice_tax.tax.report_type }}</TaxTypeCode>
<TaxRate>{{ invoice_tax.tax.rate * 100 }}</TaxRate>
<TaxRate>{{ Currency.compute(invoice.currency, invoice_tax.tax.rate * 100, euro) }}</TaxRate>
<TaxableBase>
<TotalAmount>{{ invoice_tax.base }}</TotalAmount>
<TotalAmount>{{ Currency.compute(invoice.currency, invoice_tax.base, euro) }}</TotalAmount>
{% if invoice.currency != euro %}
<EquivalentInEuros>{{ Currency.compute(invoice.currency, invoice_tax.base, euro) }}</EquivalentInEuros>
{% endif %}
</TaxableBase>
<TaxAmount>
<TotalAmount>{{ invoice_tax.amount }}</TotalAmount>
<TotalAmount>{{ Currency.compute(invoice.currency, invoice_tax.amount, euro) }}</TotalAmount>
{% if invoice.currency != euro %}
<EquivalentInEuros>{{ Currency.compute(invoice.currency, invoice_tax.amount, euro) }}</EquivalentInEuros>
{% endif %}
@ -193,7 +193,7 @@
{# TODO: EquivalenceSurchace must to have its own Tax entry or it must to go to the IVA line? TaxRate == EquivalenceSurcharge and TaxAmount == EquivalenceSurchargeAmount? #}
<EquivalenceSurcharge>{{ (invoice_tax.tax.rate * 100).quantize(Decimal('0.01')) }}</EquivalenceSurcharge>
<EquivalenceSurchargeAmount>
<TotalAmount>{{ invoice_tax.amount }}</TotalAmount>
<TotalAmount>{{ Currency.compute(invoice.currency, invoice_tax.amount, euro) }}</TotalAmount>
{% if invoice.currency != euro %}
<EquivalentInEuros>{{ Currency.compute(invoice.currency, invoice_tax.amount, euro) }}</EquivalentInEuros>
{% endif %}
@ -207,15 +207,15 @@
{% for invoice_tax in invoice.taxes_withheld %}
<Tax>
<TaxTypeCode>{{ invoice_tax.tax.report_type }}</TaxTypeCode>
<TaxRate>{{ invoice_tax.tax.rate * 100 }}</TaxRate>
<TaxRate>{{ Currency.compute(invoice.currency, invoice_tax.tax.rate * 100, euro) }}</TaxRate>
<TaxableBase>
<TotalAmount>{{ invoice_tax.base }}</TotalAmount>
<TotalAmount>{{ Currency.compute(invoice.currency, invoice_tax.base, euro) }}</TotalAmount>
{% if invoice.currency != euro %}
<EquivalentInEuros>{{ Currency.compute(invoice.currency, invoice_tax.base, euro) }}</EquivalentInEuros>
{% endif %}
</TaxableBase>
<TaxAmount>
<TotalAmount>{{ invoice_tax.amount }}</TotalAmount>
<TotalAmount>{{ Currency.compute(invoice.currency, invoice_tax.amount, euro) }}</TotalAmount>
{% if invoice.currency != euro %}
<EquivalentInEuros>{{ Currency.compute(invoice.currency, invoice_tax.amount, euro) }}</EquivalentInEuros>
{% endif %}
@ -225,13 +225,13 @@
</TaxesWithheld>
{% endif %}
<InvoiceTotals>
<TotalGrossAmount>{{ invoice.untaxed_amount }}</TotalGrossAmount>
<TotalGrossAmount>{{ Currency.compute(invoice.currency, invoice.untaxed_amount, euro) }}</TotalGrossAmount>
{# TODO: GeneralDiscounts and TotalGeneralDiscounts (account_invoice_discount_global) not supported #}
{# TODO: GeneralSurcharges and TotalGeneralSurcharges not supported #}
<TotalGrossAmountBeforeTaxes>{{ invoice.untaxed_amount }}</TotalGrossAmountBeforeTaxes>
<TotalTaxOutputs>{{ invoice.taxes_outputs | sum(attribute='amount', start=Decimal(0)) }}</TotalTaxOutputs>
<TotalTaxesWithheld>{{ invoice.taxes_withheld | sum(attribute='amount', start=Decimal(0)) }}</TotalTaxesWithheld>
<InvoiceTotal>{{ invoice.total_amount }}</InvoiceTotal>
<TotalGrossAmountBeforeTaxes>{{ Currency.compute(invoice.currency, invoice.untaxed_amount, euro) }}</TotalGrossAmountBeforeTaxes>
<TotalTaxOutputs>{{ Currency.compute(invoice.currency, invoice.taxes_outputs | sum(attribute='amount', start=Decimal(0)), euro) }}</TotalTaxOutputs>
<TotalTaxesWithheld>{{ Currency.compute(invoice.currency, invoice.taxes_withheld | sum(attribute='amount', start=Decimal(0)), euro) }}</TotalTaxesWithheld>
<InvoiceTotal>{{ Currency.compute(invoice.currency, invoice.total_amount, euro) }}</InvoiceTotal>
{# TODO: optional, not supported
- Subsidies
- PaymentsOnAccount, TotalPaymentsOnAccount
@ -239,8 +239,8 @@
- TotalFinancialExpenses (account_payment_type_cost?)
- AmountsWithheld
#}
<TotalOutstandingAmount>{{ invoice.total_amount }}</TotalOutstandingAmount>
<TotalExecutableAmount>{{ invoice.total_amount }}</TotalExecutableAmount>
<TotalOutstandingAmount>{{ Currency.compute(invoice.currency, invoice.total_amount, euro) }}</TotalOutstandingAmount>
<TotalExecutableAmount>{{ Currency.compute(invoice.currency, invoice.total_amount, euro) }}</TotalExecutableAmount>
</InvoiceTotals>
<Items>
{% for line in invoice.lines if line.type == 'line' %}
@ -258,27 +258,27 @@
<ItemDescription>{{ line.description and line.description[:2500] or '' }}</ItemDescription>
<Quantity>{{ line.quantity }}</Quantity>
<UnitOfMeasure>{{ UOM_CODE2TYPE.get(line.unit.symbol, '05') if line.unit else '05' }}</UnitOfMeasure>
<UnitPriceWithoutTax>{{ line.unit_price }}</UnitPriceWithoutTax>
<TotalCost>{{ line.amount }}</TotalCost>
<UnitPriceWithoutTax>{{ Currency.compute(invoice.currency, line.unit_price, euro) }}</UnitPriceWithoutTax>
<TotalCost>{{ Currency.compute(invoice.currency, line.amount, euro) }}</TotalCost>
{# TODO: optional, not supported
- DiscountsAndRebates (account_invoice_discount)
- Charges
#}
<GrossAmount>{{ line.amount }}</GrossAmount>
<GrossAmount>{{ Currency.compute(invoice.currency, line.amount, euro) }}</GrossAmount>
{% if line.taxes_withheld %}
<TaxesWithheld>
{% for line_tax in invoice.taxes_withheld %}
<Tax>
<TaxTypeCode>{{ line_tax.tax.report_type }}</TaxTypeCode>
<TaxRate>{{ line_tax.tax.rate * 100 }}</TaxRate>
<TaxRate>{{ Currency.compute(invoice.currency, line_tax.tax.rate * 100, euro) }}</TaxRate>
<TaxableBase>
<TotalAmount>{{ line.amount }}</TotalAmount>
<TotalAmount>{{ Currency.compute(invoice.currency, line.amount, euro) }}</TotalAmount>
{% if invoice.currency != euro %}
<EquivalentInEuros>{{ Currency.compute(invoice.currency, line.amount, euro) }}</EquivalentInEuros>
{% endif %}
</TaxableBase>
<TaxAmount>
<TotalAmount>{{ line.amount * line_tax.tax.rate }}</TotalAmount>
<TotalAmount>{{ Currency.compute(invoice.currency, line.amount * line_tax.tax.rate, euro) }}</TotalAmount>
{% if invoice.currency != euro %}
<EquivalentInEuros>{{ Currency.compute(invoice.currency, line.amount * line_tax.tax.rate, euro) }}</EquivalentInEuros>
{% endif %}
@ -291,15 +291,15 @@
{% for line_tax in line.taxes_outputs %}
<Tax>
<TaxTypeCode>{{ line_tax.tax.report_type }}</TaxTypeCode>
<TaxRate>{{ line_tax.tax.rate * 100 }}</TaxRate>
<TaxRate>{{ Currency.compute(invoice.currency, line_tax.tax.rate * 100, euro) }}</TaxRate>
<TaxableBase>
<TotalAmount>{{ line.amount }}</TotalAmount>
<TotalAmount>{{ Currency.compute(invoice.currency, line.amount, euro) }}</TotalAmount>
{% if invoice.currency != euro %}
<EquivalentInEuros>{{ Currency.compute(invoice.currency, line.amount, euro) }}</EquivalentInEuros>
{% endif %}
</TaxableBase>
<TaxAmount>
<TotalAmount>{{ line.amount * line_tax.tax.rate }}</TotalAmount>
<TotalAmount>{{ Currency.compute(invoice.currency, line.amount * line_tax.tax.rate, euro) }}</TotalAmount>
{% if invoice.currency != euro %}
<EquivalentInEuros>{{ Currency.compute(invoice.currency, line.amount * line_tax.tax.rate, euro) }}</EquivalentInEuros>
{% endif %}
@ -312,7 +312,7 @@
{# TODO: EquivalenceSurchace must to have its own Tax entry or it must to go to the IVA line? TaxRate == EquivalenceSurcharge and TaxAmount == EquivalenceSurchargeAmount? #}
<EquivalenceSurcharge>{{ (line_tax.tax.rate * 100).quantize(Decimal('0.01')) }}</EquivalenceSurcharge>
<EquivalenceSurchargeAmount>
<TotalAmount>{{ line.amount * line_tax.tax.rate }}</TotalAmount>
<TotalAmount>{{ Currency.compute(invoice.currency, line.amount * line_tax.tax.rate, euro) }}</TotalAmount>
{% if invoice.currency != euro %}
<EquivalentInEuros>{{ Currency.compute(invoice.currency, line.amount * line_tax.tax.rate, euro) }}</EquivalentInEuros>
{% endif %}
@ -340,7 +340,7 @@
{% for move_line in invoice.payment_details %}
<Installment>
<InstallmentDueDate>{{ move_line.maturity_date.isoformat() }}</InstallmentDueDate>
<InstallmentAmount>{{ ((move_line.debit - move_line.credit) | abs).quantize(Decimal('0.01')) }}</InstallmentAmount>
<InstallmentAmount>{{ Currency.compute(invoice.currency, ((move_line.debit - move_line.credit) | abs).quantize(Decimal('0.01')), euro) }}</InstallmentAmount>
<PaymentMeans>{{ move_line.payment_type.facturae_type }}</PaymentMeans>
{% if move_line.payment_type.facturae_type == '04' %}
<AccountToBeCredited>
@ -376,7 +376,7 @@
- RelatedDocuments
- Extensions
#}
<InvoiceAdditionalInformation>Factura generada con Tryton (http://www.tryton.org)</InvoiceAdditionalInformation>
<InvoiceAdditionalInformation>Factura generada con Tryton (https://www.tryton.org)</InvoiceAdditionalInformation>
</AdditionalData>
</Invoice>
</Invoices>

View File

@ -67,30 +67,6 @@ class TestAccountInvoiceFacturaeCase(ModuleTestCase):
CURRENT_PATH, 'certificate.pfx'), 'rb') as cert_file:
company.facturae_certificate = cert_file.read()
payment_term, = PaymentTerm.create([{
'name': '20 days, 40 days',
'lines': [
('create', [{
'sequence': 0,
'type': 'percent',
'divisor': 2,
'ratio': Decimal('.5'),
'relativedeltas': [('create', [{
'days': 20,
},
]),
],
}, {
'sequence': 1,
'type': 'remainder',
'relativedeltas': [('create', [{
'days': 40,
},
]),
],
}])]
}])
with set_company(company):
create_chart(company, tax=True)
@ -225,7 +201,8 @@ class TestAccountInvoiceFacturaeCase(ModuleTestCase):
Invoice.post([invoice])
Invoice.generate_facturae_default([invoice], 'privatepassword')
self.assertNotEqual(invoice.invoice_facturae, None)
self.assertEqual(invoice.invoice_facturae_filename, 'facturae-1.xsig')
def suite():
suite = trytond.tests.test_tryton.suite()

View File

@ -2,10 +2,10 @@
<!--The COPYRIGHT file at the top level of this repository
contains the full copyright notices and license terms. -->
<data>
<xpath expr="/form/notebook/page[@id='general']/field[@name='update_unit_price']"
position="after">
<newline/>
<xpath expr="/form/notebook/page[@name='legal_notice']" position="after">
<page string="Factura-e" id="factura-e">
<label name="report_type"/>
<field name="report_type"/>
</page>
</xpath>
</data>
</data>

View File

@ -2,10 +2,10 @@
<!--The COPYRIGHT file at the top level of this repository
contains the full copyright notices and license terms. -->
<data>
<xpath expr="/form/notebook/page[@id='general']/field[@name='update_unit_price']"
position="after">
<newline/>
<xpath expr="/form/notebook/page[@name='legal_notice']" position="after">
<page string="Factura-e" id="factura-e">
<label name="report_type"/>
<field name="report_type"/>
</page>
</xpath>
</data>
</data>