123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347 |
- #!/usr/bin/env python3
- # coding=utf-8
- import sys
- import os
- import time
- import odoorpc
- import arrow
- import logging
- import argparse
- from tabulate import tabulate
- import math
- SUBJECT = "FIXME Hackerspace Cotisations"
- BODY = """
- <html>
- <head>
- <meta http-equiv="content-type" content="text/html; charset=UTF-8">
- </head>
- <body>
- <pre>
- Chèr(e) membre,
- Tu trouveras ci-dessous le montant de ta cotisation. Ton association dépend
- entièrement du soutien de ses membres pour continuer à louer le local actuel,
- c'est pourquoi ton apport nous est précieux.
- Le montant de ta cotisation est celui que tu as choisi lors de ton inscription
- ou mis à jour par la suite. Il est possible de le modifier sur demande.
- Un grand merci pour ton soutien à FIXME !
- ======================
- A payer: CHF {a_payer}.-
- ======================
- Montant annuel de ta cotisation : CHF {dues}.-
- Montant reçu dans les 12 derniers mois : CHF {encaissees}.-
- Ton comité,
- P.S: Dans la mesure du possible, merci de payer en une fois dans les 30 jours
- ou d'instaurer un ordre permanent équivalent au montant mensuel.
- CCP: 12-621716-7
- IBAN: CH14 0900 0000 1262 1716 7
- BIC: POFICHBEXXX
- FIXME Hackerspace, 1004 Lausanne
- Commentaire: Prénom et nom si différent du compte bancaire utilisé pour le paiement
- Message generated by https://git.fixme.ch/FIXME/cotisations, patches welcome!
- </pre>
- </body>
- </html>
- """
- SUBJECT_COMITE = "Cotisations: merci de mettre a jour les extraits bancaires :)"
- BODY_COMITE = """
- <html>
- <head>
- <meta http-equiv="content-type" content="text/html; charset=UTF-8">
- </head>
- <body>
- <pre>
- Chèr(e) membre du comité,
- Les entrées d'argent des derniers mois n'ont pas été mise à jour sur Odoo.
- Dès que ce sera fait, ce script pourra envoyer les factures aux membres.
- Happy Hacking!
- Message generated by https://git.fixme.ch/FIXME/cotisations, patches welcome!
- </pre>
- </body>
- </html>
- """
- def get_members(odoo):
- partner = odoo.env["res.partner"]
- for p in partner.search_read([("x_status", "!=", False)]):
- yield (p)
- def get_cotisations_encaissees(p, odoo):
- line = odoo.env["account.move.line"]
- cotisations_encaissees = 0.0
- ecritures = []
- thriteen_months_ago = arrow.utcnow().shift(months=-13).format("YYYY-MM-DD")
- # Lists all cotisation transactions registered during the last 12 months
- for l in line.search_read(
- [
- ("partner_id", "=", p["id"]),
- ("account_id.name", "=", "Cotisations"),
- ("date", ">", thriteen_months_ago),
- ("credit", ">", 0),
- ]
- ):
- ecritures.append((l["date"], l["display_name"], str(l["credit"])))
- cotisations_encaissees += l["credit"]
- return cotisations_encaissees
- def send_mail(p, cotisations_encaissees, cotisations_dues, odoo):
- logging.info(
- "Preparing email to {}, cotisation: CHF {}, payé: CHF {}".format(
- p["email"], cotisations_dues, cotisations_encaissees
- )
- )
- mail_mail = odoo.env["mail.mail"]
- values = {
- "email_from": "comite@fixme.ch",
- "reply_to": "comite@fixme.ch",
- "subject": SUBJECT,
- "body_html": BODY.format(
- p=p,
- encaissees=math.ceil(cotisations_encaissees),
- a_payer=math.ceil(p["x_amount"] - cotisations_encaissees),
- dues=math.ceil(p["x_amount"]),
- ),
- "email_to": p["email"],
- }
- if not args.dryrun:
- msg_id = mail_mail.create(values)
- mail_mail.send([msg_id], raise_exception=True)
- logging.info("Message ID {} sent".format(msg_id))
- def send_mail_comite(odoo):
- logging.info("Preparing email for the comité")
- mail_mail = odoo.env["mail.mail"]
- values = {
- "email_from": "comite@fixme.ch",
- "reply_to": "comite@fixme.ch",
- "subject": SUBJECT_COMITE,
- "body_html": BODY_COMITE,
- "email_to": "comite@fixme.ch",
- }
- if not args.dryrun:
- msg_id = mail_mail.create(values)
- mail_mail.send([msg_id], raise_exception=True)
- logging.info("Message ID {} sent".format(msg_id))
- # Check if the last cotisations update is not too old
- def is_bank_statements_up_to_date(odoo):
- statement = odoo.env["account.bank.statement"]
- one_year_ago = arrow.utcnow().shift(years=-1).format("YYYY-MM-DD")
- statements = statement.search_read(
- [("state", "=", "confirm"), ("date", ">", one_year_ago)]
- )
- # If it is not up to date on the last 11 months
- if len(statements) < 11:
- logging.error(
- "Please ensure that bank statements for the last 12 months are up to date, found only {} < 11".format(
- len(statements)
- )
- )
- if args.dryrun:
- return True
- return False
- else:
- return True
- def get_last_run(odoo):
- config_parameter = odoo.env["ir.config_parameter"]
- params = config_parameter.search_read([("key", "=", "cotisations_last_run")])
- # If configuration setting doesn't yet exists, create a dummy one
- if params == []:
- last_run = arrow.get(0)
- config_parameter.create({"key": "cotisations_last_run", "value": "0"})
- elif len(params) == 1:
- param = params[0]
- last_run = arrow.get(int(param["value"]))
- else:
- raise Exception("Too many parameters found in database: {}".format(params))
- logging.info("Last run timestamp: {}".format(last_run))
- return last_run
- def set_last_run(odoo, timestamp):
- config_parameter = odoo.env["ir.config_parameter"]
- logging.info("New timestamp: {}".format(timestamp))
- params = config_parameter.search_read([("key", "=", "cotisations_last_run")])
- assert len(params) == 1
- config_parameter.write([params[0]["id"]], {"value": arrow.get().timestamp})
- def should_run(odoo):
- # Check if the last cotisations update is not too old
- # If it is too old, we should send an email to the comité
- if not is_bank_statements_up_to_date(odoo):
- logging.info("Bank statements are not up to date")
- send_mail_comite(odoo)
- return False
- # Check if the last time this script ran is old enough
- now = arrow.utcnow()
- if not args.force:
- last_run = get_last_run(odoo)
- # If the emails where send less than 3 months ago, we quit
- if now.shift(months=-3) < last_run:
- logging.info("Don't run now")
- return False
- # We finally run the script and save the timestamp in odoo database
- if not args.dryrun:
- set_last_run(odoo, now)
- return True
- def run(args):
- odoo = odoorpc.ODOO("odoo.fixme.ch", protocol="jsonrpc+ssl", port=443)
- odoo.login(os.environ["DATABASE"], os.environ["USERNAME"], os.environ["PASSWORD"])
- if not should_run(odoo):
- sys.exit(0)
- count = 0
- total_amount = 0
- up_to_date_table = []
- out_of_date_table = []
- for p in get_members(odoo):
- # On modifie les memberships, on modifie le script says JB le 6 mai 2016
- if p["x_status"] == "non_member":
- continue
- if p["x_status"] == "old_member":
- continue
- cotisations_encaissees = get_cotisations_encaissees(p, odoo)
- # If all cotisation transaction amount if lower than chosen membership, send out a reminder email
- if cotisations_encaissees < p["x_amount"]:
- out_of_date_table.append(
- (
- p["id"],
- p["display_name"],
- p["x_status"],
- p["x_amount"],
- cotisations_encaissees,
- p["email"],
- )
- )
- send_mail(p, cotisations_encaissees, p["x_amount"], odoo)
- total_amount += p["x_amount"] - cotisations_encaissees
- count += 1
- else:
- up_to_date_table.append(
- (
- p["id"],
- p["display_name"],
- p["x_status"],
- p["x_amount"],
- cotisations_encaissees,
- p["email"],
- )
- )
- print()
- print(
- "Total payé: {}".format(
- sum([a[4] for a in up_to_date_table + out_of_date_table])
- )
- )
- print(
- "Total cotisations: {}".format(
- sum([a[3] for a in up_to_date_table + out_of_date_table])
- )
- )
- print()
- print(
- "Up to date: {} (cotisations: CHF {}, payé: CHF {})".format(
- len(up_to_date_table),
- sum([a[3] for a in up_to_date_table]),
- sum([a[4] for a in up_to_date_table]),
- )
- )
- print(tabulate(up_to_date_table))
- print()
- print(
- "Out to date: {} (cotisations: CHF {}, payé: CHF {})".format(
- len(out_of_date_table),
- sum([a[3] for a in out_of_date_table]),
- sum([a[4] for a in out_of_date_table]),
- )
- )
- print(tabulate(out_of_date_table))
- print()
- if __name__ == "__main__":
- logging.basicConfig(level=logging.INFO)
- parser = argparse.ArgumentParser(
- formatter_class=argparse.RawDescriptionHelpFormatter,
- description="FIXME Cotisations",
- )
- parser.add_argument(
- "--dryrun", action="store_true", help="Don't actually send emails"
- )
- parser.add_argument(
- "--debug", action="store_true", help="Show all logging messages"
- )
- parser.add_argument(
- "--force",
- action="store_true",
- help="Force run the script even if minimum time between run has been yet been reached.",
- )
- args = parser.parse_args()
- if args.dryrun == True:
- logging.info("DRYRUN MODE")
- if args.debug == True:
- logging.getLogger().setLevel(logging.DEBUG)
- run(args)
|