dolibarrAlchemyHledger.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  1. # -*- coding: utf-8 -*-
  2. from __future__ import unicode_literals
  3. import settings
  4. import datetime
  5. from himports.dolibarrAlchemy import DolibarrSQLAlchemy
  6. #
  7. # HledgerEntry : Base class for an hledger entry
  8. #
  9. class HledgerEntry(object):
  10. accounting_years = settings.get('ACCOUNTING_YEARS')
  11. pc_default_tier = settings.get('PC_REFS')['default_tier']
  12. pc_default_client = settings.get('PC_REFS')['default_client']
  13. pc_default_supplier = settings.get('PC_REFS')['default_supplier']
  14. pc_default_income = settings.get('PC_REFS')['default_income']
  15. pc_default_expense = settings.get('PC_REFS')['default_expense']
  16. pc_default_bank = settings.get('PC_REFS')['default_bank']
  17. tva_type = settings.get('TVA_TYPE')
  18. # the sql class need to be defined in the derived classes
  19. def _sql_class(self):
  20. assert(False)
  21. # the sql_class corresponding the hledger class
  22. sql_class = property(_sql_class)
  23. # Date defining the current accounting year
  24. k_accounting_date = None
  25. def __init__(self, dolibarr_alchemy, e):
  26. super(HledgerEntry, self).__init__()
  27. self.dolibarr_alchemy = dolibarr_alchemy
  28. self.e = e
  29. self.accounting_date = e
  30. for attr in self.k_accounting_date.split('.'):
  31. self.accounting_date = getattr(self.accounting_date, attr)
  32. # Retrieve all entries corresponding to cls.sql_class
  33. @classmethod
  34. def get_entries(cls, dolibarr_alchemy):
  35. return [cls(dolibarr_alchemy, i) for i in dolibarr_alchemy.session.query(cls.sql_class).all()]
  36. # get_ledger : Print ledger output. Only defined in derived class
  37. def get_ledger(self):
  38. print("WARNING: get_ledger not done")
  39. return u""
  40. # check_pc : verify the accounting chart corresponding to the entry. Only defined in derived class.
  41. def check_pc(self):
  42. return ()
  43. # get_year: return the year corresponding to the current entry. Only defined in derived class.
  44. def get_year(self):
  45. raise Exception("TODO: get_year not implemented for class %s" % (self.__class__))
  46. # get_accounting_year: return the accouting year corresponding to the current entry.
  47. def get_accounting_year(self):
  48. date = self.accounting_date
  49. if isinstance(date, datetime.datetime):
  50. date = date.date()
  51. for (year, dbegin, dend) in HledgerEntry.accounting_years:
  52. if date >= dbegin and date <= dend:
  53. return year
  54. return str(date.year)
  55. # _value: return the value in float with 4 digits
  56. @staticmethod
  57. def _value(value):
  58. return '{number:.{digits}f}'.format(number=value, digits=4)
  59. #
  60. # HledgerJournal: A complete Hledger journal. This is a base class.
  61. #
  62. class HledgerJournal(object):
  63. def __init__(self, dolibarr_alchemy, cls_entry):
  64. self.dolibarr_alchemy = dolibarr_alchemy
  65. self.entries = cls_entry.get_entries(dolibarr_alchemy)
  66. # get_entries: return the journal entries
  67. def get_entries(self):
  68. return self.entries
  69. # get_by_year: return the journal entries by accounting year.
  70. def get_by_year(self):
  71. by_year = {}
  72. for entry in self.get_entries():
  73. entry_year = entry.get_accounting_year()
  74. if entry_year not in by_year:
  75. by_year[entry_year] = []
  76. by_year[entry_year].append(entry)
  77. return by_year
  78. # check_pc: verify there is an account for earch entry on the current journal
  79. def check_pc(self):
  80. pc_missing = set()
  81. for entry in self.get_entries():
  82. pc_missing.update(entry.check_pc())
  83. return pc_missing
  84. #
  85. # HledgerBankEntry: a bank entry
  86. #
  87. class HledgerBankEntry(HledgerEntry):
  88. k_accounting_date = 'datev'
  89. def _sql_class(self):
  90. return self.dolibarr_alchemy.Bank
  91. # get_entries: return the bank entries ordered by value date
  92. @classmethod
  93. def get_entries(cls, dolibarr_alchemy):
  94. Bank = dolibarr_alchemy.Bank
  95. entries = dolibarr_alchemy.session.query(Bank).order_by(Bank.datev, Bank.num_releve).all()
  96. return [cls(dolibarr_alchemy, e) for e in entries]
  97. # get_third_code: retrieve the third code corresponding to the entry. It could be
  98. # a supplier payment
  99. # a tax payment
  100. # a customer payment
  101. # a value-added tax payment
  102. # any payment defined in the function PC_REFS['fn_custom_codes']
  103. @classmethod
  104. def get_third_code(cls, e):
  105. third_code = ""
  106. if e.url_payment_supplier:
  107. if e.url_company:
  108. third_code = e.url_company.societe.code_compta_fournisseur
  109. if e.url_payment_sc:
  110. code = e.url_payment_sc.payment_sc.cotisation_sociale.type.code
  111. if code in settings.get('SOCIAL_REFS'):
  112. third_code = settings.get('SOCIAL_REFS')[code]
  113. if e.url_payment:
  114. if e.url_company:
  115. third_code = e.url_company.societe.code_compta
  116. if e.payment_tva:
  117. third_code = settings.get('PC_REFS')['tva_a_decaisser']
  118. if third_code == "":
  119. fns = settings.get('PC_REFS')['fn_custom_codes']
  120. for fn in fns:
  121. try:
  122. third_code = fn(e)
  123. if third_code is None or not isinstance(third_code, basestring):
  124. third_code = ""
  125. except:
  126. third_code = ""
  127. if third_code != "":
  128. break
  129. if third_code == "":
  130. third_code = cls.pc_default_tier
  131. return third_code
  132. # get_description: retrieve the description of the payment
  133. @classmethod
  134. def get_description(self, e):
  135. s_nom = ""
  136. s_description = ""
  137. if e.url_company:
  138. s_nom = e.url_company.societe.nom
  139. if e.url_payment_supplier:
  140. f_ids = [f.facture.ref_supplier for f in e.url_payment_supplier.payment_supplier.factures]
  141. s_description = "Règlement facture fournisseur - %s - %s" % (
  142. s_nom,
  143. "|".join(f_ids),
  144. )
  145. if e.url_payment:
  146. f_ids = [f.facture.facnumber for f in e.url_payment.payment.factures]
  147. s_description = "Règlement facture client - %s - %s" % (
  148. s_nom,
  149. "|".join(f_ids),
  150. )
  151. if s_description == "":
  152. s_description = s_nom + " - " + e.label
  153. return s_description
  154. # get_ledger: see @HledgerEntry.get_ledger
  155. def get_ledger(self):
  156. e = self.e
  157. s = ""
  158. s_description = self.get_description(self.e)
  159. s += "%(date)s %(description)s\n" % {
  160. 'date': e.datev.strftime("%Y/%m/%d"),
  161. 'description': s_description
  162. }
  163. third_code = self.get_third_code(self.e)
  164. ba_code = e.account.account_number
  165. if ba_code == "":
  166. ba_code = self.pc_default_bank
  167. s += " %(account)s \t %(amount)s\n" % {
  168. 'account': settings.get_ledger_account(ba_code),
  169. 'amount': self._value(-e.amount)
  170. }
  171. s += " %(account)s \t %(amount)s\n" % {
  172. 'account': settings.get_ledger_account(third_code),
  173. 'amount': self._value(e.amount)
  174. }
  175. if e.url_payment_supplier:
  176. for f in e.url_payment_supplier.payment_supplier.factures:
  177. tvas = HledgerSupplierEntry.get_tva_payment_amounts(self.dolibarr_alchemy, f.facture, journal="bank")
  178. for k in tvas:
  179. s += " %(account_tva)s \t %(amount_tva)s\n" % {
  180. 'account_tva': settings.get_ledger_account(k),
  181. 'amount_tva': self._value(tvas[k] * (f.amount / f.facture.total_ttc))
  182. }
  183. elif e.url_payment:
  184. for f in e.url_payment.payment.factures:
  185. tvas = HledgerSellEntry.get_tva_payment_amounts(self.dolibarr_alchemy, f.facture, journal="bank")
  186. for k in tvas:
  187. s += " %(account_tva)s \t %(amount_tva)s\n" % {
  188. 'account_tva': settings.get_ledger_account(k),
  189. 'amount_tva': self._value(tvas[k] * (f.amount / f.facture.total_ttc))
  190. }
  191. else:
  192. pass
  193. return s
  194. #
  195. # HledgerBillingEntry: An entry corresponding to a bill (Supplier or Client)
  196. #
  197. class HledgerBillingEntry(HledgerEntry):
  198. # is_tva_facture: return if the value added tax must be processed on the billing date
  199. @classmethod
  200. def is_tva_facture(cls, ed):
  201. return ed.productcls.tva_type == 'service_sur_debit' and ed.product_type == 1
  202. # is_tva_paiement: return if the value-added tax must be processed on the payment date
  203. @classmethod
  204. def is_tva_paiement(cls, ed):
  205. return cls.tva_type != 'service_sur_debit' or ed.product_type != 1
  206. # get_tva_amounts: return the amount of value-added taxes.
  207. @classmethod
  208. def get_tva_amounts(cls, dolibarr_alchemy, e, journal):
  209. tvas = dict()
  210. for ed in e.details:
  211. if isinstance(e, dolibarr_alchemy.Facture):
  212. total_tva = -ed.total_tva
  213. elif isinstance(e, dolibarr_alchemy.FactureFourn):
  214. total_tva = ed.tva
  215. else:
  216. raise Exception("Should be either Facture or FactureFourn")
  217. if total_tva == 0:
  218. continue
  219. tva_account = cls.get_tva_account(ed)
  220. tva_regul = cls.get_tva_regul_account(ed)
  221. if journal == "bank":
  222. if ed.product_type == 1 and cls.tva_type == 'standard':
  223. if tva_regul not in tvas:
  224. tvas[tva_regul] = 0
  225. if tva_account not in tvas:
  226. tvas[tva_account] = 0
  227. tvas[tva_account] += -total_tva
  228. tvas[tva_regul] += total_tva
  229. elif journal == "sell" or journal == "supplier":
  230. if ed.product_type == 1 and cls.tva_type == 'standard':
  231. if tva_regul not in tvas:
  232. tvas[tva_regul] = 0
  233. tvas[tva_regul] += -total_tva
  234. else:
  235. if tva_account not in tvas:
  236. tvas[tva_account] = 0
  237. tvas[tva_account] += -total_tva
  238. return tvas
  239. # get_tva_regul_account: retourn the reference corresponding to the
  240. # value-added tax regulation account
  241. @classmethod
  242. def get_tva_regul_account(cls, ed):
  243. tx = int(float(ed.tva_tx) * 100)
  244. key = "tva_regul_%s" % (tx,)
  245. return settings.get('PC_REFS')[key]
  246. # get_tva_billing_amounts: return the value-added tax amount to collect
  247. @classmethod
  248. def get_tva_billing_amounts(cls, dolibarr_alchemy, e, journal):
  249. return cls.get_tva_amounts(dolibarr_alchemy, e, journal)
  250. # get_tva_payment_amounts: return value-added tax amount to deduce
  251. @classmethod
  252. def get_tva_payment_amounts(cls, dolibarr_alchemy, e, journal):
  253. return cls.get_tva_amounts(dolibarr_alchemy, e, journal)
  254. #
  255. # HledgerSupplierEntry: Billing entry corresponding to a supplier
  256. #
  257. class HledgerSupplierEntry(HledgerBillingEntry):
  258. k_accounting_date = 'datef'
  259. def _sql_class(self):
  260. return self.dolibarr_alchemy.FactureFourn
  261. # get_entries: return the bill entries ordered by billing date
  262. @classmethod
  263. def get_entries(cls, dolibarr_alchemy):
  264. FactureFourn = dolibarr_alchemy.FactureFourn
  265. return [cls(dolibarr_alchemy, e) for e in dolibarr_alchemy.session.query(FactureFourn).order_by(FactureFourn.datef).all()]
  266. # check: check if the entry is coherent
  267. def check(self):
  268. e = self.e
  269. total_ttc = e.total_ttc
  270. total_tva = e.total_tva
  271. total_ht = e.total_ht
  272. for ed in e.details:
  273. total_ttc -= ed.total_ttc
  274. total_tva -= ed.tva
  275. total_ht -= ed.total_ht
  276. if total_ttc > 1e-10:
  277. print("Erreur dans l'écriture %s: total ttc = %s" % (e.ref_supplier, total_ttc))
  278. if total_ht > 1e-10:
  279. print("Erreur dans l'écriture %s: total ht = %s" % (e.ref_supplier, total_ht))
  280. if total_tva > 1e-10:
  281. print("Erreur dans l'écriture %s: total tva = %s" % (e.ref_supplier, total_tva))
  282. # getMissingPC: retrieve missing accounts
  283. def getMissingPC(self):
  284. e = self.e
  285. pc_missing = []
  286. if e.societe.code_compta_fournisseur == "":
  287. pc_missing.append("tiers:fournisseur: %s %s" % (e.societe.nom, e.societe.ape))
  288. for ed in e.details:
  289. if self.get_product_account_code(ed) == self.pc_default_expense:
  290. pc_missing.append("achat: %s - %s" % (e.ref_supplier, ed.description.splitlines()[0]))
  291. return pc_missing
  292. # get_ledger: return the corresponding ledger entries
  293. def get_ledger(self):
  294. e = self.e
  295. self.check()
  296. s = ""
  297. s += "%(date)s %(description)s\n" % {
  298. 'date': e.datef.strftime("%Y/%m/%d"),
  299. 'description': e.ref_supplier + " - " + e.societe.nom,
  300. }
  301. s_code = self.get_supplier_code(self.e)
  302. s += " %(compte_tiers)s \t %(amount_ttc)s\n" % {
  303. 'compte_tiers': settings.get_ledger_account(s_code),
  304. 'amount_ttc': self._value(e.total_ttc),
  305. }
  306. for ed in e.details:
  307. p_code = self.get_product_account_code(ed)
  308. s += " %(compte_produit)s \t %(amount_ht)s\n" % {
  309. 'compte_produit': settings.get_ledger_account(p_code),
  310. 'amount_ht': self._value(-ed.total_ht)
  311. }
  312. tvas = self.get_tva_billing_amounts(self.dolibarr_alchemy, self.e, journal="supplier")
  313. for k in tvas:
  314. s += " %(compte_tva)s \t %(amount_tva)s\n" % {
  315. 'compte_tva': settings.get_ledger_account(k),
  316. 'amount_tva': self._value(tvas[k]),
  317. }
  318. return s
  319. # get_tva_account: return the value-added tax account
  320. @classmethod
  321. def get_tva_account(cls, ed):
  322. p_code = cls.get_product_account_code(ed)
  323. tx = int(float(ed.tva_tx) * 100)
  324. if p_code.startswith("2"):
  325. prefix = 'tva_deductible'
  326. else:
  327. prefix = 'tva_deductible_immo'
  328. key = "%s_%s" % (prefix, tx)
  329. tva_account = settings.get('PC_REFS')[key]
  330. return tva_account
  331. # get_product_account_code: return the account code for the product of the current entry
  332. @classmethod
  333. def get_product_account_code(cls, ed):
  334. p_code = ""
  335. if ed.accounting_account:
  336. p_code = ed.accounting_account.account_number
  337. elif ed.product:
  338. p_code = ed.product.accountancy_code_buy
  339. else:
  340. p_code = cls.pc_default_expense
  341. return p_code
  342. # get_supplier_code: return the supplier account code for the current entry
  343. @classmethod
  344. def get_supplier_code(cls, e):
  345. s_code = e.societe.code_compta_fournisseur
  346. if s_code == "":
  347. s_code = cls.pc_default_supplier
  348. return s_code
  349. #
  350. # HledgerSellEntry: The billing entry corresponding to a client
  351. #
  352. class HledgerSellEntry(HledgerBillingEntry):
  353. k_accounting_date = 'datef'
  354. def _sql_class(self):
  355. return self.dolibarr_alchemy.Facture
  356. # get_entries: return the bill entries ordered by billing date
  357. @classmethod
  358. def get_entries(cls, dolibarr_alchemy):
  359. Facture = dolibarr_alchemy.Facture
  360. return [cls(dolibarr_alchemy, e) for e in dolibarr_alchemy.session.query(Facture).order_by(Facture.datef).all()]
  361. # get_ledger: return the ledger corresping to this entry
  362. def get_ledger(self):
  363. e = self.e
  364. self.check()
  365. s = ""
  366. # Date et description
  367. s += "%(date)s %(description)s\n" % {
  368. 'date': e.datef.strftime("%Y/%m/%d"),
  369. 'description': e.facnumber + " - " + e.societe.nom,
  370. }
  371. # ligne pour compte client
  372. s_code = self.get_client_code(self.e)
  373. s += " %(compte_tiers)s %(amount_ttc)s\n" % {
  374. 'compte_tiers': settings.get_ledger_account(s_code),
  375. 'amount_ttc': self._value(-e.total_ttc),
  376. }
  377. # lignes pour compte de produits
  378. for ed in e.details:
  379. p_code = self.get_product_account_code(ed)
  380. s += " %(compte_produit)s %(amount_ht)s\n" % {
  381. 'compte_produit': settings.get_ledger_account(p_code),
  382. 'amount_ht': self._value(ed.total_ht)
  383. }
  384. # lignes pour la tva
  385. tvas = self.get_tva_billing_amounts(self.dolibarr_alchemy, self.e, journal="sell")
  386. for k in tvas:
  387. s += " %(compte_tva)s %(amount_tva)s\n" % {
  388. 'compte_tva': settings.get_ledger_account(k),
  389. 'amount_tva': self._value(tvas[k]),
  390. }
  391. return s
  392. # get_tva_account: return the value-added tax account for the given product
  393. @classmethod
  394. def get_tva_account(cls, ed):
  395. tx = int(float(ed.tva_tx) * 100)
  396. key = "tva_collecte_%s" % (tx,)
  397. return settings.get('PC_REFS')[key]
  398. # getMissingPC: return the missing account for this entry
  399. def getMissingPC(self):
  400. e = self.e
  401. pc_missing = []
  402. if e.societe.code_compta == "":
  403. pc_missing.append("tiers: %s %s" % (e.societe.nom, e.societe.ape))
  404. for ed in e.details:
  405. if self.get_product_account_code(ed) == self.pc_default_income:
  406. if ed.description != "":
  407. description = ed.description.splitlines()[0]
  408. else:
  409. description = ed.description
  410. pc_missing.append("produit: %s - %s - %s" % (e.societe.nom, e.facnumber, description))
  411. return pc_missing
  412. # get_product_account_code: return the account code for the product of the current entry
  413. @classmethod
  414. def get_product_account_code(cls, ed):
  415. p_code = ""
  416. if ed.accounting_account:
  417. p_code = ed.accounting_account.account_number
  418. elif ed.product:
  419. p_code = ed.product.accountancy_code_sell
  420. else:
  421. p_code = cls.pc_default_income
  422. return p_code
  423. # get_client_code: return the account code for the client entry
  424. @classmethod
  425. def get_client_code(cls, e):
  426. s_code = e.societe.code_compta
  427. if s_code == "":
  428. s_code = cls.pc_default_client
  429. return s_code
  430. # check: check if the entry is coherent
  431. def check(self):
  432. e = self.e
  433. total_ttc = e.total_ttc
  434. total_tva = e.tva
  435. total_ht = e.total
  436. for ed in e.details:
  437. total_ttc -= ed.total_ttc
  438. total_tva -= ed.total_tva
  439. total_ht -= ed.total_ht
  440. if total_ttc > 1e-10:
  441. print("Erreur dans l'écriture %s: total ttc = %s" % (e.facnumber, total_ttc))
  442. if total_ht > 1e-10:
  443. print("Erreur dans l'écriture %s: total ht = %s" % (e.facnumber, total_ht))
  444. if total_tva > 1e-10:
  445. print("Erreur dans l'écriture %s: total tva = %s" % (e.facnumber, total_tva))
  446. #
  447. # HledgerSocialEntry: A ledger entry corresponding to a tax
  448. #
  449. class HledgerSocialEntry(HledgerEntry):
  450. k_accounting_date = 'periode'
  451. def _sql_class(self):
  452. return self.dolibarr_alchemy.CotisationsSociales
  453. # get_entries: retrieve all the entries for this entry type
  454. @classmethod
  455. def get_entries(cls, dolibarr_alchemy):
  456. CotisationsSociales = dolibarr_alchemy.CotisationsSociales
  457. entries = dolibarr_alchemy.session.query(CotisationsSociales).order_by(CotisationsSociales.date_ech).all()
  458. return [cls(dolibarr_alchemy, e) for e in entries]
  459. # get_third_code: return the third accounting code for this entry
  460. @classmethod
  461. def get_third_code(cls, e):
  462. third_code = ""
  463. if e.type.code in settings.get('SOCIAL_REFS'):
  464. third_code = settings.get('SOCIAL_REFS')[e.type.code]
  465. if third_code == "":
  466. third_code = cls.pc_default_supplier
  467. return third_code
  468. # get_social_code: return the social accounting code for this entry
  469. @classmethod
  470. def get_social_code(cls, e):
  471. s_code = ""
  472. if e.type:
  473. s_code = e.type.accountancy_code
  474. if s_code == "":
  475. s_code = cls.pc_default_expense
  476. return s_code
  477. # getMissingPC: return the missing accounting code for this entry
  478. def getMissingPC(self):
  479. e = self.e
  480. pc_missing = []
  481. if self.get_social_code(self.e) == self.pc_default_expense:
  482. pc_missing.append("expenses: %s" % (e.libelle))
  483. if self.get_third_code(self.e) == self.pc_default_supplier:
  484. pc_missing.append("tiers: %s (%s)" % (e.libelle, e.type.code))
  485. return pc_missing
  486. # get_ledger: return the ledger for this entry
  487. def get_ledger(self):
  488. e = self.e
  489. s = ""
  490. s += "%(date)s %(description)s\n" % {
  491. 'date': e.periode.strftime("%Y/%m/%d"),
  492. 'description': e.libelle + " - " + e.type.libelle
  493. }
  494. third_code = self.get_third_code(self.e)
  495. s_code = self.get_social_code(self.e)
  496. s += " %(account)s \t %(amount)s\n" % {
  497. 'account': settings.get_ledger_account(third_code),
  498. 'amount': self._value(e.amount)
  499. }
  500. s += " %(account)s \t %(amount)s\n" % {
  501. 'account': settings.get_ledger_account(s_code),
  502. 'amount': self._value(-e.amount)
  503. }
  504. return s
  505. # check: -
  506. def check(self):
  507. pass
  508. #
  509. # HledgerDolibarrSQLAlchemy: Top class for retrieving all the journals
  510. #
  511. class HledgerDolibarrSQLAlchemy(DolibarrSQLAlchemy):
  512. def get_bank_journal(self):
  513. return HledgerJournal(self, HledgerBankEntry)
  514. def get_supplier_journal(self):
  515. return HledgerJournal(self, HledgerSupplierEntry)
  516. def get_sell_journal(self):
  517. return HledgerJournal(self, HledgerSellEntry)
  518. def get_social_journal(self):
  519. return HledgerJournal(self, HledgerSocialEntry)