odex25_standard/odex25_project/project_budget/models/account_budget.py

249 lines
11 KiB
Python

# -*- coding: utf-8 -*-
from odoo import api, models, fields, tools, _
from odoo.exceptions import UserError, ValidationError
class ProjectBudget(models.Model):
_inherit = "crossovered.budget"
fiscalyear_id = fields.Many2one(required=False)
project_id = fields.Many2one('project.project', 'Project', required=False,tracking=True)
project_no = fields.Char("Project Number", related="project_id.project_no", tracking=True)
sale_order_id = fields.Many2one('sale.order', related="project_id.sale_order_id", string="Proposal")
sale_order_amount = fields.Monetary("Total Amount", related="project_id.sale_order_amount",tracking=True)
analytic_account_id = fields.Many2one('account.analytic.account', string="Analytic Account",
related="project_id.analytic_account_id")
hour_line_ids = fields.One2many('crossovered.budget.lines', 'crossovered_budget_id', string='Hour lines',
copy=False,domain=[('is_timesheet_hours', '=', True)])
expensess_line_ids = fields.One2many('crossovered.budget.lines', 'crossovered_budget_id', string='Hour lines',
copy=False,domain=[('is_timesheet_hours', '=', False)])
project_user_id = fields.Many2one("res.users", related="project_id.user_id", string="Project Manager", store=True)
total_budget_cost = fields.Monetary('Total Cost', compute='_compute_budget_amounts',
help="Total Cost", tracking=True)
projected_profit = fields.Monetary('Projected Profit', compute='_compute_budget_amounts',
help="Contract Value(With VAT & other taxes) - Total Cost",
tracking=True)
man_hours = fields.Boolean('Man hours' , related="project_id.man_hours",default=False)
@api.depends("crossovered_budget_line", "crossovered_budget_line.planned_amount",
"crossovered_budget_line.project_planned_amount")
def _compute_budget_amounts(self):
"""
Get Total Budget amounts
"""
for rec in self:
total_budget_cost = 0.0
projected_profit = 0.0
if rec.crossovered_budget_line:
total_budget_cost = sum(rec.crossovered_budget_line.mapped("project_planned_amount"))
projected_profit = rec.sale_order_amount - total_budget_cost
rec.total_budget_cost = total_budget_cost
rec.projected_profit = projected_profit
@api.onchange('project_id')
def _onchange_project(self):
"""
set budget name by project name
"""
if self.project_id:
self.name = self.project_id.display_name or "/"
self.date_from = self.project_id.start
self.date_to = self.project_id.date
else:
pass
@api.constrains('date_from', 'date_to', 'fiscalyear_id')
def _check_dates_fiscalyear(self):
"""
pass fiscalyear constrain in case of project budget
"""
if not self.project_id:
return super(ProjectBudget, self)._check_dates_fiscalyear()
else:
pass
@api.constrains('project_id')
def _check_project_id(self):
"""
make sure that the project have just one budget
"""
if self.project_id:
projects = self.env['crossovered.budget'].search(
[('project_id', '=', self.project_id.id), ('id', '!=', self.id)])
if len(projects) > 0:
raise ValidationError("The Project Must have just only one budget")
@api.constrains('project_id','crossovered_budget_line','crossovered_budget_line.qty')
def _check_project_hours(self):
"""
Make sure that budget hours not more than project Allocated hours
"""
if self.project_id:
project_hours = sum(self.project_id.project_phase_ids.mapped("estimated_hours"))
budget_hours = sum(self.hour_line_ids.mapped("qty"))
if project_hours > budget_hours:
raise ValidationError("Project Allocated hours must not be more than Man-Hours in budget!")
def action_budget_confirm(self):
self._check_project_hours()
self.write({'state': 'confirm'})
def action_budget_approve(self):
self._check_project_hours()
self.write({'state': 'approve'})
def action_budget_edit(self):
self.write({'state': 'draft'})
def action_budget_validate(self):
self.write({'state': 'validate'})
@api.constrains('sale_order_amount', 'crossovered_budget_line','crossovered_budget_line.project_planned_amount')
def budget_total(self):
for item in self:
if item.project_id and item.project_id.sale_order_id and sum(
item.crossovered_budget_line.mapped('project_planned_amount')) > item.sale_order_amount:
raise ValidationError(_('The Budget Total Amount Must Not Exceed Project Sale Value'))
def open_sale_order(self):
return {
'name': 'Sale Order',
'view_type': 'form',
'view_mode': 'tree,form',
'res_model': 'sale.order',
'type': 'ir.actions.act_window',
'domain': [('id', '=', self.project_id.sale_order_id.id)],
"res_id": self.project_id.sale_order_id.id,
}
def unlink(self):
for record in self:
if record.state != 'draft':
raise UserError(_('You cannot remove the Budget in %s state.' % (record.state)))
return super(ProjectBudget, self).unlink()
class ProjectBudgetPost(models.Model):
_inherit = "account.budget.post"
is_timesheet_hours = fields.Boolean(string='Is Timesheet Hours')
def _check_account_ids(self, vals):
# Inherit to pass the check in case of timesheet.
if ('is_timesheet_hours' in vals and not vals['is_timesheet_hours']):
return super(ProjectBudgetPost, self)._check_account_ids(vals)
else:
pass
class ProjectBudgetLine(models.Model):
_name = "crossovered.budget.lines"
_inherit = ["crossovered.budget.lines","mail.thread"]
project_id = fields.Many2one("project.project", related='crossovered_budget_id.project_id',store=True,string='Project')
is_timesheet_hours = fields.Boolean(related='general_budget_id.is_timesheet_hours',string='Is Timesheet Hours')
qty = fields.Float(string="qty/hours", tracking=True)
amount = fields.Float(string="Unit cost", tracking=True)
timesheet_hours = fields.Float(string="Consumed Hours", tracking=True)
planned_amount = fields.Monetary('Planned Amount', required=True, tracking=False)
project_planned_amount = fields.Monetary('Budget Amount', tracking=True)
project_remaining_amount = fields.Monetary('Remaining Amount', compute="_compute_project_remaining_amount",tracking=False)
date_from = fields.Date('Start Date', tracking=True)
date_to = fields.Date('End Date', tracking=True)
man_hours = fields.Boolean('Man hours' , related="project_id.man_hours",default=False)
@api.onchange('qty', 'amount','practical_amount')
def _onchange_budget_amount(self):
for rec in self:
rec.planned_amount = (rec.qty * rec.amount) * -1
rec.project_planned_amount = abs(rec.planned_amount)
def _compute_project_remaining_amount(self):
for rec in self:
rec.project_remaining_amount = 0.0
rec.planned_amount = (rec.qty * rec.amount) * -1
rec.project_planned_amount = abs(rec.planned_amount)
rec.project_remaining_amount = rec.project_planned_amount + rec.practical_amount
# todo review this funtion
def _compute_practical_amount(self):
for line in self:
if line.general_budget_id.is_timesheet_hours:
hours_domain = [('project_id', '=', line.crossovered_budget_id.project_id.id),
# ('date', '>=', line.date_from),
# ('date', '<=', line.date_to),
# ('sheet_id.state', '=', 'done')
]
total_canceled_hours = sum(
self.env['account.analytic.line'].search(hours_domain).mapped('unit_amount'))
line.timesheet_hours = total_canceled_hours
line.practical_amount = - (line.timesheet_hours * line.amount)
else:
# return super(ProjectBudgetLine, self)._compute_practical_amount()
acc_ids = line.general_budget_id.account_ids.ids
date_to = line.date_to
date_from = line.date_from
if line.analytic_account_id.id:
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),
('move_id.state', '=', 'posted')
]
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.constrains('project_id','planned_amount','practical_amount')
def _check_project_amounts(self):
for rec in self:
if rec.project_id:
if abs(rec.planned_amount) < abs(rec.practical_amount):
raise ValidationError(_("Budget amount can't be less than actual amount!"))
@api.constrains('general_budget_id', 'analytic_account_id')
def _must_have_analytical_or_budgetary_or_both(self):
for record in self:
if not record.analytic_account_id and not record.general_budget_id:
raise ValidationError(
_("You have to enter at least a budgetary position or analytic account on a budget line."))
def open_line(self):
self.ensure_one()
return {
'name': _('Budget Line'),
'view_type': 'form',
'view_mode': 'form',
'res_model': 'crossovered.budget.lines',
'res_id': self.id,
'views': [(self.env.ref('project_budget.crossovered_budget_line_log_form').id, 'form'), ],
'target': 'current',
'type': 'ir.actions.act_window',
}