PO Budget Enhancement
This commit is contained in:
parent
eda7a6948f
commit
f884274013
|
|
@ -7,9 +7,10 @@
|
||||||
'website': '',
|
'website': '',
|
||||||
'license': 'LGPL-3',
|
'license': 'LGPL-3',
|
||||||
'category': 'purchase',
|
'category': 'purchase',
|
||||||
'depends': ['base',"purchase_requisition_custom"],
|
'depends': ['base',"purchase_requisition_custom",'account','account_budget_custom'],
|
||||||
'data': [
|
'data': [
|
||||||
# 'security/ir.model.access.csv',
|
# 'security/ir.model.access.csv',
|
||||||
|
'views/views.xml',
|
||||||
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,136 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from odoo import models, _, api
|
from odoo import models, fields, api, _
|
||||||
from odoo.exceptions import ValidationError
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class PurchaseOrderLine(models.Model):
|
||||||
|
_inherit = 'purchase.order.line'
|
||||||
|
|
||||||
|
budget_id = fields.Many2one('crossovered.budget',string='Budget',help='Only approved budgets available for selected Analytic Account',)
|
||||||
|
budget_position_id = fields.Many2one('account.budget.post',string='Budgetary Position',help='Filtered by the selected Budget',)
|
||||||
|
budget_account_id = fields.Many2one('account.account',string='Budgetary Position Account',help='Filtered by selected Budgetary Position',)
|
||||||
|
selected_budget_line_id = fields.Many2one('crossovered.budget.lines',string='Selected Budget Line',help='The specific budget line to use for this order line',compute='_compute_selected_budget_line',store=True)
|
||||||
|
|
||||||
|
@api.depends('budget_id', 'budget_position_id', 'account_analytic_id')
|
||||||
|
def _compute_selected_budget_line(self):
|
||||||
|
#Compute the budget line based on selections
|
||||||
|
for line in self:
|
||||||
|
if line.budget_id and line.budget_position_id and line.account_analytic_id:
|
||||||
|
budget_line = self.env['crossovered.budget.lines'].search([
|
||||||
|
('crossovered_budget_id', '=', line.budget_id.id),
|
||||||
|
('general_budget_id', '=', line.budget_position_id.id),
|
||||||
|
('analytic_account_id', '=', line.account_analytic_id.id)
|
||||||
|
], limit=1)
|
||||||
|
line.selected_budget_line_id = budget_line
|
||||||
|
else:
|
||||||
|
line.selected_budget_line_id = False
|
||||||
|
|
||||||
|
@api.onchange('account_analytic_id')
|
||||||
|
def _onchange_account_analytic_id(self):
|
||||||
|
# When analytic account changes, restrict budgets
|
||||||
|
self.ensure_one()
|
||||||
|
if self.account_analytic_id and self.order_id.date_order:
|
||||||
|
budget_lines = self.env['crossovered.budget.lines'].search([
|
||||||
|
('analytic_account_id', '=', self.account_analytic_id.id),
|
||||||
|
('crossovered_budget_id.state', '=', 'done'),
|
||||||
|
('crossovered_budget_id.date_from', '<=', self.order_id.date_order),
|
||||||
|
('crossovered_budget_id.date_to', '>=', self.order_id.date_order),
|
||||||
|
])
|
||||||
|
budget_ids = budget_lines.mapped('crossovered_budget_id').ids
|
||||||
|
|
||||||
|
return {
|
||||||
|
'domain': {
|
||||||
|
'budget_id': [('id', 'in', budget_ids)]
|
||||||
|
},
|
||||||
|
'value': {
|
||||||
|
'budget_id': False,
|
||||||
|
'budget_position_id': False,
|
||||||
|
'budget_account_id': False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'domain': {'budget_id': []},
|
||||||
|
'value': {
|
||||||
|
'budget_id': False,
|
||||||
|
'budget_position_id': False,
|
||||||
|
'budget_account_id': False
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@api.onchange('budget_id')
|
||||||
|
def _onchange_budget_id(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if self.budget_id and self.account_analytic_id:
|
||||||
|
budget_lines = self.env['crossovered.budget.lines'].search([
|
||||||
|
('crossovered_budget_id', '=', self.budget_id.id),
|
||||||
|
('analytic_account_id', '=', self.account_analytic_id.id)
|
||||||
|
])
|
||||||
|
budget_position_ids = budget_lines.mapped('general_budget_id').ids
|
||||||
|
|
||||||
|
return {
|
||||||
|
'domain': {
|
||||||
|
'budget_position_id': [('id', 'in', budget_position_ids)]
|
||||||
|
},
|
||||||
|
'value': {
|
||||||
|
'budget_position_id': False,
|
||||||
|
'budget_account_id': False
|
||||||
|
},
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'domain': {'budget_position_id': []},
|
||||||
|
'value': {
|
||||||
|
'budget_position_id': False,
|
||||||
|
'budget_account_id': False
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@api.onchange('budget_position_id')
|
||||||
|
def _onchange_budget_position_id(self):
|
||||||
|
# when budgetary position changes, restrict available accounts
|
||||||
|
self.ensure_one()
|
||||||
|
if self.budget_position_id:
|
||||||
|
account_ids = self.budget_position_id.account_ids.ids
|
||||||
|
return {
|
||||||
|
'domain': {
|
||||||
|
'budget_account_id': [('id', 'in', account_ids)]},'value': {
|
||||||
|
'budget_account_id': False},
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'domain': {'budget_account_id': []},
|
||||||
|
'value': {
|
||||||
|
'budget_account_id': False
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@api.constrains('budget_id', 'budget_position_id', 'account_analytic_id')
|
||||||
|
def _check_budget_position(self):
|
||||||
|
"""Ensure budget position belongs to selected budget and analytic account"""
|
||||||
|
for line in self:
|
||||||
|
if line.budget_id and line.budget_position_id and line.account_analytic_id:
|
||||||
|
budget_line = self.env['crossovered.budget.lines'].search([
|
||||||
|
('crossovered_budget_id', '=', line.budget_id.id),
|
||||||
|
('general_budget_id', '=', line.budget_position_id.id),
|
||||||
|
('analytic_account_id', '=', line.account_analytic_id.id)
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
|
if not budget_line:
|
||||||
|
raise ValidationError(_(
|
||||||
|
"The selected Budgetary Position '%s' is not linked to Budget '%s' "
|
||||||
|
"for Analytic Account '%s'") % (line.budget_position_id.name,line.budget_id.name,line.account_analytic_id.name))
|
||||||
|
|
||||||
|
@api.constrains('budget_position_id', 'budget_account_id')
|
||||||
|
def _check_position_account(self):
|
||||||
|
# Ensure account belongs to selected budget position
|
||||||
|
for line in self:
|
||||||
|
if line.budget_position_id and line.budget_account_id:
|
||||||
|
if line.budget_account_id not in line.budget_position_id.account_ids:
|
||||||
|
raise ValidationError(_(
|
||||||
|
"The selected Account '%s' is not included in Budgetary Position '%s'") % (line.budget_account_id.display_name,line.budget_position_id.name))
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrder(models.Model):
|
class PurchaseOrder(models.Model):
|
||||||
_inherit = 'purchase.order'
|
_inherit = 'purchase.order'
|
||||||
|
|
||||||
|
|
@ -29,38 +158,53 @@ class PurchaseOrder(models.Model):
|
||||||
for order in self:
|
for order in self:
|
||||||
for rec in order.order_line:
|
for rec in order.order_line:
|
||||||
if rec.choosen:
|
if rec.choosen:
|
||||||
|
|
||||||
if self.purchase_cost == 'product_line':
|
if self.purchase_cost == 'product_line':
|
||||||
analytic_account = rec.account_analytic_id
|
analytic_account = rec.account_analytic_id
|
||||||
if not analytic_account:
|
if not analytic_account:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_("Please put cost center to the product line") + ': {}'.format(rec.product_id.name)
|
_("Please put cost center to the product line: %s") % rec.product_id.name)
|
||||||
)
|
|
||||||
|
|
||||||
account_id = rec.product_id.property_account_expense_id or rec.product_id.categ_id.property_account_expense_categ_id
|
|
||||||
if not account_id:
|
|
||||||
raise ValidationError(
|
|
||||||
_("This product has no expense account") + ': {}'.format(rec.product_id.name)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not analytic_account:
|
if not analytic_account:
|
||||||
raise ValidationError(_("Analytic account not set"))
|
raise ValidationError(_("Analytic account not set"))
|
||||||
|
|
||||||
budget_lines = self.env['crossovered.budget.lines'].search([
|
# Determine account_id (prioritize manually selected account)
|
||||||
('analytic_account_id', '=', analytic_account.id),
|
if rec.budget_account_id:
|
||||||
('crossovered_budget_id.state', '=', 'done'),
|
account_id = rec.budget_account_id
|
||||||
('crossovered_budget_id.date_from', '<=', self.date_order),
|
|
||||||
('crossovered_budget_id.date_to', '>=', self.date_order)
|
else:
|
||||||
])
|
account_id = (
|
||||||
|
rec.product_id.property_account_expense_id
|
||||||
|
or rec.product_id.categ_id.property_account_expense_categ_id)
|
||||||
|
|
||||||
|
if not account_id:
|
||||||
|
raise ValidationError(
|
||||||
|
_("This product has no expense account: %s") % rec.product_id.name)
|
||||||
|
|
||||||
|
# Get budget lines
|
||||||
|
if rec.selected_budget_line_id:
|
||||||
|
budget_lines = rec.selected_budget_line_id
|
||||||
|
else:
|
||||||
|
budget_domain = [
|
||||||
|
('analytic_account_id', '=', analytic_account.id),
|
||||||
|
('crossovered_budget_id.state', '=', 'done'),
|
||||||
|
('crossovered_budget_id.date_from', '<=', self.date_order),
|
||||||
|
('crossovered_budget_id.date_to', '>=', self.date_order)
|
||||||
|
]
|
||||||
|
if rec.budget_id:
|
||||||
|
budget_domain.append(('crossovered_budget_id', '=', rec.budget_id.id))
|
||||||
|
if rec.budget_position_id:
|
||||||
|
budget_domain.append(('general_budget_id', '=', rec.budget_position_id.id))
|
||||||
|
|
||||||
|
budget_lines = self.env['crossovered.budget.lines'].search(budget_domain)
|
||||||
|
|
||||||
if not budget_lines:
|
if not budget_lines:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_("No approved budget found for analytic account: %s on date: %s")
|
_("No approved budget found for analytic account: %s on date: %s")
|
||||||
% (analytic_account.name, self.date_order)
|
% (analytic_account.name, self.date_order))
|
||||||
)
|
|
||||||
|
|
||||||
valid_budget_positions = budget_lines.mapped('general_budget_id').filtered(
|
valid_budget_positions = budget_lines.mapped('general_budget_id').filtered(
|
||||||
lambda x: account_id in x.account_ids
|
lambda x: account_id in x.account_ids)
|
||||||
)
|
|
||||||
|
|
||||||
if not valid_budget_positions:
|
if not valid_budget_positions:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
|
|
@ -68,15 +212,15 @@ class PurchaseOrder(models.Model):
|
||||||
"- Expense Account: %s\n"
|
"- Expense Account: %s\n"
|
||||||
"- For Analytic Account: %s\n"
|
"- For Analytic Account: %s\n"
|
||||||
"- Product: %s")
|
"- Product: %s")
|
||||||
% (account_id.name, analytic_account.name, rec.product_id.name)
|
% (account_id.display_name, analytic_account.name, rec.product_id.name))
|
||||||
)
|
|
||||||
final_budget_line = False
|
final_budget_line = False
|
||||||
remaining_amount = rec.price_subtotal
|
remaining_amount = rec.price_subtotal
|
||||||
|
|
||||||
for budget_position in valid_budget_positions:
|
for budget_position in valid_budget_positions:
|
||||||
candidate_lines = budget_lines.filtered(
|
candidate_lines = budget_lines.filtered(
|
||||||
lambda x: x.general_budget_id.id == budget_position.id
|
lambda x: x.general_budget_id.id == budget_position.id)
|
||||||
)
|
|
||||||
for line in candidate_lines:
|
for line in candidate_lines:
|
||||||
line_remain = abs(line.remain)
|
line_remain = abs(line.remain)
|
||||||
if line_remain <= 0:
|
if line_remain <= 0:
|
||||||
|
|
@ -93,12 +237,14 @@ class PurchaseOrder(models.Model):
|
||||||
'new_balance': line_remain - used_amount,
|
'new_balance': line_remain - used_amount,
|
||||||
'account_id': account_id.id
|
'account_id': account_id.id
|
||||||
}))
|
}))
|
||||||
|
|
||||||
remaining_amount -= used_amount
|
remaining_amount -= used_amount
|
||||||
amount += used_amount
|
amount += used_amount
|
||||||
final_budget_line = line
|
final_budget_line = line
|
||||||
|
|
||||||
if remaining_amount <= 0:
|
if remaining_amount <= 0:
|
||||||
break
|
break
|
||||||
|
|
||||||
if remaining_amount <= 0:
|
if remaining_amount <= 0:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
@ -106,7 +252,7 @@ class PurchaseOrder(models.Model):
|
||||||
raise ValidationError(_(
|
raise ValidationError(_(
|
||||||
"Not enough budget available for analytic account '%s' (Expense account: %s).\n"
|
"Not enough budget available for analytic account '%s' (Expense account: %s).\n"
|
||||||
"Remaining unallocated amount: %.2f"
|
"Remaining unallocated amount: %.2f"
|
||||||
) % (analytic_account.name, account_id.name, remaining_amount))
|
) % (analytic_account.name, account_id.display_name, remaining_amount))
|
||||||
|
|
||||||
if final_budget_line:
|
if final_budget_line:
|
||||||
self.budget_id = final_budget_line.crossovered_budget_id.id
|
self.budget_id = final_budget_line.crossovered_budget_id.id
|
||||||
|
|
@ -123,5 +269,5 @@ class PurchaseOrder(models.Model):
|
||||||
'lines_ids': confirmation_lines,
|
'lines_ids': confirmation_lines,
|
||||||
'po_id': self.id
|
'po_id': self.id
|
||||||
}
|
}
|
||||||
self.env['budget.confirmation'].with_context({}).create(data)
|
self.env['budget.confirmation'].create(data)
|
||||||
self.write({'state': 'waiting'})
|
self.write({'state': 'waiting'})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<record id="view_purchase_order_form_budget_fields" model="ir.ui.view">
|
||||||
|
<field name="name">purchase.order.form.budget.fields</field>
|
||||||
|
<field name="model">purchase.order</field>
|
||||||
|
<field name="inherit_id" ref="purchase.purchase_order_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
|
||||||
|
<!-- Add fields in order lines tree view -->
|
||||||
|
<xpath expr="//field[@name='order_line']/tree/field[@name='account_analytic_id']" position="after">
|
||||||
|
<field name="budget_id"
|
||||||
|
options="{'no_create': True, 'no_open': True}"
|
||||||
|
attrs="{'readonly': [('state', 'in', ['purchase', 'done', 'cancel'])]}"/>
|
||||||
|
<field name="budget_position_id"
|
||||||
|
options="{'no_create': True, 'no_open': True}"
|
||||||
|
attrs="{'readonly': [('state', 'in', ['purchase', 'done', 'cancel'])]}"/>
|
||||||
|
<field name="budget_account_id"
|
||||||
|
optional="hide"
|
||||||
|
options="{'no_create': True, 'no_open': True}"
|
||||||
|
attrs="{'readonly': [('state', 'in', ['purchase', 'done', 'cancel'])]}"/>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
<!-- Add fields in order lines form view (for detailed editing) -->
|
||||||
|
<xpath expr="//field[@name='order_line']/form//field[@name='account_analytic_id']" position="after">
|
||||||
|
<field name="budget_id"
|
||||||
|
options="{'no_create': True, 'no_open': True}"
|
||||||
|
attrs="{'readonly': [('state', 'in', ['purchase', 'done', 'cancel'])]}"/>
|
||||||
|
<field name="budget_position_id"
|
||||||
|
options="{'no_create': True, 'no_open': True}"
|
||||||
|
attrs="{'readonly': [('state', 'in', ['purchase', 'done', 'cancel'])]}"/>
|
||||||
|
<field name="budget_account_id"
|
||||||
|
options="{'no_create': True, 'no_open': True}"
|
||||||
|
attrs="{'readonly': [('state', 'in', ['purchase', 'done', 'cancel'])]}"/>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
Loading…
Reference in New Issue