Steps to reproduce issue: 1. Have Sales installed 2. Enable Analytic Accounting and create two Projects 3. Go into both Project Settings and add distinct Analytic Accounts 4. Create a Vendor Bill 5. Add a Product Line and put both accounts in the Analytic Distribution 6. Add an account in another column to one of the existing Analytic Distribution Lines 7. Go to Projects and open the Project Updates of the Project whose account was alone in the Analytic Distribution Line 8. You get the error: > invalid literal for int() with base 10: '[int],[int]' Explanation: With commit odoo@dc696c8ed4850a83b6af721002a90f2944891923, the way IDs are stored changed. They are now computed in a csv string. A part of the Analytic Accounting pipeline didn't get updated after that change, making a lot of the features not working properly. Suggested fix: - We must take into account the multiple analytic plans in any order possible in the ID search. - Because the amount of `account.move.line` can increase rapidly, the new ID search must be indexable. - We reworked the counting query to avoid `account.move` being counted multiple times. opw-3626619 closes odoo/odoo#147664 Related: odoo/enterprise#54311 Signed-off-by: William André (wan) <wan@odoo.com>
199 lines
9.2 KiB
Python
199 lines
9.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import json
|
|
|
|
from odoo import api, fields, models, _, _lt
|
|
from odoo.osv import expression
|
|
from odoo.tools import SQL
|
|
from odoo.tools.misc import OrderedSet
|
|
|
|
|
|
class Project(models.Model):
|
|
_inherit = "project.project"
|
|
|
|
purchase_orders_count = fields.Integer('# Purchase Orders', compute='_compute_purchase_orders_count', groups='purchase.group_purchase_user')
|
|
|
|
@api.depends('analytic_account_id')
|
|
def _compute_purchase_orders_count(self):
|
|
if not self.analytic_account_id:
|
|
self.purchase_orders_count = 0
|
|
return
|
|
query = self.env['purchase.order.line']._search([])
|
|
query.add_where(
|
|
SQL(
|
|
"%s && %s",
|
|
[str(account_id) for account_id in self.analytic_account_id.ids],
|
|
self.env['purchase.order.line']._query_analytic_accounts(),
|
|
)
|
|
)
|
|
|
|
query_string, query_param = query.select(
|
|
r"""DISTINCT order_id, (regexp_matches(jsonb_object_keys(purchase_order_line.analytic_distribution), '\d+', 'g'))[1]::int as account_id"""
|
|
)
|
|
query_string = f"""
|
|
SELECT account_id, count(order_id) FROM
|
|
({query_string}) distribution
|
|
GROUP BY account_id
|
|
"""
|
|
|
|
self._cr.execute(query_string, query_param)
|
|
data = {res['account_id']: res['count'] for res in self._cr.dictfetchall()}
|
|
for project in self:
|
|
project.purchase_orders_count = data.get(project.analytic_account_id.id, 0)
|
|
|
|
# ----------------------------
|
|
# Actions
|
|
# ----------------------------
|
|
|
|
def action_open_project_purchase_orders(self):
|
|
query = self.env['purchase.order.line']._search([])
|
|
query.add_where(
|
|
SQL(
|
|
"%s && %s",
|
|
[str(self.analytic_account_id.id)],
|
|
self.env['purchase.order.line']._query_analytic_accounts(),
|
|
)
|
|
)
|
|
query_string, query_param = query.select('order_id')
|
|
self._cr.execute(query_string, query_param)
|
|
purchase_order_ids = [pol.get('order_id') for pol in self._cr.dictfetchall()]
|
|
action_window = {
|
|
'name': _('Purchase Orders'),
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'purchase.order',
|
|
'views': [[False, 'tree'], [False, 'form']],
|
|
'domain': [('id', 'in', purchase_order_ids)],
|
|
'context': {
|
|
'project_id': self.id,
|
|
}
|
|
}
|
|
if len(purchase_order_ids) == 1:
|
|
action_window['views'] = [[False, 'form']]
|
|
action_window['res_id'] = purchase_order_ids[0]
|
|
return action_window
|
|
|
|
def action_profitability_items(self, section_name, domain=None, res_id=False):
|
|
if section_name == 'purchase_order':
|
|
action = {
|
|
'name': _('Purchase Order Items'),
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'purchase.order.line',
|
|
'views': [[False, 'tree'], [False, 'form']],
|
|
'domain': domain,
|
|
'context': {
|
|
'create': False,
|
|
'edit': False,
|
|
},
|
|
}
|
|
if res_id:
|
|
action['res_id'] = res_id
|
|
if 'views' in action:
|
|
action['views'] = [
|
|
(view_id, view_type)
|
|
for view_id, view_type in action['views']
|
|
if view_type == 'form'
|
|
] or [False, 'form']
|
|
action['view_mode'] = 'form'
|
|
return action
|
|
return super().action_profitability_items(section_name, domain, res_id)
|
|
|
|
# ----------------------------
|
|
# Project Updates
|
|
# ----------------------------
|
|
|
|
def _get_stat_buttons(self):
|
|
buttons = super(Project, self)._get_stat_buttons()
|
|
if self.user_has_groups('purchase.group_purchase_user'):
|
|
self_sudo = self.sudo()
|
|
buttons.append({
|
|
'icon': 'credit-card',
|
|
'text': _lt('Purchase Orders'),
|
|
'number': self_sudo.purchase_orders_count,
|
|
'action_type': 'object',
|
|
'action': 'action_open_project_purchase_orders',
|
|
'show': self_sudo.purchase_orders_count > 0,
|
|
'sequence': 36,
|
|
})
|
|
return buttons
|
|
|
|
def _get_profitability_aal_domain(self):
|
|
return expression.AND([
|
|
super()._get_profitability_aal_domain(),
|
|
['|', ('move_line_id', '=', False), ('move_line_id.purchase_line_id', '=', False)],
|
|
])
|
|
|
|
def _add_purchase_items(self, profitability_items, with_action=True):
|
|
return False
|
|
|
|
def _get_profitability_labels(self):
|
|
labels = super()._get_profitability_labels()
|
|
labels['purchase_order'] = _lt('Purchase Orders')
|
|
return labels
|
|
|
|
def _get_profitability_sequence_per_invoice_type(self):
|
|
sequence_per_invoice_type = super()._get_profitability_sequence_per_invoice_type()
|
|
sequence_per_invoice_type['purchase_order'] = 10
|
|
return sequence_per_invoice_type
|
|
|
|
def _get_profitability_items(self, with_action=True):
|
|
profitability_items = super()._get_profitability_items(with_action)
|
|
if self.analytic_account_id:
|
|
query = self.env['purchase.order.line'].sudo()._search([
|
|
('state', 'in', ['purchase', 'done']),
|
|
'|',
|
|
('qty_invoiced', '>', 0),
|
|
'|', ('qty_to_invoice', '>', 0), ('product_uom_qty', '>', 0),
|
|
], order=self.env['purchase.order.line']._order)
|
|
query.add_where(
|
|
SQL(
|
|
"%s && %s",
|
|
[str(self.analytic_account_id.id)],
|
|
self.env['purchase.order.line']._query_analytic_accounts(),
|
|
)
|
|
)
|
|
query_string, query_param = query.select('"purchase_order_line".id', 'qty_invoiced', 'qty_to_invoice', 'product_uom_qty', 'price_unit', 'purchase_order_line.currency_id', '"purchase_order_line".analytic_distribution')
|
|
self._cr.execute(query_string, query_param)
|
|
purchase_order_line_read = [{
|
|
**pol,
|
|
'invoice_lines': self.env['purchase.order.line'].browse(pol['id']).sudo().invoice_lines, # One2Many cannot be queried, they are not columns
|
|
} for pol in self._cr.dictfetchall()]
|
|
purchase_order_line_invoice_line_ids = self._get_already_included_profitability_invoice_line_ids()
|
|
with_action = with_action and self.user_has_groups('purchase.group_purchase_user, account.group_account_invoice, account.group_account_readonly')
|
|
if purchase_order_line_read:
|
|
currency_ids = OrderedSet(pol['currency_id'] for pol in purchase_order_line_read)
|
|
|
|
amount_invoiced = amount_to_invoice = 0.0
|
|
purchase_order_line_ids = []
|
|
for pol_read in purchase_order_line_read:
|
|
purchase_order_line_invoice_line_ids.extend(pol_read['invoice_lines'].ids)
|
|
currency = self.env['res.currency'].browse(pol_read['currency_id']).with_prefetch(currency_ids)
|
|
price_unit = currency._convert(pol_read['price_unit'], self.currency_id, self.company_id)
|
|
analytic_contribution = pol_read['analytic_distribution'][str(self.analytic_account_id.id)] / 100.
|
|
amount_invoiced -= price_unit * pol_read['qty_invoiced'] * analytic_contribution if pol_read['qty_invoiced'] > 0 else 0.0
|
|
if pol_read['qty_to_invoice'] > 0:
|
|
amount_to_invoice -= price_unit * pol_read['qty_to_invoice'] * analytic_contribution
|
|
else:
|
|
amount_to_invoice -= price_unit * (pol_read['product_uom_qty'] - pol_read['qty_invoiced']) * analytic_contribution
|
|
purchase_order_line_ids.append(pol_read['id'])
|
|
costs = profitability_items['costs']
|
|
section_id = 'purchase_order'
|
|
purchase_order_costs = {'id': section_id, 'sequence': self._get_profitability_sequence_per_invoice_type()[section_id], 'billed': amount_invoiced, 'to_bill': amount_to_invoice}
|
|
if with_action and purchase_order_line_ids:
|
|
args = [section_id, [('id', 'in', purchase_order_line_ids)]]
|
|
if len(purchase_order_line_ids) == 1:
|
|
args.append(purchase_order_line_ids[0])
|
|
action = {'name': 'action_profitability_items', 'type': 'object', 'args': json.dumps(args)}
|
|
purchase_order_costs['action'] = action
|
|
costs['data'].append(purchase_order_costs)
|
|
costs['total']['billed'] += amount_invoiced
|
|
costs['total']['to_bill'] += amount_to_invoice
|
|
domain = [
|
|
('move_id.move_type', 'in', ['in_invoice', 'in_refund']),
|
|
('parent_state', 'in', ['draft', 'posted']),
|
|
('price_subtotal', '>', 0),
|
|
('id', 'not in', purchase_order_line_invoice_line_ids),
|
|
]
|
|
self._get_costs_items_from_purchase(domain, profitability_items, with_action=with_action)
|
|
return profitability_items
|