Browse Source

Merge branch 'features/alchemy' into develop

Philippe Le Brouster 8 years ago
parent
commit
f998385a60

+ 2 - 0
.gitignore

@@ -1,3 +1,5 @@
 *.pyc
 .*.swp
 python-env
+tags
+

+ 18 - 0
README.mdwn

@@ -0,0 +1,18 @@
+# himports
+
+## Usage from repository
+
+**Installation**
+
+    git clone
+    cd
+    ./utils/pyenv-init
+
+**Utilisation**
+    ./utils/pyenv-himport
+
+## Configuration
+
+You can copy the configuration file from ``himport.conf.template``
+
+    cp himport.conf.template himport.conf

+ 50 - 43
bin/himport

@@ -5,93 +5,100 @@ from __future__ import unicode_literals
 import getopt
 import locale
 import logging
-import os
-import shutil
 import sys
-import csv
-import datetime
 import codecs
 import getpass
 
 from himports import settings
-from himports.hledger import *
+from himports.dolibarrWriter import Writer
+from himports.dolibarrAlchemyHledger import HledgerDolibarrSQLAlchemy
 
 logging.basicConfig(level=logging.INFO)
 logger = logging.getLogger('hreport')
 
 sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout)
 
+
 def process_args(argv):
     options = {}
-    usage = u'himport -p' 
+    usage = u'''Usage: himport -v -y <YEAR> [ -y <YEAR> ] ...
+        options:
+            -v         : verbose mode
+            -y <YEAR>  : import the corresponding accounting year
+    '''
     try:
         opts, args = getopt.getopt(
-                argv, "hp:P:",
-            ["mysql-password=","mysql-port" ])
+            argv, "hvy:",
+            ["mysql-password=", "mysql-port", "year="]
+        )
     except getopt.GetoptError:
-        print usage 
+        print "himport: Invalid options"
+        print usage
         sys.exit(2)
 
+    options['years'] = list()
+    options['verbose'] = 0
+
     for opt, arg in opts:
         if opt == '-h':
             print usage
             sys.exit()
-        elif opt in ("-p", "--mysql-password"):
-            options['mysql_password'] = arg
-        elif opt in ("-P", "--mysql-port"):
-            options['mysql_port'] = arg
+        elif opt in ("-v", "--verbose"):
+            options['verbose'] += 1
+        elif opt in ("-y", "--year"):
+            options['years'].append(str(arg))
+
+    if len(options['years']) == 0:
+        print "You need to specify the accounting years"
+        print ""
+        print usage
+        sys.exit(1)
 
     return options
 
-def do_mysql(options):
 
-    
-    # On récupère les données via la base de données de dolibarr
+def do_sqlalchemy(options):
+    # On recupere les donnees via la base de donnees de dolibarr
     s = settings.get('MYSQL_SETTINGS')
 
     password = s['password']
-    if 'mysql_password' in options:
-        password = options['mysql_password']
     if password is None or password == "":
         password = getpass.getpass("password for mysql user '%s': " % (s['user']))
 
-    port = s['port']
-    if 'mysql_port' in options:
-        port = options['mysql_port']
-
-    dolibarr = DolibarrSQL(s['host'],port, s['database'], s['user'], password)
+    dolibarr = HledgerDolibarrSQLAlchemy(s['host'], s['port'], s['database'], s['user'], password, options['verbose'] >= 2)
     dolibarr.connect()
-    
-    bank_entries = dolibarr.get_bank_entries()
-    sell_entries = dolibarr.get_sell_entries()
-    supplier_entries = dolibarr.get_supplier_entries()
-    social_entries = dolibarr.get_social_entries()
 
-    dolibarr.disconnect()
+    bank_journal = dolibarr.get_bank_journal()
+    sell_journal = dolibarr.get_sell_journal()
+    supplier_journal = dolibarr.get_supplier_journal()
+    social_journal = dolibarr.get_social_journal()
 
-    # On vérifie s'il manque des postes comptables dans les écritures
+    # On verifie s'il manque des postes comptables dans les ecritures
     pc_missing = set()
-    pc_missing.update(bank_entries.check_pc())
-    pc_missing.update(sell_entries.check_pc())
-    pc_missing.update(supplier_entries.check_pc())
-    pc_missing.update(social_entries.check_pc())
+    pc_missing.update(bank_journal.check_pc())
+    pc_missing.update(sell_journal.check_pc())
+    pc_missing.update(supplier_journal.check_pc())
+    pc_missing.update(social_journal.check_pc())
     if len(pc_missing) > 0:
         print "WARNING: poste comptable manquant"
         for pc in pc_missing:
             sys.stdout.write("%s\n" % (pc))
-    
-    # On écrie les fichiers hledger
-    Writer.write("bank",bank_entries)            
-    Writer.write("sells",sell_entries)            
-    Writer.write("suppliers",supplier_entries)            
-    Writer.write("social",social_entries)            
-    Writer.write_hreport_plan()
+
+    # On ecrie les fichiers hledger
+    Writer.write("bank", bank_journal, options['years'])
+    Writer.write("sells", sell_journal, options['years'])
+    Writer.write("suppliers", supplier_journal, options['years'])
+    Writer.write("social", social_journal, options['years'])
+    Writer.write_hreport_chart_of_accounts(options['years'])
+
+    dolibarr.disconnect()
+
 
 def main(argv):
     locale.setlocale(locale.LC_ALL, b'fr_FR.utf-8')
     options = process_args(argv)
-    
-    do_mysql(options)
+
+    do_sqlalchemy(options)
 
 
 if __name__ == "__main__":

File diff suppressed because it is too large
+ 4777 - 0
doc/dolibarr_schema.sql


+ 235 - 125
himport.conf.template

@@ -4,154 +4,264 @@ from __future__ import unicode_literals
 import datetime
 
 #
-# GENERAL
+# OUTPUT_DIR : The base directory where to put the ledger files
 #
-OUTPUT_DIR = "./out"
+OUTPUT_DIR = "./"
 
+#
+# OUTPUT_FILES : The file paths template for each journal.
+#                   - bank : the bank entries
+#                   - sells : the sell entries
+#                   - suppliers : the supplier entries
+#                   - social : the social entries (tax, etc.)
+#                   - pc : chart of accounts
 OUTPUT_FILES = {
-    "bank":      "%year%/ecritures.d/banque.journal",
-    "sells":     "%year%/ecritures.d/vente.journal",
-    "suppliers": "%year%/ecritures.d/achat.journal",
-    "pc":        "plan.journal",
+    "bank":      "exercices/%year%/ecritures.d/banque.journal",
+    "sells":     "exercices/%year%/ecritures.d/vente.journal",
+    "suppliers": "exercices/%year%/ecritures.d/achat.journal",
+    "social":    "exercices/%year%/ecritures.d/cotisations_sociales.journal",
+    "chart_of_accounts" : "exercices/%year%/chart_of_accounrs.journal"
 }
 
+#
+# MYSQL_SETTINGS : Database definition
+#
 MYSQL_SETTINGS = {
-    "host":      "localhost",
-    "database":  "dolibarr_test",
-    "user":      "dolibarr_test",
-    "password":  "dae1ohCu",
-    "port":      3306,
+    "host":      "127.0.0.1",
+    "database":  "dolibarr",
+    "user":      "dolibarr_ro",
+    "password":  "",
+    "port":      13306,
 }
 
 #
-# Describe accounting year if not standard (from 1st january to 31 december)
+# ACCOUNTING_YEARS : Describe the accounting years if not standard (from 1st january to 31 december).
+#                    For exemple the first accounting year could start before the 1st january
 #
 ACCOUNTING_YEARS = {
     ("2012", "2011/01/01", "2012/12/31")
 }
 
-TVA_REFS = {
-    'tva_deductible': '4456',
-    'tva_collecte': '4457',
+#
+# TVA_TYPE : Sort of value-added tax. Possible values :
+#             - 'standard' : the value added tax is on delivery for goods (billing date), on payment for services (payment date).
+#             - 'service_sur_debit' : the value added tax is on delivery for goods (billing date), and on billing date for services.
+TVA_TYPE = "standard"
+
+
+#
+# PC_REFS: define the account references. This is a dictionnary of account.
+#
+PC_REFS = {
+    "tva_a_decaisser":      "44551",
+    "tva_deductible":       "44562",
+    "tva_deductible_1960":  "445621",
+    "tva_deductible_2000":  "445622",
+    "tva_deductible_immo":  "44566",
+    "tva_deductible_immo_1960":  "445661",
+    "tva_deductible_immo_2000":  "445662",
+    "tva_collecte":         "44571",
+    "tva_collecte_1960":    "445711",
+    "tva_collecte_2000":    "445712",
+    "tva_regul":            "4458",
+    "tva_regul_1960":       "44581",
+    "tva_regul_2000":       "44582",
+    "default_supplier":     "4011999",
+    "default_client":       "4111999",
+    "default_tier":         "4999999",
+    "default_bank":         "512999",
+    "default_income":       "7069999",
+    "default_expense":      "6199999",
+    "fn_custom_codes":      [
+        lambda e: u"261" if e.label == u"Souscription part sociale" and e.datev == datetime.date(year=2011, month=9, day=27) else None,
+        lambda e: u"1681" if e.bankclass.categ.id == 7 else None,
+    ],
+}
+
+SOCIAL_REFS = {
+    "TAXCFE":           "447",
+    "TAXPENALITE":      "447",
 }
 
 #
 # Plan comptables (nom, description)
 #
 PC_NAMES = {
-    '1'         : 'capitaux',
-    '11'        : 'capitaux:report à nouveau',
-    '117'       : 'capitaux:report à nouveau:positif',
-    '119'       : 'capitaux:report à nouveau:négatif',
-    '12'        : 'capitaux:résultat',
-    '120'       : 'capitaux:résultat:positif',
-    '129'       : 'capitaux:résultat:négatif',
-    '2'         : 'immobilisations',
-    '201'       : "immobilisations:incorporelles:frais d'établissement",
-    '21'        : "immobilisations:corporelles",
-    '2183'      : "immobilisations:corporelles:matériel informatique",
-    '2184'      : "immobilisations:corporelles:mobilier",
-    '281'       : "immobilisations:amortissements",          
-    '2801'      : "immobilisations:amortissements:incorporelles",
-    '2818'      : "immobilisations:amortissements:corporelles",
-    '28183'     : "immobilisations:amortissements:corporelles:matériel informatique",
-    '28184'     : "immobilisations:amortissements:corporelles:mobilier", 
-    '4'         : "tiers",
-    '40'        : "tiers:fournisseurs",
-    '401101'    : "tiers:fournisseurs:telehouse",
-    '401102'    : "tiers:fournisseurs:Liazo",
-    '401103'    : "tiers:fournisseurs:Absolight",
-    '401104'    : "tiers:fournisseurs:Tata Communication",
-    '401105'    : "tiers:fournisseurs:Lost Oasis",
-    '401106'    : "tiers:fournisseurs:RIPE NCC",
-    '401107'    : "tiers:fournisseurs:Crédit Mutuel",
-    '401108'    : "tiers:fournisseurs:LCD International",
-    '401109'    : "tiers:fournisseurs:CICP",
-    '401110'    : "tiers:fournisseurs:Alturna Network",
-    '401111'    : "tiers:fournisseurs:GANDI SAS",
-    '401112'    : "tiers:fournisseurs:AS Info",
-    '41'        : "tiers:clients",
-    '411101'    : "tiers:clients:Altern B",
-    '411102'    : "tiers:clients:FDN",
-    '411103'    : "tiers:clients:Globenet",
-    '411104'    : "tiers:clients:Linagora",
-    '411105'    : "tiers:clients:Gixe",
-    '411106'    : "tiers:clients:LAutreNet",
-    '411107'    : "tiers:clients:Rézine",
-    '411108'    : "tiers:clients:Tetaneutral",
-    '411109'    : "tiers:clients:Grenode",
-    '411110'    : "tiers:clients:Franciliens",
-    '411111'    : "tiers:clients:Illyse",
-    '411112'    : "tiers:clients:Ilico",
-    '411113'    : "tiers:clients:Octopuce",
-    '411114'    : "tiers:clients:Artefact",
-    '411115'    : "tiers:clients:NDN",
-    '411116'    : "tiers:clients:LDN",
-    '411117'    : "tiers:clients:Neutrinet",
-    '411118'    : "tiers:clients:AssoDIUT",
-    '411119'    : "tiers:clients:Rhizome",
-    '411120'    : "tiers:clients:BeTech",
-    '411121'    : "tiers:clients:personne physique",
-    '445'       : "tiers:etat:tva",
-    '4456'      : "tiers:etat:tva:déductible",
-    '4457'      : "tiers:etat:tva:collecté",
-    '468'       : "tiers:divers",
-    '4686'      : "tiers:divers:charges à payer",
-    '5'         : "finances",
-    '512'       : "finances:banque",
-    '5121'      : "finances:banque:Crédit Mutuel",
-    '5122'      : "finances:banque:GIE",
-    '532'       : "finances:chèques à encaisser",
-    '6'         : "charges",
-    '60'        : "charges:achats",
-    '604'       : "charges:achats:prestation de services",
-    '606'       : "charges:achats:non-stockés",
-    '6061'      : "charges:achats:non-stockés:fournitures non stockables",
-    '6063'      : "charges:achats:non-stockés:fournitures d'entretien et petits équipements",
-    '6064'      : "charges:achats:non-stockés:fournitures administratives",
-    '6068'      : "charges:achats:non-stockés:autres matières et fournitures",
-    '61'        : "charges:services",
-    '613'       : "charges:services:locations",
-    '613001'    : "charges:services:locations:hosting",
-    '613002'    : "charges:services:locations:lir",
-    '616'       : "charges:services:assurances",
-    '62'        : "charges:autres services",
-    '6227'      : "charges:autres services:frais d'actes",
-    '626'       : "charges:autres services:pce",
-    '626001'    : "charges:autres services:pce:internet",
-    '625'       : "charges:autres services:déplacement, missions,réceptions",
-    '627'       : "charges:autres services:banque",
-    '628'       : "charges:autres services:divers",
-    '6281'      : "charges:autres services:divers:cotisations",
-    '651'       : "charges:redevances",
-    '672'       : "charges:charges sur exercices antérieur",
-    '6811'      : "charges:amortissements",
-    '68111'     : "charges:amortissements:incorporelles",
-    '68112'     : "charges:amortissements:corporelles",
-    '7'         : "produits",
-    '706'       : "produits:services", 
-    '706001'    : "produits:services:routage",
-    '706002'    : "produits:services:commutation",
-    '706003'    : "produits:services:lir",
-    '7060031'   : "produits:services:lir:pi",
-    '7060032'   : "produits:services:lir:pa",
-    '706004'    : "produits:services:hosting",
-    '706005'    : "produits:services:transit",
-    '7060050'   : "produits:services:transit:bp-0",
-    '7060051'   : "produits:services:transit:bp-a",
-    '7060052'   : "produits:services:transit:bp-b",
-    '7060053'   : "produits:services:transit:bp-c",
-    '7060054'   : "produits:services:transit:bp-d",
-    '756'       : "produits:cotisations",
-    '77'        : "produits:exceptionnels",
-    '7718'      : "produits:exceptionnels:dons manuels",
+    "1":          "capitaux",
+    "11":         "capitaux:report à nouveau",
+    "110":        "capitaux:report à nouveau:créditeur",
+    "119":        "capitaux:report à nouveau:débiteur",
+    "12":         "capitaux:résultat",
+    "120":        "capitaux:résultat:positif",
+    "129":        "capitaux:résultat:négatif",
+    "16":         "capitaux:emprunts et dettes",
+    "168":        "capitaux:emprunts et dettes:autres",
+    "1681":       "capitaux:emprunts et dettes:autres:emprunts",
+    "2":          "immobilisations",
+    "201":        "immobilisations:incorporelles:frais d établissement",
+    "21":         "immobilisations:corporelles",
+    "2183":       "immobilisations:corporelles:matériel informatique",
+    "2184":       "immobilisations:corporelles:mobilier",
+    "26":         "immobilisations:participations",
+    "261":        "immobilisations:participations:titres",
+    "280":        "immobilisations:amortissements:incorporelles",
+    "281":        "immobilisations:amortissements:corporelles",
+    "2813":       "immobilisations:amortissements:corporelles:matériel informatique",
+    "2814":       "immobilisations:amortissements:corporelles:mobilier",
+    "4":          "tiers",
+    "40":         "tiers:fournisseurs",
+    "4011999":    "tiers:fournisseurs:XXXXXX",
+    "41":         "tiers:clients",
+    "4111999":    "tiers:clients:XXXXXX",
+    "42":         "tiers:comptes rattachés",
+    "425":        "tiers:comptes rattachés:avances et accomptes",
+    "445":        "tiers:etat:tva",
+    "44551":      "tiers:etat:tva:à décaisser",
+    "4456":       "tiers:etat:tva:déductible",
+    "44562":      "tiers:etat:tva:déductible:immobilisations",
+    "445621":     "tiers:etat:tva:déductible:immobilisations:1960",
+    "445622":     "tiers:etat:tva:déductible:immobilisations:2000",
+    "44566":      "tiers:etat:tva:déductible:autres",
+    "445661":     "tiers:etat:tva:déductible:autres:1960",
+    "445662":     "tiers:etat:tva:déductible:autres:2000",
+    "44567":      "tiers:etat:tva:déductible:crédit à reporter",
+    "44571":      "tiers:etat:tva:collecté",
+    "445711":     "tiers:etat:tva:collecté:1960",
+    "445712":     "tiers:etat:tva:collecté:2000",
+    "4458":       "tiers:etat:tva:à régulariser",
+    "44581":      "tiers:etat:tva:à régulariser:1960",
+    "44582":      "tiers:etat:tva:à régulariser:2000",
+    "447":        "tiers:etat:autres impôts",
+    "467":        "tiers:autres",
+    "468":        "tiers:divers",
+    "4686":       "tiers:divers:charges à payer",
+    "4999999":    "tiers:XXXXXX",
+    "5":          "finances",
+    "512":        "finances:banque",
+    "512999":     "finances:banque:XXXXXX",
+    "532":        "finances:chèques à encaisser",
+    "6":          "charges",
+    "60":         "charges:achats",
+    "604":        "charges:achats:prestation de services",
+    "606":        "charges:achats:non-stockés",
+    "6061":       "charges:achats:non-stockés:fournitures non stockables",
+    "6063":       "charges:achats:non-stockés:fournitures d entretien et petits équipements",
+    "6064":       "charges:achats:non-stockés:fournitures administratives",
+    "6068":       "charges:achats:non-stockés:autres matières et fournitures",
+    "61":         "charges:services",
+    "613":        "charges:services:locations",
+    "616":        "charges:services:assurances",
+    "62":         "charges:autres services",
+    "6227":       "charges:autres services:frais d actes",
+    "621":        "charges:autres services:personnel extérieur à l entreprise",
+    "624":        "charges:autres services:transport de biens et transports collectifs du personnel",
+    "625":        "charges:autres services:déplacement missions réceptions",
+    "626":        "charges:autres services:pce",
+    "627":        "charges:autres services:banque",
+    "628":        "charges:autres services:divers",
+    "6281":       "charges:autres services:divers:cotisations",
+    "63":         "charges:impots",
+    "6351":       "charges:impots:autres:direct",
+    "63511":      "charges:impots:autres:direct:cet",
+    "651":        "charges:redevances",
+    "671":        "charges:exceptionnelles",
+    "6712":       "charges:exceptionnelles:pénalités fiscales",
+    "672":        "charges:charges sur exercices antérieur",
+    "6811":       "charges:amortissements",
+    "68111":      "charges:amortissements:incorporelles",
+    "68112":      "charges:amortissements:corporelles",
+    "6999999":    "charges:XXXXXX",
+    "7":          "produits",
+    "706":        "produits:services",
+    "756":        "produits:cotisations",
+    "758":        "produits:divers",
+    "7585":       "produits:divers:dons manuels",
+    "77":         "produits:exceptionnels",
+    "7999999":    "produits:XXXXXX",
 
 
 }
 
 PC_DESCRIPTIONS = {
-    '41'  : 'Clients et comptes rattachés',
-    '5121': 'CC Crédit Mutuel',
-    '5122': 'Compte GIE',
-
+    "1":          "Capitaux",
+    "11":         "Report à nouveau",
+    "117":        "Report positif",
+    "119":        "Report négatif",
+    "12":         "Résultat",
+    "120":        "Résultat positif",
+    "129":        "Résultat négatif",
+    "16":         "Emprunts et dettes assimilés",
+    "168":        "Autres Emprunts et dettes assimilés",
+    "1681":       "Autres Emprunts assimilés",
+    "2":          "Immobilisations",
+    "201":        "Frais d'établissement",
+    "21":         "Immobilisations corporelles",
+    "2183":       "Matériel informatique",
+    "26":         "Participations",
+    "261":        "Titres de participation",
+    "280":        "Amortissements incorporelles",
+    "281":        "Amortissements corporelles",
+    "2813":       "Amortissements du matériel informatique",
+    "2814":       "Amortissements du mobiliers",
+    "4":          "Tiers",
+    "40":         "Fournisseurs",
+    "4011999":    "tiers:fournisseurs:XXXXXX",
+    "41":         "Clients",
+    "4111999":    "tiers:clients:XXXXXX",
+    "42":         "Comptes rattachés",
+    "425":        "Avances et accomptes",
+    "445":        "Taxe sur la Valeur Ajoutée",
+    "44551":      "TVA à décaisser",
+    "4456":       "TVA déductible",
+    "44562":      "TVA déductible sur immobilisations",
+    "44566":      "TVA déductible autres",
+    "44567":      "TVA déductible à reporter",
+    "4457":       "TVA collectée",
+    "44571":      "TVA collectée sur CA",
+    "447":        "Autres impôts taxes et versements assimilés",
+    "467":        "Autres Tiers",
+    "468":        "Tiers divers",
+    "4686":       "Charges à payer",
+    "5":          "Finances",
+    "512":        "Banque",
+    "512999":     "finances:banque:XXXXXX",
+    "532":        "Chèques à encaisser",
+    "6":          "Charges",
+    "60":         "Achats",
+    "604":        "Prestation de services",
+    "606":        "Achats non-stockés",
+    "6061":       "Fournitures non stockables",
+    "6063":       "Fournitures d'entretien et petits équipements",
+    "6064":       "Fournitures administratives",
+    "6068":       "Autres matières et fournitures",
+    "61":         "Services",
+    "613":        "Locations",
+    "616":        "Assurances",
+    "62":         "Autres services",
+    "6227":       "Frais d'actes",
+    "621":        "Frais de personnel extérieur à l'entreprise",
+    "624":        "Frais de transport de biens et transports collectifs du personnel",
+    "625":        "Frais de déplacement",
+    "626":        "Frais de postes et des communications électroniques",
+    "626101":     "Boîte postale",
+    "627":        "Services bancaires et assimilés",
+    "628":        "Services divers",
+    "6281":       "Cotisations",
+    "63":         "Impôts, Taxes",
+    "6351":       "Impôts directs",
+    "63511":      "Contribution Economique Teritorriale",
+    "651":        "Redevances",
+    "671":        "Charges exceptionnelles",
+    "6712":       "Amendes, Pénalités fiscales",
+    "672":        "Charges sur exercices antérieur",
+    "6811":       "Amortissements",
+    "68111":      "Amortissements incorporelles",
+    "68112":      "Amortissements corporelles",
+    "6999999":    "charges:XXXXXX",
+    "7":          "Produits",
+    "706":        "Services",
+    "756":        "Cotisations",
+    "77":         "Produits exceptionnels",
+    "7718":       "Dons manuels",
+    "7999999":    "produits:XXXXXX",
 }

+ 506 - 0
himports/dolibarrAlchemy.py

@@ -0,0 +1,506 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from sqlalchemy import Column, Integer, ForeignKey, UniqueConstraint
+from sqlalchemy.orm import relationship, sessionmaker
+from sqlalchemy.ext.declarative import declarative_base, DeferredReflection
+
+from sqlalchemy import create_engine, MetaData
+
+Base = declarative_base(cls=DeferredReflection)
+
+
+class Bank(Base):
+    __tablename__ = "llx_bank"
+    id = Column("rowid", Integer, primary_key=True)
+
+    fk_account = Column('fk_account', Integer, ForeignKey('llx_bank_account.rowid'))
+    account = relationship("BankAccount", backref="entries", lazy="subquery")
+    bankclass = relationship(
+        "BankClass", backref="bank", uselist=False,
+        lazy="subquery"
+    )
+
+    url_company = relationship(
+        "BankUrl", uselist=False, backref="bank_company", innerjoin=True,
+        primaryjoin="and_(Bank.id==BankUrl.fk_bank, " "BankUrl.type=='company')",
+        lazy="subquery",
+    )
+    url_payment = relationship(
+        "BankUrl", uselist=False, backref="bank_payement", innerjoin=True,
+        primaryjoin="and_(Bank.id==BankUrl.fk_bank, " "BankUrl.type=='payment')",
+        lazy="subquery",
+    )
+    url_payment_sc = relationship(
+        "BankUrl", uselist=False, backref="bank_payement_sc", innerjoin=True,
+        primaryjoin="and_(Bank.id==BankUrl.fk_bank, " "BankUrl.type=='payment_sc')",
+        lazy="subquery",
+    )
+    url_payment_supplier = relationship(
+        "BankUrl", uselist=False, backref="bank_payement_supplier", innerjoin=True,
+        primaryjoin="and_(Bank.id==BankUrl.fk_bank, " "BankUrl.type=='payment_supplier')",
+        lazy="subquery",
+    )
+    payment_tva = relationship(
+        "TVA", backref="bank",
+        lazy="subquery"
+    )
+
+
+class BankAccount(Base):
+    __tablename__ = "llx_bank_account"
+    id = Column("rowid", Integer, primary_key=True)
+
+
+class BankCateg(Base):
+    __tablename__ = "llx_bank_categ"
+    id = Column("rowid", Integer, primary_key=True)
+
+
+class BankClass(Base):
+    __tablename__ = "llx_bank_class"
+    id = Column('lineid', Integer, ForeignKey('llx_bank.rowid'), primary_key=True)
+    UniqueConstraint('lineid', 'fk_categ')
+
+    categ_id = Column('fk_categ', Integer, ForeignKey('llx_bank_categ.rowid'))
+    categ = relationship(
+        "BankCateg", backref="classes",
+        lazy="subquery",
+    )
+
+
+class BankUrl(Base):
+    __tablename__ = "llx_bank_url"
+    id = Column("rowid", Integer, primary_key=True)
+    fk_bank = Column('fk_bank', Integer, ForeignKey('llx_bank.rowid'))
+    url_id = Column('url_id', Integer)
+    societe = relationship(
+        "Societe", backref="bank_urls", uselist=False,
+        primaryjoin="and_(BankUrl.url_id==Societe.id, " "BankUrl.type=='company')",
+        foreign_keys=url_id,
+        lazy="subquery",
+    )
+    payment = relationship(
+        "Paiement", backref="bank_urls", uselist=False,
+        primaryjoin="and_(BankUrl.url_id==Paiement.id, " "BankUrl.type=='payment')",
+        foreign_keys=url_id,
+        lazy="subquery",
+        order_by="Paiement.datep,Paiement.id",
+    )
+    payment_sc = relationship(
+        "PaiementCharge", backref="bank_urls", uselist=False,
+        primaryjoin="and_(BankUrl.url_id==PaiementCharge.id, " "BankUrl.type=='payment_sc')",
+        foreign_keys=url_id,
+        lazy="subquery",
+        order_by="PaiementCharge.datep,PaiementCharge.id",
+    )
+    payment_supplier = relationship(
+        "PaiementFourn", backref="bank_urls", uselist=False,
+        primaryjoin="and_(BankUrl.url_id==PaiementFourn.id, " "BankUrl.type=='payment_supplier')",
+        foreign_keys=url_id,
+        lazy="subquery",
+        order_by="PaiementFourn.datep,PaiementFourn.id",
+    )
+
+
+class TVA(Base):
+    __tablename__ = "llx_tva"
+    id = Column("rowid", Integer, primary_key=True)
+    fk_bank = Column('fk_bank', Integer, ForeignKey('llx_bank.rowid'))
+
+
+class CotisationsSociales(Base):
+    __tablename__ = "llx_chargesociales"
+    id = Column("rowid", Integer, primary_key=True)
+
+    fk_type = Column('fk_type', Integer, ForeignKey('llx_c_chargesociales.id'))
+    type = relationship(
+        "CCotisationsSociales", backref="cotisations_sociales",
+        lazy="subquery",
+    )
+
+
+class CCotisationsSociales(Base):
+    __tablename__ = "llx_c_chargesociales"
+    id = Column("id", Integer, primary_key=True)
+
+
+class Commande(Base):
+    __tablename__ = "llx_commande"
+    id = Column("rowid", Integer, primary_key=True)
+
+
+class CommandeDet(Base):
+    __tablename__ = "llx_commandedet"
+    id = Column("rowid", Integer, primary_key=True)
+
+    fk_commande = Column("fk_commande", Integer, ForeignKey('llx_commande.rowid'))
+    commande = relationship(
+        "Commande", backref="details",
+        lazy="subquery",
+    )
+    fk_product = Column("fk_product", Integer, ForeignKey('llx_product.rowid'))
+    product = relationship(
+        "Product", backref="commande_details",
+        lazy="subquery",
+    )
+
+
+class Societe(Base):
+    __tablename__ = "llx_societe"
+    id = Column("rowid", Integer, primary_key=True)
+
+
+class PaiementFacture(Base):
+    __tablename__ = "llx_paiement_facture"
+    fk_paiement = Column(Integer, ForeignKey('llx_paiement.rowid'), primary_key=True)
+    fk_facture = Column(Integer, ForeignKey('llx_facture.rowid'), primary_key=True)
+    facture = relationship(
+        "Facture",
+        lazy="subquery",
+        order_by="Facture.facnumber"
+    )
+    paiement = relationship(
+        "Paiement",
+        lazy="subquery",
+        order_by="Paiement.datep,Paiement.id",
+    )
+
+
+class Paiement(Base):
+    __tablename__ = "llx_paiement"
+    id = Column("rowid", Integer, primary_key=True)
+    factures = relationship(
+        "PaiementFacture",
+        lazy="subquery",
+        order_by="PaiementFacture.fk_facture,PaiementFacture.rowid",
+    )
+
+
+class PaiementFournFactureFourn(Base):
+    __tablename__ = "llx_paiementfourn_facturefourn"
+    fk_paiementfourn = Column(Integer, ForeignKey('llx_paiementfourn.rowid'), primary_key=True)
+    fk_facturefourn = Column(Integer, ForeignKey('llx_facture_fourn.rowid'), primary_key=True)
+    facture = relationship(
+        "FactureFourn",
+        lazy="subquery",
+    )
+    paiement = relationship(
+        "PaiementFourn",
+        lazy="subquery",
+        order_by="PaiementFourn.datep,PaiementFourn.id",
+    )
+
+
+class PaiementFourn(Base):
+    __tablename__ = "llx_paiementfourn"
+    id = Column("rowid", Integer, primary_key=True)
+    factures = relationship(
+        "PaiementFournFactureFourn",
+        lazy="subquery",
+    )
+
+
+class PaiementCharge(Base):
+    __tablename__ = "llx_paiementcharge"
+    id = Column("rowid", Integer, primary_key=True)
+
+    fk_bank = Column("fk_bank", Integer, ForeignKey('llx_bank.rowid'))
+    bank = relationship(
+        "Bank", backref="paiementcharges",
+        lazy="subquery",
+    )
+    fk_charge = Column(Integer, ForeignKey('llx_chargesociales.rowid'))
+    cotisation_sociale = relationship(
+        "CotisationsSociales", backref="paiement",
+        lazy="subquery"
+    )
+
+
+class Product(Base):
+    __tablename__ = "llx_product"
+    id = Column("rowid", Integer, primary_key=True)
+
+
+class FactureFourn(Base):
+    __tablename__ = "llx_facture_fourn"
+    id = Column("rowid", Integer, primary_key=True)
+
+    fk_soc = Column(Integer, ForeignKey('llx_societe.rowid'))
+    societe = relationship(
+        'Societe',
+        backref="factures_fournisseurs",
+        lazy="subquery",
+    )
+
+    details = relationship(
+        'FactureFournDet',
+        lazy="subquery",
+    )
+
+
+class Facture(Base):
+    __tablename__ = "llx_facture"
+    id = Column("rowid", Integer, primary_key=True)
+
+    fk_soc = Column(Integer, ForeignKey('llx_societe.rowid'))
+    societe = relationship(
+        'Societe',
+        backref="factures",
+        lazy="joined",
+    )
+
+    details = relationship(
+        'FactureDet',
+        lazy="subquery",
+    )
+
+
+class FactureDet(Base):
+    __tablename__ = "llx_facturedet"
+    id = Column("rowid", Integer, primary_key=True)
+
+    fk_facture = Column(Integer, ForeignKey('llx_facture.rowid'))
+    facture = relationship(
+        'Facture',
+        lazy="subquery",
+    )
+
+    fk_product = Column(Integer, ForeignKey('llx_product.rowid'))
+    product = relationship(
+        'Product',
+        backref="facture_det",
+        lazy="subquery",
+    )
+
+    fk_code_ventilation = Column(Integer, ForeignKey('llx_accountingaccount.rowid'))
+    accounting_account = relationship(
+        'AccountingAccount',
+        backref="facture_det",
+        lazy="subquery",
+    )
+
+
+class FactureFournDet(Base):
+    __tablename__ = "llx_facture_fourn_det"
+    id = Column("rowid", Integer, primary_key=True)
+
+    fk_facture_fourn = Column(Integer, ForeignKey('llx_facture_fourn.rowid'))
+    facture = relationship(
+        'FactureFourn',
+        lazy="subquery",
+    )
+
+    fk_product = Column(Integer, ForeignKey('llx_product.rowid'))
+    product = relationship(
+        'Product',
+        backref="facture_fourn_det",
+        lazy="subquery",
+    )
+
+    fk_code_ventilation = Column(Integer, ForeignKey('llx_accountingaccount.rowid'))
+    accounting_account = relationship(
+        'AccountingAccount',
+        backref="facture_fourn_det",
+        lazy="subquery",
+    )
+
+
+class AccountingAccount(Base):
+    __tablename__ = "llx_accountingaccount"
+    id = Column("rowid", Integer, primary_key=True)
+
+
+MYSQL_QUERIES = {
+    "bank": """SELECT DISTINCT b.rowid as b_rowid,
+                  ba.ref as ba_ref,
+                  ba.label as ba_label,
+                  ba.account_number as ba_account_number,
+                  b.datev as b_datev,
+                  b.dateo as b_dateo,
+                  b.label as b_label,
+                  b.num_chq as b_num_chq,
+                  -b.amount as _b_amount,
+                    b.amount as b_amount,
+                  b.num_releve as b_num_releve,
+                  b.datec as b_datec,
+                  bu.url_id as bu_url_id,
+                  s.nom as s_nom,
+                  s.code_compta as s_code_compta,
+                  s.code_compta_fournisseur as s_code_compta_fournisseur,
+                  bca.label as bca_label,
+                  bca.rowid as   bca_rowid,
+                  bcl.lineid as bcl_lineid,
+                  ccs.code as ccs_code,
+                  ccs.libelle as ccs_label
+              FROM (llx_bank_account as ba, llx_bank as b)
+              LEFT JOIN llx_bank_url as bu ON (bu.fk_bank = b.rowid AND bu.type = 'company')
+              LEFT JOIN llx_societe as s ON bu.url_id = s.rowid
+              LEFT JOIN llx_bank_class as   bcl ON bcl.lineid = b.rowid
+              LEFT JOIN llx_bank_categ as bca ON bca.rowid = bcl.fk_categ
+              LEFT JOIN llx_paiementcharge as p ON p.fk_bank = b.rowid
+              LEFT JOIN llx_chargesociales as cs ON cs.rowid = p.fk_charge
+              LEFT JOIN llx_c_chargesociales as ccs ON cs.fk_type = ccs.id
+              WHERE ba.rowid = b.fk_account AND ba.entity = 1 and b.num_releve <> ''
+              ORDER BY b.datev, b.num_releve;""",
+
+    "sells": """SELECT DISTINCT s.rowid as s_rowid,
+                  s.nom as s_nom,
+                  s.address as s_address,
+                  s.zip as s_zip,
+                  s.town as s_town,
+                  c.code as c_code,
+                  s.phone as s_phone,
+                  s.siren as s_siren,
+                  s.siret as s_siret,
+                  s.ape as s_ape,
+                  s.idprof4 as s_idprof4,
+                  s.code_compta as s_code_compta,
+                  s.code_compta_fournisseur as s_code_compta_fournisseur,
+                  s.tva_intra as s_tva_intra,
+                  f.rowid as f_rowid,
+                  f.facnumber as f_facnumber,
+                  f.datec as f_datec,
+                  f.datef as f_datef,
+                  f.date_lim_reglement as f_date_lim_reglement,
+                  f.total as f_total,
+                  f.total_ttc as f_total_ttc,
+                  f.tva as f_tva,
+                  f.paye as f_paye,
+                  f.fk_statut as f_fk_statut,
+                  f.note_private as f_note_private,
+                  f.note_public as f_note_public,
+                  fd.rowid as fd_rowid,
+                  fd.label as fd_label,
+                  fd.description as fd_description,
+                  fd.subprice as fd_subprice,
+                  fd.tva_tx as fd_tva_tx,
+                  fd.qty as fd_qty,
+                  fd.total_ht as fd_total_ht,
+                  fd.total_tva as fd_total_tva,
+                  fd.total_ttc as fd_total_ttc,
+                  fd.date_start as fd_date_start,
+                  fd.date_end as fd_date_end,
+                  fd.special_code as fd_special_code,
+                  fd.product_type as fd_product_type,
+                  fd.fk_product as fd_fk_product,
+                  p.ref as p_ref,
+                  p.label as p_label,
+                  p.accountancy_code_sell as p_accountancy_code_sell,
+                  a.account_number as a_account_number
+                FROM llx_societe as s
+                LEFT JOIN llx_c_pays as c on s.fk_pays = c.rowid, llx_facture as f
+                LEFT JOIN llx_facture_extrafields as extra ON f.rowid = extra.fk_object , llx_facturedet as fd
+                LEFT JOIN llx_product as p on (fd.fk_product = p.rowid)
+                LEFT JOIN llx_accountingaccount as a ON fd.fk_code_ventilation = a.rowid
+                WHERE f.fk_soc = s.rowid AND f.rowid = fd.fk_facture AND f.entity = 1""",
+
+    "suppliers": """SELECT DISTINCT s.rowid as s_rowid,
+                  s.nom as s_nom,
+                  s.address as s_address,
+                  s.zip as s_zip,
+                  s.town as s_town,
+                  s.code_compta_fournisseur as s_code_supplier,
+                  c.code as c_code,
+                  s.phone as s_phone,
+                  s.siren as s_siren,
+                  s.siret as s_siret,
+                  s.ape as s_ape,
+                  s.idprof4 as s_idprof4,
+                  s.idprof5 as s_idprof5,
+                  s.idprof6 as s_idprof6,
+                  s.tva_intra as s_tva_intra,
+                  f.rowid as f_rowid,
+                  f.ref as f_ref,
+                  f.ref_supplier as f_ref_supplier,
+                  f.datec as f_datec,
+                  f.datef as f_datef,
+                  f.total_ht as f_total_ht,
+                  f.total_ttc as f_total_ttc,
+                  f.total_tva as f_total_tva,
+                  f.paye as f_paye,
+                  f.fk_statut as f_fk_statut,
+                  f.note_public as f_note_public,
+                  fd.rowid as fd_rowid,
+                  fd.description as fd_description,
+                  fd.tva_tx as fd_tva_tx,
+                  fd.qty as fd_qty,
+                  fd.remise_percent as fd_remise_percent,
+                  fd.total_ht as fd_total_ht,
+                  fd.total_ttc as fd_total_ttc,
+                  fd.tva as fd_tva,
+                  fd.product_type as fd_product_type,
+                  fd.fk_product as fd_fk_product,
+                  p.ref as p_ref,
+                  p.label as p_label,
+                  p.accountancy_code_buy as p_accountancy_code_buy,
+                  a.account_number as a_account_number FROM llx_societe as s
+                  LEFT JOIN llx_c_pays as c
+                    ON s.fk_pays = c.rowid, llx_facture_fourn as f
+                  LEFT JOIN llx_facture_fourn_extrafields as extra
+                    ON f.rowid = extra.fk_object, llx_facture_fourn_det as fd
+                  LEFT JOIN llx_product as p on (fd.fk_product = p.rowid)
+                  LEFT JOIN llx_accountingaccount as a
+                    ON fd.fk_code_ventilation = a.rowid
+                    WHERE f.fk_soc = s.rowid AND f.rowid = fd.fk_facture_fourn AND f.entity = 1""",
+
+    "social": """SELECT DISTINCT cc.libelle as cc_libelle,
+                  c.rowid as c_rowid,
+                  c.libelle as c_libelle,
+                  c.date_ech as c_date_ech,
+                  c.periode as c_periode,
+                  c.amount as c_amount,
+                  c.paye as c_paye,
+                  p.rowid as p_rowid,
+                  p.datep as p_datep,
+                  p.amount as p_amount,
+                  p.num_paiement as p_num_paiement,
+                  cc.accountancy_code as cc_acc_code,
+                  cc.code as cc_code FROM llx_c_chargesociales as cc,
+                  llx_chargesociales as c
+                  LEFT JOIN llx_paiementcharge as p
+                    ON p.fk_charge = c.rowid
+                    WHERE c.fk_type = cc.id AND c.entity = 1""",
+
+}
+
+
+class DolibarrSQLAlchemy(object):
+
+    def __init__(
+            self,
+            mysql_host,
+            mysql_port,
+            mysql_database,
+            mysql_user,
+            mysql_password,
+            echo=False):
+        self.mysql_database = mysql_database
+        self.mysql_host = mysql_host
+        self.mysql_password = mysql_password
+        self.mysql_user = mysql_user
+        self.mysql_port = mysql_port
+        self.echo = echo
+
+    def connect(self):
+        engine_str = "mysql://%s:%s@%s:%s/%s" % (
+            self.mysql_user,
+            self.mysql_password,
+            self.mysql_host,
+            self.mysql_port,
+            self.mysql_database
+        )
+        print engine_str
+        self.engine = create_engine(engine_str, echo=self.echo, encoding=str("iso8859-1"), convert_unicode=True)
+        self.metadata = MetaData(bind=self.engine)
+
+        Base.prepare(self.engine)
+
+        # create a configured "Session" class
+        Session = sessionmaker(bind=self.engine)
+
+        # create a Session
+        self.session = Session(autocommit=True)
+
+    def disconnect(self):
+        self.session.close()

+ 617 - 0
himports/dolibarrAlchemyHledger.py

@@ -0,0 +1,617 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import settings
+import datetime
+
+from himports.dolibarrAlchemy import *
+
+
+#
+# HledgerEntry : Base class for an hledger entry
+#
+class HledgerEntry(object):
+    accounting_years = settings.get('ACCOUNTING_YEARS')
+    pc_default_tier = settings.get('PC_REFS')['default_tier']
+    pc_default_client = settings.get('PC_REFS')['default_client']
+    pc_default_supplier = settings.get('PC_REFS')['default_supplier']
+    pc_default_income = settings.get('PC_REFS')['default_income']
+    pc_default_expense = settings.get('PC_REFS')['default_expense']
+    pc_default_bank = settings.get('PC_REFS')['default_bank']
+
+    tva_type = settings.get('TVA_TYPE')
+
+    # the sql_class corresponding the hledger class
+    sql_class = None
+
+    # Date defining the current accounting year
+    k_accounting_date = None
+
+    def __init__(self, e):
+        super(HledgerEntry, self).__init__()
+        self.e = e
+        self.accounting_date = e
+        for attr in self.k_accounting_date.split('.'):
+            self.accounting_date = getattr(self.accounting_date, attr)
+
+    # Retrieve all entries corresponding to cls.sql_class
+    @classmethod
+    def get_entries(cls, session):
+        return [cls(i) for i in session.query(cls.sql_class).all()]
+
+    # get_ledger : Print ledger output. Only defined in derived class
+    def get_ledger(self):
+        print "WARNING: get_ledger not done"
+        return u""
+
+    # check_pc : verify the accounting chart corresponding to the entry. Only defined in derived class.
+    def check_pc(self):
+        return ()
+
+    # get_year: return the year corresponding to the current entry. Only defined in derived class.
+    def get_year(self):
+        raise Exception("TODO: get_year not implemented for class %s" % (self.__class__))
+
+    # get_accounting_year: return the accouting year corresponding to the current entry.
+    def get_accounting_year(self):
+        date = self.accounting_date
+        if isinstance(date, datetime.datetime):
+            date = date.date()
+        for (year, dbegin, dend) in HledgerEntry.accounting_years:
+            if date >= dbegin and date <= dend:
+                return year
+
+        return str(date.year)
+
+    # _value: return the value in float with 4 digits
+    @staticmethod
+    def _value(value):
+        return '{number:.{digits}f}'.format(number=value, digits=4)
+
+
+#
+# HledgerJournal: A complete Hledger journal. This is a base class.
+#
+class HledgerJournal(object):
+    def __init__(self, session, cls_entry):
+        self.entries = cls_entry.get_entries(session)
+
+    # get_entries: return the journal entries
+    def get_entries(self):
+        return self.entries
+
+    # get_by_year: return the journal entries by accounting year.
+    def get_by_year(self):
+        by_year = {}
+        for entry in self.get_entries():
+
+            entry_year = entry.get_accounting_year()
+            if entry_year not in by_year:
+                by_year[entry_year] = []
+            by_year[entry_year].append(entry)
+        return by_year
+
+    # check_pc: verify there is an account for earch entry on the current journal
+    def check_pc(self):
+        pc_missing = set()
+        for entry in self.get_entries():
+            pc_missing.update(entry.check_pc())
+
+        return pc_missing
+
+
+#
+# HledgerBankEntry: a bank entry
+#
+class HledgerBankEntry(HledgerEntry):
+    sql_class = Bank
+    k_accounting_date = 'datev'
+
+    # get_third_code: retrieve the third code corresponding to the entry. It could be
+    #       a supplier payment
+    #       a tax payment
+    #       a customer payment
+    #       a value-added tax payment
+    #       any payment defined in the function PC_REFS['fn_custom_codes']
+    @classmethod
+    def get_third_code(cls, e):
+        third_code = ""
+        if e.url_payment_supplier:
+            if e.url_company:
+                third_code = e.url_company.societe.code_compta_fournisseur
+
+        if e.url_payment_sc:
+            code = e.url_payment_sc.payment_sc.cotisation_sociale.type.code
+            if code in settings.get('SOCIAL_REFS'):
+                third_code = settings.get('SOCIAL_REFS')[code]
+
+        if e.url_payment:
+            if e.url_company:
+                third_code = e.url_company.societe.code_compta
+
+        if e.payment_tva:
+            third_code = settings.get('PC_REFS')['tva_a_decaisser']
+
+        if third_code == "":
+            fns = settings.get('PC_REFS')['fn_custom_codes']
+            for fn in fns:
+                try:
+                    third_code = fn(e)
+                    if third_code is None or not isinstance(third_code, unicode):
+                        third_code = ""
+                except:
+                    third_code = ""
+                if third_code != "":
+                    break
+
+        if third_code == "":
+            third_code = cls.pc_default_tier
+
+        return third_code
+
+    # get_description: retrieve the description of the payment
+    @classmethod
+    def get_description(self, e):
+        s_nom = ""
+        s_description = ""
+
+        if e.url_company:
+            s_nom = e.url_company.societe.nom
+
+        if e.url_payment_supplier:
+            f_ids = [f.facture.ref_supplier for f in e.url_payment_supplier.payment_supplier.factures]
+            s_description = "Règlement facture fournisseur - %s - %s" % (
+                s_nom,
+                "|".join(f_ids),
+            )
+        if e.url_payment:
+            f_ids = [f.facture.facnumber for f in e.url_payment.payment.factures]
+            s_description = "Règlement facture client - %s - %s" % (
+                s_nom,
+                "|".join(f_ids),
+            )
+
+        if s_description == "":
+            s_description = s_nom + " - " + e.label
+
+        return s_description
+
+    # get_ledger: see @HledgerEntry.get_ledger
+    def get_ledger(self):
+        e = self.e
+        s = ""
+
+        s_description = self.get_description(self.e)
+
+        s += "%(date)s    %(description)s\n" % {
+            'date': e.datev.strftime("%Y/%m/%d"),
+            'description': s_description
+        }
+
+        third_code = self.get_third_code(self.e)
+
+        ba_code = e.account.account_number
+        if ba_code == "":
+            ba_code = self.pc_default_bank
+
+        s += "    %(account)s \t %(amount)s\n" % {
+            'account': settings.get_ledger_account(ba_code),
+            'amount': self._value(-e.amount)
+        }
+        s += "    %(account)s \t %(amount)s\n" % {
+            'account': settings.get_ledger_account(third_code),
+            'amount': self._value(e.amount)
+        }
+
+        if e.url_payment_supplier:
+            for f in e.url_payment_supplier.payment_supplier.factures:
+                tvas = HledgerSupplierEntry.get_tva_payment_amounts(f.facture, journal="bank")
+                for k in tvas:
+                    s += "    %(account_tva)s \t %(amount_tva)s\n" % {
+                        'account_tva': settings.get_ledger_account(k),
+                        'amount_tva': self._value(tvas[k] * (f.amount / f.facture.total_ttc))
+                    }
+        elif e.url_payment:
+            for f in e.url_payment.payment.factures:
+                tvas = HledgerSellEntry.get_tva_payment_amounts(f.facture, journal="bank")
+                for k in tvas:
+                    s += "    %(account_tva)s \t %(amount_tva)s\n" % {
+                        'account_tva': settings.get_ledger_account(k),
+                        'amount_tva': self._value(tvas[k] * (f.amount / f.facture.total_ttc))
+                    }
+        else:
+            pass
+
+        return s
+
+    # get_entries: return the bank entries ordered by value date
+    @classmethod
+    def get_entries(cls, session):
+        return [cls(e) for e in session.query(cls.sql_class).order_by(Bank.datev, Bank.num_releve).all()]
+
+
+#
+# HledgerBillingEntry: An entry corresponding to a bill (Supplier or Client)
+#
+class HledgerBillingEntry(HledgerEntry):
+
+    # get_entries: return the bill entries ordered by billing date
+    @classmethod
+    def get_entries(cls, session):
+        return [cls(e) for e in session.query(cls.sql_class).order_by(cls.sql_class.datef).all()]
+
+    # is_tva_facture: return if the value added tax must be processed on the billing date
+    @classmethod
+    def is_tva_facture(cls, ed):
+        return ed.productcls.tva_type == 'service_sur_debit' and ed.product_type == 1
+
+    # is_tva_paiement: return if the value-added tax must be processed on the payment date
+    @classmethod
+    def is_tva_paiement(cls, ed):
+        return cls.tva_type != 'service_sur_debit' or ed.product_type != 1
+
+    # get_tva_amounts: return the amount of value-added taxes.
+    @classmethod
+    def get_tva_amounts(cls, e, journal):
+
+        tvas = dict()
+        for ed in e.details:
+            if isinstance(e, Facture):
+                total_tva = -ed.total_tva
+            elif isinstance(e, FactureFourn):
+                total_tva = ed.tva
+            else:
+                raise Exception("Should be either Facture or FactureFourn")
+
+            if total_tva == 0:
+                continue
+
+            tva_account = cls.get_tva_account(ed)
+            tva_regul = cls.get_tva_regul_account(ed)
+
+            if journal == "bank":
+                if ed.product_type == 1 and cls.tva_type == 'standard':
+                    if tva_regul not in tvas:
+                        tvas[tva_regul] = 0
+                    if tva_account not in tvas:
+                        tvas[tva_account] = 0
+                    tvas[tva_account] += -total_tva
+                    tvas[tva_regul] += total_tva
+
+            elif journal == "sell" or journal == "supplier":
+                if ed.product_type == 1 and cls.tva_type == 'standard':
+                    if tva_regul not in tvas:
+                        tvas[tva_regul] = 0
+                    tvas[tva_regul] += -total_tva
+                else:
+                    if tva_account not in tvas:
+                        tvas[tva_account] = 0
+                    tvas[tva_account] += -total_tva
+
+        return tvas
+
+    # get_tva_regul_account: retourn the reference corresponding to the
+    #                        value-added tax regulation account
+    @classmethod
+    def get_tva_regul_account(cls, ed):
+        tx = int(float(ed.tva_tx) * 100)
+
+        key = "tva_regul_%s" % (tx,)
+
+        return settings.get('PC_REFS')[key]
+
+    # get_tva_billing_amounts: return the value-added tax amount to collect
+    @classmethod
+    def get_tva_billing_amounts(cls, e, journal):
+        return cls.get_tva_amounts(e, journal)
+
+    # get_tva_payment_amounts: return value-added tax amount to deduce
+    @classmethod
+    def get_tva_payment_amounts(cls, e, journal):
+        return cls.get_tva_amounts(e, journal)
+
+
+#
+# HledgerSupplierEntry: Billing entry corresponding to a supplier
+#
+class HledgerSupplierEntry(HledgerBillingEntry):
+    sql_class = FactureFourn
+    k_accounting_date = 'datef'
+
+    # check: check if the entry is coherent
+    def check(self):
+        e = self.e
+
+        total_ttc = e.total_ttc
+        total_tva = e.total_tva
+        total_ht = e.total_ht
+
+        for ed in e.details:
+            total_ttc -= ed.total_ttc
+            total_tva -= ed.tva
+            total_ht -= ed.total_ht
+
+        if total_ttc > 1e-10:
+            print "Erreur dans l'écriture %s: total ttc = %s" % (e.ref_supplier, total_ttc)
+        if total_ht > 1e-10:
+            print "Erreur dans l'écriture %s: total ht = %s" % (e.ref_supplier, total_ht)
+        if total_tva > 1e-10:
+            print "Erreur dans l'écriture %s: total tva = %s" % (e.ref_supplier, total_tva)
+
+    # getMissingPC: retrieve missing accounts
+    def getMissingPC(self):
+        pc_missing = []
+        if e.societe.code_compta_fournisseur == "":
+            pc_missing.append("tiers:fournisseur: %s %s" % (e.societe.nom, s.societe.ape))
+
+        for ed in e.details:
+            if self.get_product_account_code(ed) == self.pc_default_expense:
+                pc_missing.append("achat: %s - %s" % (e.ref_supplier, ed.description.splitlines()[0]))
+
+        return pc_missing
+
+    # get_ledger: return the corresponding ledger entries
+    def get_ledger(self):
+        e = self.e
+        self.check()
+
+        s = ""
+        s += "%(date)s  %(description)s\n" % {
+            'date':  e.datef.strftime("%Y/%m/%d"),
+            'description': e.ref_supplier + " - " + e.societe.nom,
+        }
+
+        s_code = self.get_supplier_code(self.e)
+        s += "    %(compte_tiers)s \t %(amount_ttc)s\n" % {
+            'compte_tiers': settings.get_ledger_account(s_code),
+            'amount_ttc': self._value(e.total_ttc),
+        }
+
+        for ed in e.details:
+            p_code = self.get_product_account_code(ed)
+            s += "    %(compte_produit)s \t %(amount_ht)s\n" % {
+                'compte_produit': settings.get_ledger_account(p_code),
+                'amount_ht': self._value(-ed.total_ht)
+            }
+
+        tvas = self.get_tva_billing_amounts(self.e, journal="supplier")
+        for k in tvas:
+            s += "    %(compte_tva)s \t %(amount_tva)s\n" % {
+                'compte_tva': settings.get_ledger_account(k),
+                'amount_tva': self._value(tvas[k]),
+            }
+
+        return s
+
+    # get_tva_account: return the value-added tax account
+    @classmethod
+    def get_tva_account(cls, ed):
+        p_code = cls.get_product_account_code(ed)
+        tx = int(float(ed.tva_tx) * 100)
+
+        if p_code.startswith("2"):
+            prefix = 'tva_deductible'
+        else:
+            prefix = 'tva_deductible_immo'
+        key = "%s_%s" % (prefix, tx)
+        tva_account = settings.get('PC_REFS')[key]
+        return tva_account
+
+    # get_product_account_code: return the account code for the product of the current entry
+    @classmethod
+    def get_product_account_code(cls, ed):
+        p_code = ""
+        if ed.accounting_account:
+            p_code = ed.accounting_account.account_number
+        elif ed.product:
+            p_code = ed.product.accountancy_code_buy
+        else:
+            p_code = cls.pc_default_expense
+        return p_code
+
+    # get_supplier_code: return the supplier account code for the current entry
+    @classmethod
+    def get_supplier_code(cls, e):
+        s_code = e.societe.code_compta_fournisseur
+        if s_code == "":
+            s_code = cls.pc_default_supplier
+        return s_code
+
+#
+# HledgerSellEntry: The billing entry corresponding to a client
+#
+class HledgerSellEntry(HledgerBillingEntry):
+    sql_class = Facture
+    k_accounting_date = 'datef'
+
+    # get_ledger: return the ledger corresping to this entry
+    def get_ledger(self):
+        e = self.e
+        self.check()
+        s = ""
+
+        # Date et description
+        s += "%(date)s  %(description)s\n" % {
+            'date':  e.datef.strftime("%Y/%m/%d"),
+            'description': e.facnumber + " - " + e.societe.nom,
+        }
+
+        # ligne pour compte client
+        s_code = self.get_client_code(self.e)
+        s += "    %(compte_tiers)s    %(amount_ttc)s\n" % {
+            'compte_tiers': settings.get_ledger_account(s_code),
+            'amount_ttc': self._value(-e.total_ttc),
+
+        }
+
+        # lignes pour compte de produits
+        for ed in e.details:
+            p_code = self.get_product_account_code(ed)
+            s += "    %(compte_produit)s   %(amount_ht)s\n" % {
+                'compte_produit': settings.get_ledger_account(p_code),
+                'amount_ht': self._value(ed.total_ht)
+            }
+
+        # lignes pour la tva
+        tvas = self.get_tva_billing_amounts(self.e, journal="sell")
+        for k in tvas:
+            s += "    %(compte_tva)s  %(amount_tva)s\n" % {
+                'compte_tva': settings.get_ledger_account(k),
+                'amount_tva': self._value(tvas[k]),
+            }
+
+        return s
+
+    # get_tva_account: return the value-added tax account for the given product
+    @classmethod
+    def get_tva_account(cls, ed):
+        tx = int(float(ed.tva_tx) * 100)
+        key = "tva_collecte_%s" % (tx,)
+        return settings.get('PC_REFS')[key]
+
+    # getMissingPC: return the missing account for this entry
+    def getMissingPC(self):
+        e = self.e
+        pc_missing = []
+        if e.societe.code_compta == "":
+            pc_missing.append("tiers: %s %s" % (e.societe.nom, s.societe.ape))
+
+        for ed in e.details:
+            if self.get_product_account_code(ed) == self.pc_default_income:
+                if ed.description != "":
+                    description = ed.description.splitlines()[0]
+                else:
+                    description = ed.description
+                pc_missing.append("produit: %s - %s - %s" % (e.societe.nom, e.facnumber, description))
+
+        return pc_missing
+
+    # get_product_account_code: return the account code for the product of the current entry
+    @classmethod
+    def get_product_account_code(cls, ed):
+        p_code = ""
+        if ed.accounting_account:
+            p_code = ed.accounting_account.account_number
+        elif ed.product:
+            p_code = ed.product.accountancy_code_sell
+        else:
+            p_code = cls.pc_default_income
+        return p_code
+
+    # get_client_code: return the account code for the client entry
+    @classmethod
+    def get_client_code(cls, e):
+        s_code = e.societe.code_compta
+        if s_code == "":
+            s_code = cls.pc_default_client
+        return s_code
+
+    # check: check if the entry is coherent
+    def check(self):
+        e = self.e
+        total_ttc = e.total_ttc
+        total_tva = e.tva
+        total_ht = e.total
+
+        for ed in e.details:
+            total_ttc -= ed.total_ttc
+            total_tva -= ed.total_tva
+            total_ht -= ed.total_ht
+
+        if total_ttc > 1e-10:
+            print "Erreur dans l'écriture %s: total ttc = %s" % (e.facnumber, total_ttc)
+        if total_ht > 1e-10:
+            print "Erreur dans l'écriture %s: total ht = %s" % (e.facnumber, total_ht)
+        if total_tva > 1e-10:
+            print "Erreur dans l'écriture %s: total tva = %s" % (e.facnumber, total_tva)
+
+
+#
+# HledgerSocialEntry: A ledger entry corresponding to a tax
+#
+class HledgerSocialEntry(HledgerEntry):
+    sql_class = CotisationsSociales
+    k_accounting_date = 'date_ech'
+
+    # get_entries: retrieve all the entries for this entry type
+    @classmethod
+    def get_entries(cls, session):
+        return [cls(e) for e in session.query(cls.sql_class).order_by(CotisationsSociales.date_ech).all()]
+
+    # get_third_code: return the third accounting code for this entry
+    @classmethod
+    def get_third_code(cls, e):
+        third_code = ""
+        if e.type.code in settings.get('SOCIAL_REFS'):
+            third_code = settings.get('SOCIAL_REFS')[e.type.code]
+        if third_code == "":
+            third_code = cls.pc_default_supplier
+        return third_code
+
+    # get_social_code: return the social accounting code for this entry
+    @classmethod
+    def get_social_code(cls, e):
+        s_code = ""
+        if e.type:
+            s_code = e.type.accountancy_code
+        if s_code == "":
+            s_code = cls.pc_default_expense
+        return s_code
+
+    # getMissingPC: return the missing accounting code for this entry
+    def getMissingPC(self):
+        pc_missing = []
+        if self.get_social_code(self.e) == self.pc_default_expense:
+            pc_missing.append("expenses: %s" % (e.libelle))
+
+        if self.get_third_code(self.e) == self.pc_default_supplier:
+            pc_missing.append("tiers: %s (%s)" % (e.libelle, e.type.code))
+
+        return pc_missing
+
+    # get_ledger: return the ledger for this entry
+    def get_ledger(self):
+        e = self.e
+
+        s = ""
+
+        s += "%(date)s    %(description)s\n" % {
+            'date': e.date_ech.strftime("%Y/%m/%d"),
+            'description': e.libelle + " - " + e.type.libelle
+        }
+
+        third_code = self.get_third_code(self.e)
+        s_code = self.get_social_code(self.e)
+
+        s += "    %(account)s \t %(amount)s\n" % {
+            'account': settings.get_ledger_account(third_code),
+            'amount': self._value(e.amount)
+        }
+
+        s += "    %(account)s \t %(amount)s\n" % {
+            'account': settings.get_ledger_account(s_code),
+            'amount': self._value(-e.amount)
+        }
+
+        return s
+
+    # check: -
+    def check(self):
+        pass
+
+
+#
+# HledgerDolibarrSQLAlchemy: Top class for retrieving all the journals
+#
+class HledgerDolibarrSQLAlchemy(DolibarrSQLAlchemy):
+    def get_bank_journal(self):
+        return HledgerJournal(self.session, HledgerBankEntry)
+
+    def get_supplier_journal(self):
+        return HledgerJournal(self.session, HledgerSupplierEntry)
+
+    def get_sell_journal(self):
+        return HledgerJournal(self.session, HledgerSellEntry)
+
+    def get_social_journal(self):
+        return HledgerJournal(self.session, HledgerSocialEntry)

+ 50 - 0
himports/dolibarrWriter.py

@@ -0,0 +1,50 @@
+import settings
+import os
+import codecs
+
+
+class Writer(object):
+    output_files = settings.get('OUTPUT_FILES')
+    output_dir = settings.get('OUTPUT_DIR')
+
+    @staticmethod
+    def write(journal, entries, years):
+        filename = Writer.output_files[journal]
+        output = os.path.join(Writer.output_dir, filename)
+        entries_by_year = entries.get_by_year()
+
+        for year in entries_by_year:
+            if year in years:
+                output_file = output.replace("%year%", year)
+                output_dir = os.path.dirname(output_file)
+                if not os.path.exists(output_dir):
+                    os.makedirs(os.path.dirname(output_file))
+                elif not os.path.isdir(output_dir):
+                    print "Error: %s is not a dir\n" % (output_dir)
+                    raise os.error()
+
+                f = codecs.open(output_file, 'w', 'utf-8')
+                for entry in entries_by_year[year]:
+                    f.write(entry.get_ledger())
+                    f.write("\n")
+                f.close()
+
+    @staticmethod
+    def write_hreport_chart_of_accounts(years):
+        pc_names = settings.get('PC_NAMES')
+        pc_descriptions = settings.get('PC_DESCRIPTIONS')
+        for year in years:
+            filename = Writer.output_files['chart_of_accounts']
+            output_file = os.path.join(Writer.output_dir, filename).replace("%year%", year)
+
+            f = codecs.open(output_file, 'w', 'utf-8')
+            f.write("%s/01/01 * Chart of account\n" % (year))
+            for pc in sorted(pc_names.keys()):
+                name = pc_names[pc]
+                if pc in pc_descriptions:
+                    desc = pc_descriptions[pc]
+                else:
+                    desc = name
+                s = "    %s    0 ;  %s  %s\n" % (name, pc, desc)
+                f.write(s)
+            f.close()

File diff suppressed because it is too large
+ 0 - 633
himports/hledger.py


+ 4 - 0
himports/settings.py

@@ -51,3 +51,7 @@ if 'ACCOUNTING_YEARS' in __settings:
         datetime.datetime.strptime(dbegin, "%Y/%m/%d").date(),
         datetime.datetime.strptime(dend, "%Y/%m/%d").date(),
         ) for (year, dbegin, dend) in __settings['ACCOUNTING_YEARS']]
+
+if 'TVA_TYPE' not in __settings or\
+        __settings['TVA_TYPE'] not in ["standard", "service_sur_debit"]:
+    raise Exception("need TVA_TYPE settings either: standard | service_sur_debit")

+ 2 - 2
requirements.pip

@@ -1,2 +1,2 @@
-
-
+MySQL-python
+sqlalchemy

+ 5 - 3
utils/pyenv-himport

@@ -1,6 +1,8 @@
-#! /bin/sh
+#! /usr/bin/env bash
+
+UTILS_DIR=$(dirname "${BASH_SOURCE[0]}")
 
 # this script aim to initialise the environement.
-. $(dirname $0)/scripts.cfg
+. $UTILS_DIR/scripts.cfg
 
-./bin/himport $@
+$UTILS_DIR/../bin/himport $@