account_journal_dashboard.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. import json
  2. import locale
  3. from datetime import datetime, timedelta
  4. from babel.dates import format_datetime, format_date
  5. from openerp import models, api, _, fields
  6. from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF
  7. class AccountMoveLine(models.Model):
  8. _inherit = "account.move.line"
  9. @api.depends('debit', 'credit')
  10. def _store_balance(self):
  11. for line in self:
  12. line.balance = line.debit - line.credit
  13. balance = fields.Float(compute='_store_balance', store=True, currency_field='company_currency_id', default=0.0, help="Technical field holding the debit - credit in order to open meaningful graph views from reports")
  14. class account_journal(models.Model):
  15. _inherit = "account.journal"
  16. @api.one
  17. def _kanban_dashboard(self):
  18. self.kanban_dashboard = json.dumps(self.get_journal_dashboard_datas())
  19. @api.one
  20. def _kanban_dashboard_graph(self):
  21. if (self.type in ['sale', 'purchase']):
  22. self.kanban_dashboard_graph = json.dumps(self.get_bar_graph_datas())
  23. elif (self.type in ['cash', 'bank']):
  24. self.kanban_dashboard_graph = json.dumps(self.get_line_graph_datas())
  25. kanban_dashboard = fields.Text(compute='_kanban_dashboard')
  26. kanban_dashboard_graph = fields.Text(compute='_kanban_dashboard_graph')
  27. show_on_dashboard = fields.Boolean(string='Show journal on dashboard', help="Whether this journal should be displayed on the dashboard or not", default=True)
  28. @api.multi
  29. def toggle_favorite(self):
  30. self.write({'show_on_dashboard': False if self.show_on_dashboard else True})
  31. return False
  32. @api.multi
  33. def get_line_graph_datas(self):
  34. data = []
  35. today = datetime.today()
  36. last_month = today + timedelta(days=-30)
  37. bank_stmt = []
  38. # Query to optimize loading of data for bank statement graphs
  39. # Return a list containing the latest bank statement balance per day for the
  40. # last 30 days for current journal
  41. query = """SELECT a.date, a.balance_end
  42. FROM account_bank_statement AS a,
  43. (SELECT c.date, max(c.id) AS stmt_id
  44. FROM account_bank_statement AS c
  45. WHERE c.journal_id = %s
  46. AND c.date > %s
  47. AND c.date <= %s
  48. GROUP BY date, id
  49. ORDER BY date, id) AS b
  50. WHERE a.id = b.stmt_id;"""
  51. self.env.cr.execute(query, (self.id, last_month, today))
  52. bank_stmt = self.env.cr.dictfetchall()
  53. last_bank_stmt = self.env['account.bank.statement'].search([('journal_id', 'in', self.ids),('date', '<=', last_month.strftime(DF))], order="date desc, id desc", limit=1)
  54. start_balance = last_bank_stmt and last_bank_stmt[0].balance_end or 0
  55. locale = self._context.get('lang', 'en_US')
  56. show_date = last_month
  57. #get date in locale format
  58. name = format_date(show_date, 'd LLLL Y', locale=locale)
  59. short_name = format_date(show_date, 'd MMM', locale=locale)
  60. data.append({'x':short_name,'y':start_balance, 'name':name})
  61. for stmt in bank_stmt:
  62. #fill the gap between last data and the new one
  63. number_day_to_add = (datetime.strptime(stmt.get('date'), DF) - show_date).days
  64. last_balance = data[len(data) - 1]['y']
  65. for day in range(0,number_day_to_add + 1):
  66. show_date = show_date + timedelta(days=1)
  67. #get date in locale format
  68. name = format_date(show_date, 'd LLLL Y', locale=locale)
  69. short_name = format_date(show_date, 'd MMM', locale=locale)
  70. data.append({'x': short_name, 'y':last_balance, 'name': name})
  71. #add new stmt value
  72. data[len(data) - 1]['y'] = stmt.get('balance_end')
  73. #continue the graph if the last statement isn't today
  74. if show_date != today:
  75. number_day_to_add = (today - show_date).days
  76. last_balance = data[len(data) - 1]['y']
  77. for day in range(0,number_day_to_add):
  78. show_date = show_date + timedelta(days=1)
  79. #get date in locale format
  80. name = format_date(show_date, 'd LLLL Y', locale=locale)
  81. short_name = format_date(show_date, 'd MMM', locale=locale)
  82. data.append({'x': short_name, 'y':last_balance, 'name': name})
  83. return [{'values': data, 'area': True, 'id': self.id}]
  84. @api.multi
  85. def get_bar_graph_datas(self):
  86. data = []
  87. today = datetime.strptime(fields.Date.context_today(self), DF)
  88. data.append({'label': _('Anteriores'), 'value':0.0, 'type': 'past'})
  89. day_of_week = int(format_datetime(today, 'e', locale=self._context.get('lang', 'en_US')))
  90. first_day_of_week = today + timedelta(days=-day_of_week+1)
  91. for i in range(-1,4):
  92. if i==0:
  93. label = _('Esta semana')
  94. elif i==3:
  95. label = _('Futuras')
  96. else:
  97. start_week = first_day_of_week + timedelta(days=i*7)
  98. end_week = start_week + timedelta(days=6)
  99. if start_week.month == end_week.month:
  100. label = str(start_week.day) + '-' +str(end_week.day)+ ' ' + format_date(end_week, 'MMM', locale=self._context.get('lang', 'en_US'))
  101. else:
  102. label = format_date(start_week, 'd MMM', locale=self._context.get('lang', 'en_US'))+'-'+format_date(end_week, 'd MMM', locale=self._context.get('lang', 'en_US'))
  103. data.append({'label':label,'value':0.0, 'type': 'past' if i<0 else 'future'})
  104. # Build SQL query to find amount aggregated by week
  105. select_sql_clause = """SELECT sum(amount_total) as total, min(date_invoice) as aggr_date from account_invoice where journal_id = %(journal_id)s and state IN ('open','paid')"""
  106. query = ''
  107. start_date = (first_day_of_week + timedelta(days=-7))
  108. for i in range(0,6):
  109. if i == 0:
  110. query += "("+select_sql_clause+" and date_invoice < '"+start_date.strftime(DF)+"')"
  111. elif i == 6:
  112. query += " UNION ALL ("+select_sql_clause+" and date_invoice >= '"+start_date.strftime(DF)+"')"
  113. else:
  114. next_date = start_date + timedelta(days=7)
  115. query += " UNION ALL ("+select_sql_clause+" and date_invoice >= '"+start_date.strftime(DF)+"' and date_invoice < '"+next_date.strftime(DF)+"')"
  116. start_date = next_date
  117. self.env.cr.execute(query, {'journal_id':self.id})
  118. query_results = self.env.cr.dictfetchall()
  119. for index in range(0, len(query_results)):
  120. if query_results[index].get('aggr_date') != None:
  121. data[index]['value'] = query_results[index].get('total')
  122. return [{'values': data, 'id': self.id}]
  123. @api.multi
  124. def get_journal_dashboard_datas(self):
  125. currency = self.currency.id or self.company_id.currency_id
  126. number_to_reconcile = last_balance = account_sum = 0
  127. ac_bnk_stmt = []
  128. title = ''
  129. number_draft = number_waiting = number_late = sum_draft = sum_waiting = sum_late = 0
  130. if self.type in ['bank', 'cash']:
  131. last_bank_stmt = self.env['account.bank.statement'].search([('journal_id', 'in', self.ids)], order="date desc, id desc", limit=1)
  132. last_balance = last_bank_stmt and last_bank_stmt[0].balance_end or 0
  133. ac_bnk_stmt = self.env['account.bank.statement'].search([('journal_id', 'in', self.ids),('state', '=', 'open')])
  134. for ac_bnk in ac_bnk_stmt:
  135. for line in ac_bnk.line_ids:
  136. if not line.journal_entry_ids:
  137. number_to_reconcile += 1
  138. # optimization to read sum of balance from account_move_line
  139. account_ids = tuple(filter(None, [self.default_debit_account_id.id, self.default_credit_account_id.id]))
  140. if account_ids:
  141. query = """SELECT sum(balance) FROM account_move_line WHERE account_id in %s;"""
  142. self.env.cr.execute(query, (account_ids,))
  143. query_results = self.env.cr.dictfetchall()
  144. if query_results and query_results[0].get('sum') != None:
  145. account_sum = query_results[0].get('sum')
  146. #TODO need to check if all invoices are in the same currency than the journal!!!!
  147. elif self.type in ['sale', 'purchase']:
  148. title = _('Bills to pay') if self.type == 'purchase' else _('Invoices owed to you')
  149. # optimization to find total and sum of invoice that are in draft, open state
  150. query = """SELECT state, amount_total, currency_id AS currency FROM account_invoice WHERE journal_id = %s AND state NOT IN ('paid', 'cancel');"""
  151. self.env.cr.execute(query, (self.id,))
  152. query_results = self.env.cr.dictfetchall()
  153. today = datetime.today()
  154. query = """SELECT amount_total, currency_id AS currency FROM account_invoice WHERE journal_id = %s AND date_invoice < %s AND state = 'open';"""
  155. self.env.cr.execute(query, (self.id, today))
  156. late_query_results = self.env.cr.dictfetchall()
  157. sum_draft = 0.0
  158. number_draft = 0
  159. number_waiting = 0
  160. for result in query_results:
  161. cur = self.env['res.currency'].browse(result.get('currency'))
  162. if result.get('state') in ['draft', 'proforma', 'proforma2']:
  163. number_draft += 1
  164. sum_draft += cur.compute(result.get('amount_total'), currency)
  165. elif result.get('state') == 'open':
  166. number_waiting += 1
  167. sum_waiting += cur.compute(result.get('amount_total'), currency)
  168. sum_late = 0.0
  169. number_late = 0
  170. for result in late_query_results:
  171. cur = self.env['res.currency'].browse(result.get('currency'))
  172. number_late += 1
  173. sum_late += cur.compute(result.get('amount_total'), currency)
  174. return {
  175. 'number_to_reconcile': number_to_reconcile,
  176. 'account_balance': self.formatLang(self.env, account_sum, currency_obj=self.currency.id or self.company_id.currency_id),
  177. 'last_balance': self.formatLang(self.env, last_balance, currency_obj=self.currency.id or self.company_id.currency_id),
  178. 'number_draft': number_draft,
  179. 'number_waiting': number_waiting,
  180. 'number_late': number_late,
  181. 'currency_id':currency.symbol,
  182. 'sum_draft': locale.format("%.2f",sum_draft , grouping=True),
  183. 'sum_waiting':locale.format("%.2f",sum_waiting , grouping=True),
  184. 'sum_late': self.formatLang(self.env, sum_late or 0.0, currency_obj=self.currency.id or self.company_id.currency_id),
  185. 'bank_statements_source': False,
  186. 'title': title,
  187. }
  188. @api.multi
  189. def action_create_new(self):
  190. ctx = self._context.copy()
  191. model = 'account.invoice'
  192. if self.type == 'sale':
  193. ctx.update({'journal_type': self.type, 'default_type': 'out_invoice', 'type': 'out_invoice', 'default_journal_id': self.id})
  194. if ctx.get('refund'):
  195. ctx.update({'default_type':'out_refund', 'type':'out_refund'})
  196. view_id = self.env.ref('account.invoice_form').id
  197. elif self.type == 'purchase':
  198. ctx.update({'journal_type': self.type, 'default_type': 'in_invoice', 'type': 'in_invoice', 'default_journal_id': self.id})
  199. if ctx.get('refund'):
  200. ctx.update({'default_type': 'in_refund', 'type': 'in_refund'})
  201. view_id = self.env.ref('account.invoice_supplier_form').id
  202. else:
  203. ctx.update({'default_journal_id': self.id})
  204. view_id = self.env.ref('account.view_move_form').id
  205. model = 'account.move'
  206. return {
  207. 'name': _('Create invoice/bill'),
  208. 'type': 'ir.actions.act_window',
  209. 'view_type': 'form',
  210. 'view_mode': 'form',
  211. 'res_model': model,
  212. 'view_id': view_id,
  213. 'context': ctx,
  214. }
  215. @api.multi
  216. def create_cash_statement(self):
  217. ctx = self._context.copy()
  218. ctx.update({'journal_id': self.id, 'default_journal_id': self.id, 'default_journal_type': 'cash'})
  219. return {
  220. 'name': _('Create cash statement'),
  221. 'type': 'ir.actions.act_window',
  222. 'view_type': 'form',
  223. 'view_mode': 'form',
  224. 'res_model': 'account.bank.statement',
  225. 'context': ctx,
  226. }
  227. @api.multi
  228. def action_open_reconcile(self):
  229. if self.type in ['bank', 'cash']:
  230. # Open reconciliation view for bank statements belonging to this journal
  231. bank_stmt = self.env['account.bank.statement'].search([('journal_id', 'in', self.ids)])
  232. return {
  233. 'type': 'ir.actions.client',
  234. 'tag': 'bank_statement_reconciliation_view',
  235. 'context': {'statement_ids': bank_stmt.ids},
  236. }
  237. else:
  238. # Open reconciliation view for customers/suppliers
  239. action_context = {'show_mode_selector': False}
  240. if self.type == 'sale':
  241. action_context.update({'mode': 'customer'})
  242. elif self.type == 'purchase':
  243. action_context.update({'mode': 'supplier'})
  244. return {
  245. 'type': 'ir.actions.client',
  246. 'tag': 'manual_reconciliation_view',
  247. 'context': action_context,
  248. }
  249. @api.multi
  250. def open_action(self):
  251. """return action based on type for related journals"""
  252. action_name = self._context.get('action_name', False)
  253. if not action_name:
  254. if self.type == 'bank':
  255. action_name = 'action_bank_statement_tree'
  256. elif self.type == 'cash':
  257. action_name = 'action_view_bank_statement_tree'
  258. elif self.type == 'sale':
  259. action_name = 'action_invoice_tree1'
  260. elif self.type == 'purchase':
  261. action_name = 'action_invoice_tree2'
  262. else:
  263. action_name = 'action_move_journal_line'
  264. _journal_invoice_type_map = {
  265. 'sale': 'out_invoice',
  266. 'purchase': 'in_invoice',
  267. 'bank': 'bank',
  268. 'cash': 'cash',
  269. 'general': 'general',
  270. }
  271. invoice_type = _journal_invoice_type_map[self.type]
  272. ctx = self._context.copy()
  273. ctx.update({
  274. 'journal_type': self.type,
  275. 'default_journal_id': self.id,
  276. 'search_default_journal_id': self.id,
  277. 'default_type': invoice_type,
  278. 'type': invoice_type
  279. })
  280. ir_model_obj = self.pool['ir.model.data']
  281. model, action_id = ir_model_obj.get_object_reference(self._cr, self._uid, 'account', action_name)
  282. action = self.pool[model].read(self._cr, self._uid, action_id, context=self._context)
  283. action['context'] = ctx
  284. action['domain'] = self._context.get('use_domain', [])
  285. return action
  286. @api.multi
  287. def open_spend_money(self):
  288. return self.open_payments_action('outbound')
  289. @api.multi
  290. def open_collect_money(self):
  291. return self.open_payments_action('inbound')
  292. @api.multi
  293. def open_transfer_money(self):
  294. return self.open_payments_action('transfer')
  295. @api.multi
  296. def open_payments_action(self, payment_type):
  297. ctx = self._context.copy()
  298. ctx.update({
  299. 'default_payment_type': payment_type,
  300. 'default_journal_id': self.id
  301. })
  302. action_rec = self.env['ir.model.data'].xmlid_to_object('account.action_account_payments')
  303. if action_rec:
  304. action = action_rec.read([])[0]
  305. action['context'] = ctx
  306. action['domain'] = [('journal_id','=',self.id),('payment_type','=',payment_type)]
  307. return action
  308. @api.multi
  309. def open_action_with_context(self):
  310. action_name = self.env.context.get('action_name', False)
  311. if not action_name:
  312. return False
  313. ctx = dict(self.env.context, default_journal_id=self.id)
  314. if ctx.get('search_default_journal', False):
  315. ctx.update(search_default_journal_id=self.id)
  316. ir_model_obj = self.pool['ir.model.data']
  317. model, action_id = ir_model_obj.get_object_reference(self._cr, self._uid, 'account', action_name)
  318. action = self.pool[model].read(self._cr, self._uid, action_id, context=self._context)
  319. action['context'] = ctx
  320. if ctx.get('use_domain', False):
  321. action['domain'] = ['|', ('journal_id', '=', self.id), ('journal_id', '=', False)]
  322. action['name'] += ' for journal '+self.name
  323. return action
  324. @api.multi
  325. def create_bank_statement(self):
  326. """return action to create a bank statements. This button should be called only on journals with type =='bank'"""
  327. self.bank_statements_source = 'manual'
  328. action = self.env.ref('account.action_bank_statement_tree').read()[0]
  329. action.update({
  330. 'views': [[False, 'form']],
  331. 'context': "{'default_journal_id': " + str(self.id) + "}",
  332. })
  333. return action
  334. def formatLang(self, env, value, digits=None, grouping=True, monetary=False, dp=False, currency_obj=False):
  335. """
  336. Assuming 'Account' decimal.precision=3:
  337. formatLang(value) -> digits=2 (default)
  338. formatLang(value, digits=4) -> digits=4
  339. formatLang(value, dp='Account') -> digits=3
  340. formatLang(value, digits=5, dp='Account') -> digits=5
  341. """
  342. if digits is None:
  343. digits = DEFAULT_DIGITS = 2
  344. if dp:
  345. decimal_precision_obj = env['decimal.precision']
  346. digits = decimal_precision_obj.precision_get(dp)
  347. elif (hasattr(value, '_field') and isinstance(value._field, (float_field, function_field)) and value._field.digits):
  348. digits = value._field.digits[1]
  349. if not digits and digits is not 0:
  350. digits = DEFAULT_DIGITS
  351. if isinstance(value, (str, unicode)) and not value:
  352. return ''
  353. lang = env.user.company_id.partner_id.lang or 'en_US'
  354. lang_objs = env['res.lang'].search([('code', '=', lang)])
  355. if not lang_objs:
  356. lang_objs = env['res.lang'].search([('code', '=', 'en_US')])
  357. lang_obj = lang_objs[0]
  358. res = lang_obj.format('%.' + str(digits) + 'f', value, grouping=grouping, monetary=monetary)
  359. #if currency_obj:
  360. # res = '%s %s' % (currency_obj.symbol, res)
  361. return res