123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538 |
- from time import sleep
- from app_logging import logger_name, init as init_logging
- import logging
- logger = logging.getLogger(logger_name)
- import requests,json
- import datetime
- import argparse
- import sys
- CONF_FILE_PATH = 'references.conf'
- LANG = "FR"
- if LANG == "EN" :
- DOLIBARR_LOGIN_TEXT = "Login"
- DOLIBARR_HOME_TEXT = "Home"
- DOLIBARR_CONTRACT_RUNNING_TEXT = "Running"
- DOLIBARR_NOT_PAID_TEXT = "Not paid"
- DOLIBARR_DATE_FORMAT = '%m/%d/%Y'
- DOLIBARR_TEXT_ENTER_PAYMENT = "Enter payment"
- DOLIBARR_TEXT_PAY = "Pay"
- DOLIBARR_TEXT_VALIDATE_PAYMENT = "Validate"
- DOLIBARR_TEXT_YES_PAYMENT = "Yes"
- DOLIBARR_TEXT_SEND_EMAIL = "Send email"
- elif LANG == "FR" :
- DOLIBARR_LOGIN_TEXT = "Identifiant"
- DOLIBARR_HOME_TEXT = "Accueil"
- DOLIBARR_CONTRACT_RUNNING_TEXT = "En Service"
- DOLIBARR_NOT_PAID_TEXT = "Impayée"
- DOLIBARR_DATE_FORMAT = '%d/%m/%Y'
- DOLIBARR_TEXT_ENTER_PAYMENT = "Saisir règlement"
- DOLIBARR_TEXT_PAY = "Payer"
- DOLIBARR_TEXT_VALIDATE_PAYMENT = "Valider"
- DOLIBARR_TEXT_YES_PAYMENT = "Oui"
- DOLIBARR_TEXT_SEND_EMAIL = "Envoyer email"
- init_logging()
- logger.debug(__name__)
- logger.info("")
- logger.info("")
- logger.info("")
- logger.info("------- welcome to Slash-Dolistripe -------")
- # Method to read config file settings
- logger.info(" --- -------------------------------------------------------------------------------- ---")
- logger.info(" --- ---------------------------------- ARG PHASE ----------------------------------- ---")
- logger.info(" --- -------------------------------------------------------------------------------- ---")
- parser = argparse.ArgumentParser()
- parser.add_argument("-v","--verbosity", help="increase output verbosity",action="store_true")
- parser.add_argument("-d","--dry", help="perform a dry run",action="store_true")
- 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")
- parser.add_argument("-p","--planned", help="trigger planned work to ",action="store_true")
- args = parser.parse_args()
- if args.verbosity:
- logger.info("Args : Debug Verbose Enabled")
- logger.setLevel(logging.DEBUG)
- else :
- logger.setLevel(logging.INFO)
- args = parser.parse_args()
- if args.dry:
- logger.info("Args : Dry Run Enabled")
- args = parser.parse_args()
- if args.mail:
- logger.info("Args : send invoice per Mails Enabled")
- logger.info(" --- -------------------------------------------------------------------------------- ---")
- logger.info(" --- ---------------------------------- CONF PHASE ---------------------------------- ---")
- logger.info(" --- -------------------------------------------------------------------------------- ---")
- logger.info("Reading Reference ConfIguration File")
- #check if file exist
- from os.path import exists
- if not exists(CONF_FILE_PATH) :
- logger.critical("'reference.conf' not found ! check if the file exist or you execute the script in the right folder. \
- \n To build a 'reference.conf' check the example in the folder.")
- sys.exit(1)
- import configparser
- config = configparser.ConfigParser()
- config.optionxform = str # to make the read Case Sensitive
- config.read(CONF_FILE_PATH)
- #readign structure of the file
- references_dict = config["list"]
- stripe_api_key = config["credentials"]["stripe_api_key"]
- dolibarr_url = config["credentials"]["dolibarr_url"]
- dolibarr_username = config["credentials"]["dolibarr_username"]
- dolibarr_password = config["credentials"]["dolibarr_password"]
- if args.planned :
- dolibarr_planned_work_key = config["credentials"]["planned_work_key"]
- dolibarr_planned_work_cron_job_id = config["credentials"]["cron_job_id"]
- contact_mail = None
- if config.has_option("credentials", "contact_mail") :
- contact_mail = config["credentials"]["contact_mail"]
- logger.debug("contact mail in configuration is : " + contact_mail)
- logger.debug("Reading CRM contract reference with link to Stripe subscriptions references")
- for reference in references_dict:
- logger.debug("link reference found : " + reference + " -> " + references_dict[reference])
- logger.info(" --- -------------------------------------------------------------------------------- ---")
- logger.info(" --- --------------------------------- STRIPE PHASE --------------------------------- ---")
- logger.info(" --- -------------------------------------------------------------------------------- ---")
- logger.info("testing connection to stripe...")
- import requests
- from requests.structures import CaseInsensitiveDict
- headers = CaseInsensitiveDict()
- headers["Accept"] = "application/json"
- headers["Authorization"] = "Bearer "+ stripe_api_key
- logger.debug("retrieving balance for test")
- r = requests.get("https://api.stripe.com/v1/balance", headers=headers)
- json_content = json.loads(r.text)
- logger.debug(json.dumps(json_content, indent=4 , ensure_ascii=False))
- assert r.status_code == 200
- logger.info("Stripe OK")
- logger.info("----------- Validating Stripe references and retrieving data -----------")
- class crm_linked_invoice :
- url : str
- date : datetime
- amount : float
- unpaid : bool = False
- class invoice_link :
- contract_number : int
- stripe_subscription_reference : str
- is_stripe_link : bool = True
-
- stripe_customer_ref : str
- stripe_customer_name : str = ""
- stripe_customer_mail : str
- stripe_paid_amount : float
- stripe_epoch_date : int
- stripe_invoice_url : str
- stripe_invoice_number : str
- crm_contract_activated : bool = False
- crm_needs_new_invoice : bool = False
- crm_needs_update : bool = False
- crm_target_invoice : crm_linked_invoice
-
- invoice_links = set()
- for reference in references_dict:
-
- link = invoice_link()
- link.contract_number = reference
- if references_dict[reference] != "0" :
- logger.info("retrieving Subscription data for reference : " + references_dict[reference] + " ")
- url = "https://api.stripe.com/v1/subscriptions/" + references_dict[reference]
- logger.debug("testing url : '" + url + "'")
- r = requests.get(url, headers=headers)
-
- assert r.status_code == 200
- subscription_json_data = json.loads(r.text)
- logger.debug("Subscription Data found : ")
- logger.debug(json.dumps(subscription_json_data, indent=4 , ensure_ascii=False))
- if subscription_json_data["status"] != "active" :
- logger.info("subscription not active, going to next")
- continue
- #retrieving customer data
- latest_invoice_reference = subscription_json_data["latest_invoice"]
- logger.debug("latest invoice reference found : " + latest_invoice_reference)
- url = "https://api.stripe.com/v1/invoices/" + latest_invoice_reference
- r = requests.get(url, headers=headers)
- assert r.status_code == 200
- latest_invoice_json_data = json.loads(r.text)
- logger.debug("latest invoice Data found : ")
- logger.debug(json.dumps(latest_invoice_json_data, indent=4, ensure_ascii=False))
-
- link.stripe_subscription_reference = references_dict[reference]
- link.stripe_paid_amount = float(float(latest_invoice_json_data["amount_paid"]) / 100 )
- link.stripe_epoch_date = latest_invoice_json_data["created"]
- link.stripe_customer_name = latest_invoice_json_data["customer_name"]
- link.stripe_customer_mail = latest_invoice_json_data["customer_email"]
- link.stripe_invoice_url = latest_invoice_json_data["hosted_invoice_url"]
- link.stripe_invoice_number = latest_invoice_json_data["number"]
- logger.info("latest invoice customer info : " + link.stripe_customer_name + " - " + link.stripe_customer_mail)
- logger.info("latest invoice paid amount : " + str(link.stripe_paid_amount) + latest_invoice_json_data["currency"])
- logger.info("latest invoice payment date : " + str(datetime.datetime.fromtimestamp(int(link.stripe_epoch_date))))
- logger.info("latest invoice url : " + link.stripe_invoice_url )
- logger.info(" ----------- Subscription Data OK")
-
- elif references_dict[reference] == "0" :
- logger.info("contract number : " + str(link.contract_number) + " is associated with no stripe subscriptions")
- link.is_stripe_link = False
- logger.info(" ----------- Free Contract OK")
-
- else :
- logger.warning("unknown formatting for the contract : " + reference)
- continue
- # subscription reference - dolibarr contract reference - name - mail - amount - date of payment.
- invoice_links.add(link)
-
- logger.info("Stripe Phase OK")
- logger.info(" --- -------------------------------------------------------------------------------- ---")
- logger.info(" --- -------------------------------- READ CRM PHASE -------------------------------- ---")
- logger.info(" --- -------------------------------------------------------------------------------- ---")
- from selenium import webdriver
- from selenium.webdriver.common.by import By
- from selenium.webdriver.common.keys import Keys
- import logging
- from selenium.webdriver.remote.remote_connection import LOGGER
- LOGGER.setLevel(logging.CRITICAL)
- from selenium.webdriver.chrome.options import Options
- # URL du serveur Selenium
- selenium_url = "http://localhost:4444/wd/hub"
- chrome_options = Options()
- chrome_options.add_argument("--lang=fr-FR")
- chrome_options.add_experimental_option('prefs', {'intl.accept_languages': 'fr-FR'})
- chrome_options.add_argument('--headless')
- chrome_options.add_argument('--disable-dev-shm-usage')
- chrome_options.add_argument("--window-size=1920,1080")
- chrome_options.add_argument("--incognito")
- driver = webdriver.Remote(
- command_executor=selenium_url,
- options=chrome_options
- )
- try :
- logger.info(dolibarr_url)
- logger.info("CRM Login")
- # Launch Microsoft Edge (Chromium)
- driver.get(str(dolibarr_url) + "/index.php")
- logger.debug(driver.title)
- #logger.debug(driver.page_source)
- #assert DOLIBARR_LOGIN_TEXT in driver.title
- driver.find_element(By.NAME,"username").send_keys(str(dolibarr_username))
- pass_field = driver.find_element(By.NAME,"password")
- pass_field.send_keys(str(dolibarr_password))
- pass_field.send_keys(Keys.RETURN)
- logger.info(driver.title)
- assert DOLIBARR_HOME_TEXT in driver.title
- logger.info("login successful")
- current_date = datetime.datetime.now()
- if args.planned :
- logger.info("preparation phase : trigger planned work in CRM")
- work_url = str(dolibarr_url) + "/public/cron/cron_run_jobs_by_url.php?securitykey=" + \
- dolibarr_planned_work_key + "&userlogin=" + dolibarr_username + "&id=" + str(dolibarr_planned_work_cron_job_id)
- driver.get(work_url)
- logger.info("temporization 1 sec....")
- sleep(1)
- # iterating over links63
- logger.info("iterating over invoice links")
- link : invoice_link
- for link in invoice_links :
- link.crm_contract_activated == False
- logger.info("treating contract : " + link.contract_number + " for " + link.stripe_customer_name)
- driver.get(dolibarr_url + "/contrat/card.php?id=" + str(link.contract_number))
- logger.debug("testing if services are activated")
- #iterating service
- contract_lines = driver.find_elements(By.XPATH,"//div[contains(@id,'contrat-lines-container')]/div")
- logger.debug(str(len(contract_lines)) + " service line found")
- for line in contract_lines :
-
- try :
- service_name = line.find_element(By.CLASS_NAME,"classfortooltip").text
- except Exception :
- element = line.find_element(By.CLASS_NAME,"fa-concierge-bell")
- service_name = element.find_element(By.XPATH,'..').text
-
-
- service_status = line.find_element(By.CLASS_NAME,"badge-status").text
- logger.debug(service_name)
- logger.debug(service_status)
- if DOLIBARR_CONTRACT_RUNNING_TEXT in service_status :
- logger.debug("crm contract have at least one service activated")
- link.crm_contract_activated = True
- if link.crm_contract_activated == False:
- logger.debug("the contract with number " + link.contract_number + " have all services disabled, SKIPPING CONTRACT")
- continue
- contract_links_table = driver.find_element(By.XPATH,'//table[@data-block="showLinkedObject"]')
- contract_links_elements = driver.find_elements(By.XPATH,'//tr[@data-element="facture"]')
-
- link.crm_needs_new_invoice = True
- if len(contract_links_elements) == 0 or contract_links_elements == None:
- link.crm_needs_new_invoice = False
-
- if not link.crm_contract_activated :
- link.crm_needs_new_invoice = False
- for element in contract_links_elements :
- current_invoice = crm_linked_invoice()
- current_invoice.url = element.find_element(By.CLASS_NAME, "linkedcol-name").find_element(By.TAG_NAME,"a").get_attribute("href")
- current_invoice.amount = float(element.find_element(By.CLASS_NAME, "linkedcol-amount").text.replace(',', "."))
- current_invoice.date = datetime.datetime.strptime(element.find_element(By.CLASS_NAME, "linkedcol-date").text, DOLIBARR_DATE_FORMAT)
-
- if element.find_element(By.CLASS_NAME, "linkedcol-statut").find_element(By.TAG_NAME,"span").get_attribute("title") == DOLIBARR_NOT_PAID_TEXT:
- logger.debug("invoice is unpaid")
- current_invoice.unpaid = True
-
- logger.debug("-----")
- if link.is_stripe_link :
- logger.debug("CRM linked invoice for customer : " + link.stripe_customer_name + " - " + link.stripe_customer_mail)
- logger.debug("CRM linked invoice url : " + current_invoice.url)
- logger.debug("CRM linked invoice date : " + str(current_invoice.date))
- logger.debug("CRM linked invoice amount : " + str(current_invoice.amount))
- logger.debug("CRM linked invoice unpaid ? : " + str(current_invoice.unpaid))
- logger.debug("-----")
-
-
- #checking if new invoice is needed for the month
- if current_invoice.date.year == current_date.year :
- if current_invoice.date.month == current_date.month :
- logger.debug("invoice link does not need a new invoice for this month")
- link.crm_needs_new_invoice = False #TODO check all month since last invoice
- if link.is_stripe_link :
- stripe_date = datetime.datetime.fromtimestamp(link.stripe_epoch_date)
- #checking if invoice is eligible to update
- if current_invoice.date.year == stripe_date.year :
- if current_invoice.date.month == stripe_date.month :
- if current_invoice.date.day <= stripe_date.day :
- if current_invoice.unpaid :
-
- if link.stripe_paid_amount == current_invoice.amount :
- logger.info("Current crm invoice is unpaid, and corresponding to same month and amount as stripe payment")
- logger.info(" ## Target invoice FOUND ! ##")
- link.crm_needs_update = True
- link.crm_target_invoice = current_invoice
- else :
- #for free contract
- if current_invoice.date.year == current_date.year :
- if current_invoice.date.month == current_date.month :
- if current_date.day >= current_invoice.date.day :
- if current_invoice.unpaid :
-
- if current_invoice.amount == 0:
- logger.info("current crm invoice is unpaid for a 0 amount (Free Contract)")
- logger.info(" ## Target invoice FOUND ! ##")
- link.crm_needs_update = True
- link.crm_target_invoice = current_invoice
-
- except Exception as e :
- logger.critical("ERROR : " + repr(e))
- driver.quit()
- exit(0)
- logger.info(" --- -------------------------------------------------------------------------------- ---")
- logger.info(" --- -------------------------------- SUMMARY PHASE --------------------------------- ---")
- logger.info(" --- -------------------------------------------------------------------------------- ---")
- logger.info("summary of actions")
- if len(invoice_links) == 0 :
- logger.info("## no action pending detected ##")
- action = False
- try :
- link : invoice_link
- for link in invoice_links :
- if link.crm_contract_activated :
- if link.crm_needs_update :
- action = True
- logger.info(" ## ------------------------------- ##")
- logger.info("@@ INVOICE UPDATE PENDING : ")
- logger.info("CRM linked invoice url : " + link.crm_target_invoice.url)
- logger.info("CRM linked invoice date : " + str(link.crm_target_invoice.amount))
- logger.info("CRM linked invoice amount : " + str(link.crm_target_invoice.date))
- logger.info("CRM linked invoice unpaid ? : " + str(link.crm_target_invoice.unpaid))
- if link.is_stripe_link :
- logger.info("stripe invoice customer info : " + link.stripe_customer_name + " - " + link.stripe_customer_mail)
- logger.info("stripe invoice paid amount : " + str(link.stripe_paid_amount) + " " + latest_invoice_json_data["currency"])
- logger.info("stripe invoice payment date : " + str(datetime.datetime.fromtimestamp(int(link.stripe_epoch_date))))
- else :
- logger.info("Invoice is from FREE CONTRACT")
- logger.info(" ## ------------------------------- ##")
-
- if link.crm_needs_new_invoice :
- Action = True
- logger.info(" ## ------------------------------- ##")
- logger.info("New Invoice Generation Pending")
- logger.info("invoice customer info : " + link.stripe_customer_name + " - " + link.stripe_customer_mail)
- date_string = format(current_date,'01/%m/%Y')
- logger.info("planned date : " + date_string)
- logger.info(" ## ------------------------------- ##")
- except Exception as e :
- logger.critical("ERROR : " + repr(e))
- driver.quit()
- exit(0)
-
- if not action :
- logger.info("## no action pending detected ##")
- if args.dry:
- driver.quit()
- logger.info("dry run enabled, exiting here...")
- exit(0)
- logger.info(" --- -------------------------------------------------------------------------------- ---")
- logger.info(" --- -------------------------------- ACTION PHASE ---------------------------------- ---")
- logger.info(" --- -------------------------------------------------------------------------------- ---")
- try :
- for link in invoice_links :
- if link.crm_contract_activated :
- if link.crm_needs_update :
- logger.info(" ## ------ Creating Payment ------ ##")
- logger.info("CRM linked invoice url : " + link.crm_target_invoice.url)
- logger.info("CRM linked invoice date : " + str(link.crm_target_invoice.amount))
- logger.info("CRM linked invoice amount : " + str(link.crm_target_invoice.date))
- logger.info("CRM linked invoice unpaid ? : " + str(link.crm_target_invoice.unpaid))
- if link.is_stripe_link :
- logger.info("stripe invoice customer info : " + link.stripe_customer_name + " - " + link.stripe_customer_mail)
- logger.info("stripe invoice paid amount : " + str(link.stripe_paid_amount) + " " + latest_invoice_json_data["currency"])
- logger.info("stripe invoice payment date : " + str(datetime.datetime.fromtimestamp(int(link.stripe_epoch_date))))
- else :
- logger.info("Invoice is from FREE CONTRACT")
-
-
- if link.is_stripe_link :
- driver.get(link.crm_target_invoice.url)
- text_arg = "//*[text()='" + DOLIBARR_TEXT_ENTER_PAYMENT + "']"
- pay_url = driver.find_element(By.XPATH, text_arg).get_attribute("href")
- driver.get(pay_url)
- stripe_payment_date = datetime.datetime.fromtimestamp(int(link.stripe_epoch_date))
- date_string = format(stripe_payment_date,DOLIBARR_DATE_FORMAT)
- driver.find_element(By.ID, "re").send_keys(date_string)
- driver.find_element(By.NAME, "comment").send_keys("Payment treated by automation - Slash-DoliStripe \n stripe invoice URL : " + link.stripe_invoice_url )
- #en cas de plusieur facture impayée dont celles qui n'ont rien a voir avec le contrat
- target_invoice_line = driver.find_element(By.CLASS_NAME,'highlight')
- target_invoice_line.find_element(By.CLASS_NAME,'AutoFillAmout').click()
- driver.find_element(By.NAME, "num_paiement").send_keys(link.stripe_invoice_number)
- driver.find_element(By.XPATH,'//input[@value="'+ DOLIBARR_TEXT_PAY + '"]').click()
- driver.find_element(By.XPATH,'//input[@value="'+ DOLIBARR_TEXT_VALIDATE_PAYMENT + '"]').click()
- else :
- driver.get(link.crm_target_invoice.url + "&action=paid")
- buttons = driver.find_element(By.CLASS_NAME,"ui-dialog-buttonset")
- logger.debug(buttons.get_attribute("innerHTML"))
- buttons.find_element(By.XPATH,'button[contains(text(), "' + DOLIBARR_TEXT_YES_PAYMENT + '")]').click()
- if args.mail:
- logger.info("sending email to client...")
- driver.find_element(By.XPATH, "//*[text()='"+ DOLIBARR_TEXT_SEND_EMAIL +"']").click()
- if link.is_stripe_link :
- #here we add the stripe mail, if there is not stripe mail the crm mail will prevail.
- driver.find_element(By.ID, "sendto").send_keys(link.stripe_customer_mail)
- if contact_mail is not None :
- driver.find_element(By.ID, "sendtocc").send_keys(contact_mail)
- driver.find_element(By.ID, "sendmail").click()
- except Exception as e :
- logger.critical("ERROR : " + repr(e))
- driver.quit()
- exit(0)
- logger.info(" --- -------------------------------------------------------------------------------- ---")
- logger.info(" --- -------------------------------- CLEANING PHASE -------------------------------- ---")
- logger.info(" --- -------------------------------------------------------------------------------- ---")
- driver.quit()
- exit(0)
|