19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:12 +01:00
parent 79f83631d5
commit 73afc09215
6267 changed files with 1534193 additions and 1130106 deletions

View file

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_move_line
from . import hr_expense
from . import project_project

View file

@ -0,0 +1,17 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
def _sale_determine_order(self):
""" For move lines created from expense, we override the normal behavior.
Note: if no SO but an AA is given on the expense, we will determine anyway the SO from its project's AAs linked,
using the same mecanism as in Vendor Bills.
"""
mapping_from_project = self._get_so_mapping_from_project()
mapping_from_expense = self._get_so_mapping_from_expense()
mapping_from_project.update(mapping_from_expense)
return mapping_from_project

View file

@ -0,0 +1,50 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class HrExpense(models.Model):
_inherit = "hr.expense"
def _compute_analytic_distribution(self):
super()._compute_analytic_distribution()
if not self.env.context.get('project_id'):
expenses_to_recompute = self.env['hr.expense']
prefetch_ids = set()
for expense in self.filtered('sale_order_id'):
expenses_to_recompute += expense
prefetch_ids.update(self.env['analytic.mixin']._get_analytic_account_ids_from_distributions(expense.analytic_distribution))
prefetch_ids.update(self.env['analytic.mixin']._get_analytic_account_ids_from_distributions(expense.sale_order_id.project_id._get_analytic_distribution()))
if expenses_to_recompute:
analytic_account_model = self.env['account.analytic.account'].with_prefetch(prefetch_ids)
for expense in expenses_to_recompute:
expense_account_ids = self.env['analytic.mixin']._get_analytic_account_ids_from_distributions(expense.analytic_distribution)
project_analytic_distribution = expense.sale_order_id.project_id._get_analytic_distribution()
project_account_ids = self.env['analytic.mixin']._get_analytic_account_ids_from_distributions(project_analytic_distribution)
project_analytic_distribution_accounts = self.env['account.analytic.account'].browse(project_account_ids)
expense_analytic_accounts = analytic_account_model.browse(expense_account_ids)
if not any(project_account.root_plan_id in expense_analytic_accounts.root_plan_id for project_account in project_analytic_distribution_accounts):
# If it is possible we keep both analytic distributions
expense.analytic_distribution = {
**(expense.analytic_distribution or {}),
**(project_analytic_distribution or {})
}
else:
# If not we keep the most prioritized one -> project
expense.analytic_distribution = expense.sale_order_id.project_id._get_analytic_distribution() or expense.analytic_distribution or {}
def action_post(self):
""" When creating the move of the expense, if the AA is given in the project of the SO, we take it as reference in the distribution.
Otherwise, we create a AA for the project of the SO and set the distribution to it.
"""
for expense in self:
project = expense.sale_order_id.project_id
if not project or expense.analytic_distribution:
continue
if not project.account_id:
project._create_analytic_account()
expense.analytic_distribution = project._get_analytic_distribution()
return super().action_post()

View file

@ -1,65 +1,62 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
from odoo import models
from collections import defaultdict
from odoo import fields, models
class Project(models.Model):
class ProjectProject(models.Model):
_inherit = 'project.project'
def _get_expenses_profitability_items(self, with_action=True):
if not self.analytic_account_id:
return {}
can_see_expense = with_action and self.user_has_groups('hr_expense.group_hr_expense_team_approver')
query = self.env['hr.expense']._search([('is_refused', '=', False), ('state', 'in', ['approved', 'done'])])
query.add_where('hr_expense.analytic_distribution ? %s', [str(self.analytic_account_id.id)])
query.order = None
query_string, query_param = query.select('sale_order_id', 'product_id', 'currency_id', 'array_agg(id) as ids', 'SUM(untaxed_amount) as untaxed_amount')
query_string = f"{query_string} GROUP BY sale_order_id, product_id, currency_id"
self._cr.execute(query_string, query_param)
expenses_read_group = [expense for expense in self._cr.dictfetchall()]
expenses_read_group = self.env['hr.expense']._read_group(
[('state', 'in', ['posted', 'in_payment', 'paid']), ('analytic_distribution', 'in', self.account_id.ids)],
groupby=['sale_order_id', 'product_id', 'currency_id'],
aggregates=['id:array_agg', 'untaxed_amount_currency:sum'],
)
if not expenses_read_group:
return {}
expenses_per_so_id = {}
expense_ids = []
amount_billed = 0.0
dict_amount_per_currency = defaultdict(lambda: 0.0)
for res in expenses_read_group:
so_id = res['sale_order_id']
product_id = res['product_id']
expenses_per_so_id.setdefault(so_id, {})[product_id] = res['ids']
can_see_expense = with_action and self.env.user.has_group('hr_expense.group_hr_expense_team_approver')
for sale_order, product, currency, ids, untaxed_amount_currency_sum in expenses_read_group:
expenses_per_so_id.setdefault(sale_order.id, {})[product.id] = ids
if can_see_expense:
expense_ids.extend(res['ids'])
dict_amount_per_currency[res['currency_id']] += res['untaxed_amount']
date = fields.Date.context_today(self)
for currency_id in dict_amount_per_currency:
if currency_id == self.company_id.currency_id.id:
amount_billed += dict_amount_per_currency[currency_id]
continue
currency = self.env['res.currency'].browse(currency_id)
amount_billed += currency._convert(dict_amount_per_currency[currency_id], self.company_id.currency_id, self.company_id, date)
expense_ids.extend(ids)
dict_amount_per_currency[currency] += untaxed_amount_currency_sum
amount_billed = 0.0
for currency, untaxed_amount_currency_sum in dict_amount_per_currency.items():
amount_billed += currency._convert(untaxed_amount_currency_sum, self.currency_id, self.company_id, round=False)
sol_read_group = self.env['sale.order.line'].sudo()._read_group(
[
('order_id', 'in', list(expenses_per_so_id.keys())),
('is_expense', '=', True),
('state', 'in', ['sale', 'done']),
('state', '=', 'sale'),
],
['order_id', 'product_id', 'untaxed_amount_to_invoice', 'untaxed_amount_invoiced'],
['order_id', 'product_id'],
lazy=False)
['order_id', 'product_id', 'currency_id'],
['untaxed_amount_to_invoice:sum', 'untaxed_amount_invoiced:sum'],
)
total_amount_expense_invoiced = total_amount_expense_to_invoice = 0.0
reinvoice_expense_ids = []
for res in sol_read_group:
expense_data_per_product_id = expenses_per_so_id[res['order_id'][0]]
product_id = res['product_id'][0]
dict_invoices_amount_per_currency = defaultdict(lambda: {'to_invoice': 0.0, 'invoiced': 0.0})
set_currency_ids = {self.currency_id.id}
for order, product, currency, untaxed_amount_to_invoice_sum, untaxed_amount_invoiced_sum in sol_read_group:
expense_data_per_product_id = expenses_per_so_id[order.id]
set_currency_ids.add(currency.id)
product_id = product.id
if product_id in expense_data_per_product_id:
total_amount_expense_to_invoice += res['untaxed_amount_to_invoice']
total_amount_expense_invoiced += res['untaxed_amount_invoiced']
dict_invoices_amount_per_currency[currency]['to_invoice'] += untaxed_amount_to_invoice_sum
dict_invoices_amount_per_currency[currency]['invoiced'] += untaxed_amount_invoiced_sum
reinvoice_expense_ids += expense_data_per_product_id[product_id]
for currency, revenues in dict_invoices_amount_per_currency.items():
total_amount_expense_to_invoice += currency._convert(revenues['to_invoice'], self.currency_id, self.company_id)
total_amount_expense_invoiced += currency._convert(revenues['invoiced'], self.currency_id, self.company_id)
section_id = 'expenses'
sequence = self._get_profitability_sequence_per_invoice_type()[section_id]
expense_data = {
@ -92,16 +89,13 @@ class Project(models.Model):
def _get_already_included_profitability_invoice_line_ids(self):
move_line_ids = super()._get_already_included_profitability_invoice_line_ids()
query = self.env['hr.expense']._search([('is_refused', '=', False), ('state', 'in', ['approved', 'done'])])
query.add_where('hr_expense.analytic_distribution ? %s', [str(self.analytic_account_id.id)])
query.order = None
query_string, query_param = query.select('sale_order_id')
query_string = f"{query_string} GROUP BY sale_order_id"
self._cr.execute(query_string, query_param)
expenses_read_group = list(self._cr.dictfetchall())
expenses_read_group = self.env['hr.expense']._read_group(
[('state', 'in', ['posted', 'in_payment', 'paid']), ('analytic_distribution', 'in', self.account_id.ids)],
groupby=['sale_order_id'],
aggregates=['__count'],
)
if not expenses_read_group:
return move_line_ids
for res in expenses_read_group:
sale_order = self.env['sale.order'].browse(res['sale_order_id'])
for sale_order, count in expenses_read_group:
move_line_ids.extend(sale_order.invoice_ids.mapped('invoice_line_ids').ids)
return move_line_ids