#! /usr/bin/env python2.7
# coding=utf-8

import lxml.html
import datetime
import getopt
import urllib2
import urllib
from cookielib import CookieJar
import urlparse
import getpass
import time
import re
import lxml.etree as etree

# username = "hildenae@gmail.com"
username = "jonp@nuug.no"
dbname   = "NORWEGIAN_UNIX_USER_GROUP"

# make sure stdout is UTF-8, also when redirecting to pipe or file
import sys
import codecs
sys.stdout = codecs.getwriter('utf8')(sys.stdout)

prodcodemap = {
    # Varenummermap mellom Sendregning (200701) og LODO (3101)
    # 1. Bruk Lodo sine numeriske varenummer/kontonavnummer
    # 2. Koden sjekker ikke om varenummer er i LODO, 
    # fordi den går ut ifra at dette er tatt 
    # forbehold for når prodcodemap er opprettet
  '200701' : '3101',
  '200702' : '3102',
  '200703' : '3100',
  '200704' : '3100', # Student
  '200705' : '3110',

  '200801' : '3101',
  '200802' : '3102',
  '200803' : '3100',
  '200804' : '3100',
  '200805' : '3110',
  '200806' : '3110', # SAGE student
  '200807' : '3100', # Personlig uten USENIX
  '200808' : '3100', # Student uten USENIX

  '200901' : '3101',
  '200902' : '3102',
  '200903' : '3100',
  '200904' : '3100',
  '200905' : '3110',
  '200906' : '3110', # SAGE student
  '200907' : '3100', # Personlig uten USENIX
  '200908' : '3100', # Student uten USENIX

  '201001' : '3101',
  '201002' : '3102',
  '201003' : '3100',
  '201004' : '3100',
  '201005' : '3110',
  '201006' : '3110', # SAGE student
  '201007' : '3100', # Personlig uten USENIX
  '201008' : '3100', # Student uten USENIX

  '201101' : '3101',
  '201102' : '3102',
  '201103' : '3100',
  '201104' : '3100',
  '201105' : '3110',
  '201106' : '3110', # SAGE student
  '201107' : '3100', # Personlig uten USENIX
  '201108' : '3100', # Student uten USENIX

  '201201' : '3101',
  '201202' : '3102',
  '201203' : '3100',
  '201204' : '3100',
  '201205' : '3110',
  '201206' : '3110', # SAGE student
  '201207' : '3100', # Personlig uten USENIX
  '201208' : '3100', # Student uten USENIX

  '201301' : '3101',
  '201302' : '3102',
  '201303' : '3100',
  '201304' : '3100',
  '201305' : '3110',
  '201306' : '3110', # SAGE student
  '201307' : '3100', # Personlig uten USENIX
  '201308' : '3100', # Student uten USENIX

  '201401' : '3101',
  '201402' : '3102',
  '201403' : '3100',
  '201404' : '3100',
  '201405' : '3110',
  '201406' : '3110', # SAGE student
  '201407' : '3100', # Personlig uten USENIX
  '201408' : '3100', # Student uten USENIX

  '201501' : '3101',
  '201502' : '3102',
  '201503' : '3100',
  '201504' : '3100',
  '201505' : '3110',
  '201506' : '3110', # SAGE student
  '201507' : '3100', # Personlig uten USENIX
  '201508' : '3100', # Student uten USENIX

  '201601' : '3101',
  '201602' : '3102',
  '201603' : '3100',
  '201604' : '3100',
  '201605' : '3110',
  '201606' : '3110', # SAGE student
  '201607' : '3100', # Personlig uten USENIX
  '201608' : '3100', # Student uten USENIX

  '201701' : '3101',
  '201702' : '3102',
  '201703' : '3100',
  '201704' : '3100',
  '201705' : '3110',
  '201706' : '3110', # SAGE student
  '201707' : '3100', # Personlig uten USENIX
  '201708' : '3100', # Student uten USENIX

  '201801' : '3101',
  '201802' : '3102',
  '201803' : '3100',
  '201804' : '3100',
  '201805' : '3110',
  '201806' : '3110', # SAGE student
  '201807' : '3100', # Personlig uten USENIX
  '201808' : '3100', # Student uten USENIX

  '201901' : '3101',
  '201902' : '3102',
  '201903' : '3100',
  '201904' : '3100',
  '201905' : '3110',
  '201906' : '3110', # SAGE student
  '201907' : '3100', # Personlig uten USENIX
  '201908' : '3100', # Student uten USENIX

  '202001' : '3101',
  '202002' : '3102',
  '202003' : '3100',
  '202004' : '3100',
  '202005' : '3110',
  '202006' : '3110', # SAGE student
  '202007' : '3100', # Personlig uten USENIX
  '202008' : '3100', # Student uten USENIX

  '202101' : '3101',
  '202102' : '3102',
  '202103' : '3100',
  '202104' : '3100',
  '202105' : '3110',
  '202106' : '3110', # SAGE student
  '202107' : '3100', # Personlig uten USENIX
  '202108' : '3100', # Student uten USENIX

  '202201' : '3101', # firmamedlem inkl. inntil 3 pers.
  '202202' : '3102', # ekstra person under firmamedlem
  '202203' : '3100', # personlig medlem
  '202204' : '3100', # studentmedlem

  '202301' : '3101', # firmamedlem inkl. inntil 3 pers.
  '202302' : '3102', # ekstra person under firmamedlem
  '202303' : '3100', # personlig medlem
  '202304' : '3100', # studentmedlem

  '202401' : '3101', # firmamedlem inkl. inntil 3 pers.
  '202402' : '3102', # ekstra person under firmamedlem
  '202403' : '3100', # personlig medlem
  '202404' : '3100', # studentmedlem

  '202501' : '3101', # firmamedlem inkl. inntil 3 pers.
  '202502' : '3102', # ekstra person under firmamedlem
  '202503' : '3100', # personlig medlem
  '202504' : '3100', # studentmedlem

  '202601' : '3101', # firmamedlem inkl. inntil 3 pers.
  '202602' : '3102', # ekstra person under firmamedlem
  '202603' : '3100', # personlig medlem
  '202604' : '3100', # studentmedlem

  # Special product codes
  '301000' : '3910', # Bidrag til FiksGataMi
  '301001' : '1033', # Bidrag til OpenStreetMap
  '301002' : '3011', # Inntekt videfilming
  '301003' : '3900'  # Diverse
  
  }

filename = "ALL.xml"

def getval(element, xpath):
  sub = element.xpath(xpath)
  if 0 < len(sub):
    return sub[0].text
  return None

def datestr2ISOstring(datestr):
    day, month, year = datestr.split('.')
    return datetime.date(2000+int(year), int(month), int(day)).strftime("%Y-%m-%d")

def convert(firstInvoice, lastInvoice):
  tree = lxml.etree.parse(filename)
  invoices = tree.xpath('/invoices/invoice')
  for invoice in invoices:
    faktura = getval(invoice, './optional/invoiceNo')

    if int(faktura) < firstInvoice or int(faktura) > lastInvoice:
        continue

    fakturadato =  datestr2ISOstring(getval(invoice, './optional/invoiceDate'))
    kundenummer = getval(invoice, './optional/recipientNo')
    total = getval(invoice, './optional/total')
    kid = getval(invoice, './optional/kid')
    fakturatype = getval(invoice, './optional/invoiceType')
    navn = getval(invoice, './name')
    kreditnota = False
    ProjectID = None

    if fakturatype != 'ordinary':
        if fakturatype == 'credit':
            kreditnota = True
            forfallsdato = fakturadato 
        else:
            ops("Dette er verken en faktura eller en kreditnota") 
    else:
        forfallsdato = datestr2ISOstring(getval(invoice, './optional/dueDate')) 

    lines = {}
    for line in invoice.xpath('./lines/line'):
      varenummer = getval(line, './prodCode');
      if not ProjectID and varenummer and (int(varenummer[:4]) > 2000 and int(varenummer[:4]) < 2070):
          ProjectID = varenummer[:4]
      verdi = getval(line, './lineTotal')
      if verdi is not None and verdi != "0.00":
        if (varenummer in lines):
            lines[varenummer]= "%.2f" % (float(lines[varenummer])+float(verdi))
        else:
            lines[varenummer] = verdi
    print "faktura %s fakturadato %s kundenr %s projectid %s" % (faktura, fakturadato, kundenummer,  ProjectID)
    print "total %s forfallsdato %s kid %s" % (total, forfallsdato, kid)
    print "Lines: %s" % lines
    if (faktura is None or fakturadato is None or kundenummer is None
        or total is None or forfallsdato is None or kid is None):
        # or total is None or forfallsdato is None or kid is None or ProjectID is None):
        # Removed test for ProjectID jonp 2017-12-31
        # ProjectID is normally None for product codes without year
        ops("Noe er None (se ovenfor)")
    opprettBilag(faktura, fakturadato, kundenummer, total, forfallsdato, kid, navn, kreditnota)
    leggInnVareLinjer(ProjectID,  faktura, fakturadato, forfallsdato, kid, kundenummer, kreditnota, lines)

def oops(root, msg):
    print(etree.tostring(root, pretty_print=True))
    print "======= UKJENT SITUASJON ======="
    print "===== SE HTMLKODE OVERNFOR ====="
    print msg
    print"================================="
    sys.exit(4)

def ops(msg):
    print "======= UKJENT SITUASJON =======" 
    print msg
    print"================================="
    sys.exit(4)

def getVoucherIDs (root):
    inputelements = root.cssselect(
            "form.voucher input[name='voucher.VoucherID']"
            )
    ids = []
    for inputelement in inputelements:
        value = inputelement.get('value')
        if value:
            ids.append(value)
    ids.sort()
    return ids

def bilagsnr(root):
    jids = root.cssselect("form.voucher input[name='voucher.JournalID']")
    if len(jids) == 0:
        oops(root, "Fant ingen journal ID")
    j = jids[0].get('value');
    for jid in jids:
        if jid.get('value') != j:
            oops(root, "To forskjellige journalID på samme side")
    return j


def okDato(dato):
    RE = re.compile(r'^\d{4}-\d{2}-\d{2}$')
    return bool(RE.search(dato))

def nyPostering(bilagsnummer, bilagsdato, periode, 
        forfallsdato, kid, kundenummer):
    formdata = {
    "type"                      : "salecash_in",
    "voucher.JournalID"         : bilagsnummer,
    "voucher.VoucherID"         : "",
    "voucher.VoucherPeriod"     : periode,
    "voucher.VoucherDate"       : bilagsdato,
    "voucher.AccountPlanID"     : kundenummer,
    "voucher.DueDate"           : forfallsdato,
    "voucher.KID"               : kid,
    "voucher.DescriptionID"     : "",
    "voucher.Description"       : "",
    "VoucherType"               : "S",
    "AccountLineID"             : "",
    "view_mvalines"             : "0",
    "view_linedetails"          : "0",
    "action_voucherline_new"    : "Ny postering til bilag %s (L)" % (bilagsnummer)
    }
    post(lphp+"t=journal.edit&", formdata)


def leggInnVareLinjer(ProjectID,  bilagsnummer, bilagsdato, forfallsdato, 
        kid, kundenummer, kreditnota, linjer):
    url = lphp+"view_mvalines=&view_linedetails"
    url = url + "=&t=journal.edit&voucher_VoucherType=S&voucher_JournalID="
    url = url +bilagsnummer + "&action_journalid_search=1"
    vids = getVoucherIDs(get(url))
    #print "orginale vids %s" % vids
    used_vids = [vids[0]]
    deler = bilagsdato.split("-");
    periode = "-".join(deler[:2])
    for linje in linjer.keys():
        # print "Linje %s" % linje  # jonp 2024-02-18
        # Skip linje if verdi is zero  # jonp 2022-01-16
        verdi = linjer[linje]
        if (verdi == "0.00"):    # test by jonp 2022-01-16  byttet varelinje til linje jonp 2024-02-18
            print "Legger IKKE inn linje i bilagsnummer %s, varenummer %s, verdi %s (verdi null)" % (bilagsnummer, linje, verdi)
        else:
            current_vids = getVoucherIDs(get(url))
            unused_vids = [vid for vid in current_vids if vid not in used_vids]

            if (len(unused_vids) < 1):
                print "Ingen ubruke VIDS - lager ny postering til bilag %s" % bilagsnummer
                nyPostering(bilagsnummer, bilagsdato, periode, 
                    forfallsdato, kid, kundenummer)
                current_vids = getVoucherIDs(get(url))
                unused_vids = [vid for vid in current_vids if vid not in used_vids]
                if (len(unused_vids) < 1):
                    ops("Klarte ikke å lage ny VID (postering) ved" +
                    "innleggelse av faktura %s" % bilagsnummer)
            #print "currend_vids %s" % current_vids
            #print "used_vids %s" % used_vids
            #print "unused_vids %s" % unused_vids     
            selected_vid = unused_vids[0]
            #print "selected_vid %s" % selected_vid
            used_vids.append(selected_vid) 
            #print linje + ":" + linjer[linje]
            varenummer = linje
            verdi = linjer[linje]
            print "Legger inn linje i bilagsnummer %s, varenummer %s, verdi %s" % (bilagsnummer, varenummer, verdi)
            # sanity check:
            if not varenummer in prodcodemap:
                ops("Kjenner ikke til varnummer %s" % (varenummer))
            # Vi sjekker ikke om varenummer er i LODO, fordi vi går ut 
            # ifra at dette er tatt forbehold for når prodcodemap er opprettet
            if float(verdi) > 100000:
                ops("Linjeverdi over 100 000,00? Kanskje " 
                    + "du skal ta denne manuelt (%s)" % (verdi))
            verdi = verdi.replace(".",",") # lodo vil ha komma

            formdata = {
                "type"                  : "salecash_in",
                "voucher.VoucherID"     : selected_vid,
                "voucher.JournalID"     : bilagsnummer,
                "AccountLineID"         : "",
                "VoucherType"           : "S",
                "view_mvalines"         : "0",
                "view_linedetails"      : "0",
                "voucher.JournalID"     : bilagsnummer,
                "voucher.VoucherDate"   : bilagsdato,
                "voucher.VoucherDateOld": bilagsdato,
                "voucher.VoucherPeriod" : periode,
                "voucher.AccountPlanID" : prodcodemap[varenummer],
                "voucher.AmountIn"      : "0,00",
                "voucher.AmountOut"     : verdi,
                "voucher.ProjectID"     : ProjectID, # NO LONGER HARD CODED !!!
                "voucher.DueDate"       : forfallsdato,
                "voucher.InvoiceID"     : bilagsnummer,
                "voucher.KID"           : kid,
                "voucher.Description"   : "",
                "action_voucher_update"    : "Lagre"
            }
            if kreditnota:
                formdata["voucher.AmountIn"] = verdi
                formdata["voucher.AmountOut"] = "0,00"

            #print formdata
            post(url, formdata)
            # OK; seriøst WTF. LODO ignorerer voucher.ProjectID dersom 
            # voucher.AccountPlanID har byttet (fra for eksempel ingenting) i den 
            # samme oppdateringa så vi må poste det samme to ganger       
            post(url, formdata)

    current_vids = getVoucherIDs(get(url))
    unused_vids = [vid for vid in current_vids if vid not in used_vids]
    if len(unused_vids) != 0:
        ops(u"Det er en rad for mye på bilag %s" % bilagsnummer)

def opprettBilag(fakturanummer, bilagsdato, kundenummer, total, forfallsdato, kid, navn, kreditnota):
    if (int(fakturanummer) < 2000 or int(fakturanummer) > 6000        
        # unormale fakturanummer 
        or int(kundenummer) < 100000 or int(kundenummer) > 200000
        or not okDato(bilagsdato) or not okDato(forfallsdato)     
        # feil i datoformat
        or len(kid) != 10   # feil lengede KID
        ):
        ops(u"Prøvde å opprette bilag : sanity checks failed")
    if float(total) > 100000:
        ops("Totalverdi over 100 000,00? Kanskje du skal ta denne manuelt(" + total + ")")
    nesteBilag = nesteBilagNr()
    if nesteBilag != fakturanummer:
        ops(u"Prøvde å opprette bilag %s med fakturanummer %s" % (nesteBilag, fakturanummer))
    if not finnesKunde(kundenummer):
        newCustomer(navn, kundenummer)
        if not finnesKunde(kundenummer):
            ops("Kunde ble ikke opprettet!!")

    total = total.replace(".",",") # lodo vil ha komma
    url = lphp + "t=journal.edit"
    deler = bilagsdato.split("-");
    periode = "-".join(deler[:2])
    print "Bilag %s bdato: %s knr %s tot %s ffd %s kid %s" % (fakturanummer, bilagsdato, kundenummer, total, forfallsdato, kid)
    formdata = {
        "type"                  : "salecredit_in",
        "voucher.VoucherID"     : "",
        "voucher.JournalID"     : fakturanummer,    # (faktuarnummer>",
        "AccountLineID"         : "",
        "VoucherType"           : "S",              # (faktura>",
        "view_mvalines"         : "0",
        "view_linedetails"      : "0",
        "voucher.JournalID"     : fakturanummer,    # (fakturanummer>",
        "JournalIDOrg"          : "",
        "voucher.VoucherDate"   : bilagsdato,       # "2012-10-15", # (bilagsdato / invoiceDate>",
        "voucher.VoucherPeriod" : periode,          # (to første ledd i invoiceDate>",
        "voucher.AccountPlanID" : kundenummer,      # (kundenummer>",
        "voucher.AmountIn"      : total,            # (total >",
        "voucher.AmountOut"     : "0,00",
        "voucher.DueDate"       : forfallsdato,     # (ffrfallsdato / dueDate>",
        "voucher.InvoiceID"     : fakturanummer,    # (fakturanummer>",
        "voucher.KID"           : kid,              # (kid>",
        "voucher.Description"   : "",
        "action_voucher_new"    : "Lagre"
    }
    if kreditnota:
        formdata["type"] = "salenotacredit_in"
        formdata["voucher.AmountIn"] = "0,00"
        formdata["voucher.AmountOut"] = total

    return post(url, formdata);

def newCustomer(navn, kundenummer):
    print "NY KUNDE: %s: %s" % (kundenummer, navn)
    url = lphp + "view_mvalines=0&view_linedetails=0&t=accountplan.reskontro"
    formdata = {
        "accountplan.AccountPlanType"   : "customer",
        "accountplan.AccountPlanID"     : kundenummer,
        "JournalID"                     : "",
        "NewAccount"                    : "1",
        "action_accountplan_new"        : "Opprett konto"
    }
   # print formdata
    post(url, formdata)

    formdata = {
    "accountplan_AccountPlanID": kundenummer,
    "JournalID":"",
    "accountplan.Active":"0",
    "accountplan.Active":"1",
    "accountplan.AccountPlanType":"customer",
    "accountplan.AccountName":navn,
    "accountplan.OrgNumber":kundenummer,
    "accountplan.EnableInvoiceAddress":"0",
    "accountplan.Address":"",
    "accountplan.VatNumber":"",
    "accountplan.ZipCode":"",
    "accountplan.City":"",
    "accountplan.EnableInvoicePoBox":"0",
    "accountplan.IPoBox":"",
    "accountplan.IPoBoxCity":"",
    "accountplan.IPoBoxZipCode":"",
    "accountplan.IPoBoxZipCodeCity":"",
    "accountplan.CountryCode":"",
    "accountplangln.GLN":"",
    "accountplan.Phone":"",
    "accountplan.Mobile":"",
    "accountplan.Email":"",
    "accountplan.Web":"",
    "accountplan.Description":"",
    "accountplan.InvoiceCommentCustomerPosition":"bottom",
    "accountplan.CustomerNumber":"",
    "accountplan.DomesticBankAccount":"",
    "accountplan.IBAN":"",
    "accountplan.EnableCurrency":"0",
    "accountplan.Currency":"",
    "accountplanswift.SWIFT":"",
    "accountplanswift.SWIFTACCOUNT":"",
    "accountplan.debittext":"Salg",
    "accountplan.DebitColor":"debitblue",
    "accountplan.credittext":"Betaling",
    "accountplan.CreditColor":"creditred",
    "accountplan.EnableQuantity":"0",
    "accountplan.EnableDepartment":"0",
    "accountplan.DepartmentID":"0",
    "accountplan.EnableProject":"0",
    "accountplan.ProjectID":"0",
    "accountplan.EnableCredit":"1",
    "accountplan.CreditDays":"14",
    "accountplan.EnableAutogiro":"0",
    "accountplan.EnableNettbank":"0",
    "accountplan.EnableMotkontoResultat":"0",
    "accountplan.MotkontoResultat1":"0",
    "accountplan.MotkontoResultat2":"0",
    "accountplan.MotkontoResultat3":"0",
    "accountplan.EnableMotkontoBalanse":"0",
    "accountplan.MotkontoBalanse1":"0",
    "accountplan.MotkontoBalanse2":"0",
    "accountplan.MotkontoBalanse3":"0",
    "accountplan.AccountLineFreeTextMatch":"",
    "action_accountplan_update"             : "Lagre (S)"
    }
    #print formdata
    #raw = formdata['accountplan.AccountName'].decode("utf-8")
    formdata['accountplan.AccountName'] = formdata['accountplan.AccountName'].encode("iso-8859-1")
    root = post(url, formdata)
    kundeliste = root.cssselect("input[name='accountplan.AccountName']")
    if kundeliste[0].get("value") != navn:
        ops(u"Navn for kunde %s er ikke %s" % (kundenummer, navn))

def nesteBilagNr():
    url = lphp+"view_mvalines=&view_linedetails=&t=journal.edit&new=1&type=salecredit_in&voucher_AccountPlanID="
    return bilagsnr(get(url))

def finnesKunde(kundenummer):
    url = lphp+"view_mvalines=&view_linedetails=&t=journal.edit&new=1&type=salecredit_in&voucher_AccountPlanID="
    root = get(url)
    kundeliste = root.cssselect("select[name='voucher.AccountPlanID'] option[value='"+kundenummer+"']")
    eitherMatch = False
    for kunde in kundeliste:
        eitherMatch = eitherMatch or kunde.text.startswith(kundenummer)
    return bool(kundeliste) and eitherMatch
    
 
def get(url):
    response = lodo.open(url)
    return lxml.html.fromstring(response.read())

def post(url, formdata):
    data_encoded = urllib.urlencode(formdata)
    response = lodo.open(url, data_encoded)
    return lxml.html.fromstring(response.read())

def lodo_login(username, dbname):
    #password = getpass.getpass("Passord for %s på lodo.no: " % (username))
    password = "xxxx"  # Sett inn passordet etter utsjekk
    
    cj = CookieJar()
    opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
    formdata = {
        "LoginFormDate" : datetime.datetime.now().isoformat()[:10],
        "username" : username,
        "password": password,
        "DB_NAME_LOGIN" : dbname,
        }
    data_encoded = urllib.urlencode(formdata)
    url = "https://login.lodo.no/lodo.php?t=lib.login&interf="
    response = opener.open(url, data_encoded)
    content = response.read()
    return opener
    
global lodo
lodo = 0
global lphp
lphp = "https://login.lodo.no/lodo.php?"

def main():
    u"""    Dette programmet tar ett eller to kommandolineargumenter, 
    fra-fakturanummer og til-fakturanummer eller bare et enkelt fakturanummer.
    Det vil deretter be om LODO-passordet til <username>.
    
    Det itererer fra første til siste argument, forsøker å finne disse 
    i filen ALL.xml og forsøker å legge disse inn i LODO.

    Dette programmet vil ikke kunne legge inn fakturaer som har et fakturanummer som er 
    mindre enn det siste bilaget på LODO. Det hånterer bare varelinjer med varenummer
    som finnes i <prodcodemap>. Det legger inn en bilagslinje med totalen for hvert
    varenummer. Det skal håntere både fakturaer og kreditnotaer.
    
    """
    try:
        opts, args = getopt.getopt(sys.argv[1:], "h", ["help"])
    except getopt.error, msg:
        print msg
        print "for help use --help"
        sys.exit(2)

    for o, a in opts:
        if o in ("-h", "--help"):
            print main.__doc__
            sys.exit(0)

    if len(args) < 1:
        print "Usage: ", sys.argv[0], " [<invoiceNo>|<start-invoiceNo> <end-invoiceNo>]"
        sys.exit(3)
    if len(args) < 2:
        arg2 = args[0]
    else:
        arg2 = args[1]

    global lodo
    lodo = lodo_login(username, dbname)
    convert(int(args[0]), int(arg2))

if __name__ == "__main__":
    main()
