main.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. from asyncio.windows_events import NULL
  2. from time import sleep
  3. from app_logging import logger_name, init as init_logging
  4. import logging
  5. logger = logging.getLogger(logger_name)
  6. import requests,json
  7. import datetime
  8. import argparse
  9. import sys
  10. CONF_FILE_PATH = 'references.conf'
  11. init_logging()
  12. logger.debug(__name__)
  13. logger.info("")
  14. logger.info("")
  15. logger.info("")
  16. logger.info("------- welcome to Slash-Dolistripe -------")
  17. # Method to read config file settings
  18. logger.info(" --- -------------------------------------------------------------------------------- ---")
  19. logger.info(" --- ---------------------------------- ARG PHASE ----------------------------------- ---")
  20. logger.info(" --- -------------------------------------------------------------------------------- ---")
  21. parser = argparse.ArgumentParser()
  22. parser.add_argument("-v","--verbosity", help="increase output verbosity",action="store_true")
  23. parser.add_argument("-d","--dry", help="perform a dry run",action="store_true")
  24. parser.add_argument("-m","--mail", help="send invoice per mail to client (you can add a contact mail copy in the reference.conf file)",action="store_true")
  25. parser.add_argument("-p","--planned", help="trigger planned work to ",action="store_true")
  26. args = parser.parse_args()
  27. if args.verbosity:
  28. logger.info("Args : Debug Verbose Enabled")
  29. logger.setLevel(logging.DEBUG)
  30. else :
  31. logger.setLevel(logging.INFO)
  32. args = parser.parse_args()
  33. if args.dry:
  34. logger.info("Args : Dry Run Enabled")
  35. args = parser.parse_args()
  36. if args.mail:
  37. logger.info("Args : send invoice per Mails Enabled")
  38. logger.info(" --- -------------------------------------------------------------------------------- ---")
  39. logger.info(" --- ---------------------------------- CONF PHASE ---------------------------------- ---")
  40. logger.info(" --- -------------------------------------------------------------------------------- ---")
  41. logger.info("Reading Reference ConfIguration File")
  42. #check if file exist
  43. from os.path import exists
  44. if not exists(CONF_FILE_PATH) :
  45. logger.critical("'reference.conf' not found ! check if the file exist or you execute the script in the right folder. \
  46. \n To build a 'reference.conf' check the example in the folder.")
  47. sys.exit(1)
  48. import configparser
  49. config = configparser.ConfigParser()
  50. config.optionxform = str # to make the read Case Sensitive
  51. config.read(CONF_FILE_PATH)
  52. #readign structure of the file
  53. references_dict = config["list"]
  54. stripe_api_key = config["credentials"]["stripe_api_key"]
  55. dolibarr_url = config["credentials"]["dolibarr_url"]
  56. dolibarr_username = config["credentials"]["dolibarr_username"]
  57. dolibarr_password = config["credentials"]["dolibarr_password"]
  58. if args.planned :
  59. dolibarr_planned_work_key = config["credentials"]["planned_work_key"]
  60. dolibarr_planned_work_cron_job_id = config["credentials"]["cron_job_id"]
  61. contact_mail = None
  62. if config.has_option("credentials", "contact_mail") :
  63. contact_mail = config["credentials"]["contact_mail"]
  64. logger.debug("contact mail in configuration is : " + contact_mail)
  65. logger.debug("Reading CRM contract reference with link to Stripe subscriptions references")
  66. for reference in references_dict:
  67. logger.debug("link reference found : " + reference + " -> " + references_dict[reference])
  68. logger.info(" --- -------------------------------------------------------------------------------- ---")
  69. logger.info(" --- --------------------------------- STRIPE PHASE --------------------------------- ---")
  70. logger.info(" --- -------------------------------------------------------------------------------- ---")
  71. logger.info("testing connection to stripe...")
  72. import requests
  73. from requests.structures import CaseInsensitiveDict
  74. headers = CaseInsensitiveDict()
  75. headers["Accept"] = "application/json"
  76. headers["Authorization"] = "Bearer "+ stripe_api_key
  77. logger.debug("retrieving balance for test")
  78. r = requests.get("https://api.stripe.com/v1/balance", headers=headers)
  79. json_content = json.loads(r.text)
  80. logger.debug(json.dumps(json_content, indent=4 , ensure_ascii=False))
  81. assert r.status_code == 200
  82. logger.info("Stripe OK")
  83. logger.info("----------- Validating Stripe references and retrieving data -----------")
  84. class crm_linked_invoice :
  85. url : str
  86. date : datetime
  87. amount : float
  88. unpaid : bool = False
  89. class invoice_link :
  90. contract_number : int
  91. stripe_subscription_reference : str
  92. is_stripe_link : bool = True
  93. stripe_customer_ref : str
  94. stripe_customer_name : str = ""
  95. stripe_customer_mail : str
  96. stripe_paid_amount : float
  97. stripe_epoch_date : int
  98. stripe_invoice_url : str
  99. stripe_invoice_number : str
  100. crm_contract_activated : bool = False
  101. crm_needs_new_invoice : bool = False
  102. crm_needs_update : bool = False
  103. crm_target_invoice : crm_linked_invoice
  104. invoice_links = set()
  105. for reference in references_dict:
  106. link = invoice_link()
  107. link.contract_number = reference
  108. if references_dict[reference] != "0" :
  109. logger.info("retrieving Subscription data for reference : " + references_dict[reference] + " ")
  110. url = "https://api.stripe.com/v1/subscriptions/" + references_dict[reference]
  111. logger.debug("testing url : '" + url + "'")
  112. r = requests.get(url, headers=headers)
  113. assert r.status_code == 200
  114. subscription_json_data = json.loads(r.text)
  115. logger.debug("Subscription Data found : ")
  116. logger.debug(json.dumps(subscription_json_data, indent=4 , ensure_ascii=False))
  117. if subscription_json_data["status"] != "active" :
  118. logger.info("subscription not active, going to next")
  119. continue
  120. #retrieving customer data
  121. latest_invoice_reference = subscription_json_data["latest_invoice"]
  122. logger.debug("latest invoice reference found : " + latest_invoice_reference)
  123. url = "https://api.stripe.com/v1/invoices/" + latest_invoice_reference
  124. r = requests.get(url, headers=headers)
  125. assert r.status_code == 200
  126. latest_invoice_json_data = json.loads(r.text)
  127. logger.debug("latest invoice Data found : ")
  128. logger.debug(json.dumps(latest_invoice_json_data, indent=4, ensure_ascii=False))
  129. link.stripe_subscription_reference = references_dict[reference]
  130. link.stripe_paid_amount = float(float(latest_invoice_json_data["amount_paid"]) / 100 )
  131. link.stripe_epoch_date = latest_invoice_json_data["created"]
  132. link.stripe_customer_name = latest_invoice_json_data["customer_name"]
  133. link.stripe_customer_mail = latest_invoice_json_data["customer_email"]
  134. link.stripe_invoice_url = latest_invoice_json_data["hosted_invoice_url"]
  135. link.stripe_invoice_number = latest_invoice_json_data["number"]
  136. logger.info("latest invoice customer info : " + link.stripe_customer_name + " - " + link.stripe_customer_mail)
  137. logger.info("latest invoice paid amount : " + str(link.stripe_paid_amount) + latest_invoice_json_data["currency"])
  138. logger.info("latest invoice payment date : " + str(datetime.datetime.fromtimestamp(int(link.stripe_epoch_date))))
  139. logger.info("latest invoice url : " + link.stripe_invoice_url )
  140. logger.info(" ----------- Subscription Data OK")
  141. elif references_dict[reference] == "0" :
  142. logger.info("contract number : " + str(link.contract_number) + " is associated with no stripe subscriptions")
  143. link.is_stripe_link = False
  144. logger.info(" ----------- Free Contract OK")
  145. else :
  146. logger.warning("unknown formatting for the contract : " + reference)
  147. continue
  148. # subscription reference - dolibarr contract reference - name - mail - amount - date of payment.
  149. invoice_links.add(link)
  150. logger.info("Stripe Phase OK")
  151. logger.info(" --- -------------------------------------------------------------------------------- ---")
  152. logger.info(" --- -------------------------------- READ CRM PHASE -------------------------------- ---")
  153. logger.info(" --- -------------------------------------------------------------------------------- ---")
  154. from selenium import webdriver
  155. from selenium.webdriver.common.keys import Keys
  156. from selenium.webdriver.common.by import By
  157. from selenium.webdriver.edge.options import Options
  158. import logging
  159. from selenium.webdriver.remote.remote_connection import LOGGER
  160. LOGGER.setLevel(logging.CRITICAL)
  161. logger.info("CRM Login")
  162. # Launch Microsoft Edge (Chromium)
  163. options = Options()
  164. options.add_experimental_option('excludeSwitches', ['enable-logging'])
  165. driver = webdriver.Edge()
  166. driver.get(str(dolibarr_url) + "/index.php")
  167. assert "Identifiant" in driver.title
  168. driver.find_element(By.NAME,"username").send_keys(str(dolibarr_username))
  169. pass_field = driver.find_element(By.NAME,"password")
  170. pass_field.send_keys(str(dolibarr_password))
  171. pass_field.send_keys(Keys.RETURN)
  172. logger.info(driver.title)
  173. assert "Accueil" in driver.title
  174. logger.info("login successful")
  175. current_date = datetime.datetime.now()
  176. if args.planned :
  177. logger.info("preparation phase : trigger planned work in CRM")
  178. work_url = str(dolibarr_url) + "/public/cron/cron_run_jobs_by_url.php?securitykey=" + \
  179. dolibarr_planned_work_key + "&userlogin=" + dolibarr_username + "&id=" + str(dolibarr_planned_work_cron_job_id)
  180. driver.get(work_url)
  181. logger.info("temporization 1 sec....")
  182. sleep(1)
  183. # iterating over links63
  184. logger.info("iterating over invoice links")
  185. link : invoice_link
  186. for link in invoice_links :
  187. link.crm_contract_activated == False
  188. logger.info("treating contract : " + link.contract_number + " for " + link.stripe_customer_name)
  189. driver.get(dolibarr_url + "/contrat/card.php?id=" + str(link.contract_number))
  190. logger.debug("testing if services are activated")
  191. #iterating service
  192. contract_lines = driver.find_elements(By.XPATH,"//div[contains(@id,'contrat-lines-container')]/div")
  193. logger.debug(str(len(contract_lines)) + " service line found")
  194. for line in contract_lines :
  195. try :
  196. service_name = line.find_element(By.CLASS_NAME,"classfortooltip").text
  197. except Exception :
  198. element = line.find_element(By.CLASS_NAME,"fa-concierge-bell")
  199. service_name = element.find_element(By.XPATH,'..').text
  200. service_status = line.find_element(By.CLASS_NAME,"badge-status").text
  201. logger.debug(service_name)
  202. logger.debug(service_status)
  203. if "En service" in service_status :
  204. logger.debug("crm contract have at least one service activated")
  205. link.crm_contract_activated = True
  206. if link.crm_contract_activated == False:
  207. logger.debug("the contract with number " + link.contract_number + " have all services disabled, SKIPPING CONTRACT")
  208. continue
  209. contract_links_table = driver.find_element(By.XPATH,'//table[@data-block="showLinkedObject"]')
  210. contract_links_elements = driver.find_elements(By.XPATH,'//tr[@data-element="facture"]')
  211. link.crm_needs_new_invoice = True
  212. if len(contract_links_elements) == 0 or contract_links_elements == None:
  213. link.crm_needs_new_invoice = False
  214. if not link.crm_contract_activated :
  215. link.crm_needs_new_invoice = False
  216. for element in contract_links_elements :
  217. current_invoice = crm_linked_invoice()
  218. current_invoice.url = element.find_element(By.CLASS_NAME, "linkedcol-name").find_element(By.TAG_NAME,"a").get_attribute("href")
  219. current_invoice.amount = float(element.find_element(By.CLASS_NAME, "linkedcol-amount").text.replace(',', "."))
  220. current_invoice.date = datetime.datetime.strptime(element.find_element(By.CLASS_NAME, "linkedcol-date").text, '%d/%m/%Y')
  221. if element.find_element(By.CLASS_NAME, "linkedcol-statut").find_element(By.TAG_NAME,"span").get_attribute("title") == "Impayée" :
  222. logger.debug("invoice is unpaid")
  223. current_invoice.unpaid = True
  224. logger.debug("-----")
  225. if link.is_stripe_link :
  226. logger.debug("CRM linked invoice for customer : " + link.stripe_customer_name + " - " + link.stripe_customer_mail)
  227. logger.debug("CRM linked invoice url : " + current_invoice.url)
  228. logger.debug("CRM linked invoice date : " + str(current_invoice.amount))
  229. logger.debug("CRM linked invoice amount : " + str(current_invoice.date))
  230. logger.debug("CRM linked invoice unpaid ? : " + str(current_invoice.unpaid))
  231. logger.debug("-----")
  232. #checking if new invoice is needed for the month
  233. if current_invoice.date.year == current_date.year :
  234. if current_invoice.date.month == current_date.month :
  235. logger.debug("invoice link does not need a new invoice for this month")
  236. link.crm_needs_new_invoice = False #TODO check all month since last invoice
  237. if link.is_stripe_link :
  238. stripe_date = datetime.datetime.fromtimestamp(link.stripe_epoch_date)
  239. #checking if invoice is eligible to update
  240. if current_invoice.date.year == stripe_date.year :
  241. if current_invoice.date.month == stripe_date.month :
  242. if current_invoice.date.day <= stripe_date.day :
  243. if current_invoice.unpaid :
  244. if link.stripe_paid_amount == current_invoice.amount :
  245. logger.info("Current crm invoice is unpaid, and corresponding to same month and amount as stripe payment")
  246. logger.info(" ## Target invoice FOUND ! ##")
  247. link.crm_needs_update = True
  248. link.crm_target_invoice = current_invoice
  249. else :
  250. #for free contract
  251. if current_invoice.date.year == current_date.year :
  252. if current_invoice.date.month == current_date.month :
  253. if current_date.day >= current_invoice.date.day :
  254. if current_invoice.unpaid :
  255. if current_invoice.amount == 0:
  256. logger.info("current crm invoice is unpaid for a 0 amount (Free Contract)")
  257. logger.info(" ## Target invoice FOUND ! ##")
  258. link.crm_needs_update = True
  259. link.crm_target_invoice = current_invoice
  260. logger.info(" --- -------------------------------------------------------------------------------- ---")
  261. logger.info(" --- -------------------------------- SUMMARY PHASE --------------------------------- ---")
  262. logger.info(" --- -------------------------------------------------------------------------------- ---")
  263. logger.info("summary of actions")
  264. if len(invoice_links) == 0 :
  265. logger.info("## no action pending detected ##")
  266. action = False
  267. link : invoice_link
  268. for link in invoice_links :
  269. if link.crm_contract_activated :
  270. if link.crm_needs_update :
  271. action = True
  272. logger.info(" ## ------------------------------- ##")
  273. logger.info("@@ INVOICE UPDATE PENDING : ")
  274. logger.info("CRM linked invoice url : " + link.crm_target_invoice.url)
  275. logger.info("CRM linked invoice date : " + str(link.crm_target_invoice.amount))
  276. logger.info("CRM linked invoice amount : " + str(link.crm_target_invoice.date))
  277. logger.info("CRM linked invoice unpaid ? : " + str(link.crm_target_invoice.unpaid))
  278. if link.is_stripe_link :
  279. logger.info("stripe invoice customer info : " + link.stripe_customer_name + " - " + link.stripe_customer_mail)
  280. logger.info("stripe invoice paid amount : " + str(link.stripe_paid_amount) + " " + latest_invoice_json_data["currency"])
  281. logger.info("stripe invoice payment date : " + str(datetime.datetime.fromtimestamp(int(link.stripe_epoch_date))))
  282. else :
  283. logger.info("Invoice is from FREE CONTRACT")
  284. logger.info(" ## ------------------------------- ##")
  285. if link.crm_needs_new_invoice :
  286. Action = True
  287. logger.info(" ## ------------------------------- ##")
  288. logger.info("New Invoice Generation Pending")
  289. logger.info("invoice customer info : " + link.stripe_customer_name + " - " + link.stripe_customer_mail)
  290. date_string = format(current_date,'01/%m/%Y')
  291. logger.info("planned date : " + date_string)
  292. logger.info(" ## ------------------------------- ##")
  293. if not action :
  294. logger.info("## no action pending detected ##")
  295. if args.dry:
  296. driver.close()
  297. logger.info("dry run enabled, exiting here...")
  298. exit(0)
  299. logger.info(" --- -------------------------------------------------------------------------------- ---")
  300. logger.info(" --- -------------------------------- ACTION PHASE ---------------------------------- ---")
  301. logger.info(" --- -------------------------------------------------------------------------------- ---")
  302. for link in invoice_links :
  303. if link.crm_contract_activated :
  304. if link.crm_needs_update :
  305. logger.info(" ## ------ Creating Payment ------ ##")
  306. logger.info("CRM linked invoice url : " + link.crm_target_invoice.url)
  307. logger.info("CRM linked invoice date : " + str(link.crm_target_invoice.amount))
  308. logger.info("CRM linked invoice amount : " + str(link.crm_target_invoice.date))
  309. logger.info("CRM linked invoice unpaid ? : " + str(link.crm_target_invoice.unpaid))
  310. if link.is_stripe_link :
  311. logger.info("stripe invoice customer info : " + link.stripe_customer_name + " - " + link.stripe_customer_mail)
  312. logger.info("stripe invoice paid amount : " + str(link.stripe_paid_amount) + " " + latest_invoice_json_data["currency"])
  313. logger.info("stripe invoice payment date : " + str(datetime.datetime.fromtimestamp(int(link.stripe_epoch_date))))
  314. else :
  315. logger.info("Invoice is from FREE CONTRACT")
  316. if link.is_stripe_link :
  317. driver.get(link.crm_target_invoice.url)
  318. pay_url = driver.find_element(By.XPATH, "//*[text()='Saisir règlement']").get_attribute("href")
  319. driver.get(pay_url)
  320. stripe_payment_date = datetime.datetime.fromtimestamp(int(link.stripe_epoch_date))
  321. date_string = format(stripe_payment_date,'{%d/%m/%Y}')
  322. driver.find_element(By.ID, "re").send_keys(date_string)
  323. driver.find_element(By.NAME, "comment").send_keys("Payment treated by automation - Slash-DoliStripe \n stripe invoice URL : " + link.stripe_invoice_url )
  324. #en cas de plusieur facture impayée dont celles qui n'ont rien a voir avec le contrat
  325. target_invoice_line = driver.find_element(By.CLASS_NAME,'highlight')
  326. target_invoice_line.find_element(By.CLASS_NAME,'AutoFillAmout').click()
  327. driver.find_element(By.NAME, "num_paiement").send_keys(link.stripe_invoice_number)
  328. driver.find_element(By.XPATH,'//input[@value="Payer"]').click()
  329. driver.find_element(By.XPATH,'//input[@value="Valider"]').click()
  330. else :
  331. driver.get(link.crm_target_invoice.url + "&action=paid")
  332. buttons = driver.find_element(By.CLASS_NAME,"ui-dialog-buttonset")
  333. logger.debug(buttons.get_attribute("innerHTML"))
  334. buttons.find_element(By.XPATH,'button[contains(text(), "Oui")]').click()
  335. if args.mail:
  336. logger.info("sending email to client...")
  337. driver.find_element(By.XPATH, "//*[text()='Envoyer email']").click()
  338. if link.is_stripe_link :
  339. #here we add the stripe mail, if there is not stripe mail the crm mail will prevail.
  340. driver.find_element(By.ID, "sendto").send_keys(link.stripe_customer_mail)
  341. if contact_mail is not None :
  342. driver.find_element(By.ID, "sendtocc").send_keys(contact_mail)
  343. driver.find_element(By.ID, "sendmail").click()
  344. logger.info(" --- -------------------------------------------------------------------------------- ---")
  345. logger.info(" --- -------------------------------- CLEANING PHASE -------------------------------- ---")
  346. logger.info(" --- -------------------------------------------------------------------------------- ---")
  347. driver.close()
  348. driver.quit()
  349. exit(0)