Commit b2a2c97c authored by Jérome Perrin's avatar Jérome Perrin

accounting_l10n_fr: Fixes for FEC

This change modifies the FEC export to conform to the test software from
https://www.economie.gouv.fr/dgfip/outil-test-des-fichiers-des-ecritures-comptables-fec 
/ https://github.com/DGFiP/Test-Compta-Demat.

A new `test_compta_demat_compatibility` to enable compatibility mode was introduced.
This will be True when rendering the report from the dialog, i.e., using `AccountingTransactionModule_viewFrenchAccountingTransactionFile` and False when
using `AccountingTransactionModule_viewFrenchAccountingTransactionFileActive` directly
so the behavior of reusing FEC export for accounting interfaces should be the same as before.


See merge request !1764
parents 7c24c322 827410dc
# coding: utf-8
from cStringIO import StringIO
import zipfile
from Products.ERP5Type.Message import translateString
......@@ -11,11 +12,30 @@ result_list = active_process.getResultList()
fec_file = context.AccountingTransactionModule_viewComptabiliteAsFECXML(
at_date=at_date,
result_list=result_list)
if test_compta_demat_compatibility:
fec_file = (fec_file
# https://github.com/DGFiP/Test-Compta-Demat/issues/37
.replace(u"’", u"'")
.replace(u"Œ", u"OE")
.replace(u"œ", u"oe")
.replace(u"Ÿ", u"Y")
# https://github.com/DGFiP/Test-Compta-Demat/issues/39
.replace(u"€", u"EUR")
)
zipbuffer = StringIO()
zipfilename = at_date.strftime('FEC-%Y%m%d.zip')
zipfileobj = zipfile.ZipFile(zipbuffer, 'w', compression=zipfile.ZIP_DEFLATED)
zipfileobj.writestr('FEC.xml', fec_file.encode('utf8'))
filename = 'FEC.xml'
if test_compta_demat_compatibility:
siren = ''
if section_uid_list:
siret_list = [b.getObject().getCorporateRegistrationCode() for b in portal.portal_catalog(uid=section_uid_list)]
siret_list = [siret for siret in siret_list if siret]
if len(siret_list) == 1:
siren = siret_list[0][:8]
filename = at_date.strftime('{siren}FEC%Y%m%d.xml').format(siren=siren)
zipfileobj.writestr(filename, fec_file.encode('utf8'))
zipfileobj.close()
attachment_list = (
......
......@@ -50,7 +50,7 @@
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>at_date, active_process, user_name</string> </value>
<value> <string>at_date, active_process, user_name, test_compta_demat_compatibility, section_uid_list</string> </value>
</item>
<item>
<key> <string>_proxy_roles</string> </key>
......
<?xml version="1.0" encoding="UTF-8"?>
<comptabilite xmlns:tal="http://xml.zope.org/namespaces/tal">
<comptabilite xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="formatA47A-I-VII-1.xsd" xmlns:tal="http://xml.zope.org/namespaces/tal">
<exercice>
<DateCloture tal:content="python: options['at_date'].strftime('%Y-%m-%d')">20141231</DateCloture>
<tal:block tal:repeat="result options/result_list"><tal:block tal:replace="structure python: result.detail.decode('zlib')"/></tal:block>
......
......@@ -29,7 +29,9 @@ context.activate().AccountingTransactionModule_viewFrenchAccountingTransactionFi
tag=tag,
aggregate_tag=aggregate_tag,
date_column=date_column,
search_kw=search_kw)
search_kw=search_kw,
test_compta_demat_compatibility=True
)
return context.Base_redirect(form_id, keep_items=dict(
portal_status_message=translateString("Report Started")))
......@@ -88,7 +88,11 @@ for journal_code, journal_lib, journal_search_kw in _groupedJournalTupleDict():
portal.portal_catalog.searchAndActivate(
method_id='AccountingTransaction_postFECResult',
method_kw=dict(section_uid_list=section_uid_list, active_process=this_journal_active_process.getRelativeUrl()),
method_kw=dict(
section_uid_list=section_uid_list,
active_process=this_journal_active_process.getRelativeUrl(),
test_compta_demat_compatibility=test_compta_demat_compatibility,
),
activate_kw=dict(tag=tag, priority=priority),
**journal_search_kw)
......@@ -105,4 +109,7 @@ for journal_code, journal_lib, journal_search_kw in _groupedJournalTupleDict():
context.activate(after_tag=(tag, aggregate_tag)).AccountingTransactionModule_aggregateFrenchAccountingTransactionFile(
at_date,
active_process.getRelativeUrl(),
user_name=user_name)
user_name=user_name,
test_compta_demat_compatibility=test_compta_demat_compatibility,
section_uid_list=section_uid_list,
)
......@@ -50,7 +50,7 @@
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>section_category, section_category_strict, from_date, at_date, group_by, simulation_state, ledger, user_name=None, tag=None, aggregate_tag=None, date_column=None, search_kw=None, **kw</string> </value>
<value> <string>section_category, section_category_strict, from_date, at_date, group_by, simulation_state, ledger, user_name=None, tag=None, aggregate_tag=None, date_column=None, search_kw=None, test_compta_demat_compatibility=False, **kw</string> </value>
</item>
<item>
<key> <string>id</string> </key>
......
......@@ -10,10 +10,12 @@ accounting_line_list = context.contentValues(portal_type=portal.getPortalAccount
if context.getSourceSectionUid() in section_uid_list:
if any([line.getSource(portal_type='Account') for line in accounting_line_list]):
source_xml = context.AccountingTransaction_viewAsSourceFECXML()
source_xml = context.AccountingTransaction_viewAsSourceFECXML(
test_compta_demat_compatibility=test_compta_demat_compatibility)
active_process.postResult(ActiveResult(detail=source_xml.encode('utf8').encode('zlib')))
if context.getDestinationSectionUid() in section_uid_list:
if any([line.getDestination(portal_type='Account') for line in accounting_line_list]):
destination_xml = context.AccountingTransaction_viewAsDestinationFECXML()
destination_xml = context.AccountingTransaction_viewAsDestinationFECXML(
test_compta_demat_compatibility=test_compta_demat_compatibility)
active_process.postResult(ActiveResult(detail=destination_xml.encode('utf8').encode('zlib')))
......@@ -50,7 +50,7 @@
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>active_process, section_uid_list</string> </value>
<value> <string>active_process, section_uid_list, test_compta_demat_compatibility</string> </value>
</item>
<item>
<key> <string>_proxy_roles</string> </key>
......
......@@ -2,13 +2,14 @@
<EcritureNum tal:content="context/getDestinationReference">Specific Reference</EcritureNum>
<EcritureDate tal:content="python: context.getStopDate().strftime('%Y-%m-%d')">Specific Date</EcritureDate>
<EcritureLib tal:content="context/getTitle">Title</EcritureLib>
<PieceRef tal:content="context/getReference">Reference</PieceRef>
<PieceRef tal:content="python: (context.getReference() or context.getDestinationReference() if options['test_compta_demat_compatibility'] else context.getReference())">Reference</PieceRef>
<PieceDate tal:content="python: context.getStopDate().strftime('%Y-%m-%d')"></PieceDate>
<tal:block tal:replace="nothing"><EcritureLet></EcritureLet>
<DateLet></DateLet></tal:block>
<ValidDate tal:content="python: context.AccountingTransaction_getDeliverDate().strftime('%Y-%m-%d')">Modification Date</ValidDate>
<tal:block tal:repeat="line python:context.contentValues(portal_type=context.getPortalAccountingMovementTypeList())"><ligne
tal:define="account python:line.getDestinationValue(portal_type='Account')" tal:condition="python: account is not None">
tal:define="account python:line.getDestinationValue(portal_type='Account')"
tal:condition="python: account is not None and (line.getQuantity() or line.getDestinationAssetDebit() or line.getDestinationAssetCredit() or not options['test_compta_demat_compatibility'])">
<CompteNum tal:content="account/Account_getGapId">Reference Compte (Account_getGapId)</CompteNum>
<CompteLib tal:content="line/Movement_getDestinationCompteLib">Translated Title Compte</CompteLib>
<tal:block tal:condition="python: account.getAccountType() in ('asset/receivable', 'liability/payable')"
......
......@@ -2,13 +2,14 @@
<EcritureNum tal:content="context/getSourceReference">Specific Reference</EcritureNum>
<EcritureDate tal:content="python: context.getStartDate().strftime('%Y-%m-%d')">Specific Date</EcritureDate>
<EcritureLib tal:content="context/getTitle">Title</EcritureLib>
<PieceRef tal:content="context/getReference">Reference</PieceRef>
<PieceRef tal:content="python: (context.getReference() or context.getSourceReference() if options['test_compta_demat_compatibility'] else context.getReference())">Reference</PieceRef>
<PieceDate tal:content="python: context.getStartDate().strftime('%Y-%m-%d')"></PieceDate>
<tal:block tal:replace="nothing"><EcritureLet></EcritureLet>
<DateLet></DateLet></tal:block>
<ValidDate tal:content="python: context.AccountingTransaction_getDeliverDate().strftime('%Y-%m-%d')">Modification Date</ValidDate>
<tal:block tal:repeat="line python:context.contentValues(portal_type=context.getPortalAccountingMovementTypeList())"><ligne
tal:define="account python:line.getSourceValue(portal_type='Account')" tal:condition="python: account is not None">
tal:define="account python:line.getSourceValue(portal_type='Account')"
tal:condition="python: account is not None and (line.getQuantity() or line.getSourceAssetDebit() or line.getSourceAssetCredit() or not options['test_compta_demat_compatibility'])">
<CompteNum tal:content="account/Account_getGapId">Reference Compte (Account_getGapId)</CompteNum>
<CompteLib tal:content="line/Movement_getSourceCompteLib">Translated Title Compte</CompteLib>
<tal:block tal:condition="python: account.getAccountType() in ('asset/receivable', 'liability/payable')"
......
......@@ -56,6 +56,13 @@ class TestAccounting_l10n_fr(AccountingTestCase):
def afterSetUp(self):
AccountingTestCase.afterSetUp(self)
# set a corporate registration code (siret) on our section organisation
# > Le numéro SIRET (ou système d'identification du répertoire des
# > établissements) identifie chaque établissement de l'entreprise.
# > Il se compose de 14 chiffres : les neuf chiffres du numéro SIREN +
# > les cinq chiffres correspondant à un numéro NIC (numéro interne de
# > classement).
self.section.setCorporateRegistrationCode('12345689 12345')
# set a french gap on test accounts
account_module = self.portal.account_module
account_module.payable.setGap('fr/pcg/4/40/401')
......@@ -90,6 +97,42 @@ class TestAccounting_l10n_fr(AccountingTestCase):
user = uf.getUser(self.username).__of__(uf)
newSecurityManager(None, user)
def validateFECXML(self, tree):
# this "xsi:noNamespaceSchemaLocation" is used by xerces parser from
# https://github.com/DGFiP/Test-Compta-Demat/
noNamespaceSchemaLocation, = tree.xpath(
'./@xsi:noNamespaceSchemaLocation',
namespaces={'xsi': 'http://www.w3.org/2001/XMLSchema-instance'})
import Products.ERP5.tests
with open(os.path.join(
os.path.dirname(Products.ERP5.tests.__file__),
'test_data',
noNamespaceSchemaLocation,
)) as f:
xmlschema_doc = etree.parse(f)
xmlschema = etree.XMLSchema(xmlschema_doc)
self.assertFalse(xmlschema.validate(etree.fromstring('<invalide/>')))
xmlschema.assertValid(tree)
def getFECFromMailMessage(self):
last_message = self.portal.MailHost._last_message
self.assertNotEqual((), last_message)
_, mto, message_text = last_message
self.assertEqual('"%s" <%s>' % (self.first_name, self.recipient_email_address), mto[0])
mail_message = email.message_from_string(message_text)
for part in mail_message.walk():
content_type = part.get_content_type()
file_name = part.get_filename()
if file_name == 'FEC-20141231.zip':
self.assertEqual('application/zip', content_type)
data = part.get_payload(decode=True)
zf = zipfile.ZipFile(StringIO(data))
self.assertIn("12345689FEC20141231.xml", zf.namelist())
return zf.open("12345689FEC20141231.xml").read()
self.fail("Attachment not found")
def test_FEC(self):
account_module = self.portal.account_module
self._makeOne(
......@@ -127,32 +170,10 @@ class TestAccounting_l10n_fr(AccountingTestCase):
simulation_state=['delivered'])
self.tic()
fec_xml = ''
last_message = self.portal.MailHost._last_message
self.assertNotEqual((), last_message)
_, mto, message_text = last_message
self.assertEqual('"%s" <%s>' % (self.first_name, self.recipient_email_address), mto[0])
mail_message = email.message_from_string(message_text)
for part in mail_message.walk():
content_type = part.get_content_type()
file_name = part.get_filename()
if file_name == 'FEC-20141231.zip':
self.assertEqual('application/zip', content_type)
data = part.get_payload(decode=True)
zf = zipfile.ZipFile(StringIO(data))
fec_xml = zf.open("FEC.xml").read()
break
else:
self.fail("Attachment not found")
# validate against official schema
import Products.ERP5.tests
schema = etree.XMLSchema(etree.XML(open(os.path.join(
os.path.dirname(Products.ERP5.tests.__file__), 'test_data',
'formatA47A-I-VII-1.xsd')).read()))
fec_xml = self.getFECFromMailMessage()
# this raise if invalid
tree = etree.fromstring(fec_xml, etree.XMLParser(schema=schema))
tree = etree.fromstring(fec_xml)
self.validateFECXML(tree)
debit_list = tree.xpath("//Debit")
self.assertEqual(6, len(debit_list))
......@@ -220,33 +241,8 @@ class TestAccounting_l10n_fr(AccountingTestCase):
ledger=ledger_list)
self.tic()
fec_xml = ''
last_message = self.portal.MailHost._last_message
self.assertNotEqual((), last_message)
_, mto, message_text = last_message
self.assertEqual('"%s" <%s>' % (self.first_name, self.recipient_email_address), mto[0])
mail_message = email.message_from_string(message_text)
for part in mail_message.walk():
content_type = part.get_content_type()
file_name = part.get_filename()
if file_name == 'FEC-20141231.zip':
self.assertEqual('application/zip', content_type)
data = part.get_payload(decode=True)
zf = zipfile.ZipFile(StringIO(data))
fec_xml = zf.open("FEC.xml").read()
break
else:
self.fail("Attachment not found")
# validate against official schema
import Products.ERP5.tests
schema = etree.XMLSchema(etree.XML(open(os.path.join(
os.path.dirname(Products.ERP5.tests.__file__), 'test_data',
'formatA47A-I-VII-1.xsd')).read()))
# this raise if invalid
tree = etree.fromstring(fec_xml, etree.XMLParser(schema=schema))
tree = etree.fromstring(self.getFECFromMailMessage())
self.validateFECXML(tree)
return tree
def test_FECWithOneLedger(self):
......@@ -425,11 +421,171 @@ class TestAccounting_l10n_fr(AccountingTestCase):
assert invoice.workflow_history['accounting_workflow'][-1]['action'] == 'deliver'
invoice.workflow_history['accounting_workflow'][-1]['time'] = DateTime(2001, 2, 3)
tree = etree.fromstring(invoice.AccountingTransaction_viewAsSourceFECXML())
tree = etree.fromstring(
invoice.AccountingTransaction_viewAsSourceFECXML(
test_compta_demat_compatibility=True))
self.assertEqual(tree.xpath('//ValidDate/text()'), ['2001-02-03'])
tree = etree.fromstring(invoice.AccountingTransaction_viewAsDestinationFECXML())
tree = etree.fromstring(
invoice.AccountingTransaction_viewAsDestinationFECXML(
test_compta_demat_compatibility=True))
self.assertEqual(tree.xpath('//ValidDate/text()'), ['2001-02-03'])
def test_EscapeTestComptaDematUnsupportedCharacters(self):
# Workaround bugs with Test Compta Demat
# https://github.com/DGFiP/Test-Compta-Demat/issues/37
# https://github.com/DGFiP/Test-Compta-Demat/issues/39
account_module = self.portal.account_module
self._makeOne(
portal_type='Purchase Invoice Transaction',
title='Des œufs, des Œufs, des Ÿ et des €',
simulation_state='delivered',
reference='1',
source_section_value=self.organisation_module.supplier,
stop_date=DateTime(2014, 2, 2),
lines=(
dict(
destination_value=account_module.payable, destination_debit=132.00),
dict(
destination_value=account_module.refundable_vat,
destination_credit=22.00),
dict(
destination_value=account_module.goods_purchase,
destination_credit=110.00)))
self.tic()
self.portal.accounting_module.AccountingTransactionModule_viewFrenchAccountingTransactionFile(
section_category='group/demo_group',
section_category_strict=False,
at_date=DateTime(2014, 12, 31),
simulation_state=['delivered'])
self.tic()
tree = etree.fromstring(self.getFECFromMailMessage())
self.validateFECXML(tree)
self.assertEqual(
tree.xpath('//EcritureLib/text()'),
[u'Des oeufs, des OEufs, des Y et des EUR'])
def test_Skip0QuantityLines(self):
# Don't include lines with 0 quantity in the output, because they are
# reported as invalid by Test Compta Demat
account_module = self.portal.account_module
destination_invoice = self._makeOne(
portal_type='Purchase Invoice Transaction',
title='destination 0',
simulation_state='delivered',
reference='destination',
source_section_value=self.organisation_module.supplier,
stop_date=DateTime(2014, 2, 2),
lines=(
dict(
destination_value=account_module.payable, destination_debit=132.00),
dict(
destination_value=account_module.refundable_vat,
destination_credit=22.00),
dict(
destination_value=account_module.refundable_vat,
destination_credit=0.00),
dict(
destination_value=account_module.goods_purchase,
destination_credit=110.00)))
self._makeOne(
portal_type='Sale Invoice Transaction',
title='source 0',
simulation_state='delivered',
reference='source',
destination_section_value=self.organisation_module.client_2,
start_date=DateTime(2014, 3, 1),
lines=(
dict(source_value=account_module.receivable, source_debit=240.00),
dict(source_value=account_module.collected_vat, source_credit=0.00),
dict(source_value=account_module.collected_vat, source_credit=40.00),
dict(source_value=account_module.goods_sales, source_credit=200.00)))
self.tic()
# make sure we don't have interaction removing the lines
self.assertEqual(
sorted(
[
(line.getDestinationDebit(), line.getSourceDebit())
for line in destination_invoice.contentValues()
]), [
(0.0, 0.0),
(0.0, 22.0),
(0.0, 110.0),
(132.0, 0.0),
])
self.portal.accounting_module.AccountingTransactionModule_viewFrenchAccountingTransactionFile(
section_category='group/demo_group',
section_category_strict=False,
at_date=DateTime(2014, 12, 31),
simulation_state=['delivered'])
self.tic()
tree = etree.fromstring(self.getFECFromMailMessage())
self.validateFECXML(tree)
self.assertEqual(
tree.xpath(
'//ecriture/PieceRef[text()="destination"]/../ligne/Debit/text()'),
['132.00', '0.00', '0.00'])
self.assertEqual(
tree.xpath(
'//ecriture/PieceRef[text()="destination"]/../ligne/Credit/text()'),
['0.00', '22.00', '110.00'])
self.assertEqual(
tree.xpath('//ecriture/PieceRef[text()="source"]/../ligne/Debit/text()'),
['240.00', '0.00', '0.00'])
self.assertEqual(
tree.xpath(
'//ecriture/PieceRef[text()="source"]/../ligne/Credit/text()'),
['0.00', '40.00', '200.00'])
def test_PieceRefDefaultValue(self):
account_module = self.portal.account_module
invoice = self._makeOne(
portal_type='Purchase Invoice Transaction',
title='Première Écriture',
simulation_state='delivered',
source_section_value=self.organisation_module.supplier,
stop_date=DateTime(2014, 2, 2),
lines=(
dict(
destination_value=account_module.payable, destination_debit=132.00),
dict(
destination_value=account_module.refundable_vat,
destination_credit=22.00),
dict(
destination_value=account_module.goods_purchase,
destination_credit=110.00)))
invoice.setSourceReference('source_reference')
invoice.setDestinationReference('destination_reference')
tree = etree.fromstring(
invoice.AccountingTransaction_viewAsSourceFECXML(
test_compta_demat_compatibility=True))
self.assertEqual(tree.xpath('//EcritureNum/text()'), ['source_reference'])
self.assertEqual(tree.xpath('//PieceRef/text()'), ['source_reference'])
tree = etree.fromstring(
invoice.AccountingTransaction_viewAsDestinationFECXML(
test_compta_demat_compatibility=True))
self.assertEqual(
tree.xpath('//EcritureNum/text()'), ['destination_reference'])
self.assertEqual(
tree.xpath('//PieceRef/text()'), ['destination_reference'])
tree = etree.fromstring(
invoice.AccountingTransaction_viewAsSourceFECXML(
test_compta_demat_compatibility=False))
self.assertEqual(tree.xpath('//EcritureNum/text()'), ['source_reference'])
self.assertEqual([n.text for n in tree.xpath('//PieceRef')], [None])
tree = etree.fromstring(
invoice.AccountingTransaction_viewAsDestinationFECXML(
test_compta_demat_compatibility=False))
self.assertEqual(
tree.xpath('//EcritureNum/text()'), ['destination_reference'])
self.assertEqual([n.text for n in tree.xpath('//PieceRef')], [None])
def test_suite():
suite = unittest.TestSuite()
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment