0004-caluire-axel-add-invoice_pdf-endpoint-53884.patch
passerelle/contrib/caluire_axel/models.py | ||
---|---|---|
9 | 9 |
# This program is distributed in the hope that it will be useful, |
10 | 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | 12 |
# GNU Affero General Public License for more details. |
13 | 13 |
# |
14 | 14 |
# You should have received a copy of the GNU Affero General Public License |
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 |
import base64 |
|
17 | 18 |
import datetime |
18 | 19 | |
19 | 20 |
from django.core.cache import cache |
20 | 21 |
from django.db import models |
22 |
from django.http import HttpResponse |
|
21 | 23 |
from django.utils import dateformat |
22 | 24 |
from django.utils.timezone import now |
23 | 25 |
from django.utils.translation import ugettext_lazy as _ |
24 | 26 | |
25 | 27 |
from passerelle.base.models import BaseResource |
26 | 28 |
from passerelle.contrib.utils import axel |
27 | 29 |
from passerelle.utils.api import endpoint |
28 | 30 |
from passerelle.utils.jsonresponse import APIError |
... | ... | |
682 | 684 |
historical=historical, |
683 | 685 |
nb_mounts_limit=nb_mounts_limit, |
684 | 686 |
) |
685 | 687 |
if invoice is None: |
686 | 688 |
raise APIError('Invoice not found', err_code='not-found') |
687 | 689 | |
688 | 690 |
return {'data': invoice} |
689 | 691 | |
692 |
@endpoint( |
|
693 |
display_category=_('Invoices'), |
|
694 |
display_order=4, |
|
695 |
name='regie', |
|
696 |
perm='can_access', |
|
697 |
pattern=r'^(?P<regie_id>[\w-]+)/invoice/(?P<invoice_id>(historical-)?\w+-\d+)/pdf/?$', |
|
698 |
example_pattern='{regie_id}/invoice/{invoice_id}/pdf', |
|
699 |
description=_('Get invoice as a PDF file'), |
|
700 |
parameters={ |
|
701 |
'NameID': {'description': _('Publik ID')}, |
|
702 |
'regie_id': {'description': _('Regie identifier'), 'example_value': 'ENF'}, |
|
703 |
'invoice_id': {'description': _('Invoice identifier'), 'example_value': 'IDFAM-42'}, |
|
704 |
'nb_mounts_limit': {'description': _('Number of months of history'), 'example_value': '12'}, |
|
705 |
}, |
|
706 |
) |
|
707 |
def invoice_pdf(self, request, regie_id, invoice_id, NameID, nb_mounts_limit='12'): |
|
708 |
# check that invoice is related to current user |
|
709 |
real_invoice_id = invoice_id.split('-')[-1] |
|
710 |
historical = invoice_id.startswith('historical-') |
|
711 |
try: |
|
712 |
link = self.get_link(NameID) |
|
713 |
invoice = self.get_invoice( |
|
714 |
regie_id=regie_id, |
|
715 |
invoice_id=real_invoice_id, |
|
716 |
family_id=link.family_id, |
|
717 |
historical=historical, |
|
718 |
nb_mounts_limit=nb_mounts_limit, |
|
719 |
) |
|
720 |
except APIError as e: |
|
721 |
e.http_status = 404 |
|
722 |
raise |
|
723 |
if invoice is None: |
|
724 |
raise APIError('Invoice not found', err_code='not-found', http_status=404) |
|
725 |
# check that PDF is available |
|
726 |
if not invoice['has_pdf']: |
|
727 |
raise APIError('PDF not available', err_code='not-available', http_status=404) |
|
728 | ||
729 |
try: |
|
730 |
result = schemas.get_pdf_facture( |
|
731 |
self, {'PORTAIL': {'GETPDFFACTURE': {'IDFACTURE': int(invoice['display_id'])}}} |
|
732 |
) |
|
733 |
except axel.AxelError as e: |
|
734 |
raise APIError( |
|
735 |
'Axel error: %s' % e, |
|
736 |
err_code='error', |
|
737 |
http_status=404, |
|
738 |
data={'xml_request': e.xml_request, 'xml_response': e.xml_response}, |
|
739 |
) |
|
740 |
b64content = base64.b64decode( |
|
741 |
result.json_response['DATA']['PORTAIL']['GETPDFFACTURE']['FACTUREPDF'] or '' |
|
742 |
) |
|
743 |
if not b64content: |
|
744 |
raise APIError('PDF error', err_code='error', http_status=404) |
|
745 |
response = HttpResponse(content_type='application/pdf') |
|
746 |
response['Content-Disposition'] = 'attachment; filename="%s.pdf"' % invoice_id |
|
747 |
response.write(b64content) |
|
748 |
return response |
|
749 | ||
690 | 750 | |
691 | 751 |
class Link(models.Model): |
692 | 752 |
resource = models.ForeignKey(CaluireAxel, on_delete=models.CASCADE) |
693 | 753 |
name_id = models.CharField(blank=False, max_length=256) |
694 | 754 |
family_id = models.CharField(blank=False, max_length=128) |
695 | 755 |
person_id = models.CharField(blank=False, max_length=128) |
696 | 756 | |
697 | 757 |
class Meta: |
passerelle/contrib/caluire_axel/schemas.py | ||
---|---|---|
76 | 76 |
get_famille_individus = Operation('GetFamilleIndividus') |
77 | 77 |
get_individu = Operation('GetIndividu') |
78 | 78 |
get_list_ecole = Operation('GetListEcole') |
79 | 79 |
get_list_activites = Operation('GetListActivites') |
80 | 80 |
create_inscription_activite = Operation('CreateInscriptionActivite', data_method='setData') |
81 | 81 |
get_agenda = Operation('GetAgenda') |
82 | 82 |
get_factures_a_payer = Operation('GetFacturesaPayer') |
83 | 83 |
get_list_factures = Operation('GetListFactures') |
84 |
get_pdf_facture = Operation('GetPdfFacture') |
|
84 | 85 | |
85 | 86 | |
86 | 87 |
LINK_SCHEMA = copy.deepcopy( |
87 | 88 |
find_individus.request_schema['properties']['PORTAIL']['properties']['FINDINDIVIDU'] |
88 | 89 |
) |
89 | 90 |
for key in ['NAISSANCE', 'CODEPOSTAL', 'VILLE', 'TEL', 'MAIL']: |
90 | 91 |
LINK_SCHEMA['properties'].pop(key) |
91 | 92 |
LINK_SCHEMA['required'].remove(key) |
passerelle/contrib/caluire_axel/xsd/Q_GetPdfFacture.xsd | ||
---|---|---|
1 |
<?xml version="1.0" encoding="utf-8" ?> |
|
2 |
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"> |
|
3 |
|
|
4 |
<xsd:complexType name="GETPDFFACTUREType"> |
|
5 |
<xsd:sequence> |
|
6 |
<xsd:element ref="IDFACTURE" /> |
|
7 |
</xsd:sequence> |
|
8 |
</xsd:complexType> |
|
9 |
|
|
10 |
<xsd:complexType name="PORTAILType"> |
|
11 |
<xsd:sequence> |
|
12 |
<xsd:element ref="GETPDFFACTURE" /> |
|
13 |
</xsd:sequence> |
|
14 |
</xsd:complexType> |
|
15 |
|
|
16 |
<xsd:element name="IDFACTURE" type="xsd:positiveInteger"/> |
|
17 |
<xsd:element name="GETPDFFACTURE" type="GETPDFFACTUREType"/> |
|
18 |
<xsd:element name="PORTAIL" type="PORTAILType"/> |
|
19 |
|
|
20 |
</xsd:schema> |
passerelle/contrib/caluire_axel/xsd/R_GetPdfFacture.xsd | ||
---|---|---|
1 |
<?xml version="1.0" encoding="utf-8" ?> |
|
2 |
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"> |
|
3 |
|
|
4 |
<xsd:redefine schemaLocation="./R_ShemaResultat.xsd"> |
|
5 |
<xsd:simpleType name="TYPEType"> |
|
6 |
<xsd:restriction base="TYPEType"> |
|
7 |
<xsd:enumeration value="GetPdfFacture" /> |
|
8 |
</xsd:restriction> |
|
9 |
</xsd:simpleType> |
|
10 |
|
|
11 |
<xsd:complexType name="PORTAILType"> |
|
12 |
<xsd:complexContent> |
|
13 |
<xsd:extension base="PORTAILType"> |
|
14 |
<xsd:sequence> |
|
15 |
<xsd:element ref="GETPDFFACTURE" minOccurs="0" maxOccurs="1"/> |
|
16 |
</xsd:sequence> |
|
17 |
</xsd:extension> |
|
18 |
</xsd:complexContent> |
|
19 |
</xsd:complexType> |
|
20 |
</xsd:redefine> |
|
21 |
|
|
22 |
<xsd:complexType name="GETPDFFACTUREType"> |
|
23 |
<xsd:sequence> |
|
24 |
<xsd:element ref="CODE"/> |
|
25 |
<xsd:element ref="FACTUREPDF"/> |
|
26 |
</xsd:sequence> |
|
27 |
</xsd:complexType> |
|
28 |
|
|
29 |
<xsd:element name="CODE" type="xsd:integer"/> |
|
30 |
<xsd:element name="FACTUREPDF" type="xsd:string"/> |
|
31 |
|
|
32 |
<xsd:element name="GETPDFFACTURE" type="GETPDFFACTUREType"/> |
|
33 |
|
|
34 |
</xsd:schema> |
tests/test_caluire_axel.py | ||
---|---|---|
1533 | 1533 |
'ENCAISSE': '40.00', |
1534 | 1534 |
'FACTURATION': 'PRESTATIONS PERISCOLAIRES SEPTEMBRE-OCTOBRE 2019', |
1535 | 1535 |
'ECHEANCE': '2019-12-04', |
1536 | 1536 |
'EMISSION': '2019-11-12', |
1537 | 1537 |
'EXISTEPDF': True, |
1538 | 1538 |
}, |
1539 | 1539 |
}, |
1540 | 1540 |
} |
1541 | ||
1542 | ||
1543 |
def test_invoice_pdf_endpoint_axel_error(app, resource): |
|
1544 |
Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42') |
|
1545 |
with mock.patch('passerelle.contrib.caluire_axel.schemas.get_factures_a_payer') as operation: |
|
1546 |
operation.side_effect = AxelError('FooBar') |
|
1547 |
resp = app.get('/caluire-axel/test/regie/MAREGIE/invoice/XXX-42/pdf?NameID=yyy', status=404) |
|
1548 |
assert resp.json['err_desc'] == "Axel error: FooBar" |
|
1549 |
assert resp.json['err'] == 'error' |
|
1550 | ||
1551 |
filepath = os.path.join(os.path.dirname(__file__), 'data/caluire_axel/invoices.xml') |
|
1552 |
with open(filepath) as xml: |
|
1553 |
content = ( |
|
1554 |
'''<PORTAIL> |
|
1555 |
<GETFACTURESAPAYER> |
|
1556 |
%s |
|
1557 |
</GETFACTURESAPAYER> |
|
1558 |
</PORTAIL>''' |
|
1559 |
% xml.read() |
|
1560 |
) |
|
1561 |
with mock_data(content, 'GetFacturesaPayer'): |
|
1562 |
with mock.patch('passerelle.contrib.caluire_axel.schemas.get_pdf_facture') as operation: |
|
1563 |
operation.side_effect = AxelError('FooBar') |
|
1564 |
resp = app.get('/caluire-axel/test/regie/MAREGIE/invoice/XXX-42/pdf?NameID=yyy', status=404) |
|
1565 |
assert resp.json['err_desc'] == "Axel error: FooBar" |
|
1566 |
assert resp.json['err'] == 'error' |
|
1567 | ||
1568 | ||
1569 |
def test_invoice_pdf_endpoint_bad_request(app, resource): |
|
1570 |
Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42') |
|
1571 |
with mock_data(None, 'GetListFactures'): |
|
1572 |
resp = app.get( |
|
1573 |
'/caluire-axel/test/regie/MAREGIE/invoice/historical-XXX-42/pdf?NameID=yyy&nb_mounts_limit=not_a_number', |
|
1574 |
status=404, |
|
1575 |
) |
|
1576 |
assert resp.json['err_desc'] == "nb_mounts_limit must be an integer" |
|
1577 |
assert resp.json['err'] == 'bad-request' |
|
1578 | ||
1579 | ||
1580 |
def test_invoice_pdf_endpoint_no_result(app, resource): |
|
1581 |
resp = app.get('/caluire-axel/test/regie/MAREGIE/invoice/XXX-42/pdf?NameID=yyy', status=404) |
|
1582 |
assert resp.json['err_desc'] == "Person not found" |
|
1583 |
assert resp.json['err'] == 'not-found' |
|
1584 | ||
1585 |
Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42') |
|
1586 |
filepath = os.path.join(os.path.dirname(__file__), 'data/caluire_axel/invoices.xml') |
|
1587 |
with open(filepath) as xml: |
|
1588 |
content = ( |
|
1589 |
'''<PORTAIL> |
|
1590 |
<GETFACTURESAPAYER> |
|
1591 |
%s |
|
1592 |
</GETFACTURESAPAYER> |
|
1593 |
</PORTAIL>''' |
|
1594 |
% xml.read() |
|
1595 |
) |
|
1596 |
with mock_data(content, 'GetFacturesaPayer'): |
|
1597 |
resp = app.get('/caluire-axel/test/regie/MAREGIE/invoice/XXX-35/pdf?NameID=yyy', status=404) |
|
1598 |
assert resp.json['err_desc'] == "Invoice not found" |
|
1599 |
assert resp.json['err'] == 'not-found' |
|
1600 | ||
1601 |
with mock_data(content, 'GetFacturesaPayer'): |
|
1602 |
resp = app.get('/caluire-axel/test/regie/MAREGIE/invoice/XXX-44/pdf?NameID=yyy', status=404) |
|
1603 |
assert resp.json['err_desc'] == "Invoice not found" |
|
1604 |
assert resp.json['err'] == 'not-found' |
|
1605 | ||
1606 |
with mock_data(content, 'GetFacturesaPayer'): |
|
1607 |
resp = app.get('/caluire-axel/test/regie/MAREGIE/invoice/XXX-43/pdf?NameID=yyy', status=404) |
|
1608 |
assert resp.json['err_desc'] == "PDF not available" |
|
1609 |
assert resp.json['err'] == 'not-available' |
|
1610 | ||
1611 |
pdf_content = '''<PORTAIL> |
|
1612 |
<GETPDFFACTURE> |
|
1613 |
<CODE>1</CODE> |
|
1614 |
<FACTUREPDF></FACTUREPDF> |
|
1615 |
</GETPDFFACTURE> |
|
1616 |
</PORTAIL>''' |
|
1617 |
with mock.patch('passerelle.contrib.caluire_axel.models.CaluireAxel.get_invoice') as invoice: |
|
1618 |
invoice.return_value = {'has_pdf': True, 'display_id': '42'} |
|
1619 |
with mock_data(pdf_content, 'GetPdfFacture'): |
|
1620 |
resp = app.get('/caluire-axel/test/regie/MAREGIE/invoice/XXX-42/pdf?NameID=yyy', status=404) |
|
1621 |
assert resp.json['err_desc'] == "PDF error" |
|
1622 |
assert resp.json['err'] == 'error' |
|
1623 | ||
1624 | ||
1625 |
def test_invoice_pdf_endpoint(app, resource): |
|
1626 |
Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42') |
|
1627 |
pdf_content = '''<PORTAIL> |
|
1628 |
<GETPDFFACTURE> |
|
1629 |
<CODE>1</CODE> |
|
1630 |
<FACTUREPDF>aGVsbG8gd29ybGQ=</FACTUREPDF> |
|
1631 |
</GETPDFFACTURE> |
|
1632 |
</PORTAIL>''' |
|
1633 |
with mock.patch('passerelle.contrib.caluire_axel.models.CaluireAxel.get_invoice') as invoice: |
|
1634 |
invoice.return_value = {'has_pdf': True, 'display_id': '42'} |
|
1635 |
with mock_data(pdf_content, 'GetPdfFacture'): |
|
1636 |
app.get('/caluire-axel/test/regie/MAREGIE/invoice/XXX-42/pdf?NameID=yyy') |
|
1637 |
assert invoice.call_args_list[0][1]['historical'] is False |
|
1638 | ||
1639 |
with mock.patch('passerelle.contrib.caluire_axel.models.CaluireAxel.get_invoice') as invoice: |
|
1640 |
invoice.return_value = {'has_pdf': True, 'display_id': '42'} |
|
1641 |
with mock_data(pdf_content, 'GetPdfFacture'): |
|
1642 |
app.get('/caluire-axel/test/regie/MAREGIE/invoice/historical-XXX-42/pdf?NameID=yyy') |
|
1643 |
assert invoice.call_args_list[0][1]['historical'] is True |
|
1541 |
- |