diff --git a/modules/ameli/__init__.py b/modules/ameli/__init__.py
new file mode 100644
index 00000000..2d8d3b89
--- /dev/null
+++ b/modules/ameli/__init__.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2013 Christophe Lampin
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+
+from .backend import AmeliBackend
+
+
+__all__ = ['AmeliBackend']
diff --git a/modules/ameli/backend.py b/modules/ameli/backend.py
new file mode 100755
index 00000000..f501f19c
--- /dev/null
+++ b/modules/ameli/backend.py
@@ -0,0 +1,91 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2013 Christophe Lampin
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+import urllib
+from weboob.capabilities.bill import ICapBill, SubscriptionNotFound, BillNotFound, Subscription, Bill
+from weboob.tools.backend import BaseBackend, BackendConfig
+from weboob.tools.value import ValueBackendPassword
+from .browser import AmeliBrowser
+
+__all__ = ['AmeliBackend']
+
+
+class AmeliBackend(BaseBackend, ICapBill):
+ NAME = 'ameli'
+ DESCRIPTION = u'Ameli website: French Health Insurance'
+ MAINTAINER = u'Christophe Lampin'
+ EMAIL = 'weboob@lampin.net'
+ VERSION = '0.g'
+ LICENSE = 'AGPLv3+'
+ BROWSER = AmeliBrowser
+ CONFIG = BackendConfig(ValueBackendPassword('login',
+ label='numero de SS',
+ masked=False),
+ ValueBackendPassword('password',
+ label='Password',
+ masked=True)
+ )
+ BROWSER = AmeliBrowser
+
+ def create_default_browser(self):
+ return self.create_browser(self.config['login'].get(),
+ self.config['password'].get())
+
+ def iter_subscription(self):
+ return self.browser.iter_subscription_list()
+
+ def get_subscription(self, _id):
+ with self.browser:
+ subscription = self.browser.get_subscription(_id)
+ if not subscription:
+ raise SubscriptionNotFound()
+ else:
+ return subscription
+
+ def iter_bills_history(self, subscription):
+ if not isinstance(subscription, Subscription):
+ subscription = self.get_subscription(subscription)
+ with self.browser:
+ return self.browser.iter_history(subscription)
+
+ def get_details(self, subscription):
+ if not isinstance(subscription, Subscription):
+ subscription = self.get_subscription(subscription)
+ with self.browser:
+ return self.browser.iter_details(subscription)
+
+ def iter_bills(self, subscription):
+ if not isinstance(subscription, Subscription):
+ subscription = self.get_subscription(subscription)
+ with self.browser:
+ return self.browser.iter_bills(subscription)
+
+ def get_bill(self, id):
+ with self.browser:
+ bill = self.browser.get_bill(id)
+ if not bill:
+ raise BillNotFound()
+ else:
+ return bill
+
+ def download_bill(self, bill):
+ if not isinstance(bill, Bill):
+ bill = self.get_bill(bill)
+ with self.browser:
+ return self.browser.readurl(bill._url,urllib.urlencode(bill._args))
diff --git a/modules/ameli/browser.py b/modules/ameli/browser.py
new file mode 100755
index 00000000..246e63b0
--- /dev/null
+++ b/modules/ameli/browser.py
@@ -0,0 +1,119 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2013 Christophe Lampin
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+import urllib
+import mechanize
+from weboob.tools.browser import BaseBrowser, BrowserIncorrectPassword
+from weboob.capabilities.bill import Detail
+from decimal import Decimal
+from .pages import AmeliBasePage, LoginPage, HomePage, AccountPage, LastPaymentsPage, PaymentDetailsPage, BillsPage
+
+__all__ = ['AmeliBrowser']
+
+class AmeliBrowser(BaseBrowser):
+ PROTOCOL = 'https'
+ DOMAIN = 'assure.ameli.fr'
+ ENCODING = None
+
+ PAGES = {'.*_pageLabel=as_login_page.*': LoginPage,
+ '.*_pageLabel=as_accueil_page.*': HomePage,
+ '.*_pageLabel=as_etat_civil_page.*': AccountPage,
+ '.*_pageLabel=as_revele_mensuel_presta_page.*': BillsPage,
+ '.*_pageLabel=as_dernier_paiement_page': LastPaymentsPage,
+ '.*_actionOverride=%2Fportlets%2Fpaiements%2Fdetailpaiements&paiements.*' : PaymentDetailsPage
+ }
+
+ loginp = '/PortailAS/appmanager/PortailAS/assure?_somtc=true&_pageLabel=as_login_page'
+ homep = '/PortailAS/appmanager/PortailAS/assure?_nfpb=true&_pageLabel=as_accueil_page'
+ accountp = '/PortailAS/appmanager/PortailAS/assure?_nfpb=true&_pageLabel=as_etat_civil_page'
+ billsp = '/PortailAS/appmanager/PortailAS/assure?_nfpb=true&_pageLabel=as_revele_mensuel_presta_page'
+ lastpaymentsp = '/PortailAS/appmanager/PortailAS/assure?_nfpb=true&_pageLabel=as_dernier_paiement_page'
+
+ is_logging = False
+
+ def home(self):
+ if not self.is_logged():
+ self.login()
+ self.location(self.homep)
+
+ def is_logged(self):
+ logged = self.page and self.page.is_logged() or self.is_logging
+ self.logger.debug('logged: %s' % (logged))
+ return logged
+
+ def login(self):
+ # Do we really need to login?
+ if self.is_logged():
+ self.logger.debug('Already logged in')
+ return
+
+ self.is_logging = True
+
+ self.location(self.loginp)
+ self.page.login(self.username, self.password)
+
+ self.is_logging = False
+
+ if not self.is_logged():
+ raise BrowserIncorrectPassword()
+
+ def iter_subscription_list(self):
+ if not self.is_on_page(AccountPage):
+ self.location(self.accountp)
+ return self.page.iter_subscription_list()
+
+ def get_subscription(self, id):
+ assert isinstance(id, basestring)
+ for sub in self.iter_subscription_list():
+ if id == sub._id:
+ return sub
+ return None
+
+ def iter_history(self, sub):
+ if not self.is_on_page(LastPaymentsPage):
+ self.location(self.lastpaymentsp)
+ urls = self.page.iter_last_payments()
+ for url in urls:
+ self.location(url)
+ assert self.is_on_page(PaymentDetailsPage)
+ for payment in self.page.iter_payment_details(sub):
+ yield payment
+
+ def iter_details(self, sub):
+ det = Detail()
+ det.id = sub.id
+ det.label = sub.label
+ det.infos = ''
+ det.price = Decimal('0.0')
+ yield det
+
+ def iter_bills(self, sub):
+ if not sub._id.isdigit():
+ return []
+ if not self.is_on_page(BillsPage):
+ self.location(self.billsp)
+ return self.page.iter_bills(sub)
+
+ def get_bill(self, id):
+ assert isinstance(id, basestring)
+ subs = self.iter_subscription_list()
+ for sub in subs:
+ for b in self.iter_bills(sub):
+ if id == b.id:
+ return b
diff --git a/modules/ameli/favicon.png b/modules/ameli/favicon.png
new file mode 100755
index 00000000..f3b2c28d
Binary files /dev/null and b/modules/ameli/favicon.png differ
diff --git a/modules/ameli/pages.py b/modules/ameli/pages.py
new file mode 100755
index 00000000..c650304e
--- /dev/null
+++ b/modules/ameli/pages.py
@@ -0,0 +1,147 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2013 Christophe Lampin
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+
+from datetime import datetime
+import re
+from decimal import Decimal
+import locale
+from weboob.tools.browser import BasePage
+from weboob.capabilities.bill import Subscription, Detail, Bill
+
+
+__all__ = ['AmeliBasePage', 'LoginPage', 'HomePage', 'AccountPage', 'LastPaymentsPage', 'PaymentDetailsPage', 'BillsPage']
+
+# Ugly array to avoid the use of french locale
+FRENCH_MONTHS = [u'janvier', u'février', u'mars', u'avril', u'mai', u'juin', u'juillet', u'août', u'septembre', u'octobre', u'novembre', u'décembre']
+
+class AmeliBasePage(BasePage):
+ def is_logged(self):
+ return len(self.document.xpath('//a[@id="logout"]')) > 0
+
+class LoginPage(AmeliBasePage):
+ def login(self, login, password):
+ self.browser.select_form('connexionCompteForm')
+ self.browser["connexioncompte_2numSecuriteSociale"] = login.encode('utf8')
+ self.browser["connexioncompte_2codeConfidentiel"] = password.encode('utf8')
+ self.browser.submit()
+
+class HomePage(AmeliBasePage):
+
+ def on_loaded(self):
+ pass
+
+
+class AccountPage(AmeliBasePage):
+
+ def iter_subscription_list(self):
+ idents = self.document.xpath('//div[contains(@class, "blocfond")]')
+ enfants = 0
+ for ident in idents:
+ if len(ident.xpath('.//h4')) == 0:
+ continue
+
+ name = self.parser.tocleanstring(ident.xpath('.//h4')[0])
+ lis = ident.xpath('.//li')
+ if len(lis) > 3:
+ number = re.sub('[^\d]+', '', ident.xpath('.//li')[3].text)
+ else:
+ enfants = enfants + 1
+ number = "AFFILIE" + str(enfants)
+ sub = Subscription(number)
+ sub._id = number
+ sub.label = unicode(name)
+ sub.subscriber = unicode(name)
+ yield sub
+
+class LastPaymentsPage(AmeliBasePage):
+
+ def iter_last_payments(self):
+ list_table = self.document.xpath('//table[@id="ligneTabDerniersPaiements"]')
+ if len(list_table) > 0:
+ table = list_table[0].xpath('.//tr')
+ for tr in table:
+ list_a = tr.xpath('.//a')
+ if len(list_a) == 0:
+ continue
+ yield list_a[0].attrib.get('href')
+
+class PaymentDetailsPage(AmeliBasePage):
+
+ def iter_payment_details(self, sub):
+ if sub._id.isdigit():
+ idx = 0
+ else:
+ idx = sub._id.replace('AFFILIE','')
+ if len(self.document.xpath('//div[@class="centrepage"]/h3')) > idx or self.document.xpath('//table[@id="DetailPaiement3"]') > idx:
+ id_str = self.document.xpath('//div[@class="centrepage"]/h3')[idx].text.strip()
+ m = re.match('.*le (.*) pour un montant de.*', id_str)
+ if m:
+ id_str = m.group(1)
+ id_date = datetime.strptime(id_str, '%d/%m/%Y').date()
+ id = sub._id + "." + datetime.strftime(id_date, "%Y%m%d")
+ table = self.document.xpath('//table[@id="DetailPaiement3"]')[idx].xpath('.//tr')
+ line = 1
+ for tr in table:
+ tds = tr.xpath('.//td');
+ if len(tds) == 0:
+ continue
+ date_str = tds[0].text
+ det = Detail()
+ det.id = id + "." + str(line)
+ det.label = unicode(tds[1].text.strip())
+ if date_str is None or date_str == '':
+ det.infos = u''
+ det.datetime = last_date
+ else:
+ det.infos = u'Payé ' + unicode(re.sub('[^\d,-]+', '', tds[2].text)) + u'€ / Base ' + unicode(re.sub('[^\d,-]+', '', tds[3].text)) + u'€ / Taux ' + unicode(re.sub('[^\d,-]+', '', tds[4].text)) + '%'
+ det.datetime = datetime.strptime(date_str, '%d/%m/%Y').date()
+ last_date = det.datetime
+ det.price = Decimal(re.sub('[^\d,-]+', '', tds[5].text).replace(',','.'))
+ line = line + 1
+ yield det
+
+class BillsPage(AmeliBasePage):
+
+ def iter_bills(self,sub):
+ table = self.document.xpath('//table[@id="tableauDecompte"]')[0].xpath('.//tr')
+ for tr in table:
+ list_tds = tr.xpath('.//td')
+ if len(list_tds) == 0:
+ continue
+ date_str = list_tds[0].text
+ month_str = date_str.split()[0]
+ date = datetime.strptime(re.sub(month_str,str(FRENCH_MONTHS.index(month_str) + 1),date_str),"%m %Y").date()
+ amount = list_tds[1].text
+ if amount is None:
+ continue
+ amount = re.sub(' euros','',amount)
+ bil = Bill()
+ bil.id = sub._id + "." + date.strftime("%Y%m")
+ bil.date = date
+ bil.label = u''+amount.strip()
+ bil.format = u'pdf'
+ filedate = date.strftime("%m%Y")
+ bil._url = '/PortailAS/PDFServletReleveMensuel.dopdf'
+ bil._args = {'PDF.moisRecherche': filedate}
+ yield bil
+
+ def get_bill(self,bill):
+ self.location(bill._url, urllib.urlencode(bill._args))
+
diff --git a/modules/ameli/test.py b/modules/ameli/test.py
new file mode 100755
index 00000000..8df432ae
--- /dev/null
+++ b/modules/ameli/test.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2013 Christophe Lampin
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+
+from weboob.tools.test import BackendTest
+
+
+__all__ = ['AmeliTest']
+
+
+class AmeliTest(BackendTest):
+ BACKEND = 'ameli'
+
+ def test_ameli(self):
+ for subscription in self.backend.iter_subscription():
+ list(self.backend.iter_bills_history(subscription.id))
+ for bill in self.backend.iter_bills(subscription.id):
+ self.backend.download_bill(bill.id)