From 9479c2d30e4a28342dfe1717f8f2ade72733f3df Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Mon, 10 May 2021 15:38:19 +0200
Subject: [PATCH 1/4] caluire-axel: add invoices endpoint (#53884)

---
 passerelle/contrib/caluire_axel/models.py     | 47 ++++++++-
 passerelle/contrib/caluire_axel/schemas.py    |  1 +
 passerelle/contrib/caluire_axel/utils.py      | 36 +++++++
 .../caluire_axel/xsd/Q_GetFacturesaPayer.xsd  | 26 +++++
 .../caluire_axel/xsd/R_GetFacturesaPayer.xsd  | 55 +++++++++++
 tests/data/caluire_axel/invoices.xml          | 19 ++++
 tests/test_caluire_axel.py                    | 96 +++++++++++++++++++
 7 files changed, 279 insertions(+), 1 deletion(-)
 create mode 100644 passerelle/contrib/caluire_axel/xsd/Q_GetFacturesaPayer.xsd
 create mode 100644 passerelle/contrib/caluire_axel/xsd/R_GetFacturesaPayer.xsd
 create mode 100644 tests/data/caluire_axel/invoices.xml

diff --git a/passerelle/contrib/caluire_axel/models.py b/passerelle/contrib/caluire_axel/models.py
index 00e48cef..111d6f2e 100644
--- a/passerelle/contrib/caluire_axel/models.py
+++ b/passerelle/contrib/caluire_axel/models.py
@@ -30,17 +30,17 @@ from . import schemas, utils
 
 class CaluireAxel(BaseResource):
 
     wsdl_url = models.CharField(
         max_length=128, blank=False, verbose_name=_('WSDL URL'), help_text=_('Caluire Axel WSDL URL')
     )
 
     category = _('Business Process Connectors')
-    _category_ordering = [_('Family account'), _('Schooling')]
+    _category_ordering = [_('Family account'), _('Schooling'), _('Invoices')]
 
     class Meta:
         verbose_name = _('Caluire Axel')
 
     def check_status(self):
         response = self.requests.get(self.wsdl_url)
         response.raise_for_status()
 
@@ -295,16 +295,61 @@ class CaluireAxel(BaseResource):
                 err_code='error',
                 data={'xml_request': e.xml_request, 'xml_response': e.xml_response},
             )
 
         schooling_data = result.json_response['DATA']['PORTAIL']['GETINDIVIDU']
 
         return {'data': schooling_data}
 
+    def get_invoices(self, regie_id, name_id):
+        link = self.get_link(name_id)
+        try:
+            result = schemas.get_factures_a_payer(
+                self,
+                {
+                    'PORTAIL': {
+                        'GETFACTURESAPAYER': {
+                            'IDENTFAMILLE': link.family_id,
+                            'IDENTREGIEFACT': regie_id,
+                        }
+                    }
+                },
+            )
+        except axel.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']['GETFACTURESAPAYER']
+        result = []
+
+        for facture in data.get('FACTURE', []):
+            result.append(utils.normalize_invoice(facture, link.family_id))
+        return result
+
+    @endpoint(
+        display_category=_('Invoices'),
+        display_order=1,
+        name='regie',
+        perm='can_access',
+        pattern=r'^(?P<regie_id>[\w-]+)/invoices/?$',
+        example_pattern='{regie_id}/invoices',
+        description=_("Get invoices to pay"),
+        parameters={
+            'NameID': {'description': _('Publik ID')},
+            'regie_id': {'description': _('Regie identifier'), 'example_value': 'ENF'},
+        },
+    )
+    def invoices(self, request, regie_id, NameID):
+        invoices_data = self.get_invoices(regie_id=regie_id, name_id=NameID)
+        return {'data': invoices_data}
+
 
 class Link(models.Model):
     resource = models.ForeignKey(CaluireAxel, on_delete=models.CASCADE)
     name_id = models.CharField(blank=False, max_length=256)
     family_id = models.CharField(blank=False, max_length=128)
     person_id = models.CharField(blank=False, max_length=128)
 
     class Meta:
diff --git a/passerelle/contrib/caluire_axel/schemas.py b/passerelle/contrib/caluire_axel/schemas.py
index 7c6f4231..c86789a7 100644
--- a/passerelle/contrib/caluire_axel/schemas.py
+++ b/passerelle/contrib/caluire_axel/schemas.py
@@ -71,16 +71,17 @@ class Operation(axel.Operation):
     base_xsd_path = BASE_XSD_PATH
     axel_schema = CaluireAxelSchema
 
 
 find_individus = Operation('FindIndividus')
 get_famille_individus = Operation('GetFamilleIndividus')
 get_individu = Operation('GetIndividu')
 get_list_ecole = Operation('GetListEcole')
+get_factures_a_payer = Operation('GetFacturesaPayer')
 
 
 LINK_SCHEMA = copy.deepcopy(
     find_individus.request_schema['properties']['PORTAIL']['properties']['FINDINDIVIDU']
 )
 for key in ['NAISSANCE', 'CODEPOSTAL', 'VILLE', 'TEL', 'MAIL']:
     LINK_SCHEMA['properties'].pop(key)
     LINK_SCHEMA['required'].remove(key)
diff --git a/passerelle/contrib/caluire_axel/utils.py b/passerelle/contrib/caluire_axel/utils.py
index 49a3377d..9892fa8a 100644
--- a/passerelle/contrib/caluire_axel/utils.py
+++ b/passerelle/contrib/caluire_axel/utils.py
@@ -16,8 +16,44 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 def get_reference_year_from_date(booking_date):
     if booking_date.month <= 8:
         # between january and august, reference year is the year just before
         return booking_date.year - 1
     return booking_date.year
+
+
+def normalize_invoice(invoice, family_id, historical=False, vendor_base=None):
+    vendor = vendor_base or {}
+    vendor.update(invoice)
+    invoice_id = '%s-%s' % (family_id, invoice['IDFACTURE'])
+    if historical:
+        invoice_id = 'historical-%s' % invoice_id
+    data = {
+        'id': invoice_id,
+        'display_id': str(invoice['IDFACTURE']),
+        'label': invoice['FACTURATION'],
+        'paid': False,
+        'total_amount': invoice['MONTANT'],
+        'has_pdf': invoice['EXISTEPDF'],
+        'vendor': {'caluire-axel': vendor},
+    }
+    if historical:
+        data.update(
+            {
+                'amount': 0,
+                'pay_limit_date': '',
+                'online_payment': False,
+            }
+        )
+    else:
+        data.update(
+            {
+                'amount': max(0, invoice['MONTANT'] - invoice['ENCAISSE']),
+                'amount_paid': invoice['ENCAISSE'],
+                'created': invoice['DATEEMISSION'],
+                'pay_limit_date': invoice['DATEECHEANCE'],
+                'online_payment': bool(invoice['MONTANT'] - invoice['ENCAISSE'] > 0),
+            }
+        )
+    return data
diff --git a/passerelle/contrib/caluire_axel/xsd/Q_GetFacturesaPayer.xsd b/passerelle/contrib/caluire_axel/xsd/Q_GetFacturesaPayer.xsd
new file mode 100644
index 00000000..8e03e8b3
--- /dev/null
+++ b/passerelle/contrib/caluire_axel/xsd/Q_GetFacturesaPayer.xsd
@@ -0,0 +1,26 @@
+<?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="PORTAILType">
+		<xsd:sequence>
+			<xsd:element ref="GETFACTURESAPAYER" minOccurs="0" maxOccurs="1"/>
+		</xsd:sequence>  
+	</xsd:complexType>
+	
+	<xsd:complexType name="GETFACTURESAPAYERType">
+		<xsd:sequence>
+			<xsd:element ref="IDENTFAMILLE"/>
+			<xsd:element ref="IDENTREGIEFACT"/>
+		</xsd:sequence>  
+	</xsd:complexType>
+	
+	<xsd:element name="IDENTFAMILLE" type="all:IDENTREQUIREDType"/>
+	<xsd:element name="IDENTREGIEFACT" type="all:IDREQUIREDType"/>
+	
+	<xsd:element name="GETFACTURESAPAYER" type="GETFACTURESAPAYERType"/>
+	
+	<xsd:element name="PORTAIL" type="PORTAILType"/>	
+		
+</xsd:schema>
\ No newline at end of file
diff --git a/passerelle/contrib/caluire_axel/xsd/R_GetFacturesaPayer.xsd b/passerelle/contrib/caluire_axel/xsd/R_GetFacturesaPayer.xsd
new file mode 100644
index 00000000..ca74fb9e
--- /dev/null
+++ b/passerelle/contrib/caluire_axel/xsd/R_GetFacturesaPayer.xsd
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<xsd:schema	xmlns:all="urn:AllAxelTypes" xmlns:ind="urn:Individu"  xmlns:xsd="http://www.w3.org/2001/XMLSchema" >
+	
+	<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="GetFacturesaPayer" />
+			</xsd:restriction>
+	    </xsd:simpleType>
+		
+		<xsd:complexType name="PORTAILType">
+			<xsd:complexContent>
+				<xsd:extension base="PORTAILType">
+					<xsd:sequence>
+							<xsd:element ref="GETFACTURESAPAYER" minOccurs="0" maxOccurs="1"/>
+					</xsd:sequence>
+				</xsd:extension>
+			 </xsd:complexContent>
+		</xsd:complexType>
+	</xsd:redefine>	
+	
+	<xsd:complexType name="FACTUREType">
+		<xsd:sequence>
+			<xsd:element ref="IDFACTURE" />
+			<xsd:element ref="MONTANT"/>
+			<xsd:element ref="ENCAISSE"/>
+			<xsd:element ref="FACTURATION"/>
+			<xsd:element ref="DATEEMISSION"/>
+			<xsd:element ref="DATEECHEANCE"/>
+			<xsd:element ref="EXISTEPDF"/>
+		</xsd:sequence> 
+	</xsd:complexType>
+	
+	<xsd:complexType name="GETFACTURESAPAYERType">
+		<xsd:sequence>
+			<xsd:element ref="CODE" />
+			<xsd:element ref="FACTURE" minOccurs="0" maxOccurs="unbounded" />
+		</xsd:sequence>
+	</xsd:complexType>
+	
+	<xsd:element name="CODE" type="xsd:integer"/>	
+	<xsd:element name="IDFACTURE" type="xsd:positiveInteger"/>
+	<xsd:element name="MONTANT" type="all:MONTANTType"/>
+	<xsd:element name="ENCAISSE" type="all:MONTANTType"/>
+	<xsd:element name="FACTURATION" type="all:NOMType"/>
+	<xsd:element name="DATEEMISSION" type="all:DATEType"/>
+	<xsd:element name="DATEECHEANCE" type="all:DATEType"/>
+	<xsd:element name="EXISTEPDF" type="all:ONEmptyType"/>
+	<xsd:element name="FACTURE" type="FACTUREType"/>
+	
+	<xsd:element name="GETFACTURESAPAYER" type="GETFACTURESAPAYERType"/>
+	
+</xsd:schema>
diff --git a/tests/data/caluire_axel/invoices.xml b/tests/data/caluire_axel/invoices.xml
new file mode 100644
index 00000000..31945ef7
--- /dev/null
+++ b/tests/data/caluire_axel/invoices.xml
@@ -0,0 +1,19 @@
+<CODE>2</CODE>
+<FACTURE>
+  <IDFACTURE>42</IDFACTURE>
+  <MONTANT>44.94</MONTANT>
+  <ENCAISSE>40.00</ENCAISSE>
+  <FACTURATION>PRESTATIONS PERISCOLAIRES SEPTEMBRE-OCTOBRE 2019</FACTURATION>
+  <DATEEMISSION>12/11/2019</DATEEMISSION>
+  <DATEECHEANCE>04/12/2019</DATEECHEANCE>
+  <EXISTEPDF>o</EXISTEPDF>
+</FACTURE>
+<FACTURE>
+  <IDFACTURE>43</IDFACTURE>
+  <MONTANT>44.94</MONTANT>
+  <ENCAISSE>0.00</ENCAISSE>
+  <FACTURATION>PRESTATIONS PERISCOLAIRES NOVEMBRE 2019</FACTURATION>
+  <DATEEMISSION>12/12/2019</DATEEMISSION>
+  <DATEECHEANCE>04/01/2020</DATEECHEANCE>
+  <EXISTEPDF>n</EXISTEPDF>
+</FACTURE>
diff --git a/tests/test_caluire_axel.py b/tests/test_caluire_axel.py
index b595c89a..00640712 100644
--- a/tests/test_caluire_axel.py
+++ b/tests/test_caluire_axel.py
@@ -691,8 +691,104 @@ def test_child_schooling_info(app, resource, family_data):
             'passerelle.contrib.caluire_axel.models.CaluireAxel.get_family_data',
             return_value=family_data,
         ):
             resp = app.get(
                 '/caluire-axel/test/child_schooling_info?NameID=yyy&idpersonne=50632&schooling_date=2021-05-10'
             )
     assert resp.json['err'] == 0
     assert set(resp.json['data'].keys()) == set(['CODE', 'INDIVIDU', 'SCOLAIRE'])
+
+
+def test_invoices_endpoint_axel_error(app, resource):
+    Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42')
+    with mock.patch('passerelle.contrib.caluire_axel.schemas.get_factures_a_payer') as operation:
+        operation.side_effect = AxelError('FooBar')
+        resp = app.get('/caluire-axel/test/regie/MAREGIE/invoices?NameID=yyy')
+    assert resp.json['err_desc'] == "Axel error: FooBar"
+    assert resp.json['err'] == 'error'
+
+
+def test_invoices_endpoint_no_result(app, resource):
+    resp = app.get('/caluire-axel/test/regie/MAREGIE/invoices?NameID=yyy')
+    assert resp.json['err_desc'] == "Person not found"
+    assert resp.json['err'] == 'not-found'
+
+
+def test_invoices_endpoint_no_invoice(app, resource):
+    Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42')
+    content = '''<PORTAIL>
+    <GETFACTURESAPAYER>
+        <CODE>0</CODE>
+    </GETFACTURESAPAYER>
+</PORTAIL>'''
+    with mock_getdata(content, 'GetFacturesaPayer'):
+        resp = app.get('/caluire-axel/test/regie/MAREGIE/invoices?NameID=yyy')
+    assert resp.json['err'] == 0
+    assert resp.json['data'] == []
+
+
+def test_invoices_endpoint(app, resource):
+    Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42')
+    filepath = os.path.join(os.path.dirname(__file__), 'data/caluire_axel/invoices.xml')
+    with open(filepath) as xml:
+        content = (
+            '''
+<PORTAIL>
+  <GETFACTURESAPAYER>
+    %s
+  </GETFACTURESAPAYER>
+</PORTAIL>'''
+            % xml.read()
+        )
+    with mock_getdata(content, 'GetFacturesaPayer'):
+        resp = app.get('/caluire-axel/test/regie/MAREGIE/invoices?NameID=yyy')
+    assert resp.json['err'] == 0
+    assert resp.json['data'] == [
+        {
+            'id': 'XXX-42',
+            'display_id': '42',
+            'label': 'PRESTATIONS PERISCOLAIRES SEPTEMBRE-OCTOBRE 2019',
+            'amount': '4.94',
+            'total_amount': '44.94',
+            'amount_paid': '40.00',
+            'online_payment': True,
+            'created': '2019-11-12',
+            'pay_limit_date': '2019-12-04',
+            'has_pdf': True,
+            'paid': False,
+            'vendor': {
+                'caluire-axel': {
+                    'IDFACTURE': 42,
+                    'MONTANT': '44.94',
+                    'ENCAISSE': '40.00',
+                    'FACTURATION': 'PRESTATIONS PERISCOLAIRES SEPTEMBRE-OCTOBRE 2019',
+                    'DATEECHEANCE': '2019-12-04',
+                    'DATEEMISSION': '2019-11-12',
+                    'EXISTEPDF': True,
+                }
+            },
+        },
+        {
+            'id': 'XXX-43',
+            'display_id': '43',
+            'label': 'PRESTATIONS PERISCOLAIRES NOVEMBRE 2019',
+            'amount': '44.94',
+            'total_amount': '44.94',
+            'amount_paid': '0.00',
+            'online_payment': True,
+            'created': '2019-12-12',
+            'pay_limit_date': '2020-01-04',
+            'has_pdf': False,
+            'paid': False,
+            'vendor': {
+                'caluire-axel': {
+                    'IDFACTURE': 43,
+                    'FACTURATION': 'PRESTATIONS PERISCOLAIRES NOVEMBRE 2019',
+                    'MONTANT': '44.94',
+                    'ENCAISSE': '0.00',
+                    'DATEECHEANCE': '2020-01-04',
+                    'DATEEMISSION': '2019-12-12',
+                    'EXISTEPDF': False,
+                }
+            },
+        },
+    ]
-- 
2.30.2

