[IMP] analytic, *: Json field for analytic distribution
The `analytic_distribution` field is a Json. It was stored temporarily as a char. Search is not available yet, so we do queries by hand when we need to search on keys. Also added a constraint on account_analytic_distribution_model, so we don't have models with accounts specific to a company when the model has no company or another company. It would cause an issue when looking at the models from another company. X-original-commit: 7064c95aa04e5138bb12ae97acfee04ebb67cc0e Part-of: odoo/odoo#103097
This commit is contained in:
@@ -41,10 +41,16 @@ class AccountAnalyticAccount(models.Model):
|
||||
|
||||
def action_view_invoice(self):
|
||||
self.ensure_one()
|
||||
query = self.env['account.move.line']._search([('move_id.move_type', 'in', self.env['account.move'].get_sale_types())])
|
||||
query.order = None
|
||||
query.add_where('analytic_distribution ? %s', [str(self.id)])
|
||||
query_string, query_param = query.select('DISTINCT move_id')
|
||||
self._cr.execute(query_string, query_param)
|
||||
move_ids = [line.get('move_id') for line in self._cr.dictfetchall()]
|
||||
result = {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "account.move",
|
||||
"domain": [('line_ids.analytic_distribution_stored_char', '=ilike', f'%"{self.id}":%'), ('move_type', 'in', self.env['account.move'].get_sale_types())],
|
||||
"domain": [('id', 'in', move_ids)],
|
||||
"context": {"create": False},
|
||||
"name": "Customer Invoices",
|
||||
'view_mode': 'tree,form',
|
||||
@@ -53,10 +59,16 @@ class AccountAnalyticAccount(models.Model):
|
||||
|
||||
def action_view_vendor_bill(self):
|
||||
self.ensure_one()
|
||||
query = self.env['account.move.line']._search([('move_id.move_type', 'in', self.env['account.move'].get_purchase_types())])
|
||||
query.order = None
|
||||
query.add_where('analytic_distribution ? %s', [str(self.id)])
|
||||
query_string, query_param = query.select('DISTINCT move_id')
|
||||
self._cr.execute(query_string, query_param)
|
||||
move_ids = [line.get('move_id') for line in self._cr.dictfetchall()]
|
||||
result = {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "account.move",
|
||||
"domain": [('line_ids.analytic_distribution_stored_char', '=ilike', f'%"{self.id}":%'), ('move_type', 'in', self.env['account.move'].get_purchase_types())],
|
||||
"domain": [('id', 'in', move_ids)],
|
||||
"context": {"create": False},
|
||||
"name": "Vendor Bills",
|
||||
'view_mode': 'tree,form',
|
||||
|
||||
@@ -15,7 +15,7 @@ INTEGRITY_HASH_LINE_FIELDS = ('debit', 'credit', 'account_id', 'partner_id')
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
_name = "account.move.line"
|
||||
_inherit = 'analytic.mixin'
|
||||
_inherit = "analytic.mixin"
|
||||
_description = "Journal Item"
|
||||
_order = "date desc, move_name desc, sequence, id"
|
||||
_check_company_auto = True
|
||||
@@ -352,6 +352,9 @@ class AccountMoveLine(models.Model):
|
||||
comodel_name='account.analytic.line', inverse_name='move_line_id',
|
||||
string='Analytic lines',
|
||||
)
|
||||
analytic_distribution = fields.Json(
|
||||
inverse="_inverse_analytic_distribution",
|
||||
) # add the inverse function used to trigger the creation/update of the analytic lines accordingly (field originally defined in the analytic mixin)
|
||||
|
||||
# === Early Pay fields === #
|
||||
discount_date = fields.Date(
|
||||
@@ -1071,10 +1074,10 @@ class AccountMoveLine(models.Model):
|
||||
line.term_key = False
|
||||
|
||||
@api.depends('account_id', 'partner_id', 'product_id')
|
||||
def _compute_analytic_distribution_stored_char(self):
|
||||
def _compute_analytic_distribution(self):
|
||||
for line in self:
|
||||
if line.display_type == 'product' or not line.move_id.is_invoice(include_receipts=True):
|
||||
distribution = self.env['account.analytic.distribution.model']._get_distributionjson({
|
||||
distribution = self.env['account.analytic.distribution.model']._get_distribution({
|
||||
"product_id": line.product_id.id,
|
||||
"product_categ_id": line.product_id.categ_id.id,
|
||||
"partner_id": line.partner_id.id,
|
||||
@@ -1082,8 +1085,7 @@ class AccountMoveLine(models.Model):
|
||||
"account_prefix": line.account_id.code,
|
||||
"company_id": line.company_id.id,
|
||||
})
|
||||
line.analytic_distribution_stored_char = distribution or line.analytic_distribution_stored_char
|
||||
line._compute_analytic_distribution()
|
||||
line.analytic_distribution = distribution or line.analytic_distribution
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# INVERSE METHODS
|
||||
@@ -1125,7 +1127,6 @@ class AccountMoveLine(models.Model):
|
||||
@api.onchange('analytic_distribution')
|
||||
def _inverse_analytic_distribution(self):
|
||||
""" Unlink and recreate analytic_lines when modifying the distribution."""
|
||||
super()._inverse_analytic_distribution()
|
||||
lines_to_modify = self.env['account.move.line'].browse([
|
||||
line.id for line in self if line.parent_state == "posted"
|
||||
])
|
||||
@@ -2365,7 +2366,6 @@ class AccountMoveLine(models.Model):
|
||||
def _prepare_analytic_lines(self):
|
||||
self.ensure_one()
|
||||
analytic_line_vals = []
|
||||
self._compute_analytic_distribution()
|
||||
if self.analytic_distribution:
|
||||
# distribution_on_each_plan corresponds to the proportion that is distributed to each plan to be able to
|
||||
# give the real amount when we achieve a 100% distribution
|
||||
|
||||
@@ -182,8 +182,8 @@ class AccountMoveLine(models.Model):
|
||||
)
|
||||
AND (
|
||||
NOT tax.analytic
|
||||
OR (base_line.analytic_distribution_stored_char IS NULL AND account_move_line.analytic_distribution_stored_char IS NULL)
|
||||
OR base_line.analytic_distribution_stored_char = account_move_line.analytic_distribution_stored_char
|
||||
OR (base_line.analytic_distribution IS NULL AND account_move_line.analytic_distribution IS NULL)
|
||||
OR base_line.analytic_distribution = account_move_line.analytic_distribution
|
||||
)
|
||||
LEFT JOIN affecting_base_tax_ids tax_line_tax_ids ON tax_line_tax_ids.id = account_move_line.id
|
||||
JOIN affecting_base_tax_ids base_line_tax_ids ON base_line_tax_ids.id = base_line.id
|
||||
|
||||
@@ -21,10 +21,12 @@ class TestAccountAnalyticAccount(AccountTestInvoicingCommon):
|
||||
cls.analytic_account_a = cls.env['account.analytic.account'].create({
|
||||
'name': 'analytic_account_a',
|
||||
'plan_id': cls.default_plan.id,
|
||||
'company_id': False,
|
||||
})
|
||||
cls.analytic_account_b = cls.env['account.analytic.account'].create({
|
||||
'name': 'analytic_account_b',
|
||||
'plan_id': cls.default_plan.id,
|
||||
'company_id': False,
|
||||
})
|
||||
|
||||
def create_invoice(self, partner, product):
|
||||
@@ -123,7 +125,7 @@ class TestAccountAnalyticAccount(AccountTestInvoicingCommon):
|
||||
|
||||
# Partner and product match, score 2
|
||||
invoice = self.create_invoice(self.partner_a, self.product_a)
|
||||
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {self.analytic_account_b.id: 100})
|
||||
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {str(self.analytic_account_b.id): 100})
|
||||
|
||||
# Match the partner but not the product, score 0
|
||||
invoice = self.create_invoice(self.partner_a, self.product_b)
|
||||
@@ -131,7 +133,7 @@ class TestAccountAnalyticAccount(AccountTestInvoicingCommon):
|
||||
|
||||
# Product match, score 1
|
||||
invoice = self.create_invoice(self.partner_b, self.product_a)
|
||||
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {self.analytic_account_a.id: 100})
|
||||
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {str(self.analytic_account_a.id): 100})
|
||||
|
||||
# No rule match with the product, score 0
|
||||
invoice = self.create_invoice(self.partner_b, self.product_b)
|
||||
@@ -141,10 +143,12 @@ class TestAccountAnalyticAccount(AccountTestInvoicingCommon):
|
||||
"""Test that the distribution is recomputed if and only if it is needed when changing the partner."""
|
||||
self.env['account.analytic.distribution.model'].create([{
|
||||
'partner_id': self.partner_a.id,
|
||||
'analytic_distribution': {self.analytic_account_a.id: 100}
|
||||
'analytic_distribution': {self.analytic_account_a.id: 100},
|
||||
'company_id': False,
|
||||
}, {
|
||||
'partner_id': self.partner_b.id,
|
||||
'analytic_distribution': {self.analytic_account_b.id: 100}
|
||||
'analytic_distribution': {self.analytic_account_b.id: 100},
|
||||
'company_id': False,
|
||||
}])
|
||||
|
||||
invoice = self.create_invoice(self.env['res.partner'], self.product_a)
|
||||
@@ -153,27 +157,27 @@ class TestAccountAnalyticAccount(AccountTestInvoicingCommon):
|
||||
|
||||
# A model is found, set the new values
|
||||
invoice.partner_id = self.partner_a
|
||||
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {self.analytic_account_a.id: 100})
|
||||
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {str(self.analytic_account_a.id): 100})
|
||||
|
||||
# A model is found, set the new values
|
||||
invoice.partner_id = self.partner_b
|
||||
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {self.analytic_account_b.id: 100})
|
||||
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {str(self.analytic_account_b.id): 100})
|
||||
|
||||
# No model is found, don't change previously set values
|
||||
invoice.partner_id = invoice.company_id.partner_id
|
||||
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {self.analytic_account_b.id: 100})
|
||||
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {str(self.analytic_account_b.id): 100})
|
||||
|
||||
# No model is found, don't change previously set values
|
||||
invoice.partner_id = False
|
||||
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {self.analytic_account_b.id: 100})
|
||||
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {str(self.analytic_account_b.id): 100})
|
||||
|
||||
# It manual value is not erased in form view when saving
|
||||
with Form(invoice) as invoice_form:
|
||||
invoice_form.partner_id = self.partner_a
|
||||
with invoice_form.invoice_line_ids.edit(0) as line_form:
|
||||
self.assertEqual(line_form.analytic_distribution, {self.analytic_account_a.id: 100})
|
||||
self.assertEqual(line_form.analytic_distribution, {str(self.analytic_account_a.id): 100})
|
||||
line_form.analytic_distribution = {self.analytic_account_b.id: 100}
|
||||
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {self.analytic_account_b.id: 100})
|
||||
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {str(self.analytic_account_b.id): 100})
|
||||
|
||||
def test_mandatory_plan_validation(self):
|
||||
invoice = self.create_invoice(self.partner_b, self.product_a)
|
||||
|
||||
@@ -1173,7 +1173,7 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
|
||||
'code': 'TEST'
|
||||
})
|
||||
|
||||
analytic_distribution = {analytic_account.id: 100}
|
||||
analytic_distribution = {str(analytic_account.id): 100.00}
|
||||
|
||||
move_form = Form(self.invoice)
|
||||
with move_form.invoice_line_ids.edit(0) as line_form:
|
||||
@@ -1291,7 +1291,7 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
|
||||
'code': 'TEST1'
|
||||
})
|
||||
|
||||
analytic_distribution = {analytic_account.id: 100}
|
||||
analytic_distribution = {str(analytic_account.id): 100.00}
|
||||
|
||||
self.invoice.write({'invoice_line_ids': [(1, self.invoice.invoice_line_ids.ids[0], {
|
||||
'analytic_distribution': analytic_distribution,
|
||||
|
||||
@@ -78,7 +78,7 @@ class AccountingTestTemplConsistency(TransactionCase):
|
||||
)
|
||||
# lines
|
||||
self.check_fields_consistency('account.reconcile.model.line.template', 'account.reconcile.model.line', exceptions=['chart_template_id'])
|
||||
self.check_fields_consistency('account.reconcile.model.line', 'account.reconcile.model.line.template', exceptions=['company_id', 'journal_id', 'analytic_distribution_stored_char', 'amount'])
|
||||
self.check_fields_consistency('account.reconcile.model.line', 'account.reconcile.model.line.template', exceptions=['company_id', 'journal_id', 'analytic_distribution', 'amount'])
|
||||
|
||||
def test_account_group_fields(self):
|
||||
'''Test fields consistency for ('account.group', 'account.group.template')
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
<field name="blocked"/>
|
||||
</group>
|
||||
<group string="Analytic" groups="analytic.group_analytic_accounting">
|
||||
<field name="analytic_distribution_stored_char" invisible="1"/>
|
||||
<field name="analytic_distribution" widget="analytic_distribution" groups="analytic.group_analytic_accounting"
|
||||
readonly="1"/>
|
||||
</group>
|
||||
@@ -187,7 +186,6 @@
|
||||
<field name="matching_number" readonly="1" optional="show"/>
|
||||
<field name="amount_residual" sum="Total Residual" string="Residual" readonly="1" optional="hide" attrs="{'invisible':[('is_account_reconcile', '=', False)]}"/>
|
||||
<field name="amount_residual_currency" sum="Total Residual in Currency" string="Residual in Currency" readonly="1" optional="hide" attrs="{'invisible':['|', ('is_same_currency', '=', True), ('is_account_reconcile', '=', False)]}"/>
|
||||
<field name="analytic_distribution_stored_char" invisible="1"/>
|
||||
<field name="analytic_distribution" widget="analytic_distribution"
|
||||
groups="analytic.group_analytic_accounting"
|
||||
optional="show"
|
||||
@@ -885,7 +883,6 @@
|
||||
options="{'no_create': True}"
|
||||
domain="[('deprecated', '=', False), ('account_type', 'not in', ('asset_receivable', 'liability_payable')), ('company_id', '=', parent.company_id), ('is_off_balance', '=', False)]"
|
||||
attrs="{'required': [('display_type', 'not in', ('line_note', 'line_section'))]}"/>
|
||||
<field name="analytic_distribution_stored_char" invisible="1"/>
|
||||
<field name="analytic_distribution" widget="analytic_distribution"
|
||||
groups="analytic.group_analytic_accounting"
|
||||
optional="show"
|
||||
@@ -994,7 +991,6 @@
|
||||
<group>
|
||||
<field name="account_id" options="{'no_create': True}" domain="[('company_id', '=', company_id)]" context="{'partner_id': partner_id, 'move_type': parent.move_type}"/>
|
||||
<field name="tax_ids" widget="many2many_tags"/>
|
||||
<field name="analytic_distribution_stored_char" invisible="1"/>
|
||||
<field name="analytic_distribution" widget="analytic_distribution" groups="analytic.group_analytic_accounting"/>
|
||||
</group>
|
||||
<label for="name" string="Description" attrs="{'invisible': [('display_type', 'in', ('line_note', 'line_section'))]}"/>
|
||||
@@ -1056,7 +1052,6 @@
|
||||
domain="['|', ('parent_id', '=', False), ('is_company', '=', True)]"
|
||||
attrs="{'column_invisible': [('parent.move_type', '!=', 'entry')]}"/>
|
||||
<field name="name" widget="section_and_note_text" optional="show"/>
|
||||
<field name="analytic_distribution_stored_char" invisible="1"/>
|
||||
<field name="analytic_distribution" widget="analytic_distribution"
|
||||
groups="analytic.group_analytic_accounting"
|
||||
optional="show"
|
||||
@@ -1140,7 +1135,6 @@
|
||||
<field name="account_id" domain="[('company_id', '=', parent.company_id), ('deprecated', '=', False)]"/>
|
||||
<field name="partner_id" domain="['|', ('parent_id', '=', False), ('is_company', '=', True)]"/>
|
||||
<field name="name"/>
|
||||
<field name="analytic_distribution_stored_char" invisible="1"/>
|
||||
<field name="analytic_distribution" widget="analytic_distribution" groups="analytic.group_analytic_accounting"/>
|
||||
<field name="amount_currency" groups="base.group_multi_currency"/>
|
||||
<field name="company_currency_id" invisible="1"/>
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
<field name="show_force_tax_included" invisible="1"/>
|
||||
<field name="force_tax_included"
|
||||
attrs="{'invisible': [('show_force_tax_included', '=', False)]}" force_save="1"/>
|
||||
<field name="analytic_distribution_stored_char" invisible="1"/>
|
||||
<field name="analytic_distribution" widget="analytic_distribution"
|
||||
groups="analytic.group_analytic_accounting"
|
||||
options="{'account_field': 'account_id', 'business_domain': 'general'}"/>
|
||||
@@ -244,7 +243,6 @@
|
||||
<field name="tax_ids"
|
||||
widget="many2many_tags"
|
||||
optional="hide"/>
|
||||
<field name="analytic_distribution_stored_char" invisible="1"/>
|
||||
<field name="analytic_distribution" widget="analytic_distribution"
|
||||
groups="analytic.group_analytic_accounting"
|
||||
options="{'account_field': 'account_id', 'business_domain': 'general'}"/>
|
||||
|
||||
@@ -21,6 +21,7 @@ that have no counterpart in the general financial accounts.
|
||||
'views/analytic_account_views.xml',
|
||||
'views/analytic_plan_views.xml',
|
||||
'views/analytic_distribution_model_views.xml',
|
||||
'data/analytic_data.xml'
|
||||
],
|
||||
'demo': [
|
||||
'data/analytic_account_demo.xml'
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<data noupdate="1">
|
||||
<record id="decimal_percentage_analytic" model="decimal.precision">
|
||||
<field name="name">Percentage Analytic</field>
|
||||
<field name="digits">10</field>
|
||||
<field name="digits" eval="2"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
import json
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class AccountAnalyticDistributionModel(models.Model):
|
||||
_name = 'account.analytic.distribution.model'
|
||||
@@ -26,10 +27,27 @@ class AccountAnalyticDistributionModel(models.Model):
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
default=lambda self: self.env.company,
|
||||
ondelete='cascade',
|
||||
help="Select a company for which the analytic distribution will be used (e.g. create new customer invoice or Sales order if we select this company, it will automatically take this as an analytic account)",
|
||||
)
|
||||
|
||||
@api.constrains('company_id')
|
||||
def _check_company_accounts(self):
|
||||
query = """
|
||||
SELECT model.id
|
||||
FROM account_analytic_distribution_model model
|
||||
JOIN account_analytic_account account
|
||||
ON model.analytic_distribution ? CAST(account.id AS VARCHAR)
|
||||
WHERE account.company_id IS NOT NULL
|
||||
AND (model.company_id IS NULL
|
||||
OR model.company_id != account.company_id)
|
||||
GROUP BY model.id
|
||||
"""
|
||||
self.env.cr.execute(query)
|
||||
if self.env.cr.dictfetchone():
|
||||
raise UserError(_('You defined a distribution with analytic account(s) belonging to a specific company but a model shared between companies or with a different company'))
|
||||
|
||||
@api.model
|
||||
def _get_distribution(self, vals):
|
||||
""" Returns the distribution model that has the most fields that corresponds to the vals given
|
||||
@@ -71,9 +89,3 @@ class AccountAnalyticDistributionModel(models.Model):
|
||||
return [(fname, 'in', value)]
|
||||
else:
|
||||
return [(fname, 'in', [value, False])]
|
||||
|
||||
@api.model
|
||||
def _get_distributionjson(self, vals):
|
||||
""" Returns the distribution model as a json for the compute_analytic_distribution_stored_char functions"""
|
||||
distribution = self._get_distribution(vals)
|
||||
return json.dumps(distribution) if distribution else None
|
||||
|
||||
@@ -1,49 +1,51 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import models, fields, api
|
||||
import json
|
||||
from odoo.tools import float_repr
|
||||
from odoo.tools.float_utils import float_round
|
||||
|
||||
|
||||
class AnalyticMixin(models.AbstractModel):
|
||||
_name = 'analytic.mixin'
|
||||
_description = 'Analytic Mixin'
|
||||
|
||||
# We create 2 different fields, with a computed binary field, so we don't have to decode encode each time the json.
|
||||
# We also format the float values of the stored field, so we can use it as key (for tax detail for ex.)
|
||||
analytic_distribution_stored_char = fields.Char(
|
||||
compute="_compute_analytic_distribution_stored_char", store=True, copy=True)
|
||||
analytic_distribution = fields.Binary(
|
||||
string="Analytic",
|
||||
compute="_compute_analytic_distribution",
|
||||
inverse="_inverse_analytic_distribution",
|
||||
readonly=False,
|
||||
analytic_distribution = fields.Json(
|
||||
'Analytic',
|
||||
compute="_compute_analytic_distribution", store=True, copy=True, readonly=False,
|
||||
precompute=True
|
||||
)
|
||||
|
||||
def _compute_analytic_distribution_stored_char(self):
|
||||
def init(self):
|
||||
# Add a gin index for json search on the keys, on the models that actually have a table
|
||||
query = ''' SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_name=%s '''
|
||||
self.env.cr.execute(query, [self._table])
|
||||
if self.env.cr.dictfetchone():
|
||||
query = f"""
|
||||
CREATE INDEX IF NOT EXISTS {self._table}_analytic_distribution_index
|
||||
ON {self._table} USING gin(analytic_distribution);
|
||||
"""
|
||||
self.env.cr.execute(query)
|
||||
|
||||
def _compute_analytic_distribution(self):
|
||||
pass
|
||||
|
||||
@api.depends('analytic_distribution_stored_char')
|
||||
def _compute_analytic_distribution(self):
|
||||
for record in self:
|
||||
if record.analytic_distribution_stored_char:
|
||||
distribution_to_return = {}
|
||||
distribution_json = json.loads(record.analytic_distribution_stored_char)
|
||||
for account, distribution in distribution_json.items():
|
||||
distribution_to_return[int(account)] = float(distribution)
|
||||
# Check if the account exists, can be removed when we have a constraint between account and model
|
||||
account_ids = self.env['account.analytic.account'].browse(distribution_to_return.keys()).exists().ids
|
||||
record.analytic_distribution = {account_id: distribution_to_return[account_id] for account_id in account_ids}
|
||||
|
||||
@api.onchange('analytic_distribution')
|
||||
def _inverse_analytic_distribution(self):
|
||||
def write(self, vals):
|
||||
""" Format the analytic_distribution float value, so equality on analytic_distribution can be done """
|
||||
decimal_precision = self.env['decimal.precision'].precision_get('Percentage Analytic')
|
||||
self.env.remove_to_compute(self._fields['analytic_distribution_stored_char'], self)
|
||||
for record in self:
|
||||
if not record.analytic_distribution:
|
||||
record.analytic_distribution_stored_char = None
|
||||
else:
|
||||
distribution_to_return = {}
|
||||
for account, distribution in record.analytic_distribution.items():
|
||||
distribution_to_return[account] = float_repr(distribution, decimal_precision)
|
||||
record.analytic_distribution_stored_char = json.dumps(distribution_to_return)
|
||||
vals = self._sanitize_values(vals, decimal_precision)
|
||||
return super().write(vals)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
""" Format the analytic_distribution float value, so equality on analytic_distribution can be done """
|
||||
decimal_precision = self.env['decimal.precision'].precision_get('Percentage Analytic')
|
||||
vals_list = [self._sanitize_values(vals, decimal_precision) for vals in vals_list]
|
||||
return super().create(vals_list)
|
||||
|
||||
def _sanitize_values(self, vals, decimal_precision):
|
||||
""" Normalize the float of the distribution """
|
||||
if 'analytic_distribution' in vals:
|
||||
vals['analytic_distribution'] = vals.get('analytic_distribution') and {
|
||||
account_id: float_round(distribution, decimal_precision) for account_id, distribution in vals['analytic_distribution'].items()}
|
||||
return vals
|
||||
|
||||
+8
-1
@@ -79,6 +79,12 @@ export class AnalyticDistribution extends Component {
|
||||
if (this.editingRecord) {
|
||||
await this.fetchAllPlans(this.props);
|
||||
}
|
||||
const args = {
|
||||
domain: [["name", "like", "Percentage Analytic"]],
|
||||
fields: ["digits"],
|
||||
context: [],
|
||||
}
|
||||
this.decimal_precision = await this.orm.call("decimal.precision", "search_read", [], args);
|
||||
await this.formatData(this.props);
|
||||
}
|
||||
|
||||
@@ -559,7 +565,8 @@ export class AnalyticDistribution extends Component {
|
||||
}
|
||||
|
||||
formatPercentage(value) {
|
||||
return formatPercentage(value / 100, { digits: [false, 2] });
|
||||
return formatPercentage(value / 100, { digits: [false, this.decimal_precision[0].digits || 2] });
|
||||
|
||||
}
|
||||
}
|
||||
AnalyticDistribution.template = "analytic_distribution";
|
||||
|
||||
@@ -63,7 +63,7 @@ QUnit.module("Analytic", (hooks) => {
|
||||
fields: {
|
||||
label: { string: "Label", type: "char" },
|
||||
amount: { string: "Amount", type: "float" },
|
||||
analytic_distribution: { string: "Analytic", type: "char" },
|
||||
analytic_distribution: { string: "Analytic", type: "json" },
|
||||
move_id: { string: "Account Move", type: "many2one", relation: "move" },
|
||||
},
|
||||
records: [
|
||||
@@ -81,6 +81,15 @@ QUnit.module("Analytic", (hooks) => {
|
||||
{ id: 1, display_name: "INV0001", line_ids: [1, 2]},
|
||||
{ id: 2, display_name: "INV0002", line_ids: [3, 4]},
|
||||
],
|
||||
},
|
||||
"decimal.precision": {
|
||||
fields: {
|
||||
name: { string: "Name", type: "char" },
|
||||
digits: { string: "Digits", type: "int" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, name: "Percentage Analytic", digits: 2}
|
||||
]
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -114,8 +114,8 @@ class TestAnalyticAccount(TransactionCase):
|
||||
distribution_json = self.env['account.analytic.distribution.model']._get_distribution({
|
||||
"partner_id": self.partner_a.id,
|
||||
})
|
||||
self.assertEqual(distribution_json, self.distribution_1.analytic_distribution, "Distribution 1 should be given")
|
||||
self.assertEqual(distribution_json, {str(self.analytic_account_3.id): 100}, "Distribution 1 should be given")
|
||||
distribution_json = self.env['account.analytic.distribution.model']._get_distribution({
|
||||
"partner_id": self.partner_b.id,
|
||||
})
|
||||
self.assertEqual(distribution_json, self.distribution_2.analytic_distribution, "Distribution 2 should be given")
|
||||
self.assertEqual(distribution_json, {str(self.analytic_account_2.id): 100}, "Distribution 2 should be given")
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<field name="model">account.analytic.distribution.model</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Analytic Distribution Model">
|
||||
<field name="analytic_distribution_stored_char" invisible="1"/>
|
||||
<field name="partner_id" optional="show"/>
|
||||
<field name="partner_category_id" optional="hide"/>
|
||||
<field name="company_id" groups="base.group_multi_company" optional="show"/>
|
||||
@@ -21,7 +20,6 @@
|
||||
<form string="Analytic Distribution Model">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="analytic_distribution_stored_char" invisible="1"/>
|
||||
<group string="Simultaneous conditions to meet" colspan="2">
|
||||
<group>
|
||||
<field name="partner_id"/>
|
||||
|
||||
@@ -307,16 +307,15 @@ class HrExpense(models.Model):
|
||||
expenses = expenses - exp
|
||||
|
||||
@api.depends('product_id', 'account_id')
|
||||
def _compute_analytic_distribution_stored_char(self):
|
||||
def _compute_analytic_distribution(self):
|
||||
for expense in self:
|
||||
distribution = self.env['account.analytic.distribution.model']._get_distributionjson({
|
||||
distribution = self.env['account.analytic.distribution.model']._get_distribution({
|
||||
'product_id': expense.product_id.id,
|
||||
'product_categ_id': expense.product_id.categ_id.id,
|
||||
'account_prefix': expense.account_id.code,
|
||||
'company_id': expense.company_id.id,
|
||||
})
|
||||
expense.analytic_distribution_stored_char = distribution or expense.analytic_distribution_stored_char
|
||||
expense._compute_analytic_distribution()
|
||||
expense.analytic_distribution = distribution or expense.analytic_distribution
|
||||
|
||||
@api.constrains('payment_mode')
|
||||
def _check_payment_mode(self):
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<span t-field="line.description"/><br/>
|
||||
<span>
|
||||
<div t-foreach="line.analytic_distribution" t-as="distribution">
|
||||
<t t-esc="line.env['account.analytic.account'].browse(distribution)[0].name"/>: <t t-esc="line.analytic_distribution.get(distribution)"/>
|
||||
<t t-esc="line.env['account.analytic.account'].browse(int(distribution))[0].name"/>: <t t-esc="line.analytic_distribution.get(distribution)"/>
|
||||
</div>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
@@ -199,7 +199,7 @@ class TestExpenses(TestExpenseCommon):
|
||||
'product_id': self.product_b.id,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'tax_line_id': False,
|
||||
'analytic_distribution': {self.analytic_account_2.id: 100},
|
||||
'analytic_distribution': {str(self.analytic_account_2.id): 100},
|
||||
},
|
||||
# Product line (company currency):
|
||||
{
|
||||
@@ -210,7 +210,7 @@ class TestExpenses(TestExpenseCommon):
|
||||
'product_id': self.product_a.id,
|
||||
'currency_id': self.company_data['currency'].id,
|
||||
'tax_line_id': False,
|
||||
'analytic_distribution': {self.analytic_account_1.id: 100},
|
||||
'analytic_distribution': {str(self.analytic_account_1.id): 100},
|
||||
},
|
||||
])
|
||||
|
||||
@@ -319,7 +319,7 @@ class TestExpenses(TestExpenseCommon):
|
||||
'unit_amount': 350.00,
|
||||
'tax_ids': [(6, 0, [self.tax_purchase_a.id])],
|
||||
'sheet_id': expense.id,
|
||||
'analytic_distribution': {self.analytic_account_1.id: 100},
|
||||
'analytic_distribution': {str(self.analytic_account_1.id): 100},
|
||||
})
|
||||
|
||||
expense.action_submit_sheet()
|
||||
|
||||
@@ -56,7 +56,6 @@
|
||||
<field name="activity_ids" widget="list_activity" optional="show"/>
|
||||
<field name="accounting_date" optional="hide" groups="account.group_account_invoice,account.group_account_readonly" readonly="0"/>
|
||||
<field name="reference" optional="hide" groups="account.group_account_readonly"/>
|
||||
<field name="analytic_distribution_stored_char" invisible="1"/>
|
||||
<field name="analytic_distribution" widget="analytic_distribution"
|
||||
optional="show"
|
||||
groups="analytic.group_analytic_accounting"
|
||||
@@ -194,7 +193,6 @@
|
||||
<field name="accounting_date" attrs="{'invisible': ['|', ('accounting_date', '=', False), ('state', 'not in', ['approved', 'done'])]}" />
|
||||
<field name="account_id" options="{'no_create': True}" domain="[('account_type', 'not in', ('asset_receivable','liability_payable','asset_cash','liability_credit_card')), ('company_id', '=', company_id)]" groups="account.group_account_readonly" attrs="{'readonly': ['|', ('is_editable', '=', False), ('sheet_is_editable', '=', False)]}" context="{'default_company_id': company_id}"/>
|
||||
<field name="sheet_id" invisible="1"/>
|
||||
<field name="analytic_distribution_stored_char" invisible="1"/>
|
||||
<field name="analytic_distribution" widget="analytic_distribution"
|
||||
groups="analytic.group_analytic_accounting"
|
||||
options="{'product_field': 'product_id', 'account_field': 'account_id', 'business_domain': 'expense'}"
|
||||
@@ -753,7 +751,6 @@
|
||||
<field name="attachment_number" invisible="1"/>
|
||||
<button name="action_get_attachment_view" type="object" icon="fa-paperclip" aria-label="View Attachments" title="View Attachments" class="float-end pe-0" readonly="True" attrs="{'invisible': [('attachment_number', '=', 0)]}"/>
|
||||
<field name="reference" optional="hide" readonly="True"/>
|
||||
<field name="analytic_distribution_stored_char" invisible="1"/>
|
||||
<field name="analytic_distribution" widget="analytic_distribution"
|
||||
groups="analytic.group_analytic_accounting"
|
||||
optional="show"
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
<field name="total_amount" force_save="1" attrs="{'readonly': [('product_has_cost', '=', True)]}"/>
|
||||
<field name="tax_ids" widget="many2many_tags" attrs="{'readonly': [('product_has_tax', '=', False)]}"/>
|
||||
<field name="amount_tax"/>
|
||||
<field name="analytic_distribution_stored_char" invisible="1"/>
|
||||
<field name="analytic_distribution" widget="analytic_distribution"
|
||||
optional="show"
|
||||
groups="analytic.group_analytic_accounting"/>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
from collections import Counter
|
||||
|
||||
from odoo import api, fields, models, _, _lt
|
||||
from odoo.osv import expression
|
||||
@@ -16,11 +17,13 @@ class Project(models.Model):
|
||||
if not self.analytic_account_id:
|
||||
self.expenses_count = 0
|
||||
return
|
||||
query = self.env['hr.expense']._search([])
|
||||
query.add_where('hr_expense.analytic_distribution ?| array[%s]', [str(account_id) for account_id in self.analytic_account_id.ids])
|
||||
query_string, query_param = query.select('analytic_distribution')
|
||||
self._cr.execute(query_string, query_param)
|
||||
mapped_data = Counter(account for data in self._cr.dictfetchall() for account in data['analytic_distribution'])
|
||||
for project in self:
|
||||
expenses = self.env['hr.expense'].search([
|
||||
('analytic_distribution_stored_char', '=ilike', f'%"{project.analytic_account_id.id}":%')
|
||||
])
|
||||
project.expenses_count = len(expenses)
|
||||
project.expenses_count = mapped_data.get(project.analytic_account_id.id, 0)
|
||||
|
||||
# ----------------------------
|
||||
# Actions
|
||||
@@ -64,14 +67,13 @@ class Project(models.Model):
|
||||
if not self.analytic_account_id:
|
||||
return {}
|
||||
can_see_expense = with_action and self.user_has_groups('hr_expense.group_hr_expense_team_approver')
|
||||
expenses_read_group = self.env['hr.expense'].sudo()._read_group(
|
||||
[('analytic_distribution_stored_char', '=ilike', f'%"{self.analytic_account_id.id}":%'),
|
||||
('is_refused', '=', False),
|
||||
('state', 'in', ['approved', 'done'])],
|
||||
['untaxed_amount', 'ids:array_agg(id)'],
|
||||
[],
|
||||
)
|
||||
if not expenses_read_group or not expenses_read_group[0]['__count']:
|
||||
query = self.env['hr.expense']._search([('is_refused', '=', False), ('state', 'in', ['approved', 'done'])])
|
||||
query.order = None
|
||||
query.add_where('hr_expense.analytic_distribution ? %s', [str(self.analytic_account_id.id)])
|
||||
query_string, query_param = query.select('array_agg(id) as ids', 'SUM(untaxed_amount) as untaxed_amount')
|
||||
self._cr.execute(query_string, query_param)
|
||||
expenses_read_group = [expense for expense in self._cr.dictfetchall()]
|
||||
if not expenses_read_group or not expenses_read_group[0].get('ids'):
|
||||
return {}
|
||||
expense_data = expenses_read_group[0]
|
||||
section_id = 'expenses'
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import json
|
||||
|
||||
from collections import defaultdict
|
||||
from odoo import api, fields, models, _, _lt
|
||||
from odoo.osv import expression
|
||||
|
||||
@@ -17,33 +18,40 @@ class Project(models.Model):
|
||||
if not self.analytic_account_id:
|
||||
self.purchase_orders_count = 0
|
||||
return
|
||||
query = self.env['purchase.order.line']._search([])
|
||||
query.add_where('purchase_order_line.analytic_distribution ?| array[%s]', [str(account_id) for account_id in self.analytic_account_id.ids])
|
||||
mapped_data = defaultdict(set)
|
||||
query_string, query_param = query.select('analytic_distribution', 'order_id')
|
||||
self._cr.execute(query_string, query_param)
|
||||
for data in self._cr.dictfetchall():
|
||||
for analytic_account_id in data['analytic_distribution']:
|
||||
mapped_data[int(analytic_account_id)].add(data['order_id'])
|
||||
for project in self:
|
||||
purchase_orders = self.env['purchase.order'].search([
|
||||
('order_line.analytic_distribution_stored_char', '=ilike', f'%"{project.analytic_account_id.id}":%')
|
||||
])
|
||||
project.purchase_orders_count = len(purchase_orders)
|
||||
project.purchase_orders_count = len(mapped_data.get(project.analytic_account_id.id, []))
|
||||
|
||||
# ----------------------------
|
||||
# Actions
|
||||
# ----------------------------
|
||||
|
||||
def action_open_project_purchase_orders(self):
|
||||
purchase_orders = self.env['purchase.order'].search([
|
||||
('order_line.analytic_distribution_stored_char', '=ilike', f'%"{self.analytic_account_id.id}":%')
|
||||
])
|
||||
query = self.env['purchase.order.line']._search([])
|
||||
query.add_where('purchase_order_line.analytic_distribution ? %s', [str(self.analytic_account_id.id)])
|
||||
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_orders.ids)],
|
||||
'domain': [('id', 'in', purchase_order_ids)],
|
||||
'context': {
|
||||
'project_id': self.id,
|
||||
}
|
||||
}
|
||||
if len(purchase_orders) == 1:
|
||||
if len(purchase_order_ids) == 1:
|
||||
action_window['views'] = [[False, 'form']]
|
||||
action_window['res_id'] = purchase_orders.id
|
||||
action_window['res_id'] = purchase_order_ids[0]
|
||||
return action_window
|
||||
|
||||
def action_profitability_items(self, section_name, domain=None, res_id=False):
|
||||
@@ -108,13 +116,16 @@ class Project(models.Model):
|
||||
def _get_profitability_items(self, with_action=True):
|
||||
profitability_items = super()._get_profitability_items(with_action)
|
||||
if self.analytic_account_id:
|
||||
purchase_order_line_read = self.env['purchase.order.line'].sudo().search_read([
|
||||
('analytic_distribution_stored_char', '=ilike', f'%"{self.analytic_account_id.id}":%'),
|
||||
query = self.env['purchase.order.line'].sudo()._search([
|
||||
('state', 'in', ['purchase', 'done']),
|
||||
'|',
|
||||
('qty_invoiced', '>', 0),
|
||||
'|', ('qty_to_invoice', '>', 0), ('product_uom_qty', '>', 0),
|
||||
], ['qty_invoiced', 'qty_to_invoice', 'product_uom_qty', 'price_unit'])
|
||||
])
|
||||
query.add_where('purchase_order_line.analytic_distribution ? %s', [str(self.analytic_account_id.id)])
|
||||
query_string, query_param = query.select('"purchase_order_line".id', 'qty_invoiced', 'qty_to_invoice', 'product_uom_qty', 'price_unit')
|
||||
self._cr.execute(query_string, query_param)
|
||||
purchase_order_line_read = [pol for pol in self._cr.dictfetchall()]
|
||||
if purchase_order_line_read:
|
||||
amount_invoiced = amount_to_invoice = 0.0
|
||||
purchase_order_line_ids = []
|
||||
|
||||
@@ -13,24 +13,21 @@ class Project(models.Model):
|
||||
if not self.analytic_account_id:
|
||||
return {}
|
||||
can_see_expense = with_action and self.user_has_groups('hr_expense.group_hr_expense_team_approver')
|
||||
expenses_read_group = self.env['hr.expense'].sudo()._read_group(
|
||||
[
|
||||
('analytic_distribution_stored_char', '=ilike', f'%"{self.analytic_account_id.id}":%'),
|
||||
('is_refused', '=', False),
|
||||
('state', 'in', ['approved', 'done']),
|
||||
],
|
||||
['sale_order_id', 'product_id', 'ids:array_agg(id)', 'untaxed_amount'],
|
||||
['sale_order_id', 'product_id'],
|
||||
lazy=False,
|
||||
)
|
||||
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', 'array_agg(id) as ids', 'SUM(untaxed_amount) as untaxed_amount')
|
||||
query_string = f"{query_string} GROUP BY sale_order_id, product_id"
|
||||
self._cr.execute(query_string, query_param)
|
||||
expenses_read_group = [expense for expense in self._cr.dictfetchall()]
|
||||
if not expenses_read_group:
|
||||
return {}
|
||||
expenses_per_so_id = {}
|
||||
expense_ids = []
|
||||
amount_billed = 0.0
|
||||
for res in expenses_read_group:
|
||||
so_id = res['sale_order_id'] and res['sale_order_id'][0]
|
||||
product_id = res['product_id'] and res['product_id'][0]
|
||||
so_id = res['sale_order_id']
|
||||
product_id = res['product_id']
|
||||
expenses_per_so_id.setdefault(so_id, {})[product_id] = res['ids']
|
||||
if can_see_expense:
|
||||
expense_ids.extend(res['ids'])
|
||||
|
||||
@@ -1131,18 +1131,17 @@ class PurchaseOrderLine(models.Model):
|
||||
return datetime.today() + relativedelta(days=seller.delay if seller else 0)
|
||||
|
||||
@api.depends('product_id', 'order_id.partner_id')
|
||||
def _compute_analytic_distribution_stored_char(self):
|
||||
def _compute_analytic_distribution(self):
|
||||
for line in self:
|
||||
if not line.display_type:
|
||||
distribution = self.env['account.analytic.distribution.model']._get_distributionjson({
|
||||
distribution = self.env['account.analytic.distribution.model']._get_distribution({
|
||||
"product_id": line.product_id.id,
|
||||
"product_categ_id": line.product_id.categ_id.id,
|
||||
"partner_id": line.order_id.partner_id.id,
|
||||
"partner_category_id": line.order_id.partner_id.category_id.ids,
|
||||
"company_id": line.company_id.id,
|
||||
})
|
||||
line.analytic_distribution_stored_char = distribution or line.analytic_distribution_stored_char
|
||||
line._compute_analytic_distribution()
|
||||
line.analytic_distribution = distribution or line.analytic_distribution
|
||||
|
||||
@api.onchange('product_id')
|
||||
def onchange_product_id(self):
|
||||
|
||||
@@ -327,7 +327,7 @@ class TestPurchaseToInvoice(AccountTestInvoicingCommon):
|
||||
'analytic_distribution': {analytic_account_default.id: 100},
|
||||
'product_id': self.product_order.id,
|
||||
})
|
||||
analytic_distribution_manual = {analytic_account_manual.id: 100}
|
||||
analytic_distribution_manual = {str(analytic_account_manual.id): 100}
|
||||
|
||||
po_form = Form(self.env['purchase.order'].with_context(tracking_disable=True))
|
||||
po_form.partner_id = self.partner_a
|
||||
@@ -373,9 +373,9 @@ class TestPurchaseToInvoice(AccountTestInvoicingCommon):
|
||||
purchase_order = po_form.save()
|
||||
purchase_order_line = purchase_order.order_line
|
||||
|
||||
self.assertEqual(purchase_order_line.analytic_distribution, {analytic_account_super.id: 100}, "The analytic account should be set to 'Super Account'")
|
||||
self.assertEqual(purchase_order_line.analytic_distribution, {str(analytic_account_super.id): 100}, "The analytic account should be set to 'Super Account'")
|
||||
purchase_order_line.write({'product_id': great_product.id})
|
||||
self.assertEqual(purchase_order_line.analytic_distribution, {analytic_account_great.id: 100}, "The analytic account should be set to 'Great Account'")
|
||||
self.assertEqual(purchase_order_line.analytic_distribution, {str(analytic_account_great.id): 100}, "The analytic account should be set to 'Great Account'")
|
||||
|
||||
po_no_analytic_distribution = self.env['purchase.order'].create({
|
||||
'partner_id': self.env.ref('base.res_partner_1').id,
|
||||
|
||||
@@ -237,7 +237,6 @@
|
||||
force_save="1" domain="[('purchase_ok', '=', True), '|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]"/>
|
||||
<field name="name" widget="section_and_note_text"/>
|
||||
<field name="date_planned" optional="hide" attrs="{'required': [('display_type', '=', False)]}" force_save="1"/>
|
||||
<field name="analytic_distribution_stored_char" invisible="1"/>
|
||||
<field name="analytic_distribution" widget="analytic_distribution"
|
||||
optional="hide"
|
||||
groups="analytic.group_analytic_accounting"
|
||||
@@ -285,7 +284,6 @@
|
||||
</group>
|
||||
<group>
|
||||
<field name="date_planned" widget="date" attrs="{'required': [('display_type', '=', False)]}"/>
|
||||
<field name="analytic_distribution_stored_char" invisible="1"/>
|
||||
<field name="analytic_distribution" widget="analytic_distribution"
|
||||
groups="analytic.group_analytic_accounting"
|
||||
options="{'product_field': 'product_id', 'business_domain': 'purchase_order'}"/>
|
||||
@@ -738,7 +736,6 @@
|
||||
domain="[('type_tax_use', '=', 'purchase')]"/>
|
||||
<field name="date_planned" widget="date" readonly="1"/>
|
||||
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
|
||||
<field name="analytic_distribution_stored_char" invisible="1"/>
|
||||
<field name="analytic_distribution" widget="analytic_distribution"
|
||||
groups="analytic.group_analytic_accounting"
|
||||
options="{'product_field': 'product_id', 'business_domain': 'purchase_order'}"/>
|
||||
|
||||
@@ -161,7 +161,6 @@
|
||||
<field name="product_uom_category_id" invisible="1"/>
|
||||
<field name="product_uom_id" string="UoM" groups="uom.group_uom" optional="show" attrs="{'required': [('product_id', '!=', False)]}"/>
|
||||
<field name="schedule_date" optional="hide"/>
|
||||
<field name="analytic_distribution_stored_char" invisible="1"/>
|
||||
<field name="analytic_distribution" widget="analytic_distribution"
|
||||
optional="hide"
|
||||
groups="analytic.group_analytic_accounting"
|
||||
@@ -177,7 +176,6 @@
|
||||
<field name="product_uom_category_id" invisible="1"/>
|
||||
<field name="product_uom_id" />
|
||||
<field name="schedule_date"/>
|
||||
<field name="analytic_distribution_stored_char" invisible="1"/>
|
||||
<field name="analytic_distribution" widget="analytic_distribution"
|
||||
groups="analytic.group_analytic_accounting"
|
||||
options="{'product_field': 'product_id', 'business_domain': 'purchase_order'}"/>
|
||||
|
||||
@@ -876,18 +876,17 @@ class SaleOrderLine(models.Model):
|
||||
line.untaxed_amount_to_invoice = amount_to_invoice
|
||||
|
||||
@api.depends('order_id.partner_id', 'product_id')
|
||||
def _compute_analytic_distribution_stored_char(self):
|
||||
def _compute_analytic_distribution(self):
|
||||
for line in self:
|
||||
if not line.display_type and line.state == 'draft':
|
||||
distribution = line.env['account.analytic.distribution.model']._get_distributionjson({
|
||||
distribution = line.env['account.analytic.distribution.model']._get_distribution({
|
||||
"product_id": line.product_id.id,
|
||||
"product_categ_id": line.product_id.categ_id.id,
|
||||
"partner_id": line.order_id.partner_id.id,
|
||||
"partner_category_id": line.order_id.partner_id.category_id.ids,
|
||||
"company_id": line.company_id.id,
|
||||
})
|
||||
line.analytic_distribution_stored_char = distribution or line.analytic_distribution_stored_char
|
||||
line._compute_analytic_distribution()
|
||||
line.analytic_distribution = distribution or line.analytic_distribution
|
||||
|
||||
@api.depends('product_id', 'state', 'qty_invoiced', 'qty_delivered')
|
||||
def _compute_product_updatable(self):
|
||||
@@ -997,7 +996,7 @@ class SaleOrderLine(models.Model):
|
||||
def _get_protected_fields(self):
|
||||
return [
|
||||
'product_id', 'name', 'price_unit', 'product_uom', 'product_uom_qty',
|
||||
'tax_id', 'analytic_distribution_stored_char'
|
||||
'tax_id', 'analytic_distribution'
|
||||
]
|
||||
|
||||
def _update_line_quantity(self, values):
|
||||
|
||||
@@ -402,9 +402,9 @@ class TestSalesTeam(SaleCommon):
|
||||
'order_id': sale_order.id,
|
||||
})
|
||||
|
||||
self.assertEqual(sol.analytic_distribution, {analytic_account_super.id: 100}, "The analytic distribution should be set to Super Account")
|
||||
self.assertEqual(sol.analytic_distribution, {str(analytic_account_super.id): 100}, "The analytic distribution should be set to Super Account")
|
||||
sol.write({'product_id': great_product.id})
|
||||
self.assertEqual(sol.analytic_distribution, {analytic_account_great.id: 100}, "The analytic distribution should be set to Great Account")
|
||||
self.assertEqual(sol.analytic_distribution, {str(analytic_account_great.id): 100}, "The analytic distribution should be set to Great Account")
|
||||
|
||||
so_no_analytic_account = self.env['sale.order'].create({
|
||||
'partner_id': self.env.ref('base.res_partner_1').id,
|
||||
|
||||
@@ -447,7 +447,7 @@ class TestSaleToInvoice(TestSaleCommon):
|
||||
down_payment.create_invoices()
|
||||
|
||||
aml = self.env['account.move.line'].search([('move_id', 'in', so.invoice_ids.ids)])[0]
|
||||
self.assertRecordValues(aml, [{'analytic_distribution': {analytic_account_default.id: 100}}])
|
||||
self.assertRecordValues(aml, [{'analytic_distribution': {str(analytic_account_default.id): 100}}])
|
||||
|
||||
def test_invoice_analytic_account_so_not_default(self):
|
||||
""" Tests whether, when an analytic account rule is set and the so has an analytic account,
|
||||
@@ -486,7 +486,7 @@ class TestSaleToInvoice(TestSaleCommon):
|
||||
down_payment.create_invoices()
|
||||
|
||||
aml = self.env['account.move.line'].search([('move_id', 'in', so.invoice_ids.ids)])[0]
|
||||
self.assertRecordValues(aml, [{'analytic_distribution': {analytic_account_default.id: 100, analytic_account_so.id: 100}}])
|
||||
self.assertRecordValues(aml, [{'analytic_distribution': {str(analytic_account_default.id): 100, str(analytic_account_so.id): 100}}])
|
||||
|
||||
def test_invoice_after_product_return_price_not_default(self):
|
||||
so = self.env['sale.order'].create({
|
||||
|
||||
@@ -363,7 +363,6 @@
|
||||
<div name="lead">
|
||||
<field name="customer_lead" class="oe_inline"/> days
|
||||
</div>
|
||||
<field name="analytic_distribution_stored_char" invisible="1"/>
|
||||
<field name="analytic_distribution" widget="analytic_distribution"
|
||||
groups="analytic.group_analytic_accounting"
|
||||
options="{'product_field': 'product_id', 'business_domain': 'sale_order'}"/>
|
||||
@@ -440,12 +439,10 @@
|
||||
domain="[('sale_ok', '=', True), '|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]"
|
||||
widget="sol_product_many2one"/>
|
||||
<field name="name" widget="section_and_note_text" optional="show"/>
|
||||
<field name="analytic_distribution_stored_char" invisible="1"/>
|
||||
<field name="analytic_distribution" widget="analytic_distribution"
|
||||
optional="hide"
|
||||
groups="analytic.group_analytic_accounting"
|
||||
options="{'product_field': 'product_id'}"
|
||||
business_domain_compute="'sale', 'business_domain': 'sale_order'}"/>
|
||||
options="{'product_field': 'product_id', 'business_domain': 'sale_order'}"/>
|
||||
<field
|
||||
name="product_uom_qty"
|
||||
decoration-info="(not display_type and invoice_status == 'to invoice')" decoration-bf="(not display_type and invoice_status == 'to invoice')"
|
||||
|
||||
@@ -6,8 +6,8 @@ from odoo import models
|
||||
class AccountMoveLine(models.Model):
|
||||
_inherit = 'account.move.line'
|
||||
|
||||
def _compute_analytic_distribution_stored_char(self):
|
||||
def _compute_analytic_distribution(self):
|
||||
# when a project creates an aml, it adds an analytic account to it. the following filter is to save this
|
||||
# analytic account from being overridden by analytic default rules and lack thereof
|
||||
project_amls = self.filtered(lambda aml: aml.analytic_distribution and any(aml.sale_line_ids.project_id))
|
||||
super(AccountMoveLine, self - project_amls)._compute_analytic_distribution_stored_char()
|
||||
super(AccountMoveLine, self - project_amls)._compute_analytic_distribution()
|
||||
|
||||
@@ -152,23 +152,24 @@ class Project(models.Model):
|
||||
return action
|
||||
|
||||
def action_open_project_invoices(self):
|
||||
invoices = self.env['account.move'].search([
|
||||
('line_ids.analytic_distribution_stored_char', '=ilike', f'%"{self.analytic_account_id.id}":%'),
|
||||
('move_type', '=', 'out_invoice')
|
||||
])
|
||||
query = self.env['account.move.line']._search([('move_id.move_type', '=', 'out_invoice')])
|
||||
query.add_where('analytic_distribution ? %s', [str(self.analytic_account_id.id)])
|
||||
query_string, query_param = query.select('DISTINCT move_id')
|
||||
self._cr.execute(query_string, query_param)
|
||||
invoice_ids = [line.get('move_id') for line in self._cr.dictfetchall()]
|
||||
action = {
|
||||
'name': _('Invoices'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'account.move',
|
||||
'views': [[False, 'tree'], [False, 'form'], [False, 'kanban']],
|
||||
'domain': [('id', 'in', invoices.ids)],
|
||||
'domain': [('id', 'in', invoice_ids)],
|
||||
'context': {
|
||||
'create': False,
|
||||
}
|
||||
}
|
||||
if len(invoices) == 1:
|
||||
if len(invoice_ids) == 1:
|
||||
action['views'] = [[False, 'form']]
|
||||
action['res_id'] = invoices.id
|
||||
action['res_id'] = invoice_ids[0]
|
||||
return action
|
||||
|
||||
# ----------------------------
|
||||
@@ -446,22 +447,24 @@ class Project(models.Model):
|
||||
return buttons
|
||||
|
||||
def action_open_project_vendor_bills(self):
|
||||
vendor_bills = self.env['account.move'].search([
|
||||
('line_ids.analytic_distribution_stored_char', '=ilike', f'%"{self.analytic_account_id.id}":%'),
|
||||
('move_type', '=', 'in_invoice')])
|
||||
query = self.env['account.move.line']._search([('move_id.move_type', '=', 'in_invoice')])
|
||||
query.add_where('analytic_distribution ? %s', [str(self.analytic_account_id.id)])
|
||||
query_string, query_param = query.select('DISTINCT move_id')
|
||||
self._cr.execute(query_string, query_param)
|
||||
vendor_bill_ids = [line.get('move_id') for line in self._cr.dictfetchall()]
|
||||
action_window = {
|
||||
'name': _('Vendor Bills'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'account.move',
|
||||
'views': [[False, 'tree'], [False, 'form'], [False, 'kanban']],
|
||||
'domain': [('id', 'in', vendor_bills.ids)],
|
||||
'domain': [('id', 'in', vendor_bill_ids)],
|
||||
'context': {
|
||||
'create': False,
|
||||
}
|
||||
}
|
||||
if len(vendor_bills) == 1:
|
||||
if len(vendor_bill_ids) == 1:
|
||||
action_window['views'] = [[False, 'form']]
|
||||
action_window['res_id'] = vendor_bills.id
|
||||
action_window['res_id'] = vendor_bill_ids[0]
|
||||
return action_window
|
||||
|
||||
class ProjectTask(models.Model):
|
||||
|
||||
@@ -685,7 +685,7 @@ class TestSaleService(TestCommonSaleTimesheet):
|
||||
|
||||
# Check that the resulting invoice line and the project have the same analytic account
|
||||
invoice_line = self.sale_order.invoice_ids.line_ids.filtered(lambda line: line.product_id == product_add)
|
||||
self.assertEqual(invoice_line.analytic_distribution, {self.project_global.analytic_account_id.id: 100},
|
||||
self.assertEqual(invoice_line.analytic_distribution, {str(self.project_global.analytic_account_id.id): 100},
|
||||
"SOL's analytic distribution should contain the project analytic account")
|
||||
|
||||
def test_sale_timesheet_invoice(self):
|
||||
|
||||
@@ -301,6 +301,9 @@ class SelectionConverter(models.AbstractModel):
|
||||
options.update(
|
||||
selection=dict(type='selection', string=_('Selection'), description=_('By default the widget uses the field information'), required=True)
|
||||
)
|
||||
options.update(
|
||||
selection=dict(type='json', string=_('Json'), description=_('By default the widget uses the field information'), required=True)
|
||||
)
|
||||
return options
|
||||
|
||||
@api.model
|
||||
|
||||
Reference in New Issue
Block a user