toulouse_axel: invoice history (#39028)

This commit is contained in:
Lauréline Guérin 2020-01-20 09:15:03 +01:00
parent d088429533
commit 03b86d74af
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
5 changed files with 385 additions and 26 deletions

View File

@ -210,9 +210,9 @@ OperationResult = namedtuple('OperationResult', ['json_response', 'xml_request',
class Operation(object):
def __init__(self, operation, prefix='Dui/'):
def __init__(self, operation, prefix='Dui/', request_root_element='PORTAIL'):
self.operation = operation
self.request_converter = xml_schema_converter('%sQ_%s.xsd' % (prefix, operation), 'PORTAIL')
self.request_converter = xml_schema_converter('%sQ_%s.xsd' % (prefix, operation), request_root_element)
self.response_converter = xml_schema_converter('%sR_%s.xsd' % (prefix, operation), 'PORTAILSERVICE')
self.name = re.sub(
'(.?)([A-Z])',
@ -280,6 +280,7 @@ form_maj_famille_dui = Operation('FormMajFamilleDui')
form_paiement_dui = Operation('FormPaiementDui')
ref_facture_a_payer = Operation('RefFactureAPayer')
ref_facture_pdf = Operation('RefFacturePDF', prefix='')
list_dui_factures = Operation('ListeDuiFacturesPayeesRecettees', request_root_element='LISTFACTURE')
class ToulouseAxel(BaseResource):
@ -753,22 +754,38 @@ class ToulouseAxel(BaseResource):
}
}
def normalize_invoice(self, invoice, dui):
def normalize_invoice(self, invoice, dui, historical=False, vendor_base=None):
vendor = vendor_base or {}
vendor.update(invoice)
invoice_id = '%s-%s' % (dui, invoice['IDFACTURE'])
if historical:
invoice_id = 'historical-%s' % invoice_id
data = {
'id': invoice_id,
'display_id': str(invoice['IDFACTURE']),
'label': invoice['LIBELLE'],
'amount': invoice['RESTEAPAYER'],
'total_amount': invoice['MONTANTTOTAL'],
'created': invoice['DATEEMISSION'],
'pay_limit_date': invoice['DATEECHEANCE'],
'has_pdf': True if invoice['EXISTEPDF'] == '1' else False,
'paid': False,
'vendor': {'toulouse-axel': invoice},
'vendor': {'toulouse-axel': vendor},
}
pay_limit_date = datetime.datetime.strptime(invoice['DATEECHEANCE'], '%Y-%m-%d').date()
data['online_payment'] = data['amount'] > 0 and pay_limit_date >= datetime.date.today()
if historical:
data.update({
'amount': 0,
'total_amount': invoice['MONTANT'],
'created': invoice['EMISSION'],
'pay_limit_date': '',
'online_payment': False,
'has_pdf': invoice['IPDF'] == '1',
})
else:
data.update({
'amount': invoice['RESTEAPAYER'],
'total_amount': invoice['MONTANTTOTAL'],
'created': invoice['DATEEMISSION'],
'pay_limit_date': invoice['DATEECHEANCE'],
'has_pdf': invoice['EXISTEPDF'] == '1',
})
pay_limit_date = datetime.datetime.strptime(invoice['DATEECHEANCE'], '%Y-%m-%d').date()
data['online_payment'] = data['amount'] > 0 and pay_limit_date >= datetime.date.today()
return data
def get_invoices(self, regie_id, dui=None, name_id=None):
@ -793,8 +810,40 @@ class ToulouseAxel(BaseResource):
result.append(self.normalize_invoice(facture, dui))
return result
def get_invoice(self, regie_id, invoice_id, dui=None, name_id=None):
invoices_data = self.get_invoices(regie_id=regie_id, dui=dui, name_id=name_id)
def get_historical_invoices(self, name_id):
link = self.get_link(name_id)
try:
result = list_dui_factures(
self,
{'LISTFACTURE': {'NUMDUI': link.dui, 'DEBUT': '1970-01-01'}})
except AxelError as e:
raise APIError(
'Axel error: %s' % e,
err_code='error',
data={'xml_request': e.xml_request,
'xml_response': e.xml_response})
data = result.json_response['DATA']['PORTAIL']['LISTFACTURE']
result = []
for direction in data.get('DIRECTION', []):
for facture in direction.get('FACTURE', []):
result.append(
self.normalize_invoice(
facture,
link.dui,
historical=True,
vendor_base={
'NUMDIRECTION': direction['NUMDIRECTION'],
'IDDIRECTION': direction['IDDIRECTION'],
'LIBDIRECTION': direction['LIBDIRECTION'],
}))
return result
def get_invoice(self, regie_id, invoice_id, dui=None, name_id=None, historical=None):
if historical:
invoices_data = self.get_historical_invoices(name_id=name_id)
else:
invoices_data = self.get_invoices(regie_id=regie_id, dui=dui, name_id=name_id)
for invoice in invoices_data:
if invoice['display_id'] == invoice_id:
return invoice
@ -816,7 +865,21 @@ class ToulouseAxel(BaseResource):
@endpoint(
name='regie',
perm='can_access',
pattern=r'^(?P<regie_id>[\w-]+)/invoice/(?P<invoice_id>\w+-\d+)/?$',
pattern=r'^(?P<regie_id>[\w-]+)/invoices/history/?$',
example_pattern='{regie_id}/invoices/history/',
description=_("Get invoices already paid"),
parameters={
'NameID': {'description': _('Publik ID')},
'regie_id': {'description': _('Regie identifier'), 'example_value': '42-PERISCOL'}
})
def invoices_history(self, request, regie_id, NameID):
invoices_data = self.get_historical_invoices(name_id=NameID)
return {'data': invoices_data}
@endpoint(
name='regie',
perm='can_access',
pattern=r'^(?P<regie_id>[\w-]+)/invoice/(?P<invoice_id>(historical-)?\w+-\d+)/?$',
example_pattern='{regie_id}/invoice/{invoice_id}/',
description=_('Get invoice details'),
parameters={
@ -825,8 +888,9 @@ class ToulouseAxel(BaseResource):
'invoice_id': {'description': _('Invoice identifier'), 'example_value': 'DUI-42'}
})
def invoice(self, request, regie_id, invoice_id, NameID):
invoice_id = invoice_id.split('-')[1]
invoice = self.get_invoice(regie_id=regie_id, name_id=NameID, invoice_id=invoice_id)
real_invoice_id = invoice_id.split('-')[-1]
historical = invoice_id.startswith('historical-')
invoice = self.get_invoice(regie_id=regie_id, name_id=NameID, invoice_id=real_invoice_id, historical=historical)
if invoice is None:
raise APIError('Invoice not found', err_code='not-found')
@ -835,7 +899,7 @@ class ToulouseAxel(BaseResource):
@endpoint(
name='regie',
perm='can_access',
pattern=r'^(?P<regie_id>[\w-]+)/invoice/(?P<invoice_id>\w+-\d+)/pdf/?$',
pattern=r'^(?P<regie_id>[\w-]+)/invoice/(?P<invoice_id>(historical-)?\w+-\d+)/pdf/?$',
example_pattern='{regie_id}/invoice/{invoice_id}/pdf/',
description=_('Get invoice as a PDF file'),
parameters={
@ -845,9 +909,10 @@ class ToulouseAxel(BaseResource):
})
def invoice_pdf(self, request, regie_id, invoice_id, NameID):
# check that invoice is related to current user
invoice_id = invoice_id.split('-')[1]
real_invoice_id = invoice_id.split('-')[-1]
historical = invoice_id.startswith('historical-')
try:
invoice = self.get_invoice(regie_id=regie_id, name_id=NameID, invoice_id=invoice_id)
invoice = self.get_invoice(regie_id=regie_id, name_id=NameID, invoice_id=real_invoice_id, historical=historical)
except APIError as e:
e.http_status = 404
raise
@ -897,15 +962,11 @@ class ToulouseAxel(BaseResource):
data = json.loads(request.body)
dui, invoice_id = invoice_id.split('-')
invoices_data = self.get_invoices(regie_id=regie_id, dui=dui)
transaction_amount = None
for invoice in invoices_data:
if invoice['display_id'] == invoice_id:
transaction_amount = invoice['amount']
break
if transaction_amount is None:
invoice = self.get_invoice(regie_id=regie_id, dui=dui, invoice_id=invoice_id)
if invoice is None:
raise APIError('Invoice not found', err_code='not-found')
transaction_amount = invoice['amount']
transaction_id = data['transaction_id']
transaction_date = encode_datetime(data['transaction_date'])
post_data = {

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8" ?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:all="urn:AllAxelTypes">
<xsd:import schemaLocation="../AllAxelTypes.xsd" namespace="urn:AllAxelTypes" />
<xsd:complexType name="listefacture">
<xsd:all>
<xsd:element ref="NUMDUI" minOccurs="1" maxOccurs="1"/>
<xsd:element ref="DEBUT" minOccurs="1" maxOccurs="1"/>
</xsd:all>
</xsd:complexType>
<xsd:element name="DEBUT" type="all:DATEREQUIREDType"/>
<xsd:element name="NUMDUI" type="all:IDENTREQUIREDType"/>
<xsd:element name="LISTFACTURE" type="listefacture"/>
</xsd:schema>

View File

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8" ?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:all="urn:AllAxelTypes">
<xsd:import schemaLocation="../AllAxelTypes.xsd" namespace="urn:AllAxelTypes" />
<xsd:redefine schemaLocation="../R_ShemaResultat.xsd">
<xsd:simpleType name="TYPEType">
<xsd:restriction base="TYPEType">
<xsd:enumeration value="ListeDuiFacturesPayeesRecettees" />
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="PORTAILType">
<xsd:complexContent>
<xsd:extension base="PORTAILType">
<xsd:sequence>
<xsd:element ref="LISTFACTURE" minOccurs="0"
maxOccurs="1" />
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
</xsd:redefine>
<xsd:complexType name="listfacture">
<xsd:sequence>
<xsd:element ref="NBFACTURERESTANTE" />
<xsd:element ref="DIRECTION" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="direction">
<xsd:sequence>
<xsd:element ref="NUMDIRECTION" />
<xsd:element ref="IDDIRECTION" />
<xsd:element ref="LIBDIRECTION" />
<xsd:element ref="FACTURE" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="facture">
<xsd:sequence>
<xsd:element ref="IDAXEL" />
<xsd:element ref="IDFAMILLE" />
<xsd:element ref="EMISSION" />
<xsd:element ref="MONTANT" />
<xsd:element ref="IDFACTURE" />
<xsd:element ref="NOFACTURE" />
<xsd:element ref="LIBELLE" />
<xsd:element ref="IPDF" />
</xsd:sequence>
</xsd:complexType>
<xsd:simpleType name="idaxel">
<xsd:restriction base="xsd:string">
<xsd:pattern value="AXEL" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="libelle">
<xsd:restriction base="xsd:string">
<xsd:minLength value="1" />
<xsd:maxLength value="50" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="positiveInteger" id="positiveInteger">
<xsd:restriction base="xsd:nonNegativeInteger">
<xsd:minInclusive value="0" />
</xsd:restriction>
</xsd:simpleType>
<xsd:element name="NUMDIRECTION" type="xsd:nonNegativeInteger" />
<xsd:element name="IDDIRECTION" type="all:IDREQUIREDType" />
<xsd:element name="LIBDIRECTION" type="all:NOMREQUIREDType" />
<xsd:element name="IDAXEL" type="idaxel" />
<xsd:element name="IDFAMILLE" type="all:IDENTREQUIREDType" />
<xsd:element name="EMISSION" type="all:DATEREQUIREDType" />
<xsd:element name="MONTANT" type="xsd:decimal" />
<xsd:element name="IDFACTURE" type="xsd:positiveInteger" />
<xsd:element name="NOFACTURE" type="xsd:positiveInteger" />
<xsd:element name="LIBELLE" type="libelle" />
<xsd:element name="IPDF" type="all:ONType" />
<xsd:element name="NBFACTURERESTANTE" type="positiveInteger" />
<xsd:element name="DIRECTION" type="direction" />
<xsd:element name="FACTURE" type="facture" />
<xsd:element name="LISTFACTURE" type="listfacture" />
</xsd:schema>

View File

@ -0,0 +1,35 @@
<PORTAIL>
<LISTFACTURE>
<NBFACTURERESTANTE>0</NBFACTURERESTANTE>
<DIRECTION>
<NUMDIRECTION>10</NUMDIRECTION>
<IDDIRECTION>DIR-A</IDDIRECTION>
<LIBDIRECTION>DIRECTION A</LIBDIRECTION>
<FACTURE>
<IDAXEL>AXEL</IDAXEL>
<IDFAMILLE>XXX</IDFAMILLE>
<EMISSION>23/03/2017</EMISSION>
<MONTANT>28.98</MONTANT>
<IDFACTURE>42</IDFACTURE>
<NOFACTURE>42</NOFACTURE>
<LIBELLE>PRESTATIONS SEPTEMBRE 2015</LIBELLE>
<IPDF>O</IPDF>
</FACTURE>
</DIRECTION>
<DIRECTION>
<NUMDIRECTION>11</NUMDIRECTION>
<IDDIRECTION>DIR-B</IDDIRECTION>
<LIBDIRECTION>DIRECTION B</LIBDIRECTION>
<FACTURE>
<IDAXEL>AXEL</IDAXEL>
<IDFAMILLE>XXX</IDFAMILLE>
<EMISSION>23/03/2017</EMISSION>
<MONTANT>28.98</MONTANT>
<IDFACTURE>43</IDFACTURE>
<NOFACTURE>43</NOFACTURE>
<LIBELLE>PRESTATIONS OCTOBRE 2015</LIBELLE>
<IPDF>O</IPDF>
</FACTURE>
</DIRECTION>
</LISTFACTURE>
</PORTAIL>

View File

@ -34,6 +34,7 @@ from passerelle.contrib.toulouse_axel.models import (
ToulouseAxel,
form_maj_famille_dui,
form_paiement_dui,
list_dui_factures,
ref_date_gestion_dui,
ref_famille_dui,
ref_facture_a_payer,
@ -314,6 +315,20 @@ def test_operation_ref_facture_a_payer(resource, content):
})
@pytest.mark.parametrize('content', [
'<PORTAIL><LISTFACTURE/></PORTAIL>',
])
def test_operation_list_dui_factures(resource, content):
with mock_getdata(content, 'ListeDuiFacturesPayeesRecettees'):
with pytest.raises(AxelError):
list_dui_factures(resource, {
'LISTFACTURE': {
'NUMDUI': 'XXX',
'DEBUT': '1970-01-01'
}
})
@pytest.mark.parametrize('content', [
"<PORTAIL><PDF FOO='BAR'></PDF></PORTAIL>",
])
@ -1337,6 +1352,100 @@ def test_invoices_endpoint(app, resource):
]
def test_invoices_history_endpoint_axel_error(app, resource):
Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
with mock.patch('passerelle.contrib.toulouse_axel.models.list_dui_factures') as operation:
operation.side_effect = AxelError('FooBar')
resp = app.get('/toulouse-axel/test/regie/MAREGIE/invoices/history?NameID=yyy')
assert resp.json['err_desc'] == "Axel error: FooBar"
assert resp.json['err'] == 'error'
def test_invoices_history_endpoint_no_result(app, resource):
resp = app.get('/toulouse-axel/test/regie/MAREGIE/invoices/history?NameID=yyy')
assert resp.json['err_desc'] == "Person not found"
assert resp.json['err'] == 'not-found'
def test_invoices_history_endpoint_no_invoices(app, resource):
Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
content = '''<PORTAIL>
<LISTFACTURE>
<NBFACTURERESTANTE>0</NBFACTURERESTANTE>
</LISTFACTURE>
</PORTAIL>'''
with mock_getdata(content, 'ListeDuiFacturesPayeesRecettees'):
resp = app.get('/toulouse-axel/test/regie/MAREGIE/invoices/history?NameID=yyy')
assert resp.json['err'] == 0
assert resp.json['data'] == []
def test_invoices_history_endpoint(app, resource):
Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
filepath = os.path.join(os.path.dirname(__file__), 'data/toulouse_axel/invoices_history.xml')
with open(filepath) as xml:
content = xml.read()
with mock_getdata(content, 'ListeDuiFacturesPayeesRecettees'):
resp = app.get('/toulouse-axel/test/regie/MAREGIE/invoices/history?NameID=yyy')
assert resp.json['err'] == 0
assert resp.json['data'] == [
{
'amount': 0,
'created': '2017-03-23',
'display_id': '42',
'has_pdf': False,
'id': 'historical-XXX-42',
'label': 'PRESTATIONS SEPTEMBRE 2015',
'online_payment': False,
'paid': False,
'pay_limit_date': '',
'total_amount': '28.98',
'vendor': {
'toulouse-axel': {
'EMISSION': '2017-03-23',
'IDAXEL': 'AXEL',
'IDDIRECTION': 'DIR-A',
'IDFACTURE': 42,
'IDFAMILLE': 'XXX',
'IPDF': 'O',
'LIBDIRECTION': 'DIRECTION A',
'LIBELLE': 'PRESTATIONS SEPTEMBRE 2015',
'MONTANT': '28.98',
'NOFACTURE': 42,
'NUMDIRECTION': 10
}
}
},
{
'amount': 0,
'created': '2017-03-23',
'display_id': '43',
'has_pdf': False,
'id': 'historical-XXX-43',
'label': 'PRESTATIONS OCTOBRE 2015',
'online_payment': False,
'paid': False,
'pay_limit_date': '',
'total_amount': '28.98',
'vendor': {
'toulouse-axel': {
'EMISSION': '2017-03-23',
'IDAXEL': 'AXEL',
'IDDIRECTION': 'DIR-B',
'IDFACTURE': 43,
'IDFAMILLE': 'XXX',
'IPDF': 'O',
'LIBDIRECTION': 'DIRECTION B',
'LIBELLE': 'PRESTATIONS OCTOBRE 2015',
'MONTANT': '28.98',
'NOFACTURE': 43,
'NUMDIRECTION': 11
}
}
}
]
def test_invoice_endpoint_axel_error(app, resource):
Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
with mock.patch('passerelle.contrib.toulouse_axel.models.ref_facture_a_payer') as operation:
@ -1400,6 +1509,40 @@ def test_invoice_endpoint(app, resource):
}
}
filepath = os.path.join(os.path.dirname(__file__), 'data/toulouse_axel/invoices_history.xml')
with open(filepath) as xml:
content = xml.read()
with mock_getdata(content, 'ListeDuiFacturesPayeesRecettees'):
resp = app.get('/toulouse-axel/test/regie/MAREGIE/invoice/historical-XXX-42?NameID=yyy')
assert resp.json['err'] == 0
assert resp.json['data'] == {
'amount': 0,
'created': '2017-03-23',
'display_id': '42',
'has_pdf': False,
'id': 'historical-XXX-42',
'label': 'PRESTATIONS SEPTEMBRE 2015',
'online_payment': False,
'paid': False,
'pay_limit_date': '',
'total_amount': '28.98',
'vendor': {
'toulouse-axel': {
'EMISSION': '2017-03-23',
'IDAXEL': 'AXEL',
'IDDIRECTION': 'DIR-A',
'IDFACTURE': 42,
'IDFAMILLE': 'XXX',
'IPDF': 'O',
'LIBDIRECTION': 'DIRECTION A',
'LIBELLE': 'PRESTATIONS SEPTEMBRE 2015',
'MONTANT': '28.98',
'NOFACTURE': 42,
'NUMDIRECTION': 10
}
}
}
def test_invoice_pdf_endpoint_axel_error(app, resource):
Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
@ -1464,6 +1607,13 @@ def test_invoice_pdf_endpoint(app, resource):
invoice.return_value = {'has_pdf': True, 'display_id': '42'}
with mock_getdata(pdf_content, 'RefFacturePDF'):
app.get('/toulouse-axel/test/regie/MAREGIE/invoice/XXX-42/pdf?NameID=yyy')
assert invoice.call_args_list[0][1]['historical'] is False
with mock.patch('passerelle.contrib.toulouse_axel.models.ToulouseAxel.get_invoice') as invoice:
invoice.return_value = {'has_pdf': True, 'display_id': '42'}
with mock_getdata(pdf_content, 'RefFacturePDF'):
app.get('/toulouse-axel/test/regie/MAREGIE/invoice/historical-XXX-42/pdf?NameID=yyy')
assert invoice.call_args_list[0][1]['historical'] is True
def test_pay_invoice_endpoint_axel_error(app, resource):