[IMP] analytic, *: block validation with mandatory plan

* = {account, sale, purchase, hr_expense}
When a plan is mandatory for SOL, POL or expenses, the flow should still be blocked when pressing the button, and not only when creating analytic lines.

We add the info in the context of the button, so automatic flows are still not blocked.

When the applicability rule requires a mandatory analytic distribution, raise an error upon :
- confirming PO,
- sending or confirming SO
- approving Expense Report by manager.

t-3040929

closes odoo/odoo#107166

X-original-commit: 8623a7850288828759db125b1054b0fed6b6b135
Signed-off-by: William André (wan) <wan@odoo.com>
This commit is contained in:
gawa-odoo
2022-12-04 17:58:01 +01:00
parent 4d7a9eea13
commit 8bf6bee153
10 changed files with 63 additions and 29 deletions
+6 -17
View File
@@ -6,7 +6,7 @@ from functools import lru_cache
from odoo import api, fields, models, Command, _
from odoo.exceptions import ValidationError, UserError
from odoo.tools import frozendict, formatLang, format_date, float_is_zero, float_compare
from odoo.tools import frozendict, formatLang, format_date, float_is_zero
from odoo.tools.sql import create_index
from odoo.addons.web.controllers.utils import clean_action
@@ -2352,32 +2352,21 @@ class AccountMoveLine(models.Model):
# ANALYTIC
# -------------------------------------------------------------------------
def _validate_distribution(self):
def _validate_analytic_distribution(self):
for line in self.filtered(lambda line: line.display_type == 'product'):
mandatory_plans_ids = [plan['id'] for plan in self.env['account.analytic.plan'].sudo().get_relevant_plans(**{
line._validate_distribution(**{
'product': line.product_id.id,
'account': line.account_id.id,
'business_domain': line.move_id.move_type in ['out_invoice', 'out_refund', 'out_receipt'] and 'invoice'
or line.move_id.move_type in ['in_invoice', 'in_refund', 'in_receipt'] and 'bill'
or 'general',
'company_id': self.company_id.id,
}) if plan['applicability'] == 'mandatory']
if not mandatory_plans_ids:
continue
distribution_by_root_plan = {}
for analytic_account_id, percentage in (line.analytic_distribution or {}).items():
root_plan = self.env['account.analytic.account'].browse(int(analytic_account_id)).root_plan_id
distribution_by_root_plan[root_plan.id] = distribution_by_root_plan.get(root_plan.id, 0) + percentage
for plan_id in mandatory_plans_ids:
if float_compare(distribution_by_root_plan.get(plan_id, 0), 100, precision_digits=2) != 0:
raise ValidationError(_("One or more lines require a 100% analytic distribution."))
'company_id': line.company_id.id,
})
def _create_analytic_lines(self):
""" Create analytic items upon validation of an account.move.line having an analytic distribution.
"""
if self.env.context.get('validate_analytic', False):
self._validate_distribution()
self._validate_analytic_distribution()
analytic_line_vals = []
for line in self:
analytic_line_vals.extend(line._prepare_analytic_lines())
+18 -3
View File
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields, api
from odoo.tools.float_utils import float_round
from odoo import models, fields, api, _
from odoo.tools.float_utils import float_round, float_compare
from odoo.exceptions import ValidationError
class AnalyticMixin(models.AbstractModel):
_name = 'analytic.mixin'
@@ -47,6 +47,21 @@ class AnalyticMixin(models.AbstractModel):
vals_list = [self._sanitize_values(vals, decimal_precision) for vals in vals_list]
return super().create(vals_list)
def _validate_distribution(self, **kwargs):
if self.env.context.get('validate_analytic', False):
mandatory_plans_ids = [plan['id'] for plan in self.env['account.analytic.plan'].sudo().get_relevant_plans(**kwargs) if plan['applicability'] == 'mandatory']
if not mandatory_plans_ids:
return
decimal_precision = self.env['decimal.precision'].precision_get('Percentage Analytic')
distribution_by_root_plan = {}
for analytic_account_id, percentage in (self.analytic_distribution or {}).items():
root_plan = self.env['account.analytic.account'].browse(int(analytic_account_id)).root_plan_id
distribution_by_root_plan[root_plan.id] = distribution_by_root_plan.get(root_plan.id, 0) + percentage
for plan_id in mandatory_plans_ids:
if float_compare(distribution_by_root_plan.get(plan_id, 0), 100, precision_digits=decimal_precision) != 0:
raise ValidationError(_("One or more lines require a 100% analytic distribution."))
def _sanitize_values(self, vals, decimal_precision):
""" Normalize the float of the distribution """
if 'analytic_distribution' in vals:
@@ -100,7 +100,7 @@ class TestAnalyticAccount(TransactionCase):
plans_json = self.env['account.analytic.plan'].get_relevant_plans(**kwargs)
self.assertEqual(1, len(plans_json), "Plan 1 should be unavailable")
kwargs = {'business_domain': 'purchase'}
kwargs = {'business_domain': 'purchase_order'}
plans_json = self.env['account.analytic.plan'].get_relevant_plans(**kwargs)
self.assertEqual(2, len(plans_json), "Both plans should be available")
+9
View File
@@ -1154,6 +1154,7 @@ class HrExpenseSheet(models.Model):
def approve_expense_sheets(self):
self._check_can_approve()
self._validate_analytic_distribution()
duplicates = self.expense_line_ids.duplicate_expense_ids.filtered(lambda exp: exp.state in ['approved', 'done'])
if duplicates:
action = self.env["ir.actions.act_window"]._for_xml_id('hr_expense.hr_expense_approve_duplicate_action')
@@ -1161,6 +1162,14 @@ class HrExpenseSheet(models.Model):
return action
self._do_approve()
def _validate_analytic_distribution(self):
for line in self.expense_line_ids:
line._validate_distribution(**{
'account': line.account_id.id,
'business_domain': 'expense',
'company_id': line.company_id.id,
})
def _do_approve(self):
self._check_can_approve()
@@ -662,6 +662,7 @@
string="Approve"
type="object"
data-hotkey="v"
context="{'validate_analytic': True}"
attrs="{'invisible': ['|', ('can_approve', '=', False), ('state', '!=', 'submit')]}"
class="oe_highlight o_expense_sheet_approve"/>
<button name="action_sheet_move_create"
+9
View File
@@ -474,6 +474,7 @@ class PurchaseOrder(models.Model):
for order in self:
if order.state not in ['draft', 'sent']:
continue
order.order_line._validate_analytic_distribution()
order._add_supplier_to_product()
# Deal with double validation process
if order._approval_allowed():
@@ -1423,3 +1424,11 @@ class PurchaseOrderLine(models.Model):
values={'line': self, 'qty_received': new_qty},
subtype_id=self.env.ref('mail.mt_note').id
)
def _validate_analytic_distribution(self):
for line in self.filtered(lambda l: not l.display_type):
line._validate_distribution(**{
'product': line.product_id.id,
'business_domain': 'purchase_order',
'company_id': line.company_id.id,
})
+2 -2
View File
@@ -136,12 +136,12 @@
<header>
<button name="action_rfq_send" states="draft" string="Send by Email" type="object" context="{'send_rfq':True}" class="oe_highlight" data-hotkey="g"/>
<button name="print_quotation" string="Print RFQ" type="object" states="draft" class="oe_highlight" groups="base.group_user" data-hotkey="k"/>
<button name="button_confirm" type="object" states="sent" string="Confirm Order" class="oe_highlight" id="bid_confirm" data-hotkey="v"/>
<button name="button_confirm" type="object" states="sent" string="Confirm Order" context="{'validate_analytic': True}" class="oe_highlight" id="bid_confirm" data-hotkey="v"/>
<button name="button_approve" type="object" states='to approve' string="Approve Order" class="oe_highlight" groups="purchase.group_purchase_manager" data-hotkey="z"/>
<button name="action_create_invoice" string="Create Bill" type="object" class="oe_highlight" context="{'create_bill':True}" attrs="{'invisible': ['|', ('state', 'not in', ('purchase', 'done')), ('invoice_status', 'in', ('no', 'invoiced'))]}" data-hotkey="w"/>
<button name="action_rfq_send" states="sent" string="Re-Send by Email" type="object" context="{'send_rfq':True}" data-hotkey="g"/>
<button name="print_quotation" string="Print RFQ" type="object" states="sent" groups="base.group_user" data-hotkey="k"/>
<button name="button_confirm" type="object" states="draft" string="Confirm Order" id="draft_confirm"/>
<button name="button_confirm" type="object" states="draft" context="{'validate_analytic': True}" string="Confirm Order" id="draft_confirm"/>
<button name="action_rfq_send" states="purchase" string="Send PO by Email" type="object" context="{'send_rfq':False}" data-hotkey="g"/>
<button name="confirm_reminder_mail" string="Confirm Receipt Date" type="object" attrs="{'invisible': ['|','|', ('state', 'not in', ('purchase', 'done')), ('mail_reminder_confirmed', '=', True), ('date_planned', '=', False)]}" groups="base.group_no_one" data-hotkey="o"/>
<button name="action_create_invoice" string="Create Bill" type="object" context="{'create_bill':True}" attrs="{'invisible': ['|', '|', ('state', 'not in', ('purchase', 'done')), ('invoice_status', 'not in', ('no', 'invoiced')), ('order_line', '=', [])]}" data-hotkey="w"/>
+3
View File
@@ -706,6 +706,7 @@ class SaleOrder(models.Model):
def action_quotation_send(self):
""" Opens a wizard to compose an email, with relevant mail template loaded by default """
self.ensure_one()
self.order_line._validate_analytic_distribution()
lang = self.env.context.get('lang')
mail_template = self._find_mail_template()
if mail_template and mail_template.lang:
@@ -782,6 +783,8 @@ class SaleOrder(models.Model):
", ".join(self._get_forbidden_state_confirm()),
))
self.order_line._validate_analytic_distribution()
for order in self:
if order.partner_id in order.message_partner_ids:
continue
+8
View File
@@ -1105,6 +1105,14 @@ class SaleOrderLine(models.Model):
"""
return {}
def _validate_analytic_distribution(self):
for line in self.filtered(lambda l: not l.display_type and l.state in ['draft', 'sent']):
line._validate_distribution(**{
'product': line.product_id.id,
'business_domain': 'sale_order',
'company_id': line.company_id.id,
})
#=== CORE METHODS OVERRIDES ===#
def name_get(self):
+6 -6
View File
@@ -201,18 +201,18 @@
<button id="create_invoice_percentage" name="%(sale.action_view_sale_advance_payment_inv)d" string="Create Invoice"
type="action" context="{'default_advance_payment_method': 'percentage'}" data-hotkey="q"
attrs="{'invisible': ['|',('invoice_status', '!=', 'no'), ('state', '!=', 'sale')]}"/>
<button name="action_quotation_send" string="Send by Email" type="object" states="draft" class="btn-primary" data-hotkey="g"/>
<button name="action_quotation_send" string="Send by Email" type="object" states="draft" class="btn-primary" data-hotkey="g" context="{'validate_analytic': True}"/>
<button name="action_quotation_send" type="object" string="Send PRO-FORMA Invoice"
groups="sale.group_proforma_sales" class="btn-primary"
attrs="{'invisible': ['|', ('state', '!=', 'draft'), ('invoice_count','&gt;=',1)]}" context="{'proforma': True}"/>
attrs="{'invisible': ['|', ('state', '!=', 'draft'), ('invoice_count','&gt;=',1)]}" context="{'proforma': True, 'validate_analytic': True}"/>
<button name="action_confirm" id="action_confirm" data-hotkey="v"
string="Confirm" class="btn-primary" type="object"
string="Confirm" class="btn-primary" type="object" context="{'validate_analytic': True}"
attrs="{'invisible': [('state', 'not in', ['sent'])]}"/>
<button name="action_confirm" data-hotkey="v"
string="Confirm" type="object"
string="Confirm" type="object" context="{'validate_analytic': True}"
attrs="{'invisible': [('state', 'not in', ['draft'])]}"/>
<button name="action_quotation_send" type="object" string="Send PRO-FORMA Invoice" groups="sale.group_proforma_sales" attrs="{'invisible': ['|', ('state', '=', 'draft'), ('invoice_count','&gt;=',1)]}" context="{'proforma': True}"/>
<button name="action_quotation_send" string="Send by Email" type="object" states="sent,sale" data-hotkey="g"/>
<button name="action_quotation_send" type="object" string="Send PRO-FORMA Invoice" groups="sale.group_proforma_sales" attrs="{'invisible': ['|', ('state', '=', 'draft'), ('invoice_count','&gt;=',1)]}" context="{'proforma': True, 'validate_analytic': True}"/>
<button name="action_quotation_send" string="Send by Email" type="object" states="sent,sale" data-hotkey="g" context="{'validate_analytic': True}"/>
<button name="action_cancel" type="object" string="Cancel" attrs="{'invisible': ['|', ('state', 'not in', ['draft', 'sent','sale']), ('id', '=', False)]}" data-hotkey="z"/>
<button name="action_draft" states="cancel" type="object" string="Set to Quotation" data-hotkey="w"/>
<field name="state" widget="statusbar" statusbar_visible="draft,sent,sale"/>