448 lines
21 KiB
Python
448 lines
21 KiB
Python
import json
|
|
from datetime import datetime, timedelta
|
|
|
|
from babel.dates import format_datetime, format_date
|
|
|
|
from flectra import models, api, _, fields
|
|
from flectra.release import version
|
|
from flectra.tools import DEFAULT_SERVER_DATE_FORMAT as DF
|
|
from flectra.tools.misc import formatLang
|
|
|
|
class account_journal(models.Model):
|
|
_inherit = "account.journal"
|
|
|
|
@api.one
|
|
def _kanban_dashboard(self):
|
|
self.kanban_dashboard = json.dumps(self.get_journal_dashboard_datas())
|
|
|
|
@api.one
|
|
def _kanban_dashboard_graph(self):
|
|
if (self.type in ['sale', 'purchase']):
|
|
self.kanban_dashboard_graph = json.dumps(self.get_bar_graph_datas())
|
|
elif (self.type in ['cash', 'bank']):
|
|
self.kanban_dashboard_graph = json.dumps(self.get_line_graph_datas())
|
|
|
|
kanban_dashboard = fields.Text(compute='_kanban_dashboard')
|
|
kanban_dashboard_graph = fields.Text(compute='_kanban_dashboard_graph')
|
|
show_on_dashboard = fields.Boolean(string='Show journal on dashboard', help="Whether this journal should be displayed on the dashboard or not", default=True)
|
|
color = fields.Integer("Color Index", default=0)
|
|
account_setup_bank_data_done = fields.Boolean(string='Bank setup marked as done', related='company_id.account_setup_bank_data_done', help="Technical field used in the special view for the setup bar step.")
|
|
|
|
def _graph_title_and_key(self):
|
|
if self.type == 'sale':
|
|
return ['', _('Sales: Untaxed Total')]
|
|
elif self.type == 'purchase':
|
|
return ['', _('Purchase: Untaxed Total')]
|
|
elif self.type == 'cash':
|
|
return ['', _('Cash: Balance')]
|
|
elif self.type == 'bank':
|
|
return ['', _('Bank: Balance')]
|
|
|
|
@api.multi
|
|
def get_line_graph_datas(self):
|
|
data = []
|
|
today = datetime.today()
|
|
last_month = today + timedelta(days=-30)
|
|
bank_stmt = []
|
|
# Query to optimize loading of data for bank statement graphs
|
|
# Return a list containing the latest bank statement balance per day for the
|
|
# last 30 days for current journal
|
|
query = """SELECT a.date, a.balance_end
|
|
FROM account_bank_statement AS a,
|
|
(SELECT c.date, max(c.id) AS stmt_id
|
|
FROM account_bank_statement AS c
|
|
WHERE c.journal_id = %s
|
|
AND c.date > %s
|
|
AND c.date <= %s
|
|
GROUP BY date, id
|
|
ORDER BY date, id) AS b
|
|
WHERE a.id = b.stmt_id;"""
|
|
|
|
self.env.cr.execute(query, (self.id, last_month, today))
|
|
bank_stmt = self.env.cr.dictfetchall()
|
|
|
|
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)
|
|
start_balance = last_bank_stmt and last_bank_stmt[0].balance_end or 0
|
|
|
|
locale = self._context.get('lang') or 'en_US'
|
|
show_date = last_month
|
|
#get date in locale format
|
|
name = format_date(show_date, 'd LLLL Y', locale=locale)
|
|
short_name = format_date(show_date, 'd MMM', locale=locale)
|
|
data.append({'x':short_name,'y':start_balance, 'name':name})
|
|
|
|
for stmt in bank_stmt:
|
|
#fill the gap between last data and the new one
|
|
number_day_to_add = (datetime.strptime(stmt.get('date'), DF) - show_date).days
|
|
last_balance = data[len(data) - 1]['y']
|
|
for day in range(0,number_day_to_add + 1):
|
|
show_date = show_date + timedelta(days=1)
|
|
#get date in locale format
|
|
name = format_date(show_date, 'd LLLL Y', locale=locale)
|
|
short_name = format_date(show_date, 'd MMM', locale=locale)
|
|
data.append({'x': short_name, 'y':last_balance, 'name': name})
|
|
#add new stmt value
|
|
data[len(data) - 1]['y'] = stmt.get('balance_end')
|
|
|
|
#continue the graph if the last statement isn't today
|
|
if show_date != today:
|
|
number_day_to_add = (today - show_date).days
|
|
last_balance = data[len(data) - 1]['y']
|
|
for day in range(0,number_day_to_add):
|
|
show_date = show_date + timedelta(days=1)
|
|
#get date in locale format
|
|
name = format_date(show_date, 'd LLLL Y', locale=locale)
|
|
short_name = format_date(show_date, 'd MMM', locale=locale)
|
|
data.append({'x': short_name, 'y':last_balance, 'name': name})
|
|
|
|
[graph_title, graph_key] = self._graph_title_and_key()
|
|
color = '#875A7B' if '+e' in version else '#7c7bad'
|
|
return [{'values': data, 'title': graph_title, 'key': graph_key, 'area': True, 'color': color}]
|
|
|
|
@api.multi
|
|
def get_bar_graph_datas(self):
|
|
data = []
|
|
today = datetime.strptime(fields.Date.context_today(self), DF)
|
|
data.append({'label': _('Past'), 'value':0.0, 'type': 'past'})
|
|
day_of_week = int(format_datetime(today, 'e', locale=self._context.get('lang') or 'en_US'))
|
|
first_day_of_week = today + timedelta(days=-day_of_week+1)
|
|
for i in range(-1,4):
|
|
if i==0:
|
|
label = _('This Week')
|
|
elif i==3:
|
|
label = _('Future')
|
|
else:
|
|
start_week = first_day_of_week + timedelta(days=i*7)
|
|
end_week = start_week + timedelta(days=6)
|
|
if start_week.month == end_week.month:
|
|
label = str(start_week.day) + '-' +str(end_week.day)+ ' ' + format_date(end_week, 'MMM', locale=self._context.get('lang') or 'en_US')
|
|
else:
|
|
label = format_date(start_week, 'd MMM', locale=self._context.get('lang') or 'en_US')+'-'+format_date(end_week, 'd MMM', locale=self._context.get('lang') or 'en_US')
|
|
data.append({'label':label,'value':0.0, 'type': 'past' if i<0 else 'future'})
|
|
|
|
# Build SQL query to find amount aggregated by week
|
|
(select_sql_clause, query_args) = self._get_bar_graph_select_query()
|
|
query = ''
|
|
start_date = (first_day_of_week + timedelta(days=-7))
|
|
for i in range(0,6):
|
|
if i == 0:
|
|
query += "("+select_sql_clause+" and date < '"+start_date.strftime(DF)+"')"
|
|
elif i == 5:
|
|
query += " UNION ALL ("+select_sql_clause+" and date >= '"+start_date.strftime(DF)+"')"
|
|
else:
|
|
next_date = start_date + timedelta(days=7)
|
|
query += " UNION ALL ("+select_sql_clause+" and date >= '"+start_date.strftime(DF)+"' and date < '"+next_date.strftime(DF)+"')"
|
|
start_date = next_date
|
|
|
|
self.env.cr.execute(query, query_args)
|
|
query_results = self.env.cr.dictfetchall()
|
|
for index in range(0, len(query_results)):
|
|
if query_results[index].get('aggr_date') != None:
|
|
data[index]['value'] = query_results[index].get('total')
|
|
|
|
[graph_title, graph_key] = self._graph_title_and_key()
|
|
return [{'values': data, 'title': graph_title, 'key': graph_key}]
|
|
|
|
def _get_bar_graph_select_query(self):
|
|
"""
|
|
Returns a tuple containing the base SELECT SQL query used to gather
|
|
the bar graph's data as its first element, and the arguments dictionary
|
|
for it as its second.
|
|
"""
|
|
return ("""SELECT sum(residual_company_signed) as total, min(date) as aggr_date
|
|
FROM account_invoice
|
|
WHERE journal_id = %(journal_id)s and state = 'open'""", {'journal_id':self.id})
|
|
|
|
@api.multi
|
|
def get_journal_dashboard_datas(self):
|
|
currency = self.currency_id or self.company_id.currency_id
|
|
number_to_reconcile = last_balance = account_sum = 0
|
|
title = ''
|
|
number_draft = number_waiting = number_late = 0
|
|
sum_draft = sum_waiting = sum_late = 0.0
|
|
if self.type in ['bank', 'cash']:
|
|
last_bank_stmt = self.env['account.bank.statement'].search([('journal_id', 'in', self.ids)], order="date desc, id desc", limit=1)
|
|
last_balance = last_bank_stmt and last_bank_stmt[0].balance_end or 0
|
|
#Get the number of items to reconcile for that bank journal
|
|
self.env.cr.execute("""SELECT COUNT(DISTINCT(line.id))
|
|
FROM account_bank_statement_line AS line
|
|
LEFT JOIN account_bank_statement AS st
|
|
ON line.statement_id = st.id
|
|
WHERE st.journal_id IN %s AND st.state = 'open' AND line.amount != 0.0
|
|
AND not exists (select 1 from account_move_line aml where aml.statement_line_id = line.id)
|
|
""", (tuple(self.ids),))
|
|
number_to_reconcile = self.env.cr.fetchone()[0]
|
|
# optimization to read sum of balance from account_move_line
|
|
account_ids = tuple(ac for ac in [self.default_debit_account_id.id, self.default_credit_account_id.id] if ac)
|
|
if account_ids:
|
|
amount_field = 'balance' if (not self.currency_id or self.currency_id == self.company_id.currency_id) else 'amount_currency'
|
|
query = """SELECT sum(%s) FROM account_move_line WHERE account_id in %%s AND date <= %%s;""" % (amount_field,)
|
|
self.env.cr.execute(query, (account_ids, fields.Date.today(),))
|
|
query_results = self.env.cr.dictfetchall()
|
|
if query_results and query_results[0].get('sum') != None:
|
|
account_sum = query_results[0].get('sum')
|
|
#TODO need to check if all invoices are in the same currency than the journal!!!!
|
|
elif self.type in ['sale', 'purchase']:
|
|
title = _('Bills to pay') if self.type == 'purchase' else _('Invoices owed to you')
|
|
|
|
(query, query_args) = self._get_open_bills_to_pay_query()
|
|
self.env.cr.execute(query, query_args)
|
|
query_results_to_pay = self.env.cr.dictfetchall()
|
|
|
|
(query, query_args) = self._get_draft_bills_query()
|
|
self.env.cr.execute(query, query_args)
|
|
query_results_drafts = self.env.cr.dictfetchall()
|
|
|
|
today = datetime.today()
|
|
query = """SELECT amount_total, currency_id AS currency, type FROM account_invoice WHERE journal_id = %s AND date < %s AND state = 'open';"""
|
|
self.env.cr.execute(query, (self.id, today))
|
|
late_query_results = self.env.cr.dictfetchall()
|
|
(number_waiting, sum_waiting) = self._count_results_and_sum_amounts(query_results_to_pay, currency)
|
|
(number_draft, sum_draft) = self._count_results_and_sum_amounts(query_results_drafts, currency)
|
|
(number_late, sum_late) = self._count_results_and_sum_amounts(late_query_results, currency)
|
|
|
|
difference = currency.round(last_balance-account_sum) + 0.0
|
|
return {
|
|
'number_to_reconcile': number_to_reconcile,
|
|
'account_balance': formatLang(self.env, currency.round(account_sum) + 0.0, currency_obj=currency),
|
|
'last_balance': formatLang(self.env, currency.round(last_balance) + 0.0, currency_obj=currency),
|
|
'difference': formatLang(self.env, difference, currency_obj=currency) if difference else False,
|
|
'number_draft': number_draft,
|
|
'number_waiting': number_waiting,
|
|
'number_late': number_late,
|
|
'sum_draft': formatLang(self.env, currency.round(sum_draft) + 0.0, currency_obj=currency),
|
|
'sum_waiting': formatLang(self.env, currency.round(sum_waiting) + 0.0, currency_obj=currency),
|
|
'sum_late': formatLang(self.env, currency.round(sum_late) + 0.0, currency_obj=currency),
|
|
'currency_id': currency.id,
|
|
'bank_statements_source': self.bank_statements_source,
|
|
'title': title,
|
|
}
|
|
|
|
def _get_open_bills_to_pay_query(self):
|
|
"""
|
|
Returns a tuple contaning the SQL query used to gather the open bills
|
|
data as its first element, and the arguments dictionary to use to run
|
|
it as its second.
|
|
"""
|
|
return ("""SELECT state, amount_total, currency_id AS currency, type
|
|
FROM account_invoice
|
|
WHERE journal_id = %(journal_id)s AND state = 'open';""", {'journal_id':self.id})
|
|
|
|
def _get_draft_bills_query(self):
|
|
"""
|
|
Returns a tuple containing as its first element the SQL query used to
|
|
gather the bills in draft state data, and the arguments
|
|
dictionary to use to run it as its second.
|
|
"""
|
|
return ("""SELECT state, amount_total, currency_id AS currency, type
|
|
FROM account_invoice
|
|
WHERE journal_id = %(journal_id)s AND state = 'draft';""", {'journal_id':self.id})
|
|
|
|
def _count_results_and_sum_amounts(self, results_dict, target_currency):
|
|
""" Loops on a query result to count the total number of invoices and sum
|
|
their amount_total field (expressed in the given target currency).
|
|
"""
|
|
rslt_count = 0
|
|
rslt_sum = 0.0
|
|
for result in results_dict:
|
|
cur = self.env['res.currency'].browse(result.get('currency'))
|
|
rslt_count += 1
|
|
|
|
type_factor = result.get('type') in ('in_refund', 'out_refund') and -1 or 1
|
|
rslt_sum += type_factor * cur.compute(result.get('amount_total'), target_currency)
|
|
return (rslt_count, rslt_sum)
|
|
|
|
@api.multi
|
|
def action_create_new(self):
|
|
ctx = self._context.copy()
|
|
model = 'account.invoice'
|
|
if self.type == 'sale':
|
|
ctx.update({'journal_type': self.type, 'default_type': 'out_invoice', 'type': 'out_invoice', 'default_journal_id': self.id})
|
|
if ctx.get('refund'):
|
|
ctx.update({'default_type':'out_refund', 'type':'out_refund'})
|
|
view_id = self.env.ref('account.invoice_form').id
|
|
elif self.type == 'purchase':
|
|
ctx.update({'journal_type': self.type, 'default_type': 'in_invoice', 'type': 'in_invoice', 'default_journal_id': self.id})
|
|
if ctx.get('refund'):
|
|
ctx.update({'default_type': 'in_refund', 'type': 'in_refund'})
|
|
view_id = self.env.ref('account.invoice_supplier_form').id
|
|
else:
|
|
ctx.update({'default_journal_id': self.id, 'view_no_maturity': True})
|
|
view_id = self.env.ref('account.view_move_form').id
|
|
model = 'account.move'
|
|
return {
|
|
'name': _('Create invoice/bill'),
|
|
'type': 'ir.actions.act_window',
|
|
'view_type': 'form',
|
|
'view_mode': 'form',
|
|
'res_model': model,
|
|
'view_id': view_id,
|
|
'context': ctx,
|
|
}
|
|
|
|
@api.multi
|
|
def create_cash_statement(self):
|
|
ctx = self._context.copy()
|
|
ctx.update({'journal_id': self.id, 'default_journal_id': self.id, 'default_journal_type': 'cash'})
|
|
return {
|
|
'name': _('Create cash statement'),
|
|
'type': 'ir.actions.act_window',
|
|
'view_type': 'form',
|
|
'view_mode': 'form',
|
|
'res_model': 'account.bank.statement',
|
|
'context': ctx,
|
|
}
|
|
|
|
@api.multi
|
|
def action_open_reconcile(self):
|
|
if self.type in ['bank', 'cash']:
|
|
# Open reconciliation view for bank statements belonging to this journal
|
|
bank_stmt = self.env['account.bank.statement'].search([('journal_id', 'in', self.ids)])
|
|
return {
|
|
'type': 'ir.actions.client',
|
|
'tag': 'bank_statement_reconciliation_view',
|
|
'context': {'statement_ids': bank_stmt.ids, 'company_ids': self.mapped('company_id').ids},
|
|
}
|
|
else:
|
|
# Open reconciliation view for customers/suppliers
|
|
action_context = {'show_mode_selector': False, 'company_ids': self.mapped('company_id').ids}
|
|
if self.type == 'sale':
|
|
action_context.update({'mode': 'customers'})
|
|
elif self.type == 'purchase':
|
|
action_context.update({'mode': 'suppliers'})
|
|
return {
|
|
'type': 'ir.actions.client',
|
|
'tag': 'manual_reconciliation_view',
|
|
'context': action_context,
|
|
}
|
|
|
|
@api.multi
|
|
def open_action(self):
|
|
"""return action based on type for related journals"""
|
|
action_name = self._context.get('action_name', False)
|
|
if not action_name:
|
|
if self.type == 'bank':
|
|
action_name = 'action_bank_statement_tree'
|
|
elif self.type == 'cash':
|
|
action_name = 'action_view_bank_statement_tree'
|
|
elif self.type == 'sale':
|
|
action_name = 'action_invoice_tree1'
|
|
elif self.type == 'purchase':
|
|
action_name = 'action_invoice_tree2'
|
|
else:
|
|
action_name = 'action_move_journal_line'
|
|
|
|
_journal_invoice_type_map = {
|
|
('sale', None): 'out_invoice',
|
|
('purchase', None): 'in_invoice',
|
|
('sale', 'refund'): 'out_refund',
|
|
('purchase', 'refund'): 'in_refund',
|
|
('bank', None): 'bank',
|
|
('cash', None): 'cash',
|
|
('general', None): 'general',
|
|
}
|
|
invoice_type = _journal_invoice_type_map[(self.type, self._context.get('invoice_type'))]
|
|
|
|
ctx = self._context.copy()
|
|
ctx.pop('group_by', None)
|
|
ctx.update({
|
|
'journal_type': self.type,
|
|
'default_journal_id': self.id,
|
|
'search_default_journal_id': self.id,
|
|
'default_type': invoice_type,
|
|
'type': invoice_type
|
|
})
|
|
|
|
[action] = self.env.ref('account.%s' % action_name).read()
|
|
if not self.env.context.get('use_domain'):
|
|
ctx['search_default_journal_id'] = self.id
|
|
action['context'] = ctx
|
|
action['domain'] = self._context.get('use_domain', [])
|
|
account_invoice_filter = self.env.ref('account.view_account_invoice_filter', False)
|
|
if action_name in ['action_invoice_tree1', 'action_invoice_tree2']:
|
|
action['search_view_id'] = account_invoice_filter and account_invoice_filter.id or False
|
|
if action_name in ['action_bank_statement_tree', 'action_view_bank_statement_tree']:
|
|
action['views'] = False
|
|
action['view_id'] = False
|
|
return action
|
|
|
|
@api.multi
|
|
def open_spend_money(self):
|
|
return self.open_payments_action('outbound')
|
|
|
|
@api.multi
|
|
def open_collect_money(self):
|
|
return self.open_payments_action('inbound')
|
|
|
|
@api.multi
|
|
def open_transfer_money(self):
|
|
return self.open_payments_action('transfer')
|
|
|
|
@api.multi
|
|
def open_payments_action(self, payment_type):
|
|
ctx = self._context.copy()
|
|
ctx.update({
|
|
'default_payment_type': payment_type,
|
|
'default_journal_id': self.id
|
|
})
|
|
ctx.pop('group_by', None)
|
|
action_rec = self.env['ir.model.data'].xmlid_to_object('account.action_account_payments')
|
|
if action_rec:
|
|
action = action_rec.read([])[0]
|
|
action['context'] = ctx
|
|
action['domain'] = [('journal_id','=',self.id),('payment_type','=',payment_type)]
|
|
return action
|
|
|
|
@api.multi
|
|
def open_action_with_context(self):
|
|
action_name = self.env.context.get('action_name', False)
|
|
if not action_name:
|
|
return False
|
|
ctx = dict(self.env.context, default_journal_id=self.id)
|
|
if ctx.get('search_default_journal', False):
|
|
ctx.update(search_default_journal_id=self.id)
|
|
ctx.pop('group_by', None)
|
|
ir_model_obj = self.env['ir.model.data']
|
|
model, action_id = ir_model_obj.get_object_reference('account', action_name)
|
|
[action] = self.env[model].browse(action_id).read()
|
|
action['context'] = ctx
|
|
if ctx.get('use_domain', False):
|
|
action['domain'] = ['|', ('journal_id', '=', self.id), ('journal_id', '=', False)]
|
|
action['name'] += ' for journal ' + self.name
|
|
return action
|
|
|
|
@api.multi
|
|
def create_bank_statement(self):
|
|
"""return action to create a bank statements. This button should be called only on journals with type =='bank'"""
|
|
self.bank_statements_source = 'manual'
|
|
action = self.env.ref('account.action_bank_statement_tree').read()[0]
|
|
action.update({
|
|
'views': [[False, 'form']],
|
|
'context': "{'default_journal_id': " + str(self.id) + "}",
|
|
})
|
|
return action
|
|
|
|
#####################
|
|
# Setup Steps Stuff #
|
|
#####################
|
|
@api.model
|
|
def retrieve_account_dashboard_setup_bar(self):
|
|
""" Returns the data used by the setup bar on the Accounting app dashboard."""
|
|
company = self.env.user.company_id
|
|
return {
|
|
'show_setup_bar': not company.account_setup_bar_closed,
|
|
'company': company.account_setup_company_data_done,
|
|
'bank': company.account_setup_bank_data_done,
|
|
'fiscal_year': company.account_setup_fy_data_done,
|
|
'chart_of_accounts': company.account_setup_coa_done,
|
|
'initial_balance': company.opening_move_posted(),
|
|
}
|
|
|
|
def mark_bank_setup_as_done_action(self):
|
|
""" Marks the 'bank setup' step as done in the setup bar and in the company."""
|
|
self.company_id.account_setup_bank_data_done = True
|
|
|
|
def unmark_bank_setup_as_done_action(self):
|
|
""" Marks the 'bank setup' step as not done in the setup bar and in the company."""
|
|
self.company_id.account_setup_bank_data_done = False
|