dolibarrAlchemyHledger.py 20 KB

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