diff --git a/addons/account_budget/__manifest__.py b/addons/account_budget/__manifest__.py
index b9b7766c65a..d3c9cbfab40 100644
--- a/addons/account_budget/__manifest__.py
+++ b/addons/account_budget/__manifest__.py
@@ -2,30 +2,11 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
- 'name': 'Budgets Management',
+ 'name': 'Budget Management',
'category': 'Accounting',
'description': """
-This module allows accountants to manage analytic and crossovered budgets.
-==========================================================================
-
-Once the Budgets are defined (in Invoicing/Budgets/Budgets), the Project Managers
-can set the planned amount on each Analytic Account.
-
-The accountant has the possibility to see the total of amount planned for each
-Budget in order to ensure the total planned is not greater/lower than what he
-planned for this Budget. Each list of record can also be switched to a graphical
-view of it.
-
-Three reports are available:
-----------------------------
- 1. The first is available from a list of Budgets. It gives the spreading, for
- these Budgets, of the Analytic Accounts.
-
- 2. The second is a summary of the previous one, it only gives the spreading,
- for the selected Budgets, of the Analytic Accounts.
-
- 3. The last one is available from the Analytic Chart of Accounts. It gives
- the spreading, for the selected Analytic Accounts of Budgets.
+Use budgets to compare actual with expected revenues and costs
+--------------------------------------------------------------
""",
'website': 'https://www.odoo.com/page/accounting',
'depends': ['account'],
diff --git a/addons/account_budget/data/account_budget_demo.xml b/addons/account_budget/data/account_budget_demo.xml
index 2c8654419ee..cd3b2f6a5e1 100644
--- a/addons/account_budget/data/account_budget_demo.xml
+++ b/addons/account_budget/data/account_budget_demo.xml
@@ -2,21 +2,205 @@
+
-
-
+
+
-
-
+
+
-
-
+
+
+
+
+ -35000
+
+
+
+
+
+
+ 10000
+
+
+
+
+
+
+ 10000
+
+
+
+
+
+
+ 12000
+
+
+
+
+
+
+ 15000
+
+
+
+
+
+
+ 15000
+
+
+
+
+
+
+ 15000
+
+
+
+
+
+
+ 13000
+
+
+
+
+
+
+ 9000
+
+
+
+
+
+
+ 8000
+
+
+
+
+
+
+ 15000
+
+
+
+
+
+
+ 15000
+
+
+
+
+
+
+ 18000
+
+
+
+
-
-
+
+
-
-
+
+
+
+
+
+
+
+
+ -55000
+
+
+
+
+
+
+ 9000
+
+
+
+
+
+
+ 8000
+
+
+
+
+
+
+ 10000
+
+
+
+
+
+
+ 14000
+
+
+
+
+
+
+ 16000
+
+
+
+
+
+
+ 13000
+
+
+
+
+
+
+ 10000
+
+
+
+
+
+
+ 8000
+
+
+
+
+
+
+ 7000
+
+
+
+
+
+
+ 12000
+
+
+
+
+
+
+ 17000
+
+
+
+
+
+
+ 17000
+
diff --git a/addons/account_budget/models/account_budget.py b/addons/account_budget/models/account_budget.py
index b27f389cdbc..961c957c2d7 100644
--- a/addons/account_budget/models/account_budget.py
+++ b/addons/account_budget/models/account_budget.py
@@ -2,8 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
-from odoo.tools import ustr
-from odoo.exceptions import UserError, ValidationError
+from odoo.exceptions import ValidationError
# ---------------------------------------------------------
# Budgets
@@ -16,7 +15,6 @@ class AccountBudgetPost(models.Model):
name = fields.Char('Name', required=True)
account_ids = fields.Many2many('account.account', 'account_budget_rel', 'budget_id', 'account_id', 'Accounts',
domain=[('deprecated', '=', False)])
- crossovered_budget_line = fields.One2many('crossovered.budget.lines', 'general_budget_id', 'Budget Lines')
company_id = fields.Many2one('res.company', 'Company', required=True,
default=lambda self: self.env['res.company']._company_default_get('account.budget.post'))
@@ -47,7 +45,7 @@ class CrossoveredBudget(models.Model):
_inherit = ['mail.thread']
name = fields.Char('Budget Name', required=True, states={'done': [('readonly', True)]})
- creating_user_id = fields.Many2one('res.users', 'Responsible', default=lambda self: self.env.user)
+ user_id = fields.Many2one('res.users', 'Responsible', default=lambda self: self.env.user, oldname='creating_user_id')
date_from = fields.Date('Start Date', required=True, states={'done': [('readonly', True)]})
date_to = fields.Date('End Date', required=True, states={'done': [('readonly', True)]})
state = fields.Selection([
@@ -87,87 +85,194 @@ class CrossoveredBudgetLines(models.Model):
_name = "crossovered.budget.lines"
_description = "Budget Line"
+ name = fields.Char(compute='_compute_line_name')
crossovered_budget_id = fields.Many2one('crossovered.budget', 'Budget', ondelete='cascade', index=True, required=True)
analytic_account_id = fields.Many2one('account.analytic.account', 'Analytic Account')
- general_budget_id = fields.Many2one('account.budget.post', 'Budgetary Position', required=True)
+ analytic_group_id = fields.Many2one('account.analytic.group', 'Analytic Group', related='analytic_account_id.group_id', readonly=True)
+ general_budget_id = fields.Many2one('account.budget.post', 'Budgetary Position')
date_from = fields.Date('Start Date', required=True)
date_to = fields.Date('End Date', required=True)
paid_date = fields.Date('Paid Date')
- planned_amount = fields.Float('Planned Amount', required=True, digits=0)
- practical_amount = fields.Float(compute='_compute_practical_amount', string='Practical Amount', digits=0)
- theoritical_amount = fields.Float(compute='_compute_theoritical_amount', string='Theoretical Amount', digits=0)
- percentage = fields.Float(compute='_compute_percentage', string='Achievement')
+ currency_id = fields.Many2one('res.currency', related='company_id.currency_id', readonly=True)
+ planned_amount = fields.Monetary(
+ 'Planned Amount', required=True,
+ help="Amount you plan to earn/spend. Record a positive amount if it is a revenue and a negative amount if it is a cost.")
+ practical_amount = fields.Monetary(
+ compute='_compute_practical_amount', string='Practical Amount', help="Amount really earned/spent.")
+ theoritical_amount = fields.Monetary(
+ compute='_compute_theoritical_amount', string='Theoretical Amount',
+ help="Amount you are supposed to have earned/spent at this date.")
+ percentage = fields.Float(
+ compute='_compute_percentage', string='Achievement',
+ help="Comparison between practical and theoretical amount. This measure tells you if you are below or over budget.")
company_id = fields.Many2one(related='crossovered_budget_id.company_id', comodel_name='res.company',
string='Company', store=True, readonly=True)
+ is_above_budget = fields.Boolean(compute='_is_above_budget')
+ crossovered_budget_state = fields.Selection(related='crossovered_budget_id.state', string='Budget State', store=True, readonly=True)
+
+ @api.model
+ def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
+ # overrides the default read_group in order to compute the computed fields manually for the group
+
+ result = super(CrossoveredBudgetLines, self).read_group(domain, fields, groupby, offset=offset, limit=limit,
+ orderby=orderby, lazy=lazy)
+ fields_list = ['practical_amount', 'theoritical_amount', 'percentage']
+ if any(x in fields for x in fields_list):
+ for group_line in result:
+
+ # initialise fields to compute to 0 if they are requested
+ if 'practical_amount' in fields:
+ group_line['practical_amount'] = 0
+ if 'theoritical_amount' in fields:
+ group_line['theoritical_amount'] = 0
+ if 'percentage' in fields:
+ group_line['percentage'] = 0
+ group_line['practical_amount'] = 0
+ group_line['theoritical_amount'] = 0
+
+ if group_line.get('__domain'):
+ all_budget_lines_that_compose_group = self.search(group_line['__domain'])
+ else:
+ all_budget_lines_that_compose_group = self.search([])
+ for budget_line_of_group in all_budget_lines_that_compose_group:
+ if 'practical_amount' in fields or 'percentage' in fields:
+ group_line['practical_amount'] += budget_line_of_group.practical_amount
+
+ if 'theoritical_amount' in fields or 'percentage' in fields:
+ group_line['theoritical_amount'] += budget_line_of_group.theoritical_amount
+
+ if 'percentage' in fields:
+ if group_line['theoritical_amount']:
+ # use a weighted average
+ group_line['percentage'] = float(
+ (group_line['practical_amount'] or 0.0) / group_line['theoritical_amount']) * 100
+
+ return result
+
+ @api.multi
+ def _is_above_budget(self):
+ for line in self:
+ if line.theoritical_amount >= 0:
+ line.is_above_budget = line.practical_amount > line.theoritical_amount
+ else:
+ line.is_above_budget = line.practical_amount < line.theoritical_amount
+
+ @api.multi
+ def _compute_line_name(self):
+ #just in case someone opens the budget line in form view
+ computed_name = self.crossovered_budget_id.name
+ if self.general_budget_id:
+ computed_name += ' - ' + self.general_budget_id.name
+ if self.analytic_account_id:
+ computed_name += ' - ' + self.analytic_account_id.name
+ self.name = computed_name
@api.multi
def _compute_practical_amount(self):
for line in self:
- result = 0.0
acc_ids = line.general_budget_id.account_ids.ids
- date_to = self.env.context.get('wizard_date_to') or line.date_to
- date_from = self.env.context.get('wizard_date_from') or line.date_from
+ date_to = line.date_to
+ date_from = line.date_from
if line.analytic_account_id.id:
- self.env.cr.execute("""
- SELECT SUM(amount)
- FROM account_analytic_line
- WHERE account_id=%s
- AND (date between to_date(%s,'yyyy-mm-dd') AND to_date(%s,'yyyy-mm-dd'))
- AND general_account_id=ANY(%s)""",
- (line.analytic_account_id.id, date_from, date_to, acc_ids,))
- result = self.env.cr.fetchone()[0] or 0.0
- line.practical_amount = result
+ analytic_line_obj = self.env['account.analytic.line']
+ domain = [('account_id', '=', line.analytic_account_id.id),
+ ('date', '>=', date_from),
+ ('date', '<=', date_to),
+ ]
+ if acc_ids:
+ domain += [('general_account_id', 'in', acc_ids)]
+
+ where_query = analytic_line_obj._where_calc(domain)
+ analytic_line_obj._apply_ir_rules(where_query, 'read')
+ from_clause, where_clause, where_clause_params = where_query.get_sql()
+ select = "SELECT SUM(amount) from " + from_clause + " where " + where_clause
+
+ else:
+ aml_obj = self.env['account.move.line']
+ domain = [('account_id', 'in',
+ line.general_budget_id.account_ids.ids),
+ ('date', '>=', date_from),
+ ('date', '<=', date_to)
+ ]
+ where_query = aml_obj._where_calc(domain)
+ aml_obj._apply_ir_rules(where_query, 'read')
+ from_clause, where_clause, where_clause_params = where_query.get_sql()
+ select = "SELECT sum(credit)-sum(debit) from " + from_clause + " where " + where_clause
+
+ self.env.cr.execute(select, where_clause_params)
+ line.practical_amount = self.env.cr.fetchone()[0] or 0.0
@api.multi
def _compute_theoritical_amount(self):
- today = fields.Datetime.now()
+ # beware: 'today' variable is mocked in the python tests and thus, its implementation matter
+ today = fields.Datetime.from_string(fields.Date.today())
for line in self:
- # Used for the report
-
- if self.env.context.get('wizard_date_from') and self.env.context.get('wizard_date_to'):
- date_from = fields.Datetime.from_string(self.env.context.get('wizard_date_from'))
- date_to = fields.Datetime.from_string(self.env.context.get('wizard_date_to'))
- if date_from < fields.Datetime.from_string(line.date_from):
- date_from = fields.Datetime.from_string(line.date_from)
- elif date_from > fields.Datetime.from_string(line.date_to):
- date_from = False
-
- if date_to > fields.Datetime.from_string(line.date_to):
- date_to = fields.Datetime.from_string(line.date_to)
- elif date_to < fields.Datetime.from_string(line.date_from):
- date_to = False
-
- theo_amt = 0.00
- if date_from and date_to:
- line_timedelta = fields.Datetime.from_string(line.date_to) - fields.Datetime.from_string(line.date_from)
- elapsed_timedelta = date_to - date_from
- if elapsed_timedelta.days > 0:
- theo_amt = (elapsed_timedelta.total_seconds() / line_timedelta.total_seconds()) * line.planned_amount
- else:
- if line.paid_date:
- if fields.Datetime.from_string(line.date_to) <= fields.Datetime.from_string(line.paid_date):
- theo_amt = 0.00
- else:
- theo_amt = line.planned_amount
+ if line.paid_date:
+ if fields.Datetime.to_string(today) <= line.paid_date:
+ theo_amt = 0.00
else:
- line_timedelta = fields.Datetime.from_string(line.date_to) - fields.Datetime.from_string(line.date_from)
- elapsed_timedelta = fields.Datetime.from_string(today) - (fields.Datetime.from_string(line.date_from))
-
- if elapsed_timedelta.days < 0:
- # If the budget line has not started yet, theoretical amount should be zero
- theo_amt = 0.00
- elif line_timedelta.days > 0 and fields.Datetime.from_string(today) < fields.Datetime.from_string(line.date_to):
- # If today is between the budget line date_from and date_to
- theo_amt = (elapsed_timedelta.total_seconds() / line_timedelta.total_seconds()) * line.planned_amount
- else:
- theo_amt = line.planned_amount
+ theo_amt = line.planned_amount
+ else:
+ line_timedelta = fields.Datetime.from_string(line.date_to) - fields.Datetime.from_string(
+ line.date_from)
+ elapsed_timedelta = today - fields.Datetime.from_string(line.date_from)
+ if elapsed_timedelta.days < 0:
+ # If the budget line has not started yet, theoretical amount should be zero
+ theo_amt = 0.00
+ elif line_timedelta.days > 0 and today < fields.Datetime.from_string(line.date_to):
+ # If today is between the budget line date_from and date_to
+ theo_amt = (elapsed_timedelta.total_seconds() / line_timedelta.total_seconds()) * line.planned_amount
+ else:
+ theo_amt = line.planned_amount
line.theoritical_amount = theo_amt
@api.multi
def _compute_percentage(self):
for line in self:
if line.theoritical_amount != 0.00:
- line.percentage = float((line.practical_amount or 0.0) / line.theoritical_amount) * 100
+ line.percentage = float((line.practical_amount or 0.0) / line.theoritical_amount)
else:
line.percentage = 0.00
+
+ @api.constrains('general_budget_id', 'analytic_account_id')
+ def _must_have_analytical_or_budgetary_or_both(self):
+ if not self.analytic_account_id and not self.general_budget_id:
+ raise ValidationError(
+ _("You have to enter at least a budgetary position or analytic account on a budget line."))
+
+ @api.multi
+ def action_open_budget_entries(self):
+ if self.analytic_account_id:
+ # if there is an analytic account, then the analytic items are loaded
+ action = self.env['ir.actions.act_window'].for_xml_id('analytic', 'account_analytic_line_action_entries')
+ action['domain'] = [('account_id', '=', self.analytic_account_id.id),
+ ('date', '>=', self.date_from),
+ ('date', '<=', self.date_to)
+ ]
+ if self.general_budget_id:
+ action['domain'] += [('general_account_id', 'in', self.general_budget_id.account_ids.ids)]
+ else:
+ # otherwise the journal entries booked on the accounts of the budgetary postition are opened
+ action = self.env['ir.actions.act_window'].for_xml_id('account', 'action_account_moves_all_a')
+ action['domain'] = [('account_id', 'in',
+ self.general_budget_id.account_ids.ids),
+ ('date', '>=', self.date_from),
+ ('date', '<=', self.date_to)
+ ]
+ return action
+
+ @api.one
+ @api.constrains('date_from', 'date_to')
+ def _line_dates_between_budget_dates(self):
+ budget_date_from = fields.Datetime.from_string(self.crossovered_budget_id.date_from)
+ budget_date_to = fields.Datetime.from_string(self.crossovered_budget_id.date_to)
+ if self.date_from:
+ date_from = fields.Datetime.from_string(self.date_from)
+ if date_from < budget_date_from or date_from > budget_date_to:
+ raise ValidationError(_('"Start Date" of the budget line should be included in the Period of the budget'))
+
+ if self.date_to:
+ date_to = fields.Datetime.from_string(self.date_to)
+ if date_to < budget_date_from or date_to > budget_date_to:
+ raise ValidationError(_('"End Date" of the budget line should be included in the Period of the budget'))
diff --git a/addons/account_budget/tests/common.py b/addons/account_budget/tests/common.py
index 1452f0f0d51..81b6c88d8c0 100644
--- a/addons/account_budget/tests/common.py
+++ b/addons/account_budget/tests/common.py
@@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.account.tests.account_test_classes import AccountingTestCase
-import time
+from datetime import datetime
class TestAccountBudgetCommon(AccountingTestCase):
@@ -30,74 +30,74 @@ class TestAccountBudgetCommon(AccountingTestCase):
self.env['crossovered.budget.lines'].create({
'analytic_account_id': self.ref('analytic.analytic_partners_camp_to_camp'),
'general_budget_id': self.account_budget_post_sales0.id,
- 'date_from': str(time.localtime(time.time())[0] + 1) + '-01-01',
- 'date_to': str(time.localtime(time.time())[0] + 1) + '-01-31',
+ 'date_from': str(datetime.now().year + 1) + '-01-01',
+ 'date_to': str(datetime.now().year + 1) + '-01-31',
'planned_amount': 500.0,
'crossovered_budget_id': self.ref('account_budget.crossovered_budget_budgetpessimistic0'),
})
self.env['crossovered.budget.lines'].create({
'analytic_account_id': self.ref('analytic.analytic_partners_camp_to_camp'),
'general_budget_id': self.account_budget_post_sales0.id,
- 'date_from': str(time.localtime(time.time())[0] + 1) + '-02-07',
- 'date_to': str(time.localtime(time.time())[0] + 1) + '-02-28',
+ 'date_from': str(datetime.now().year + 1) + '-02-07',
+ 'date_to': str(datetime.now().year + 1) + '-02-28',
'planned_amount': 900.0,
'crossovered_budget_id': self.ref('account_budget.crossovered_budget_budgetoptimistic0'),
})
self.env['crossovered.budget.lines'].create({
'analytic_account_id': self.ref('analytic.analytic_partners_camp_to_camp'),
'general_budget_id': self.account_budget_post_sales0.id,
- 'date_from': str(time.localtime(time.time())[0] + 1) + '-03-01',
- 'date_to': str(time.localtime(time.time())[0] + 1) + '-03-15',
+ 'date_from': str(datetime.now().year + 1) + '-03-01',
+ 'date_to': str(datetime.now().year + 1) + '-03-15',
'planned_amount': 300.0,
'crossovered_budget_id': self.ref('account_budget.crossovered_budget_budgetoptimistic0'),
})
self.env['crossovered.budget.lines'].create({
'analytic_account_id': self.ref('analytic.analytic_our_super_product'),
'general_budget_id': self.account_budget_post_sales0.id,
- 'date_from': str(time.localtime(time.time())[0] + 1) + '-03-16',
- 'paid_date': str(time.localtime(time.time())[0] + 1) + '-12-03',
- 'date_to': str(time.localtime(time.time())[0] + 1) + '-03-31',
+ 'date_from': str(datetime.now().year + 1) + '-03-16',
+ 'paid_date': str(datetime.now().year + 1) + '-12-03',
+ 'date_to': str(datetime.now().year + 1) + '-03-31',
'planned_amount': 375.0,
'crossovered_budget_id': self.ref('account_budget.crossovered_budget_budgetpessimistic0'),
})
self.env['crossovered.budget.lines'].create({
'analytic_account_id': self.ref('analytic.analytic_our_super_product'),
'general_budget_id': self.account_budget_post_sales0.id,
- 'date_from': str(time.localtime(time.time())[0] + 1) + '-05-01',
- 'paid_date': str(time.localtime(time.time())[0] + 1) + '-12-03',
- 'date_to': str(time.localtime(time.time())[0] + 1) + '-05-31',
+ 'date_from': str(datetime.now().year + 1) + '-05-01',
+ 'paid_date': str(datetime.now().year + 1) + '-12-03',
+ 'date_to': str(datetime.now().year + 1) + '-05-31',
'planned_amount': 375.0,
'crossovered_budget_id': self.ref('account_budget.crossovered_budget_budgetoptimistic0'),
})
self.env['crossovered.budget.lines'].create({
'analytic_account_id': self.ref('analytic.analytic_seagate_p2'),
'general_budget_id': self.account_budget_post_sales0.id,
- 'date_from': str(time.localtime(time.time())[0] + 1) + '-07-16',
- 'date_to': str(time.localtime(time.time())[0] + 1) + '-07-31',
+ 'date_from': str(datetime.now().year + 1) + '-07-16',
+ 'date_to': str(datetime.now().year + 1) + '-07-31',
'planned_amount': 20000.0,
'crossovered_budget_id': self.ref('account_budget.crossovered_budget_budgetpessimistic0'),
})
self.env['crossovered.budget.lines'].create({
'analytic_account_id': self.ref('analytic.analytic_seagate_p2'),
'general_budget_id': self.account_budget_post_sales0.id,
- 'date_from': str(time.localtime(time.time())[0] + 1) + '-02-01',
- 'date_to': str(time.localtime(time.time())[0] + 1) + '-02-28',
+ 'date_from': str(datetime.now().year + 1) + '-02-01',
+ 'date_to': str(datetime.now().year + 1) + '-02-28',
'planned_amount': 20000.0,
'crossovered_budget_id': self.ref('account_budget.crossovered_budget_budgetoptimistic0'),
})
self.env['crossovered.budget.lines'].create({
'analytic_account_id': self.ref('analytic.analytic_seagate_p2'),
'general_budget_id': self.account_budget_post_sales0.id,
- 'date_from': str(time.localtime(time.time())[0] + 1) + '-09-16',
- 'date_to': str(time.localtime(time.time())[0] + 1) + '-09-30',
+ 'date_from': str(datetime.now().year + 1) + '-09-16',
+ 'date_to': str(datetime.now().year + 1) + '-09-30',
'planned_amount': 10000.0,
'crossovered_budget_id': self.ref('account_budget.crossovered_budget_budgetpessimistic0'),
})
self.env['crossovered.budget.lines'].create({
'analytic_account_id': self.ref('analytic.analytic_seagate_p2'),
'general_budget_id': self.account_budget_post_sales0.id,
- 'date_from': str(time.localtime(time.time())[0] + 1) + '-10-01',
- 'date_to': str(time.localtime(time.time())[0] + 1) + '-12-31',
+ 'date_from': str(datetime.now().year + 1) + '-10-01',
+ 'date_to': str(datetime.now().year + 1) + '-12-31',
'planned_amount': 10000.0,
'crossovered_budget_id': self.ref('account_budget.crossovered_budget_budgetoptimistic0'),
})
@@ -122,64 +122,64 @@ class TestAccountBudgetCommon(AccountingTestCase):
self.env['crossovered.budget.lines'].create({
'analytic_account_id': self.ref('analytic.analytic_partners_camp_to_camp'),
'general_budget_id': self.account_budget_post_purchase0.id,
- 'date_from': str(time.localtime(time.time())[0] + 1) + '-01-01',
- 'date_to': str(time.localtime(time.time())[0] + 1) + '-01-31',
+ 'date_from': str(datetime.now().year + 1) + '-01-01',
+ 'date_to': str(datetime.now().year + 1) + '-01-31',
'planned_amount': -500.0,
'crossovered_budget_id': self.ref('account_budget.crossovered_budget_budgetpessimistic0'),
})
self.env['crossovered.budget.lines'].create({
'analytic_account_id': self.ref('analytic.analytic_partners_camp_to_camp'),
'general_budget_id': self.account_budget_post_purchase0.id,
- 'date_from': str(time.localtime(time.time())[0] + 1) + '-02-01',
- 'date_to': str(time.localtime(time.time())[0] + 1) + '-02-28',
+ 'date_from': str(datetime.now().year + 1) + '-02-01',
+ 'date_to': str(datetime.now().year + 1) + '-02-28',
'planned_amount': -250.0,
'crossovered_budget_id': self.ref('account_budget.crossovered_budget_budgetoptimistic0'),
})
self.env['crossovered.budget.lines'].create({
'analytic_account_id': self.ref('analytic.analytic_our_super_product'),
'general_budget_id': self.account_budget_post_purchase0.id,
- 'date_from': str(time.localtime(time.time())[0] + 1) + '-04-01',
- 'date_to': str(time.localtime(time.time())[0] + 1) + '-04-30',
+ 'date_from': str(datetime.now().year + 1) + '-04-01',
+ 'date_to': str(datetime.now().year + 1) + '-04-30',
'planned_amount': -150.0,
'crossovered_budget_id': self.ref('account_budget.crossovered_budget_budgetpessimistic0'),
})
self.env['crossovered.budget.lines'].create({
'analytic_account_id': self.ref('analytic.analytic_seagate_p2'),
'general_budget_id': self.account_budget_post_purchase0.id,
- 'date_from': str(time.localtime(time.time())[0] + 1) + '-06-01',
- 'date_to': str(time.localtime(time.time())[0] + 1) + '-06-15',
+ 'date_from': str(datetime.now().year + 1) + '-06-01',
+ 'date_to': str(datetime.now().year + 1) + '-06-15',
'planned_amount': -7500.0,
'crossovered_budget_id': self.ref('account_budget.crossovered_budget_budgetpessimistic0'),
})
self.env['crossovered.budget.lines'].create({
'analytic_account_id': self.ref('analytic.analytic_seagate_p2'),
'general_budget_id': self.account_budget_post_purchase0.id,
- 'date_from': str(time.localtime(time.time())[0] + 1) + '-06-16',
- 'date_to': str(time.localtime(time.time())[0] + 1) + '-06-30',
+ 'date_from': str(datetime.now().year + 1) + '-06-16',
+ 'date_to': str(datetime.now().year + 1) + '-06-30',
'planned_amount': -5000.0,
'crossovered_budget_id': self.ref('account_budget.crossovered_budget_budgetpessimistic0'),
})
self.env['crossovered.budget.lines'].create({
'analytic_account_id': self.ref('analytic.analytic_seagate_p2'),
'general_budget_id': self.account_budget_post_purchase0.id,
- 'date_from': str(time.localtime(time.time())[0] + 1) + '-07-01',
- 'date_to': str(time.localtime(time.time())[0] + 1) + '-07-15',
+ 'date_from': str(datetime.now().year + 1) + '-07-01',
+ 'date_to': str(datetime.now().year + 1) + '-07-15',
'planned_amount': -2000.0,
'crossovered_budget_id': self.ref('account_budget.crossovered_budget_budgetoptimistic0'),
})
self.env['crossovered.budget.lines'].create({
'analytic_account_id': self.ref('analytic.analytic_seagate_p2'),
'general_budget_id': self.account_budget_post_purchase0.id,
- 'date_from': str(time.localtime(time.time())[0] + 1) + '-08-16',
- 'date_to': str(time.localtime(time.time())[0] + 1) + '-08-31',
+ 'date_from': str(datetime.now().year + 1) + '-08-16',
+ 'date_to': str(datetime.now().year + 1) + '-08-31',
'planned_amount': -3000.0,
'crossovered_budget_id': self.ref('account_budget.crossovered_budget_budgetpessimistic0'),
})
self.env['crossovered.budget.lines'].create({
'analytic_account_id': self.ref('analytic.analytic_seagate_p2'),
'general_budget_id': self.account_budget_post_purchase0.id,
- 'date_from': str(time.localtime(time.time())[0] + 1) + '-09-01',
- 'date_to': str(time.localtime(time.time())[0] + 1) + '-09-15',
+ 'date_from': str(datetime.now().year + 1) + '-09-01',
+ 'date_to': str(datetime.now().year + 1) + '-09-15',
'planned_amount': -1000.0,
'crossovered_budget_id': self.ref('account_budget.crossovered_budget_budgetoptimistic0'),
})
diff --git a/addons/account_budget/tests/test_theoreticalamount.py b/addons/account_budget/tests/test_theoreticalamount.py
index 80f1ff6438a..082d39b9b14 100644
--- a/addons/account_budget/tests/test_theoreticalamount.py
+++ b/addons/account_budget/tests/test_theoreticalamount.py
@@ -7,9 +7,13 @@ try:
except ImportError:
from mock import patch
-from odoo.fields import Datetime
+from odoo.fields import Datetime, Date
from odoo.tests import tagged
+from datetime import datetime, timedelta
+import logging
+
+_logger = logging.getLogger(__name__)
# ---------------------------------------------------------
# Tests
# ---------------------------------------------------------
@@ -17,12 +21,7 @@ from odoo.tests import tagged
class TestTheoreticalAmount(TestAccountBudgetCommon):
def setUp(self):
super(TestTheoreticalAmount, self).setUp()
- crossovered_budget = self.env['crossovered.budget'].create({
- 'name': 'test budget name',
- 'date_from': '2014-01-01',
- 'date_to': '2014-12-31',
- })
- crossovered_budget_line_obj = self.env['crossovered.budget.lines']
+ #create the budgetary position
user_type_id = self.ref('account.data_account_type_revenue')
tag_id = self.ref('account.account_tag_operating')
account_rev = self.env['account.account'].create({
@@ -35,76 +34,66 @@ class TestTheoreticalAmount(TestAccountBudgetCommon):
'name': 'Sales',
'account_ids': [(4, account_rev.id, 0)],
})
+ #create the budget and budget lines
+ first_january = datetime.now().replace(day=1, month=1)
+ self.last_day_of_budget = first_january + timedelta(days=364) # will be 30th of December or 31th in case of leap year
+
+ crossovered_budget = self.env['crossovered.budget'].create({
+ 'name': 'test budget name',
+ 'date_from': str(first_january.date()),
+ 'date_to': str(self.last_day_of_budget.date()),
+ })
+ crossovered_budget_line_obj = self.env['crossovered.budget.lines']
self.line = crossovered_budget_line_obj.create({
'crossovered_budget_id': crossovered_budget.id,
'general_budget_id': buget_post.id,
- 'date_from': '2014-01-01',
- 'date_to': '2014-12-31',
+ 'date_from': str(first_january.date()),
+ 'date_to': str(self.last_day_of_budget.date()),
'planned_amount': -364,
})
+ self.paid_date_line = crossovered_budget_line_obj.create({
+ 'crossovered_budget_id': crossovered_budget.id,
+ 'general_budget_id': buget_post.id,
+ 'date_from': str(first_january.date()),
+ 'date_to': str(self.last_day_of_budget.date()),
+ 'planned_amount': -364,
+ 'paid_date': str(datetime.now().year) + '-09-09',
+ })
- self.patcher = patch('odoo.addons.account_budget.models.account_budget.fields.Datetime', wraps=Datetime)
- self.mock_datetime = self.patcher.start()
+ self.patcher = patch('odoo.addons.account_budget.models.account_budget.fields.Date', wraps=Date)
+ self.mock_date = self.patcher.start()
- def test_01(self):
- """Start"""
- date = Datetime.to_string(Datetime.from_string('2014-01-01 00:00:00'))
- self.mock_datetime.now.return_value = date
- self.assertAlmostEqual(self.line.theoritical_amount, 0)
+ def test_theoritical_amount_without_paid_date(self):
+ test_list = [
+ (str(datetime.now().year) + '-01-01', 0),
+ (str(datetime.now().year) + '-01-02', -1),
+ (str(datetime.now().year) + '-01-03', -2),
+ (str(datetime.now().year) + '-01-11', -10),
+ (str(datetime.now().year) + '-02-20', -50),
+ (str(self.last_day_of_budget.date()), -364),
+ ]
+ for date, expected_amount in test_list:
+ _logger.info("Checking theoritical amount for the date: " + date)
+ self.mock_date.today.return_value = date
+ self.assertAlmostEqual(self.line.theoritical_amount, expected_amount)
+ #invalidate the cache of the budget lines to recompute the theoritical amount at next iteration
+ self.line.invalidate_cache()
- def test_02(self):
- """After 24 hours"""
- date = Datetime.to_string(Datetime.from_string('2014-01-02 00:00:00'))
- self.mock_datetime.now.return_value = date
- self.assertAlmostEqual(self.line.theoritical_amount, -1)
-
- def test_03(self):
- """After 36 hours"""
- date = Datetime.to_string(Datetime.from_string('2014-01-02 12:00:00'))
- self.mock_datetime.now.return_value = date
- self.assertAlmostEqual(self.line.theoritical_amount, -1.5)
-
- def test_04(self):
- """After 48 hours"""
- date = Datetime.to_string(Datetime.from_string('2014-01-03 00:00:00'))
- self.mock_datetime.now.return_value = date
- self.assertAlmostEqual(self.line.theoritical_amount, -2)
-
- def test_05(self):
- """After 10 days"""
- date = Datetime.to_string(Datetime.from_string('2014-01-11 00:00:00'))
- self.mock_datetime.now.return_value = date
- self.assertAlmostEqual(self.line.theoritical_amount, -10)
-
- def test_06(self):
- """After 50 days"""
- date = Datetime.to_string(Datetime.from_string('2014-02-20 00:00:00'))
- self.mock_datetime.now.return_value = date
- self.assertAlmostEqual(self.line.theoritical_amount, -50)
-
- def test_07(self):
- """After 182 days, exactly half of the budget line"""
- date = Datetime.to_string(Datetime.from_string('2014-07-02 00:00:00'))
- self.mock_datetime.now.return_value = date
- self.assertAlmostEqual(self.line.theoritical_amount, -182)
-
- def test_08(self):
- """After 308 days at noon"""
- date = Datetime.to_string(Datetime.from_string('2014-11-05 12:00:00')) # remember, remember
- self.mock_datetime.now.return_value = date
- self.assertAlmostEqual(self.line.theoritical_amount, -308.5)
-
- def test_09(self):
- """One day before"""
- date = Datetime.to_string(Datetime.from_string('2014-12-30 00:00:00'))
- self.mock_datetime.now.return_value = date
- self.assertAlmostEqual(self.line.theoritical_amount, -363)
-
- def test_10(self):
- """At last"""
- date = Datetime.to_string(Datetime.from_string('2014-12-31 00:00:00'))
- self.mock_datetime.now.return_value = date
- self.assertAlmostEqual(self.line.theoritical_amount, -364)
+ def test_theoritical_amount_with_paid_date(self):
+ test_list = [
+ (str(datetime.now().year) + '-01-01', 0),
+ (str(datetime.now().year) + '-01-02', 0),
+ (str(datetime.now().year) + '-09-08', 0),
+ (str(datetime.now().year) + '-09-09', -364),
+ (str(datetime.now().year) + '-09-10', -364),
+ (str(self.last_day_of_budget.date()), -364),
+ ]
+ for date, expected_amount in test_list:
+ _logger.info("Checking theoritical amount for the date: " + date)
+ self.mock_date.today.return_value = date
+ self.assertAlmostEqual(self.paid_date_line.theoritical_amount, expected_amount)
+ #invalidate the cache of the budget lines to recompute the theoritical amount at next iteration
+ self.paid_date_line.invalidate_cache()
def tearDown(self):
self.patcher.stop()
diff --git a/addons/account_budget/views/account_budget_views.xml b/addons/account_budget/views/account_budget_views.xml
index 6188e5e9e7b..d9404e2389d 100644
--- a/addons/account_budget/views/account_budget_views.xml
+++ b/addons/account_budget/views/account_budget_views.xml
@@ -1,310 +1,364 @@
-
-
- account.budget.post.search
- account.budget.post
-
-
-
-
-
-
-
+
+
+ account.budget.post.search
+ account.budget.post
+
+
+
+
+
+
+
-
- account.budget.post.tree
- account.budget.post
-
-
+
+ account.budget.post.tree
+ account.budget.post
+
+
+
+
+
+
+
+
+
+ Budgetary Positions
+ account.budget.post
+ form
+ tree,kanban,form
+
+
+
+
+
+
+
+ account.budget.post.form
+ account.budget.post
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
- Budgetary Positions
- account.budget.post
- form
- tree,kanban,form
-
-
-
-
-
-
-
- account.budget.post.form
- account.budget.post
-
-
+
+
-
- crossovered.budget.view.form
- crossovered.budget
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ crossovered.budget.view.tree
+ crossovered.budget
+
+
+
+
+
+
+
+
+
+
+
+
+
+ crossovered.budget.kanban
+ crossovered.budget
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- crossovered.budget.view.tree
- crossovered.budget
-
-
-
-
-
-
-
-
-
-
-
-
-
- crossovered.budget.kanban
- crossovered.budget
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
+
+
+
+
-
-
-
-
-
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
- crossovered.budget.search
- crossovered.budget
-
-
-
-
-
-
-
-
-
-
-
+
+ crossovered.budget.search
+ crossovered.budget
+
+
+
+
+
+
+
+
+
+
+
-
- Budgets
- crossovered.budget
- form
- tree,kanban,form
-
-
-
-
- Create a new budget
-
- A budget is a forecast of your company's income and/or expenses
- expected for a period in the future. A budget is defined on some
- financial accounts and/or analytic accounts (that may represent
- projects, departments, categories of products, etc.)
-
- By keeping track of where your money goes, you may be less
- likely to overspend, and more likely to meet your financial
- goals. Forecast a budget by detailing the expected revenue per
- analytic account and monitor its evolution based on the actuals
- realised during that period.
-
-
-
-
-
-
- account.budget.line.search
- crossovered.budget.lines
-
-
-
-
-
-
-
-
- crossovered.budget.line.tree
- crossovered.budget.lines
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- crossovered.budget.line.form
- crossovered.budget.lines
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ Budgets
+ crossovered.budget
+ form
+ tree,kanban,form
+
+
+
+
+ Click to create a new budget.
+
+
+ Use budgets to compare actual with expected revenues and costs
+
+
+
+
-
- Budgets
- crossovered.budget.lines
- form
- tree,form
-
-
+
+ account.budget.line.search
+ crossovered.budget.lines
+
+
+
+
+
+
+
+
+
-
+
+
-
-
+
+ crossovered.budget.line.tree
+ crossovered.budget.lines
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ crossovered.budget.line.form
+ crossovered.budget.lines
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ crossovered.budget.line.pivot
+ crossovered.budget.lines
+
+
+
+
+
+
+
+
+
+
+
+
+ crossovered.budget.line.graph
+ crossovered.budget.lines
+
+
+
+
+
+
+
+
+
+
+ Budgets Analysis
+ crossovered.budget.lines
+ form
+ tree,form,pivot,graph
+
+ {'search_default_group_crossevered_budgdet_id': True, 'search_default_filter_not_cancelled':True}
+
+
+
+
+
+