[REF] *: Analytic Apocalypse

The goal of this commit is to get rid of the analytic tags as they were confusing, serving tag purposes as well as distribution on analytic accounts.

Everywhere analytic tags were used as a distribution have been replaced with a new widget that will dispatch distribution on analytic accounts. If there was an analytic account field next to the tags, it has been included in the distribution.

Analytic tags that were used simply as information tags have been removed.

To fill the new widget, there are now 2 kind of rules that will help fill and prefill it.
The first are applicability: previous groups have been removed, and have by replaced by plans. Each account is required to have a plan. These plans define when they are available in the widget: a default applicability per plan and applicability lines that can specify rules following the context of the widget.

The second one are distribution models, that will replace previous default rules but follow the same principles. The accounts (and so the plans) that will be given by the distribution model can override the applicability rules from before.

closes odoo/odoo#98914

Related: odoo/upgrade#3885
Related: odoo/enterprise#30743
Signed-off-by: William André (wan) <wan@odoo.com>
Co-authored-by: Habib (ayh) <ayh@odoo.com>
This commit is contained in:
gawa-odoo
2022-09-20 12:36:01 +02:00
committed by william
co-authored by Habib
parent 20b8f4c7e8
commit 7e3403068f
118 changed files with 3047 additions and 1589 deletions
+4 -2
View File
@@ -48,7 +48,10 @@ You could use this simplified accounting in case you work with an (external) acc
'wizard/account_validate_move_view.xml',
'views/res_company_views.xml',
'views/product_view.xml',
'views/account_analytic_view.xml',
'views/account_analytic_plan_views.xml',
'views/account_analytic_account_views.xml',
'views/account_analytic_distribution_model_views.xml',
'views/account_analytic_line_views.xml',
'views/report_invoice.xml',
'report/account_invoice_report_view.xml',
'views/account_cash_rounding_view.xml',
@@ -67,7 +70,6 @@ You could use this simplified accounting in case you work with an (external) acc
'report/account_hash_integrity_templates.xml',
'views/res_currency.xml',
'views/account_menuitem.xml',
'views/account_analytic_default_view.xml',
'wizard/account_tour_upload_bill.xml',
'wizard/accrued_orders.xml',
'views/bill_preview_template.xml',
+3 -1
View File
@@ -12,13 +12,15 @@ from . import account_payment_term
from . import account_move
from . import account_move_line
from . import account_move_line_tax_details
from . import account_analytic_default
from . import account_partial_reconcile
from . import account_full_reconcile
from . import account_payment
from . import account_payment_method
from . import account_bank_statement
from . import chart_template
from . import account_analytic_account
from . import account_analytic_distribution_model
from . import account_analytic_plan
from . import account_analytic_line
from . import account_journal_dashboard
from . import product
@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
class AccountAnalyticAccount(models.Model):
_inherit = 'account.analytic.account'
invoice_count = fields.Integer(
"Invoice Count",
compute='_compute_invoice_count',
)
vendor_bill_count = fields.Integer(
"Vendor Bill Count",
compute='_compute_vendor_bill_count',
)
@api.depends('line_ids')
def _compute_invoice_count(self):
sale_types = self.env['account.move'].get_sale_types(include_receipts=True)
domain = [
('move_line_id.move_id.move_type', 'in', sale_types),
('account_id', 'in', self.ids)
]
groups = self.env['account.analytic.line']._read_group(domain, ['move_line_id.move_id:count_distinct'], ['account_id'])
moves_count_mapping = dict((g['account_id'][0], g['account_id_count']) for g in groups)
for account in self:
account.invoice_count = moves_count_mapping.get(account.id, 0)
@api.depends('line_ids')
def _compute_vendor_bill_count(self):
purchase_types = self.env['account.move'].get_purchase_types(include_receipts=True)
domain = [
('move_line_id.move_id.move_type', 'in', purchase_types),
('account_id', 'in', self.ids)
]
groups = self.env['account.analytic.line']._read_group(domain, ['move_line_id.move_id:count_distinct'], ['account_id'])
moves_count_mapping = dict((g['account_id'][0], g['account_id_count']) for g in groups)
for account in self:
account.vendor_bill_count = moves_count_mapping.get(account.id, 0)
def action_view_invoice(self):
self.ensure_one()
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())],
"context": {"create": False},
"name": "Customer Invoices",
'view_mode': 'tree,form',
}
return result
def action_view_vendor_bill(self):
self.ensure_one()
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())],
"context": {"create": False},
"name": "Vendor Bills",
'view_mode': 'tree,form',
}
return result
@@ -1,68 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
class AccountAnalyticDefault(models.Model):
_name = "account.analytic.default"
_description = "Analytic Distribution"
_rec_name = "analytic_id"
_order = "sequence"
sequence = fields.Integer(string='Sequence', help="Gives the sequence order when displaying a list of analytic distribution")
analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account')
analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags')
product_id = fields.Many2one('product.product', string='Product', ondelete='cascade', help="Select a product which will use analytic account specified in analytic default (e.g. create new customer invoice or Sales order if we select this product, it will automatically take this as an analytic account)")
partner_id = fields.Many2one('res.partner', string='Partner', ondelete='cascade', help="Select a partner which will use analytic account specified in analytic default (e.g. create new customer invoice or Sales order if we select this partner, it will automatically take this as an analytic account)")
account_id = fields.Many2one('account.account', string='Account', ondelete='cascade', help="Select an accounting account which will use analytic account specified in analytic default (e.g. create new customer invoice or Sales order if we select this account, it will automatically take this as an analytic account)")
user_id = fields.Many2one('res.users', string='User', ondelete='cascade', help="Select a user which will use analytic account specified in analytic default.")
company_id = fields.Many2one('res.company', string='Company', ondelete='cascade', help="Select a company which will use analytic account specified in analytic default (e.g. create new customer invoice or Sales order if we select this company, it will automatically take this as an analytic account)")
date_start = fields.Date(string='Start Date', help="Default start date for this Analytic Account.")
date_stop = fields.Date(string='End Date', help="Default end date for this Analytic Account.")
@api.constrains('analytic_id', 'analytic_tag_ids')
def _check_account_or_tags(self):
if any(not default.analytic_id
and not any(tag.analytic_distribution_ids for tag in default.analytic_tag_ids)
for default in self
):
raise ValidationError(_('An analytic default requires an analytic account or an analytic tag used for analytic distribution.'))
@api.model
def account_get(self, product_id=None, partner_id=None, account_id=None, user_id=None, date=None, company_id=None):
domain = []
if product_id:
domain += ['|', ('product_id', '=', product_id)]
domain += [('product_id', '=', False)]
if partner_id:
domain += ['|', ('partner_id', '=', partner_id)]
domain += [('partner_id', '=', False)]
if account_id:
domain += ['|', ('account_id', '=', account_id)]
domain += [('account_id', '=', False)]
if company_id:
domain += ['|', ('company_id', '=', company_id)]
domain += [('company_id', '=', False)]
if user_id:
domain += ['|', ('user_id', '=', user_id)]
domain += [('user_id', '=', False)]
if date:
domain += ['|', ('date_start', '<=', date), ('date_start', '=', False)]
domain += ['|', ('date_stop', '>=', date), ('date_stop', '=', False)]
best_index = -1
res = self.env['account.analytic.default']
for rec in self.search(domain):
index = 0
if rec.product_id: index += 1
if rec.partner_id: index += 1
if rec.account_id: index += 1
if rec.company_id: index += 1
if rec.user_id: index += 1
if rec.date_start: index += 1
if rec.date_stop: index += 1
if index > best_index:
res = rec
best_index = index
return res
@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
from odoo import fields, models
class AccountAnalyticDistributionModel(models.Model):
_inherit = 'account.analytic.distribution.model'
account_prefix = fields.Char(
string='Financial Accounts Prefix',
help="Prefix that defines which accounts from the financial accounting this model should apply on.",
)
product_id = fields.Many2one(
'product.product',
string='Product',
ondelete='cascade',
help="Select a product for which the analytic distribution will be used (e.g. create new customer invoice or Sales order if we select this product, it will automatically take this as an analytic account)",
)
product_categ_id = fields.Many2one(
'product.category',
string='Product Category',
ondelete='cascade',
help="Select a product category which will use analytic account specified in analytic default (e.g. create new customer invoice or Sales order if we select this product, it will automatically take this as an analytic account)",
)
def _create_domain(self, fname, value):
if not fname == 'account_prefix':
return super()._create_domain(fname, value)
def _check_score(self, key, value):
if key == 'account_prefix':
if value.startswith(self.account_prefix):
return 1
else:
return -1
else:
return super()._check_score(key, value)
+40 -108
View File
@@ -1,129 +1,61 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class AccountAnalyticAccount(models.Model):
_inherit = 'account.analytic.account'
invoice_count = fields.Integer("Invoice Count", compute='_compute_invoice_count')
vendor_bill_count = fields.Integer("Vendor Bill Count", compute='_compute_vendor_bill_count')
@api.constrains('company_id')
def _check_company_consistency(self):
analytic_accounts = self.filtered('company_id')
if not analytic_accounts:
return
self.flush_recordset(['company_id'])
self.env['account.move.line'].flush_model(['analytic_account_id', 'company_id'])
self._cr.execute('''
SELECT line.id
FROM account_move_line line
JOIN account_analytic_account account ON account.id = line.analytic_account_id
WHERE line.analytic_account_id IN %s
AND line.company_id != account.company_id
''', [tuple(analytic_accounts.ids)])
if self._cr.fetchone():
raise UserError(_("You can't set a different company on your analytic account since there are some journal items linked to it."))
@api.depends('line_ids')
def _compute_invoice_count(self):
sale_types = self.env['account.move'].get_sale_types()
domain = [
('move_id.state', '=', 'posted'),
('move_id.move_type', 'in', sale_types),
('analytic_account_id', 'in', self.ids)
]
groups = self.env['account.move.line']._read_group(domain, ['move_id:count_distinct'], ['analytic_account_id'])
moves_count_mapping = dict((g['analytic_account_id'][0], g['move_id']) for g in groups)
for account in self:
account.invoice_count = moves_count_mapping.get(account.id, 0)
@api.depends('line_ids')
def _compute_vendor_bill_count(self):
purchase_types = self.env['account.move'].get_purchase_types()
domain = [
('move_id.state', '=', 'posted'),
('move_id.move_type', 'in', purchase_types),
('analytic_account_id', 'in', self.ids)
]
groups = self.env['account.move.line']._read_group(domain, ['move_id:count_distinct'], ['analytic_account_id'])
moves_count_mapping = dict((g['analytic_account_id'][0], g['move_id']) for g in groups)
for account in self:
account.vendor_bill_count = moves_count_mapping.get(account.id, 0)
def action_view_invoice(self):
self.ensure_one()
result = {
"type": "ir.actions.act_window",
"res_model": "account.move",
"domain": [('id', 'in', self.line_ids.move_id.move_id.ids), ('move_type', 'in', self.env['account.move'].get_sale_types())],
"context": {"create": False},
"name": "Customer Invoices",
'view_mode': 'tree,form',
}
return result
def action_view_vendor_bill(self):
self.ensure_one()
result = {
"type": "ir.actions.act_window",
"res_model": "account.move",
"domain": [('id', 'in', self.line_ids.move_id.move_id.ids), ('move_type', 'in', self.env['account.move'].get_purchase_types())],
"context": {"create": False},
"name": "Vendor Bills",
'view_mode': 'tree,form',
}
return result
class AccountAnalyticTag(models.Model):
_inherit = 'account.analytic.tag'
@api.constrains('company_id')
def _check_company_consistency(self):
analytic_tags = self.filtered('company_id')
if not analytic_tags:
return
self.flush_recordset(['company_id'])
self.env['account.move.line'].flush_model(['company_id'])
self._cr.execute('''
SELECT line.id
FROM account_analytic_tag_account_move_line_rel tag_rel
JOIN account_analytic_tag tag ON tag.id = tag_rel.account_analytic_tag_id
JOIN account_move_line line ON line.id = tag_rel.account_move_line_id
WHERE tag_rel.account_analytic_tag_id IN %s
AND line.company_id != tag.company_id
''', [tuple(analytic_tags.ids)])
if self._cr.fetchone():
raise UserError(_("You can't set a different company on your analytic tags since there are some journal items linked to it."))
class AccountAnalyticLine(models.Model):
_inherit = 'account.analytic.line'
_description = 'Analytic Line'
product_id = fields.Many2one('product.product', string='Product', check_company=True)
general_account_id = fields.Many2one('account.account', string='Financial Account', ondelete='restrict', readonly=True,
related='move_id.account_id', store=True, domain="[('deprecated', '=', False), ('company_id', '=', company_id)]",
compute_sudo=True)
move_id = fields.Many2one('account.move.line', string='Journal Item', ondelete='cascade', index=True, check_company=True)
product_id = fields.Many2one(
'product.product',
string='Product',
check_company=True,
)
general_account_id = fields.Many2one(
'account.account',
string='Financial Account',
ondelete='restrict',
domain="[('deprecated', '=', False), ('company_id', '=', company_id)]",
readonly=True,
related='move_line_id.account_id',
store=True,
compute_sudo=True,
)
journal_id = fields.Many2one(
'account.journal',
string='Financial Journal',
check_company=True,
readonly=True,
related='move_line_id.journal_id',
store=True,
)
partner_id = fields.Many2one(
readonly=False,
compute="_compute_partner_id",
store=True,
)
move_line_id = fields.Many2one(
'account.move.line',
string='Journal Item',
ondelete='cascade',
index=True,
check_company=True,
)
code = fields.Char(size=8)
ref = fields.Char(string='Ref.')
category = fields.Selection(selection_add=[('invoice', 'Customer Invoice'), ('vendor_bill', 'Vendor Bill')])
@api.depends('move_line_id')
def _compute_partner_id(self):
for line in self:
line.partner_id = line.move_line_id.partner_id or line.partner_id
@api.onchange('product_id', 'product_uom_id', 'unit_amount', 'currency_id')
def on_change_unit_amount(self):
if not self.product_id:
return {}
result = 0.0
prod_accounts = self.product_id.product_tmpl_id.with_company(self.company_id)._get_product_accounts()
unit = self.product_uom_id
account = prod_accounts['expense']
@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
from odoo import fields, models
class AccountAnalyticApplicability(models.Model):
_inherit = 'account.analytic.applicability'
_description = "Analytic Plan's Applicabilities"
account_prefix = fields.Char(
string='Financial Accounts Prefix',
help="Prefix that defines which accounts from the financial accounting this applicability should apply on.",
)
product_categ_id = fields.Many2one(
'product.category',
string='Product Category'
)
def _get_score(self, **kwargs):
score = super(AccountAnalyticApplicability, self)._get_score(**kwargs)
if score == -1:
return -1
product = self.env['product.product'].browse(kwargs.get('product', None))
account = self.env['account.account'].browse(kwargs.get('account', None))
if account and self.account_prefix:
if account.code.startswith(self.account_prefix):
score += 1
else:
return -1
if product and self.product_categ_id:
if product.categ_id == self.product_categ_id:
score += 1
else:
return -1
return score
+5 -14
View File
@@ -1203,12 +1203,6 @@ class AccountMove(models.Model):
taxes = None
if grouping_dict.get('tax_ids'):
taxes = self.env['account.tax'].browse(grouping_dict['tax_ids'][0][2])
analytic_account = None
if grouping_dict.get('analytic_account_id'):
analytic_account = self.env['account.analytic.account'].browse(grouping_dict['analytic_account_id'])
analytic_tags = None
if grouping_dict.get('analytic_tag_ids'):
analytic_tags = self.env['account.analytic.tag'].browse(grouping_dict['analytic_tag_ids'][0][2])
kwargs['base_lines'].append(self.env['account.tax']._convert_to_tax_base_line_dict(
None,
@@ -1218,8 +1212,7 @@ class AccountMove(models.Model):
price_unit=values['price_subtotal'],
quantity=1.0,
account=self.env['account.account'].browse(grouping_dict['account_id']),
analytic_account=analytic_account,
analytic_tags=analytic_tags,
analytic_distribution=values.get('analytic_distribution'),
price_subtotal=values['price_subtotal'],
is_refund=move.move_type in ('out_refund', 'in_refund'),
handle_price_include=False,
@@ -1520,7 +1513,7 @@ class AccountMove(models.Model):
def _inverse_partner_id(self):
for invoice in self:
if invoice.is_invoice(True):
for line in invoice.line_ids:
for line in invoice.line_ids + invoice.invoice_line_ids:
if line.partner_id != invoice.commercial_partner_id:
line.partner_id = invoice.commercial_partner_id
line._inverse_partner_id()
@@ -2789,8 +2782,7 @@ class AccountMove(models.Model):
'partner_id': base_line['partner'].id,
'currency_id': base_line['currency'].id,
'account_id': cash_discount_account.id,
'analytic_account_id': base_line['analytic_account'].id,
'analytic_tag_ids': [Command.set(base_line['analytic_tags'].ids)],
'analytic_distribution': base_line['analytic_distribution'],
}
base_detail = resulting_delta_base_details.setdefault(frozendict(grouping_dict), {
'balance': 0.0,
@@ -2841,8 +2833,7 @@ class AccountMove(models.Model):
'account_id': tax_detail['account_id'],
'partner_id': tax_detail['partner_id'],
'currency_id': tax_detail['currency_id'],
'analytic_account_id': tax_detail['analytic_account_id'],
'analytic_tag_ids': tax_detail['analytic_tag_ids'],
'analytic_distribution': tax_detail['analytic_distribution'],
'tax_repartition_line_id': tax_rep.id,
'tax_ids': tax_detail['tax_ids'],
'tax_tag_ids': tax_detail['tax_tag_ids'],
@@ -3245,7 +3236,7 @@ class AccountMove(models.Model):
move.date = move._get_accounting_date(move.invoice_date or move.date, affects_tax_report)
# Create the analytic lines in batch is faster as it leads to less cache invalidation.
to_post.mapped('line_ids').create_analytic_lines()
to_post.mapped('line_ids')._create_analytic_lines()
to_post.write({
'state': 'posted',
'posted_before': True,
+69 -123
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
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
@@ -15,6 +15,7 @@ INTEGRITY_HASH_LINE_FIELDS = ('debit', 'credit', 'account_id', 'partner_id')
class AccountMoveLine(models.Model):
_name = "account.move.line"
_inherit = 'analytic.mixin'
_description = "Journal Item"
_order = "date desc, move_name desc, sequence, id"
_check_company_auto = True
@@ -346,24 +347,9 @@ class AccountMoveLine(models.Model):
# === Analytic fields === #
analytic_line_ids = fields.One2many(
comodel_name='account.analytic.line', inverse_name='move_id',
comodel_name='account.analytic.line', inverse_name='move_line_id',
string='Analytic lines',
)
analytic_account_id = fields.Many2one(
comodel_name='account.analytic.account',
string='Analytic Account',
compute="_compute_analytic_account_id", store=True, readonly=False,
index='btree_not_null',
check_company=True,
copy=True,
)
analytic_tag_ids = fields.Many2many(
comodel_name='account.analytic.tag',
string='Analytic Tags',
compute="_compute_analytic_tag_ids", store=True, readonly=False,
check_company=True,
copy=True,
)
# === Early Pay fields === #
discount_date = fields.Date(
@@ -893,7 +879,7 @@ class AccountMoveLine(models.Model):
return tax_ids
@api.depends('tax_ids', 'currency_id', 'partner_id', 'account_id', 'group_tax_id', 'analytic_tag_ids', 'analytic_account_id')
@api.depends('tax_ids', 'currency_id', 'partner_id', 'account_id', 'group_tax_id', 'analytic_distribution')
def _compute_tax_key(self):
for line in self:
if line.tax_repartition_line_id:
@@ -902,8 +888,7 @@ class AccountMoveLine(models.Model):
'group_tax_id': line.group_tax_id.id,
'account_id': line.account_id.id,
'currency_id': line.currency_id.id,
'analytic_tag_ids': [(6, 0, line.analytic_tag_ids.ids or [])],
'analytic_account_id': line.analytic_account_id.id,
'analytic_distribution': line.analytic_distribution,
'tax_ids': [(6, 0, line.tax_ids.ids)],
'tax_tag_ids': [(6, 0, line.tax_tag_ids.ids)],
'partner_id': line.partner_id.id,
@@ -912,7 +897,7 @@ class AccountMoveLine(models.Model):
else:
line.tax_key = frozendict({'id': line.id})
@api.depends('tax_ids', 'currency_id', 'partner_id', 'analytic_tag_ids', 'analytic_account_id', 'balance', 'partner_id', 'move_id.partner_id', 'price_unit')
@api.depends('tax_ids', 'currency_id', 'partner_id', 'analytic_distribution', 'balance', 'partner_id', 'move_id.partner_id', 'price_unit')
def _compute_all_tax(self):
for line in self:
sign = line.move_id.direction_sign
@@ -955,8 +940,7 @@ class AccountMoveLine(models.Model):
'group_tax_id': tax['group'] and tax['group'].id or False,
'account_id': tax['account_id'] or line.account_id.id,
'currency_id': line.currency_id.id,
'analytic_tag_ids': [(6, 0, tax['analytic'] and line.analytic_tag_ids.ids or [])],
'analytic_account_id': tax['analytic'] and line.analytic_account_id.id,
'analytic_distribution': tax['analytic'] and line.analytic_distribution,
'tax_ids': [(6, 0, tax['tax_ids'])],
'tax_tag_ids': [(6, 0, tax['tag_ids'])],
'partner_id': line.move_id.partner_id.id or line.partner_id.id,
@@ -981,8 +965,7 @@ class AccountMoveLine(models.Model):
if line.display_type == 'epd' and line.company_id.early_pay_discount_computation == 'mixed':
line.epd_key = frozendict({
'account_id': line.account_id.id,
'analytic_account_id': line.analytic_account_id.id,
'analytic_tag_ids': [Command.set(line.analytic_tag_ids.ids)],
'analytic_distribution': line.analytic_distribution,
'tax_ids': [Command.set(line.tax_ids.ids)],
'tax_tag_ids': [Command.set(line.tax_tag_ids.ids)],
'move_id': line.move_id.id,
@@ -990,7 +973,7 @@ class AccountMoveLine(models.Model):
else:
line.epd_key = False
@api.depends('move_id.needed_terms', 'account_id', 'analytic_account_id', 'analytic_tag_ids', 'tax_ids', 'tax_tag_ids', 'company_id')
@api.depends('move_id.needed_terms', 'account_id', 'analytic_distribution', 'tax_ids', 'tax_tag_ids', 'company_id')
def _compute_epd_needed(self):
for line in self:
line.epd_dirty = True
@@ -1013,8 +996,7 @@ class AccountMoveLine(models.Model):
frozendict({
'move_id': line.move_id.id,
'account_id': line.account_id.id,
'analytic_account_id': line.analytic_account_id.id,
'analytic_tag_ids': [Command.set(line.analytic_tag_ids.ids)],
'analytic_distribution': line.analytic_distribution,
'tax_ids': [Command.set(line.tax_ids.ids)],
'tax_tag_ids': [Command.set(line.tax_tag_ids.ids)],
'display_type': 'epd',
@@ -1076,35 +1058,19 @@ class AccountMoveLine(models.Model):
else:
line.term_key = False
@api.depends('product_id', 'account_id', 'partner_id', 'date')
def _compute_analytic_account_id(self):
for record in self:
if record.display_type == 'product' or not record.move_id.is_invoice(include_receipts=True):
rec = self.env['account.analytic.default'].account_get(
product_id=record.product_id.id,
partner_id=record.partner_id.commercial_partner_id.id or record.move_id.partner_id.commercial_partner_id.id,
account_id=record.account_id.id,
user_id=record.env.uid,
date=record.date,
company_id=record.move_id.company_id.id
)
if rec:
record.analytic_account_id = rec.analytic_id
@api.depends('product_id', 'account_id', 'partner_id', 'date')
def _compute_analytic_tag_ids(self):
for record in self:
if record.display_type == 'product' or not record.move_id.is_invoice(include_receipts=True):
rec = self.env['account.analytic.default'].account_get(
product_id=record.product_id.id,
partner_id=record.partner_id.commercial_partner_id.id or record.move_id.partner_id.commercial_partner_id.id,
account_id=record.account_id.id,
user_id=record.env.uid,
date=record.date,
company_id=record.move_id.company_id.id
)
if rec:
record.analytic_tag_ids = rec.analytic_tag_ids
@api.depends('account_id', 'partner_id', 'product_id')
def _compute_analytic_distribution_stored_char(self):
for line in self:
distribution = self.env['account.analytic.distribution.model']._get_distributionjson({
"product_id": line.product_id.id,
"product_categ_id": line.product_id.categ_id.id,
"partner_id": line.partner_id.id,
"partner_category_id": line.partner_id.category_id.ids,
"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()
# -------------------------------------------------------------------------
# INVERSE METHODS
@@ -1134,6 +1100,16 @@ class AccountMoveLine(models.Model):
for line in self:
line.balance = line.debit - line.credit
@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"
])
lines_to_modify.analytic_line_ids.unlink()
lines_to_modify._create_analytic_lines()
# -------------------------------------------------------------------------
# ONCHANGE METHODS
# -------------------------------------------------------------------------
@@ -2334,88 +2310,60 @@ class AccountMoveLine(models.Model):
# ANALYTIC
# -------------------------------------------------------------------------
def _prepare_analytic_line(self):
""" Prepare the values used to create() an account.analytic.line upon validation of an account.move.line having
an analytic account. This method is intended to be extended in other modules.
:return list of values to create analytic.line
:rtype list
def _create_analytic_lines(self):
""" Create analytic items upon validation of an account.move.line having an analytic distribution.
"""
result = []
for move_line in self:
amount = (move_line.credit or 0.0) - (move_line.debit or 0.0)
default_name = move_line.name or (move_line.ref or '/' + ' -- ' + (move_line.partner_id and move_line.partner_id.name or '/'))
category = 'other'
if move_line.move_id.is_sale_document():
category = 'invoice'
elif move_line.move_id.is_purchase_document():
category = 'vendor_bill'
result.append({
'name': default_name,
'date': move_line.date,
'account_id': move_line.analytic_account_id.id,
'group_id': move_line.analytic_account_id.group_id.id,
'tag_ids': [(6, 0, move_line._get_analytic_tag_ids())],
'unit_amount': move_line.quantity,
'product_id': move_line.product_id and move_line.product_id.id or False,
'product_uom_id': move_line.product_uom_id and move_line.product_uom_id.id or False,
'amount': amount,
'general_account_id': move_line.account_id.id,
'ref': move_line.ref,
'move_id': move_line.id,
'user_id': move_line.move_id.invoice_user_id.id or self._uid,
'partner_id': move_line.partner_id.id,
'company_id': move_line.analytic_account_id.company_id.id or move_line.move_id.company_id.id,
'category': category,
})
return result
analytic_line_vals = []
for line in self:
analytic_line_vals.extend(line._prepare_analytic_lines())
def _prepare_analytic_distribution_line(self, distribution):
self.env['account.analytic.line'].create(analytic_line_vals)
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
distribution_on_each_plan = {}
for account_id, distribution in self.analytic_distribution.items():
line_values = self._prepare_analytic_distribution_line(float(distribution), account_id, distribution_on_each_plan)
if not float_is_zero(line_values.get("amount"), precision_digits=self.env.company.currency_id.decimal_places):
analytic_line_vals.append(line_values)
return analytic_line_vals
def _prepare_analytic_distribution_line(self, distribution, account_id, distribution_on_each_plan):
""" Prepare the values used to create() an account.analytic.line upon validation of an account.move.line having
analytic tags with analytic distribution.
"""
self.ensure_one()
amount = -self.balance * distribution.percentage / 100.0
account_id = int(account_id)
account = self.env['account.analytic.account'].browse(account_id)
distribution_plan = distribution_on_each_plan.get(account.root_plan_id, 0) + distribution
if self.env.company.currency_id.compare_amounts(distribution_plan, 100) == 0:
amount = -self.balance * (100 - distribution_on_each_plan.get(account.root_plan_id, 0)) / 100.0
else:
amount = -self.balance * distribution / 100.0
distribution_on_each_plan[account.root_plan_id] = distribution_plan
default_name = self.name or (self.ref or '/' + ' -- ' + (self.partner_id and self.partner_id.name or '/'))
return {
'name': default_name,
'date': self.date,
'account_id': distribution.account_id.id,
'group_id': distribution.account_id.group_id.id,
'account_id': account_id,
'partner_id': self.partner_id.id,
'tag_ids': [(6, 0, [distribution.tag_id.id] + self._get_analytic_tag_ids())],
'unit_amount': self.quantity,
'product_id': self.product_id and self.product_id.id or False,
'product_uom_id': self.product_uom_id and self.product_uom_id.id or False,
'amount': amount,
'general_account_id': self.account_id.id,
'ref': self.ref,
'move_id': self.id,
'move_line_id': self.id,
'user_id': self.move_id.invoice_user_id.id or self._uid,
'company_id': distribution.account_id.company_id.id or self.company_id.id or self.env.company.id,
'company_id': account.company_id.id or self.company_id.id or self.env.company.id,
}
def _get_analytic_tag_ids(self):
self.ensure_one()
return self.analytic_tag_ids.filtered(lambda r: not r.active_analytic_distribution).ids
def create_analytic_lines(self):
""" Create analytic items upon validation of an account.move.line having an analytic account or an analytic distribution.
"""
lines_to_create_analytic_entries = self.env['account.move.line']
analytic_line_vals = []
for obj_line in self:
for tag in obj_line.analytic_tag_ids.filtered('active_analytic_distribution'):
for distribution in tag.analytic_distribution_ids:
analytic_line_vals.append(obj_line._prepare_analytic_distribution_line(distribution))
if obj_line.analytic_account_id:
lines_to_create_analytic_entries |= obj_line
# create analytic entries in batch
if lines_to_create_analytic_entries:
analytic_line_vals += lines_to_create_analytic_entries._prepare_analytic_line()
self.env['account.analytic.line'].create(analytic_line_vals)
# -------------------------------------------------------------------------
# MISC
# -------------------------------------------------------------------------
@@ -2485,8 +2433,7 @@ class AccountMoveLine(models.Model):
quantity=self.quantity if is_invoice else 1.0,
discount=self.discount if is_invoice else 0.0,
account=self.account_id,
analytic_account=self.analytic_account_id,
analytic_tags=self.analytic_tag_ids,
analytic_distribution=self.analytic_distribution,
price_subtotal=sign * self.amount_currency,
is_refund=self.is_refund,
rate=(abs(self.amount_currency) / abs(self.balance)) if self.balance else 1.0
@@ -2509,8 +2456,7 @@ class AccountMoveLine(models.Model):
tax_repartition_line=self.tax_repartition_line_id,
group_tax=self.group_tax_id,
account=self.account_id,
analytic_account=self.analytic_account_id,
analytic_tags=self.analytic_tag_ids,
analytic_distribution=self.analytic_distribution,
tax_amount=sign * self.amount_currency,
)
@@ -182,8 +182,8 @@ class AccountMoveLine(models.Model):
)
AND (
NOT tax.analytic
OR (base_line.analytic_account_id IS NULL AND account_move_line.analytic_account_id IS NULL)
OR base_line.analytic_account_id = account_move_line.analytic_account_id
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
)
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
@@ -37,6 +37,7 @@ class AccountReconcileModelPartnerMapping(models.Model):
class AccountReconcileModelLine(models.Model):
_name = 'account.reconcile.model.line'
_inherit = 'analytic.mixin'
_description = 'Rules for the reconciliation model'
_order = 'sequence, id'
_check_company_auto = True
@@ -72,9 +73,6 @@ class AccountReconcileModelLine(models.Model):
* Fixed: The fixed value of the writeoff. The amount will count as a debit if it is negative, as a credit if it is positive.
* From Label: There is no need for regex delimiter, only the regex is needed. For instance if you want to extract the amount from\nR:9672938 10/07 AX 9415126318 T:5L:NA BRT: 3358,07 C:\nYou could enter\nBRT: ([\d,]+)""")
tax_ids = fields.Many2many('account.tax', string='Taxes', ondelete='restrict', check_company=True)
analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account', ondelete='set null', check_company=True)
analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags', check_company=True,
relation='account_reconcile_model_analytic_tag_rel')
@api.onchange('tax_ids')
def _onchange_tax_ids(self):
@@ -138,8 +136,7 @@ class AccountReconcileModelLine(models.Model):
'name': self.label,
'account_id': self.account_id.id,
'partner_id': partner.id,
'analytic_account_id': self.analytic_account_id.id,
'analytic_tag_ids': [Command.set(self.analytic_tag_ids.ids)],
'analytic_distribution': self.analytic_distribution,
'tax_ids': [Command.set(taxes.ids)],
'reconcile_model_id': self.model_id.id,
}
@@ -484,8 +481,7 @@ class AccountReconcileModel(models.Model):
'balance': balance,
'debit': balance > 0 and balance or 0,
'credit': balance < 0 and -balance or 0,
'analytic_account_id': tax.analytic and base_line_dict['analytic_account_id'],
'analytic_tag_ids': tax.analytic and base_line_dict['analytic_tag_ids'],
'analytic_distribution': tax.analytic and base_line_dict['analytic_distribution'],
'tax_repartition_line_id': tax_res['tax_repartition_line_id'],
'tax_ids': [(6, 0, tax_res['tax_ids'])],
'tax_tag_ids': [(6, 0, tax_res['tag_ids'])],
@@ -534,8 +530,7 @@ class AccountReconcileModel(models.Model):
'credit': balance < 0 and -balance or 0,
'account_id': line.account_id.id,
'currency_id': currency.id,
'analytic_account_id': line.analytic_account_id.id,
'analytic_tag_ids': [(6, 0, line.analytic_tag_ids.ids)],
'analytic_distribution': line.analytic_distribution,
'reconcile_model_id': self.id,
'journal_id': line.journal_id.id,
'tax_ids': [],
+6 -10
View File
@@ -719,7 +719,7 @@ class AccountTax(models.Model):
def _convert_to_tax_base_line_dict(
self, base_line,
partner=None, currency=None, product=None, taxes=None, price_unit=None, quantity=None,
discount=None, account=None, analytic_account=None, analytic_tags=None, price_subtotal=None,
discount=None, account=None, analytic_distribution=None, price_subtotal=None,
is_refund=False, rate=None,
handle_price_include=None,
extra_context=None,
@@ -734,8 +734,7 @@ class AccountTax(models.Model):
'quantity': quantity or 0.0,
'discount': discount or 0.0,
'account': account or self.env['account.account'],
'analytic_account': analytic_account or self.env['account.analytic.account'],
'analytic_tags': analytic_tags or self.env['account.analytic.tag'],
'analytic_distribution': analytic_distribution,
'price_subtotal': price_subtotal or 0.0,
'is_refund': is_refund,
'rate': rate or 1.0,
@@ -747,7 +746,7 @@ class AccountTax(models.Model):
def _convert_to_tax_line_dict(
self, tax_line,
partner=None, currency=None, taxes=None, tax_tags=None, tax_repartition_line=None,
group_tax=None, account=None, analytic_account=None, analytic_tags=None, tax_amount=None,
group_tax=None, account=None, analytic_distribution=None, tax_amount=None,
):
return {
'record': tax_line,
@@ -758,8 +757,7 @@ class AccountTax(models.Model):
'tax_repartition_line': tax_repartition_line or self.env['account.tax.repartition.line'],
'group_tax': group_tax or self.env['account.tax'],
'account': account or self.env['account.account'],
'analytic_account': analytic_account or self.env['account.analytic.account'],
'analytic_tags': analytic_tags or self.env['account.analytic.tag'],
'analytic_distribution': analytic_distribution,
'tax_amount': tax_amount or 0.0,
}
@@ -783,8 +781,7 @@ class AccountTax(models.Model):
'tax_ids': [Command.set(tax_vals['tax_ids'])],
'tax_tag_ids': [Command.set(tax_vals['tag_ids'])],
'tax_id': tax_vals['group'].id if tax_vals['group'] else tax_vals['id'],
'analytic_tag_ids': [Command.set(line_vals['analytic_tags'].ids if tax_vals['analytic'] else [])],
'analytic_account_id': line_vals['analytic_account'].id if tax_vals['analytic'] else False,
'analytic_distribution': line_vals['analytic_distribution'] if tax_vals['analytic'] else {},
}
@api.model
@@ -805,8 +802,7 @@ class AccountTax(models.Model):
'tax_ids': [Command.set(line_vals['taxes'].ids)],
'tax_tag_ids': [Command.set(line_vals['tax_tags'].ids)],
'tax_id': (line_vals['group_tax'] or tax).id,
'analytic_tag_ids': [Command.set(line_vals['analytic_tags'].ids if tax.analytic else [])],
'analytic_account_id': line_vals['analytic_account'].id if tax.analytic else False,
'analytic_distribution': line_vals['analytic_distribution'] if tax.analytic else {},
}
@api.model
@@ -70,7 +70,6 @@ class ResConfigSettings(models.TransientModel):
domain="[('reconcile', '=', True), ('account_type', '=', 'asset_current')]",
help="Intermediary account used when moving from a liquidity account to another.")
module_account_accountant = fields.Boolean(string='Accounting')
group_analytic_tags = fields.Boolean(string='Analytic Tags', implied_group='analytic.group_analytic_tags')
group_warning_account = fields.Boolean(string="Warnings in Invoices", implied_group='account.group_warning_account')
group_cash_rounding = fields.Boolean(string="Cash Rounding", implied_group='account.group_cash_rounding')
# group_show_line_subtotals_tax_excluded and group_show_line_subtotals_tax_included are opposite,
@@ -47,7 +47,6 @@ class AccountInvoiceReport(models.Model):
product_categ_id = fields.Many2one('product.category', string='Product Category', readonly=True)
invoice_date_due = fields.Date(string='Due Date', readonly=True)
account_id = fields.Many2one('account.account', string='Revenue/Expense Account', readonly=True, domain=[('deprecated', '=', False)])
analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account', groups="analytic.group_analytic_accounting")
price_subtotal = fields.Float(string='Untaxed Total', readonly=True)
price_total = fields.Float(string='Total', readonly=True)
price_average = fields.Float(string='Average Price', readonly=True, group_operator="avg")
@@ -59,7 +58,7 @@ class AccountInvoiceReport(models.Model):
],
'account.move.line': [
'quantity', 'price_subtotal', 'price_total', 'amount_residual', 'balance', 'amount_currency',
'move_id', 'product_id', 'product_uom_id', 'account_id', 'analytic_account_id',
'move_id', 'product_id', 'product_uom_id', 'account_id',
'journal_id', 'company_id', 'currency_id', 'partner_id',
],
'product.product': ['product_tmpl_id'],
@@ -81,7 +80,6 @@ class AccountInvoiceReport(models.Model):
line.move_id,
line.product_id,
line.account_id,
line.analytic_account_id,
line.journal_id,
line.company_id,
line.company_currency_id,
@@ -128,17 +128,6 @@
</field>
</record>
<record id="view_account_invoice_report_search_analytic_accounting" model="ir.ui.view">
<field name="name">account.invoice.report.search_analytic_accounting</field>
<field name="model">account.invoice.report</field>
<field name="inherit_id" ref="account.view_account_invoice_report_search"/>
<field name="arch" type="xml">
<filter name="status" position="after">
<filter string="Analytic Account" name="analytic_account_id" context="{'group_by':'analytic_account_id'}" groups="analytic.group_analytic_accounting"/>
</filter>
</field>
</record>
<record id="action_account_invoice_report_all_supp" model="ir.actions.act_window">
<field name="name">Invoices Analysis</field>
<field name="res_model">account.invoice.report</field>
@@ -291,14 +291,6 @@
<field name="groups" eval="[(4, ref('account.group_account_invoice'))]"/>
</record>
<!-- account analytic default-->
<record id="analytic_default_comp_rule" model="ir.rule">
<field name="name">Analytic Default multi company rule</field>
<field name="model_id" ref="model_account_analytic_default"/>
<field eval="True" name="global"/>
<field name="domain_force">['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
</record>
<record id="report_external_value_comp_rule" model="ir.rule">
<field name="name">Report External Value multi-company</field>
<field name="model_id" ref="model_account_report_external_value"/>
+3 -3
View File
@@ -58,6 +58,9 @@ access_account_analytic_account,account.analytic.account,analytic.model_account_
access_account_analytic_line_readonly,account.analytic.line invoice,model_account_analytic_line,account.group_account_readonly,1,0,0,0
access_account_analytic_line_invoice,account.analytic.line invoice,model_account_analytic_line,account.group_account_invoice,1,1,1,1
access_account_analytic_accountant,account.analytic.account accountant,analytic.model_account_analytic_account,account.group_account_user,1,1,1,1
access_account_analytic_plan_accountant,account.analytic.plan accountant,analytic.model_account_analytic_plan,account.group_account_user,1,1,1,1
access_account_analytic_distribution_readonly,account.analytic.distribution invoice,analytic.model_account_analytic_distribution_model,account.group_account_readonly,1,0,0,0
access_account_analytic_distribution_invoice,account.analytic.distribution invoice,analytic.model_account_analytic_distribution_model,account.group_account_invoice,1,1,1,1
access_account_journal_readonly,account.journal,model_account_journal,account.group_account_readonly,1,0,0,0
access_account_journal_manager,account.journal,model_account_journal,account.group_account_manager,1,1,1,1
@@ -130,9 +133,6 @@ access_account_financial_year_op,access.account.financial.year.op,model_account_
access_account_setup_bank_manual_config,access.account.setup.bank.manual.config,model_account_setup_bank_manual_config,account.group_account_manager,1,1,1,0
access_account_invoice_send,access.account.invoice.send,model_account_invoice_send,account.group_account_invoice,1,1,1,0
access_account_analytic_default,account.analytic.default,model_account_analytic_default,account.group_account_readonly,1,0,0,0
access_account_analytic_default_analytic,account.analytic.default analytic,model_account_analytic_default,analytic.group_analytic_accounting,1,0,0,0
access_account_analytic_default_invoice,account.analytic.default invoice,model_account_analytic_default,account.group_account_invoice,1,1,1,1
access_account_tour_upload_bill,account.tour.upload.bill,model_account_tour_upload_bill,account.group_account_manager,1,1,1,0
access_account_tour_upload_bill_email_confirm,account.tour.upload.bill.email.confirm,model_account_tour_upload_bill_email_confirm,account.group_account_manager,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
58 access_account_group access_account_journal_group_all account.group account.journal.group all model_account_group model_account_journal_group account.group_account_readonly 1 0 0 0
59 access_account_root_manager access_account_journal_group_manager account.root account.journal.group manager model_account_root model_account_journal_group account.group_account_manager 1 0 1 0 1 0 1
60 access_account_root access_account_group_manager account.root account.group model_account_root model_account_group account.group_account_readonly account.group_account_manager 1 0 1 0 1 0 1
61 access_account_group account.group model_account_group account.group_account_readonly 1 0 0 0
62 access_account_root_manager account.root model_account_root account.group_account_manager 1 0 0 0
63 access_account_root account.root model_account_root account.group_account_readonly 1 0 0 0
64 access_account_account_manager account.account model_account_account account.group_account_manager 1 1 1 1
65 access_account_account account.account.readonly model_account_account account.group_account_readonly 1 0 0 0
66 access_account_account_user account.account user model_account_account base.group_user 1 0 0 0
133
134
135
136
137
138
+156 -33
View File
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged
from odoo.tests import tagged, Form
from odoo.exceptions import UserError
from odoo import Command
@tagged('post_install', '-at_install')
@@ -11,49 +12,171 @@ class TestAccountAnalyticAccount(AccountTestInvoicingCommon):
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.env.user.write({
'groups_id': [
(4, cls.env.ref('analytic.group_analytic_accounting').id),
(4, cls.env.ref('analytic.group_analytic_tags').id),
],
})
cls.env.user.groups_id += cls.env.ref('analytic.group_analytic_accounting')
# By default, tests are run with the current user set on the first company.
cls.env.user.company_id = cls.company_data['company']
cls.test_analytic_account = cls.env['account.analytic.account'].create({'name': 'test_analytic_account'})
cls.test_analytic_tag = cls.env['account.analytic.tag'].create({'name': 'test_analytic_tag'})
cls.default_plan = cls.env['account.analytic.plan'].create({'name': 'Default', 'company_id': False})
cls.analytic_account_a = cls.env['account.analytic.account'].create({
'name': 'analytic_account_a',
'plan_id': cls.default_plan.id,
})
cls.analytic_account_b = cls.env['account.analytic.account'].create({
'name': 'analytic_account_b',
'plan_id': cls.default_plan.id,
})
def test_changing_analytic_company(self):
''' Ensure you can't change the company of an account.analytic.account if there are some journal entries '''
self.env['account.move'].create({
'move_type': 'entry',
'date': '2019-01-01',
'line_ids': [
(0, 0, {
'name': 'line_debit',
'account_id': self.company_data['default_account_revenue'].id,
'analytic_account_id': self.test_analytic_account.id,
'analytic_tag_ids': [(6, 0, self.test_analytic_tag.ids)],
}),
(0, 0, {
'name': 'line_credit',
'account_id': self.company_data['default_account_revenue'].id,
}),
],
""" Ensure you can't change the company of an account.analytic.account if there are analytic lines linked to
the account
"""
self.env['account.analytic.line'].create({
'name': 'company specific account',
'account_id': self.analytic_account_a.id,
'amount': 100,
})
# Set a different company on the analytic account.
with self.assertRaises(UserError), self.cr.savepoint():
self.test_analytic_account.company_id = self.company_data_2['company']
self.analytic_account_a.company_id = self.company_data_2['company']
# Making the analytic account not company dependent is allowed.
self.test_analytic_account.company_id = False
self.analytic_account_a.company_id = False
# Set a different company on the analytic tag.
with self.assertRaises(UserError), self.cr.savepoint():
self.test_analytic_tag.company_id = self.company_data_2['company']
def test_analytic_lines(self):
''' Ensures analytic lines are created when posted and are recreated when editing the account.move'''
def get_analytic_lines():
return self.env['account.analytic.line'].search([
('move_line_id', 'in', out_invoice.line_ids.ids)
]).sorted('amount')
# Making the analytic tag not company dependent is allowed.
self.test_analytic_tag.company_id = False
out_invoice = self.env['account.move'].create([{
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'invoice_line_ids': [Command.create({
'product_id': self.product_a.id,
'price_unit': 200.0,
'analytic_distribution': {
self.analytic_account_a.id: 100,
self.analytic_account_b.id: 50,
},
})]
}])
out_invoice.action_post()
# Analytic lines are created when posting the invoice
self.assertRecordValues(get_analytic_lines(), [{
'amount': 100,
'account_id': self.analytic_account_b.id,
'partner_id': self.partner_a.id,
'product_id': self.product_a.id,
}, {
'amount': 200,
'account_id': self.analytic_account_a.id,
'partner_id': self.partner_a.id,
'product_id': self.product_a.id,
}])
# Analytic lines are updated when a posted invoice's distribution changes
out_invoice.invoice_line_ids.analytic_distribution = {
self.analytic_account_a.id: 100,
self.analytic_account_b.id: 25,
}
self.assertRecordValues(get_analytic_lines(), [{
'amount': 50,
'account_id': self.analytic_account_b.id,
}, {
'amount': 200,
'account_id': self.analytic_account_a.id,
}])
# Analytic lines are deleted when resetting to draft
out_invoice.button_draft()
self.assertFalse(get_analytic_lines())
def test_plan_score(self):
"""Test that the plans are applied correctly based on the score"""
def create_invoice(partner, product):
return self.env['account.move'].create([{
'move_type': 'out_invoice',
'partner_id': partner.id,
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'invoice_line_ids': [Command.create({
'product_id': product.id,
})]
}])
self.env['account.analytic.distribution.model'].create([{
'product_id': self.product_a.id,
'analytic_distribution': {self.analytic_account_a.id: 100}
}, {
'partner_id': self.partner_a.id,
'product_id': self.product_a.id,
'analytic_distribution': {self.analytic_account_b.id: 100}
}])
# Partner and product match, score 2
invoice = create_invoice(self.partner_a, self.product_a)
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {self.analytic_account_b.id: 100})
# Match the partner but not the product, score 0
invoice = create_invoice(self.partner_a, self.product_b)
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, False)
# Product match, score 1
invoice = create_invoice(self.partner_b, self.product_a)
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {self.analytic_account_a.id: 100})
# No rule match with the product, score 0
invoice = create_invoice(self.partner_b, self.product_b)
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, False)
def test_plan_application(self):
"""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}
}, {
'partner_id': self.partner_b.id,
'analytic_distribution': {self.analytic_account_b.id: 100}
}])
invoice = self.env['account.move'].create([{
'move_type': 'out_invoice',
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'invoice_line_ids': [Command.create({
'product_id': self.product_a.id,
})]
}])
# No model is found, don't put anything
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, False)
# 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})
# 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})
# 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})
# 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})
# 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})
line_form.analytic_distribution = {self.analytic_account_b.id: 100}
self.assertEqual(invoice.invoice_line_ids.analytic_distribution, {self.analytic_account_b.id: 100})
@@ -1164,57 +1164,49 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
def test_out_invoice_line_onchange_analytic(self):
self.env.user.groups_id += self.env.ref('analytic.group_analytic_accounting')
self.env.user.groups_id += self.env.ref('analytic.group_analytic_tags')
analytic_tag = self.env['account.analytic.tag'].create({
'name': 'test_analytic_tag',
})
analytic_plan = self.env['account.analytic.plan'].create({'name': 'Plan Test', 'company_id': False})
analytic_account = self.env['account.analytic.account'].create({
'name': 'test_analytic_account',
'partner_id': self.invoice.partner_id.id,
'plan_id': analytic_plan.id,
'code': 'TEST'
})
analytic_distribution = {analytic_account.id: 100}
move_form = Form(self.invoice)
with move_form.invoice_line_ids.edit(0) as line_form:
line_form.analytic_account_id = analytic_account
line_form.analytic_tag_ids.add(analytic_tag)
line_form.analytic_distribution = analytic_distribution
move_form.save()
# The tax is not flagged as an analytic one. It should change nothing on the taxes.
self.assertInvoiceValues(self.invoice, [
{
**self.product_line_vals_1,
'analytic_account_id': analytic_account.id,
'analytic_tag_ids': analytic_tag.ids,
'analytic_distribution': analytic_distribution,
},
{
**self.product_line_vals_2,
'analytic_account_id': False,
'analytic_tag_ids': [],
'analytic_distribution': False,
},
{
**self.tax_line_vals_1,
'analytic_account_id': False,
'analytic_tag_ids': [],
'analytic_distribution': False,
},
{
**self.tax_line_vals_2,
'analytic_account_id': False,
'analytic_tag_ids': [],
'analytic_distribution': False,
},
{
**self.term_line_vals_1,
'analytic_account_id': False,
'analytic_tag_ids': [],
'analytic_distribution': False,
},
], self.move_vals)
move_form = Form(self.invoice)
with move_form.invoice_line_ids.edit(0) as line_form:
line_form.analytic_account_id = self.env['account.analytic.account']
line_form.analytic_tag_ids.clear()
line_form.analytic_distribution = {}
move_form.save()
# Enable the analytic
@@ -1222,8 +1214,7 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
move_form = Form(self.invoice)
with move_form.invoice_line_ids.edit(0) as line_form:
line_form.analytic_account_id = analytic_account
line_form.analytic_tag_ids.add(analytic_tag)
line_form.analytic_distribution = analytic_distribution
move_form.save()
# The tax is flagged as an analytic one.
@@ -1231,103 +1222,94 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
self.assertInvoiceValues(self.invoice, [
{
**self.product_line_vals_1,
'analytic_account_id': analytic_account.id,
'analytic_tag_ids': analytic_tag.ids,
'analytic_distribution': analytic_distribution,
},
{
**self.product_line_vals_2,
'analytic_account_id': False,
'analytic_tag_ids': [],
'analytic_distribution': False,
},
{
**self.tax_line_vals_1,
'amount_currency': -150.0,
'credit': 150.0,
'analytic_account_id': analytic_account.id,
'analytic_tag_ids': analytic_tag.ids,
'analytic_distribution': analytic_distribution,
},
{
**self.tax_line_vals_1,
'amount_currency': -30.0,
'credit': 30.0,
'analytic_account_id': False,
'analytic_tag_ids': [],
'analytic_distribution': False,
},
{
**self.tax_line_vals_2,
'analytic_account_id': False,
'analytic_tag_ids': [],
'analytic_distribution': False,
},
{
**self.term_line_vals_1,
'analytic_account_id': False,
'analytic_tag_ids': [],
'analytic_distribution': False,
},
], self.move_vals)
move_form = Form(self.invoice)
with move_form.invoice_line_ids.edit(0) as line_form:
line_form.analytic_account_id = self.env['account.analytic.account']
line_form.analytic_tag_ids.clear()
line_form.analytic_distribution = {}
with move_form.invoice_line_ids.edit(1) as line_form:
line_form.analytic_account_id = self.env['account.analytic.account']
line_form.analytic_tag_ids.clear()
line_form.analytic_distribution = {}
move_form.save()
# The tax line has been removed.
self.assertInvoiceValues(self.invoice, [
{
**self.product_line_vals_1,
'analytic_account_id': False,
'analytic_tag_ids': [],
'analytic_distribution': False,
},
{
**self.product_line_vals_2,
'analytic_account_id': False,
'analytic_tag_ids': [],
'analytic_distribution': False,
},
{
**self.tax_line_vals_1,
'analytic_account_id': False,
'analytic_tag_ids': [],
'analytic_distribution': False,
},
{
**self.tax_line_vals_2,
'analytic_account_id': False,
'analytic_tag_ids': [],
'analytic_distribution': False,
},
{
**self.term_line_vals_1,
'analytic_account_id': False,
'analytic_tag_ids': [],
'analytic_distribution': False,
},
], self.move_vals)
def test_out_invoice_line_onchange_analytic_2(self):
self.env.user.groups_id += self.env.ref('analytic.group_analytic_accounting')
analytic_plan = self.env['account.analytic.plan'].create({'name': 'Plan Test', 'company_id': False})
analytic_account = self.env['account.analytic.account'].create({
'name': 'test_analytic_account1',
'plan_id': analytic_plan.id,
'code': 'TEST1'
})
analytic_distribution = {analytic_account.id: 100}
self.invoice.write({'invoice_line_ids': [(1, self.invoice.invoice_line_ids.ids[0], {
'analytic_account_id': analytic_account.id,
'analytic_distribution': analytic_distribution,
})]})
self.assertRecordValues(self.invoice.invoice_line_ids, [
{'analytic_account_id': analytic_account.id},
{'analytic_account_id': False},
{'analytic_distribution': analytic_distribution},
{'analytic_distribution': False},
])
# We can remove the analytic account, it is not recomputed by an invalidation
self.invoice.write({'invoice_line_ids': [(1, self.invoice.invoice_line_ids.ids[0], {
'analytic_account_id': False,
'analytic_distribution': False,
})]})
self.assertRecordValues(self.invoice.invoice_line_ids, [
{'analytic_account_id': False},
{'analytic_account_id': False},
{'analytic_distribution': False},
{'analytic_distribution': False},
])
def test_out_invoice_line_onchange_cash_rounding_1(self):
@@ -3573,7 +3573,8 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon):
move_form.invoice_date = fields.Date.from_string('2019-01-01')
move_form.partner_id = self.partner_a
self.cash_basis_tax_a_third_amount.analytic = True
test_analytic_account = self.env['account.analytic.account'].create({'name': 'test_analytic_account'})
analytic_plan = self.env['account.analytic.plan'].create({'name': 'Plan Test', 'company_id': False})
test_analytic_account = self.env['account.analytic.account'].create({'name': 'test_analytic_account', 'plan_id': analytic_plan.id})
tax = self.cash_basis_tax_a_third_amount
@@ -3583,7 +3584,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon):
line_form.product_id = self.product_a
line_form.tax_ids.clear()
line_form.tax_ids.add(tax)
line_form.analytic_account_id = test_analytic_account
line_form.analytic_distribution = {test_analytic_account.id: 100}
line_form.price_unit = 100
# line with analytic account, will generate other 2 lines in CABA move
@@ -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_account_id', 'analytic_tag_ids', 'amount'])
self.check_fields_consistency('account.reconcile.model.line', 'account.reconcile.model.line.template', exceptions=['company_id', 'journal_id', 'analytic_distribution_stored_char', 'amount'])
def test_account_group_fields(self):
'''Test fields consistency for ('account.group', 'account.group.template')
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="account_analytic_account_view_form_inherit" model="ir.ui.view">
<field name="name">account.analytic.account.form.inherit</field>
<field name="model">account.analytic.account</field>
<field name="inherit_id" ref="analytic.view_account_analytic_account_form"/>
<field eval="9" name="priority"/>
<field name="arch" type="xml">
<div name="button_box" position="inside">
<button class="oe_stat_button" type="object" name="action_view_invoice"
icon="fa-pencil-square-o" attrs="{'invisible': [('invoice_count', '=', 0)]}">
<field string="Customer Invoices" name="invoice_count" widget="statinfo"/>
</button>
<button class="oe_stat_button" type="object" name="action_view_vendor_bill"
icon="fa-file-text-o" attrs="{'invisible': [('vendor_bill_count', '=', 0)]}">
<field string="Vendor Bills" name="vendor_bill_count" widget="statinfo"/>
</button>
</div>
</field>
</record>
</data>
</odoo>
@@ -1,119 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_account_analytic_default_tree" model="ir.ui.view">
<field name="name">account.analytic.default.tree</field>
<field name="model">account.analytic.default</field>
<field name="arch" type="xml">
<tree string="Analytic Defaults">
<field name="sequence" widget="handle"/>
<field name="analytic_id" required="0" groups="analytic.group_analytic_accounting"/>
<field name="analytic_tag_ids" widget="many2many_tags" groups="analytic.group_analytic_tags"/>
<field name="product_id"/>
<field name="partner_id"/>
<field name="user_id"/>
<field name="account_id"/>
<field name="date_start"/>
<field name="date_stop"/>
<field name="company_id" groups="base.group_multi_company"/>
</tree>
</field>
</record>
<record id="view_account_analytic_default_form" model="ir.ui.view">
<field name="name">account.analytic.default.form</field>
<field name="model">account.analytic.default</field>
<field name="arch" type="xml">
<form string="Analytic Defaults">
<sheet>
<group col="4">
<separator string="Analytic Default Rule" colspan="4"/>
<field name="analytic_id" groups="analytic.group_analytic_accounting" string="Account"/>
<field name="analytic_tag_ids" widget="many2many_tags" groups="analytic.group_analytic_tags" string="Tags"/>
<separator string="Conditions" colspan="4"/>
<field name="product_id"/>
<field name="partner_id"/>
<field name="user_id"/>
<field name="account_id"/>
<field name="date_start"/>
<field name="date_stop"/>
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_account_analytic_default_kanban" model="ir.ui.view">
<field name="name">account.analytic.default.kanban</field>
<field name="model">account.analytic.default</field>
<field name="arch" type="xml">
<kanban class="o_kanban_mobile">
<field name="analytic_id"/>
<field name="date_start"/>
<field name="date_stop"/>
<field name="product_id"/>
<field name="partner_id"/>
<field name="user_id"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_card oe_kanban_global_click">
<div>
<strong><span><field name="analytic_id"/></span></strong>
</div>
<div t-if="record.date_start.value"><i class="fa fa-calendar"></i> From <field name="date_start"/> <t t-if="record.date_stop.value">to <field name="date_stop"/></t></div>
<div t-if="record.product_id.value"><strong>Product</strong> <field name="product_id"/> </div>
<div t-if="record.partner_id.value"><strong>Customer</strong> <field name="partner_id"/> </div>
<div class="float-end">
<field name="user_id" widget="many2one_avatar_user"/>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="view_account_analytic_default_form_search" model="ir.ui.view">
<field name="name">account.analytic.default.search</field>
<field name="model">account.analytic.default</field>
<field name="arch" type="xml">
<search string="Accounts">
<field name="date_stop"/>
<field name="analytic_id" groups="analytic.group_analytic_accounting"/>
<field name="product_id"/>
<field name="partner_id"/>
<field name="user_id"/>
<field name="company_id" groups="base.group_multi_company"/>
<group expand="0" string="Group By">
<filter string="User" name="user" context="{'group_by':'user_id'}" help="User"/>
<filter string="Partner" name="partner" context="{'group_by':'partner_id'}" help="Partner"/>
<filter string="Product" name="product" context="{'group_by':'product_id'}" help="Product" />
<filter string="Analytic Account" name="analyticacc" context="{'group_by':'analytic_id'}" help="Analytic Account" groups="analytic.group_analytic_accounting"/>
<filter string="Company" name="company" context="{'group_by':'company_id'}" groups="base.group_multi_company" />
</group>
</search>
</field>
</record>
<record id="action_analytic_default_list" model="ir.actions.act_window">
<field name="name">Analytic Defaults Rules</field>
<field name="res_model">account.analytic.default</field>
<field name="view_mode">tree,kanban,form</field>
<field name="search_view_id" ref="view_account_analytic_default_form_search"/>
<field name="context">{"search_default_current":1}</field>
</record>
<menuitem id="menu_analytic_default_list"
action="action_analytic_default_list"
parent="account.menu_analytic_accounting"/>
<record id="analytic_rule_action_user" model="ir.actions.act_window">
<field name="name">Analytic Rules</field>
<field name="res_model">account.analytic.default</field>
<field name="context">{'search_default_user_id': [active_id], 'default_user_id': active_id}</field>
<field name="binding_model_id" ref="base.model_res_users"/>
<field name="binding_view_types">form</field>
<field name="groups_id" eval="[(4, ref('analytic.group_analytic_accounting'))]"/>
</record>
</odoo>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="account_analytic_distribution_model_tree_inherit" model="ir.ui.view">
<field name="name">account.analytic.distribution.model.inherit.tree</field>
<field name="model">account.analytic.distribution.model</field>
<field name="inherit_id" ref="analytic.account_analytic_distribution_model_tree_view"/>
<field name="arch" type="xml">
<data>
<xpath expr="//field[@name='partner_id']" position="before">
<field name="account_prefix"/>
</xpath>
<xpath expr="//field[@name='partner_category_id']" position="after">
<field name="product_id"/>
<field name="product_categ_id"/>
</xpath>
</data>
</field>
</record>
<record id="account_analytic_distribution_model_form_inherit" model="ir.ui.view">
<field name="name">account.analytic.distribution.model.inherit.form</field>
<field name="model">account.analytic.distribution.model</field>
<field name="inherit_id" ref="analytic.account_analytic_distribution_model_form_view"/>
<field name="arch" type="xml">
<data>
<xpath expr="//field[@name='partner_id']" position="before">
<field name="account_prefix"/>
</xpath>
<xpath expr="//field[@name='partner_category_id']" position="after">
<field name="product_id"/>
<field name="product_categ_id"/>
</xpath>
</data>
</field>
</record>
</data>
</odoo>
@@ -7,7 +7,7 @@
<field name="inherit_id" ref="analytic.view_account_analytic_line_form"/>
<field name="arch" type="xml">
<data>
<xpath expr="//field[@name='tag_ids']" position="after">
<xpath expr="//field[@name='amount']" position="after">
<field name="ref"/>
<field name="partner_id"/>
</xpath>
@@ -18,7 +18,7 @@
<group/> <!-- put Accounting group under Amount group -->
<group name="accounting" string="Accounting">
<field name="general_account_id" options="{'no_create': True}"/>
<field name="move_id" options="{'no_create': True}"/>
<field name="move_line_id" options="{'no_create': True}"/>
</group>
</group>
</data>
@@ -33,7 +33,7 @@
<xpath expr="//field[@name='account_id']" position="after">
<field name="ref" invisible="context.get('to_invoice', False)" optional="hide"/>
<field name="general_account_id" optional="hide"/>
<field name="move_id" optional="hide"/>
<field name="move_line_id" optional="hide"/>
<field name="product_id" optional="hide"/>
</xpath>
</data>
@@ -69,25 +69,15 @@
</field>
</record>
<record id="account_analytic_account_view_form_inherit" model="ir.ui.view">
<field name="name">account.analytic.account.form.inherit</field>
<field name="model">account.analytic.account</field>
<field name="inherit_id" ref="analytic.view_account_analytic_account_form"/>
<field eval="9" name="priority"/>
<record id="view_account_analytic_line_filter_inherit" model="ir.ui.view">
<field name="name">account.analytic.line.select.inherit</field>
<field name="model">account.analytic.line</field>
<field name="inherit_id" ref="analytic.view_account_analytic_line_filter"/>
<field name="arch" type="xml">
<div name="button_box" position="inside">
<button class="oe_stat_button" type="object" name="action_view_invoice"
icon="fa-pencil-square-o" attrs="{'invisible': [('invoice_count', '=', 0)]}">
<field string="Customer Invoices" name="invoice_count" widget="statinfo"/>
</button>
<button class="oe_stat_button" type="object" name="action_view_vendor_bill"
icon="fa-file-text-o" attrs="{'invisible': [('vendor_bill_count', '=', 0)]}">
<field string="Vendor Bills" name="vendor_bill_count" widget="statinfo"/>
</button>
</div>
<xpath expr="//filter[@name='group_date']" position="after">
<filter string="Category" name='category' context="{'group_by': 'category'}"/>
</xpath>
</field>
</record>
</data>
</odoo>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="account_analytic_plan_form_view_inherit_account" model="ir.ui.view">
<field name="name">account.analytic.plan.inherit.form</field>
<field name="model">account.analytic.plan</field>
<field name="inherit_id" ref="analytic.account_analytic_plan_form_view"/>
<field name="arch" type="xml">
<data>
<xpath expr="//field[@name='applicability_ids']//field[@name='business_domain']" position="after">
<field name="account_prefix"/>
<field name="product_categ_id"/>
</xpath>
</data>
</field>
</record>
</data>
</odoo>
+4 -4
View File
@@ -35,6 +35,7 @@
<menuitem id="menu_finance_reports" name="Reporting" sequence="20" groups="account.group_account_readonly,account.group_account_invoice">
<menuitem id="account_reports_management_menu" name="Management" sequence="4">
<menuitem id="menu_action_account_invoice_report_all" name="Invoice Analysis" action="action_account_invoice_report_all" sequence="1"/>
<menuitem id="menu_action_analytic_lines_tree" name="Analytic Items" action="analytic.account_analytic_line_action_entries" groups="analytic.group_analytic_accounting" sequence="2"/>
</menuitem>
<menuitem id="account_reports_legal_statements_menu" name="Statement Reports" sequence="1" groups="account.group_account_readonly"/>
</menuitem>
@@ -63,11 +64,10 @@
<menuitem id="menu_product_product_categories" action="product.product_category_action_form" sequence="3"/>
<menuitem id="menu_action_rounding_form_view" action="rounding_list_action" groups="account.group_cash_rounding" sequence="4"/>
</menuitem>
<menuitem id="menu_analytic_accounting" name="Analytic Accounting" groups="analytic.group_analytic_accounting,analytic.group_analytic_tags" sequence="6">
<menuitem id="menu_analytic_accounting" name="Analytic Accounting" groups="analytic.group_analytic_accounting" sequence="6">
<menuitem id="menu_analytic__distribution_model" name="Analytic Distribution Models" action="analytic.action_analytic_distribution_model" groups="analytic.group_analytic_accounting" sequence="1"/>
<menuitem id="account_analytic_def_account" action="analytic.action_account_analytic_account_form" groups="analytic.group_analytic_accounting" sequence="1"/>
<menuitem id="account_analytic_group_menu" name="Analytic Account Groups" action="analytic.account_analytic_group_action" groups="analytic.group_analytic_accounting" sequence="2"/>
<menuitem id="account_analytic_tag_menu" name="Analytic Tags" action="analytic.account_analytic_tag_action" groups="analytic.group_analytic_tags"/>
<menuitem id="menu_action_analytic_lines_tree" name="Analytic Items" action="analytic.account_analytic_line_action_entries" groups="analytic.group_analytic_accounting" sequence="35"/>
<menuitem id="account_analytic_plan_menu" name="Analytic Plans" action="analytic.account_analytic_plan_action" groups="analytic.group_analytic_accounting" sequence="2"/>
</menuitem>
</menuitem>
</menuitem>
+31 -37
View File
@@ -69,12 +69,10 @@
<group string="States">
<field name="blocked"/>
</group>
<group string="Analytic" groups="analytic.group_analytic_accounting,analytic.group_analytic_tags">
<field name="analytic_account_id" groups="analytic.group_analytic_accounting"
domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]"
<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"/>
<field name="analytic_tag_ids" groups="analytic.group_analytic_tags"
widget="many2many_tags"/>
</group>
</group>
</page>
@@ -163,18 +161,17 @@
<field name="model">account.move.line</field>
<field eval="100" name="priority"/>
<field name="arch" type="xml">
<tree string="Journal Items" create="false" edit="false" expand="context.get('expand', False)" multi_edit="1" sample="1">
<tree string="Journal Items" create="false" edit="true" expand="context.get('expand', False)" multi_edit="1" sample="1">
<field name="date" readonly="1"/>
<field name="company_id" invisible="1"/>
<field name="company_id" groups="base.group_multi_company" optional="hide"/>
<field name="journal_id" options='{"no_open":True}' optional="hide"/>
<field name="company_id" groups="base.group_multi_company" readonly="1" optional="hide"/>
<field name="journal_id" readonly="1" options='{"no_open":True}' optional="hide"/>
<field name="move_name" string="Journal Entry" widget="open_move_widget"/>
<field name="account_id" options="{'no_open': True, 'no_create': True}" domain="[('company_id', '=', company_id)]" groups="account.group_account_readonly"/>
<field name="partner_id" optional="show" attrs="{'readonly':[('account_type','in', ('liability_payable','asset_receivable'))]}"/>
<field name="ref" optional="hide"/>
<field name="product_id" optional="hide"/>
<field name="product_id" readonly="1" optional="hide"/>
<field name="name" optional="show"/>
<field name="analytic_account_id" groups="analytic.group_analytic_accounting" optional="show" attrs="{'readonly':[('parent_state','=','posted')]}"/>
<field name="tax_ids" widget="many2many_tags" width="0.5" optional="hide" readonly="1"/>
<field name="amount_currency" groups="base.group_multi_currency" optional="show" readonly="1" attrs="{'invisible':[('is_same_currency', '=', True)]}"/>
<field name="currency_id" readonly="1" groups="base.group_multi_currency" optional="hide" string="Currency" attrs="{'invisible':[('is_same_currency', '=', True)]}"/>
@@ -184,14 +181,19 @@
<field name="discount_date" string="Discount Date"/>
<field name="discount_amount_currency" string="Discount Amount"/>
<field name="tax_line_id" string="Originator Tax" optional="hide" readonly="1"/>
<field name="date_maturity" optional="hide"/>
<field name="date_maturity" readonly="1" optional="hide"/>
<field name="balance" sum="Total Balance" optional="hide" readonly="1"/>
<field name="cumulated_balance" optional="hide"/>
<field name="matching_number" optional="show"/>
<field name="amount_residual" sum="Total Residual" string="Residual" optional="hide" attrs="{'invisible':[('is_account_reconcile', '=', False)]}"/>
<field name="amount_residual_currency" sum="Total Residual in Currency" string="Residual in Currency" optional="hide" attrs="{'invisible':['|', ('is_same_currency', '=', True), ('is_account_reconcile', '=', False)]}"/>
<field name="cumulated_balance" readonly="1" optional="hide"/>
<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"
options="{'product_field': 'product_id', 'account_field': 'account_id', 'force_applicability': 'optional'}"
/>
<field name="move_id" invisible="1"/>
<field name="parent_state" invisible="1"/>
<field name="account_type" invisible="1"/>
<field name="statement_id" invisible="1"/>
@@ -274,7 +276,7 @@
<field name="tax_base_amount" sum="Total Base Amount"/>
<field name="tax_audit"/>
</field>
<field name="analytic_account_id" position="attributes">
<field name="analytic_distribution" position="attributes">
<attribute name="optional">hide</attribute>
</field>
<field name="debit" position="attributes">
@@ -345,7 +347,6 @@
<separator/>
<filter string="Report Dates" name="date_between" domain="[('date', '&gt;=', context.get('date_from')), ('date', '&lt;=', context.get('date_to'))]" invisible="1"/>
<separator/>
<filter string="Report Analytic Accounts" name="analytic_accounts" domain="[('analytic_account_id', 'in', context.get('analytic_ids'))]" invisible="1"/>
<group expand="0" string="Group By">
<filter string="Journal Entry" name="group_by_move" domain="[]" context="{'group_by': 'move_name'}"/>
<filter string="Account" name="group_by_account" domain="[]" context="{'group_by': 'account_id'}"/>
@@ -884,15 +885,12 @@
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_account_id"
domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]"
<field name="analytic_distribution_stored_char" invisible="1"/>
<field name="analytic_distribution" widget="analytic_distribution"
groups="analytic.group_analytic_accounting"
optional="show"/>
<field name="analytic_tag_ids"
domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]"
groups="analytic.group_analytic_tags"
optional="show"
widget="many2many_tags"/>
options="{'product_field': 'product_id', 'account_field': 'account_id'}"
business_domain_compute="parent.move_type in ['out_invoice', 'out_refund', 'out_receipt'] and 'sale' or parent.move_type in ['in_invoice', 'in_refund', 'in_receipt'] and 'purchase' or 'general'"/>
<field name="quantity" optional="show"/>
<field name="product_uom_category_id" invisible="1"/>
<field name="product_uom_id" string="UoM" groups="uom.group_uom" optional="show"/>
@@ -994,10 +992,10 @@
<field name="discount" string="Disc.%"/>
</group>
<group>
<field name="analytic_tag_ids" groups="analytic.group_analytic_tags" widget="many2many_tags"/>
<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_account_id" 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"/>
</group>
<label for="name" string="Description" attrs="{'invisible': [('display_type', 'in', ('line_note', 'line_section'))]}"/>
<label for="name" string="Section" attrs="{'invisible': [('display_type', '!=', 'line_section')]}"/>
@@ -1058,16 +1056,12 @@
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_account_id"
optional="hide"
domain="['|', ('company_id', '=', parent.company_id), ('company_id', '=', False)]"
<field name="analytic_distribution_stored_char" invisible="1"/>
<field name="analytic_distribution" widget="analytic_distribution"
groups="analytic.group_analytic_accounting"
attrs="{'invisible': [('display_type', 'in', ('line_section', 'line_note'))]}"/>
<field name="analytic_tag_ids"
optional="show"
groups="analytic.group_analytic_tags"
widget="many2many_tags"
attrs="{'invisible': [('display_type', 'in', ('line_section', 'line_note'))]}"/>
options="{'product_field': 'product_id', 'account_field': 'account_id'}"
business_domain_compute="parent.move_type in ['out_invoice', 'out_refund', 'out_receipt'] and 'sale' or parent.move_type in ['in_invoice', 'in_refund', 'in_receipt'] and 'purchase' or 'general'"/>
<field name="date_maturity"
optional="hide"
invisible="context.get('view_no_maturity')"
@@ -1146,8 +1140,8 @@
<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_account_id" groups="analytic.group_analytic_accounting"/>
<field name="analytic_tag_ids" groups="analytic.group_analytic_tags" widget="many2many_tags"/>
<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"/>
<field name="company_id" invisible="1"/>
@@ -24,8 +24,11 @@
<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_account_id" domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]" groups="analytic.group_analytic_accounting"/>
<field name="analytic_tag_ids" groups="analytic.group_analytic_tags" widget="many2many_tags"/>
<!-- TODO widget="analytic_distribution" -->
<field name="analytic_distribution_stored_char" invisible="1"/>
<field name="analytic_distribution" invisible="1" groups="analytic.group_analytic_accounting"
options="{'product_field': 'product_id', 'account_field': 'account_id'}"
business_domain_compute="parent.move_type in ['out_invoice', 'out_refund', 'out_receipt'] and 'sale' or parent.move_type in ['in_invoice', 'in_refund', 'in_receipt'] and 'purchase' or 'general'"/>
<field name="company_id" invisible="1"/>
</group>
<group>
@@ -243,13 +246,12 @@
<field name="tax_ids"
widget="many2many_tags"
optional="hide"/>
<field name="analytic_account_id"
groups="analytic.group_analytic_accounting"
optional="hide"/>
<field name="analytic_tag_ids"
widget="many2many_tags"
groups="analytic.group_analytic_tags"
optional="hide"/>
<field name="analytic_distribution_stored_char" invisible="1"/>
<!-- TODO widget="analytic_distribution" -->
<field name="analytic_distribution"
invisible="1" groups="analytic.group_analytic_accounting"
options="{'product_field': 'product_id', 'account_field': 'account_id'}"
business_domain_compute="parent.move_type in ['out_invoice', 'out_refund', 'out_receipt'] and 'sale' or parent.move_type in ['in_invoice', 'in_refund', 'in_receipt'] and 'purchase' or 'general'"/>
<field name="force_tax_included"
widget="boolean_toggle"
attrs="{'invisible': [('show_force_tax_included', '=', False)]}"
@@ -636,19 +636,6 @@
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box"
id="tag_analytic_entries"
groups="account.group_account_user">
<div class="o_setting_left_pane">
<field name="group_analytic_tags"/>
</div>
<div class="o_setting_right_pane">
<label for="group_analytic_tags"/>
<div class="text-muted">
Allows to tag analytic entries and to manage analytic distributions
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box" id="account_budget" title="This allows accountants to manage analytic and crossovered budgets. Once the master budgets and the budgets are defined, the project managers can set the planned amount on each analytic account." groups="account.group_account_user">
<div class="o_setting_left_pane">
<field name="module_account_budget" widget="upgrade_boolean"/>
+12 -2
View File
@@ -17,12 +17,22 @@ that have no counterpart in the general financial accounts.
'data': [
'security/analytic_security.xml',
'security/ir.model.access.csv',
'views/analytic_line_views.xml',
'views/analytic_account_views.xml',
'views/analytic_plan_views.xml',
'views/analytic_distribution_model_views.xml',
],
'demo': [
'data/analytic_demo.xml',
'data/analytic_account_demo.xml',
'data/analytic_account_demo.xml'
],
'assets': {
'web.assets_backend': [
'analytic/static/src/components/**/*',
],
'web.qunit_suite_tests': [
'analytic/static/tests/*.js',
],
},
'installable': True,
'license': 'LGPL-3',
}
+25 -22
View File
@@ -1,101 +1,104 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Analytic Groups -->
<record id="analytic_group_projects" model="account.analytic.group">
<!-- Analytic Plans -->
<record id="analytic_plan_projects" model="account.analytic.plan">
<field name="name">Projects</field>
<field name="default_applicability">optional</field>
</record>
<record id="analytic_group_departments" model="account.analytic.group">
<record id="analytic_plan_departments" model="account.analytic.plan">
<field name="name">Departments</field>
<field name="default_applicability">optional</field>
</record>
<record id="analytic_group_internal" model="account.analytic.group">
<record id="analytic_plan_internal" model="account.analytic.plan">
<field name="name">Internal</field>
<field name="default_applicability">unavailable</field>
</record>
<!-- Analytic Accounts -->
<record id="analytic_absences" model="account.analytic.account">
<field name="name">Time Off</field>
<field name="group_id" ref="analytic.analytic_group_internal"/>
<field name="plan_id" ref="analytic.analytic_plan_internal"/>
</record>
<record id="analytic_internal" model="account.analytic.account">
<field name="name">Operating Costs</field>
<field name="group_id" ref="analytic.analytic_group_internal"/>
<field name="plan_id" ref="analytic.analytic_plan_internal"/>
</record>
<record id="analytic_our_super_product" model="account.analytic.account">
<field name="name">Our Super Product</field>
<field name="partner_id" ref="base.res_partner_2"/>
<field name="group_id" ref="analytic.analytic_group_projects"/>
<field name="plan_id" ref="analytic.analytic_plan_projects"/>
</record>
<record id="analytic_seagate_p2" model="account.analytic.account">
<field name="name">Seagate P2</field>
<field name="partner_id" ref="base.res_partner_2"/>
<field name="group_id" ref="analytic.analytic_group_projects"/>
<field name="plan_id" ref="analytic.analytic_plan_projects"/>
</record>
<record id="analytic_millennium_industries" model="account.analytic.account">
<field name="name">Millennium Industries</field>
<field name="partner_id" ref="base.res_partner_3"/>
<field name="group_id" ref="analytic.analytic_group_projects"/>
<field name="plan_id" ref="analytic.analytic_plan_projects"/>
</record>
<record id="analytic_integration_c2c" model="account.analytic.account">
<field name="name">CampToCamp</field>
<field name="partner_id" ref="base.res_partner_12"/>
<field name="group_id" ref="analytic.analytic_group_projects"/>
<field name="plan_id" ref="analytic.analytic_plan_projects"/>
</record>
<record id="analytic_agrolait" model="account.analytic.account">
<field name="name">Deco Addict</field>
<field name="partner_id" ref="base.res_partner_2"/>
<field name="group_id" ref="analytic.analytic_group_projects"/>
<field name="plan_id" ref="analytic.analytic_plan_projects"/>
</record>
<record id="analytic_asustek" model="account.analytic.account">
<field name="name">Asustek</field>
<field name="partner_id" ref="base.res_partner_1"/>
<field name="group_id" ref="analytic.analytic_group_projects"/>
<field name="plan_id" ref="analytic.analytic_plan_projects"/>
</record>
<record id="analytic_deltapc" model="account.analytic.account">
<field name="name">Delta PC</field>
<field name="partner_id" ref="base.res_partner_4"/>
<field name="group_id" ref="analytic.analytic_group_projects"/>
<field name="plan_id" ref="analytic.analytic_plan_projects"/>
</record>
<record id="analytic_spark" model="account.analytic.account">
<field name="name">Spark Systems</field>
<field name="partner_id" ref="base.res_partner_1"/>
<field name="group_id" ref="analytic.analytic_group_projects"/>
<field name="plan_id" ref="analytic.analytic_plan_projects"/>
</record>
<record id="analytic_nebula" model="account.analytic.account">
<field name="name">Nebula</field>
<field name="partner_id" ref="base.res_partner_12"/>
<field name="group_id" ref="analytic.analytic_group_projects"/>
<field name="plan_id" ref="analytic.analytic_plan_projects"/>
</record>
<record id="analytic_luminous_technologies" model="account.analytic.account">
<field name="name">Luminous Technologies</field>
<field name="partner_id" ref="base.res_partner_3"/>
<field name="group_id" ref="analytic.analytic_group_projects"/>
<field name="plan_id" ref="analytic.analytic_plan_projects"/>
</record>
<record id="analytic_desertic_hispafuentes" model="account.analytic.account">
<field name="name">Desertic - Hispafuentes</field>
<field name="partner_id" ref="base.res_partner_12"/>
<field name="group_id" ref="analytic.analytic_group_projects"/>
<field name="plan_id" ref="analytic.analytic_plan_projects"/>
</record>
<record id="analytic_think_big_systems" model="account.analytic.account">
<field name="name">Lumber Inc</field>
<field name="partner_id" ref="base.res_partner_18"/>
<field name="group_id" ref="analytic.analytic_group_projects"/>
<field name="plan_id" ref="analytic.analytic_plan_projects"/>
</record>
<record id="analytic_partners_camp_to_camp" model="account.analytic.account">
<field name="name">Camp to Camp</field>
<field name="partner_id" ref="base.res_partner_12"/>
<field name="group_id" ref="analytic.analytic_group_projects"/>
<field name="plan_id" ref="analytic.analytic_plan_projects"/>
</record>
<record id="analytic_administratif" model="account.analytic.account">
<field name="name">Administrative</field>
<field name="group_id" ref="analytic.analytic_group_departments"/>
<field name="plan_id" ref="analytic.analytic_plan_departments"/>
</record>
<record id="analytic_commercial_marketing" model="account.analytic.account">
<field name="name">Commercial &amp; Marketing</field>
<field name="group_id" ref="analytic.analytic_group_departments"/>
<field name="plan_id" ref="analytic.analytic_plan_departments"/>
</record>
<record id="analytic_rd_department" model="account.analytic.account">
<field name="name">Research &amp; Development</field>
<field name="group_id" ref="analytic.analytic_group_departments"/>
<field name="plan_id" ref="analytic.analytic_plan_departments"/>
</record>
</data>
</odoo>
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="decimal_percentage_analytic" model="decimal.precision">
<field name="name">Percentage Analytic</field>
<field name="digits">10</field>
</record>
</data>
</odoo>
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="tag_contract" model="account.analytic.tag">
<field name="name">Contracts</field>
</record>
</data>
</odoo>
+4
View File
@@ -1,5 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import analytic_plan
from . import analytic_account
from . import analytic_line
from . import analytic_mixin
from . import analytic_distribution_model
from . import res_config_settings
+117 -113
View File
@@ -3,57 +3,9 @@
from collections import defaultdict
from odoo import api, fields, models, _
from odoo.osv import expression
from odoo.exceptions import ValidationError
from odoo.exceptions import UserError
class AccountAnalyticDistribution(models.Model):
_name = 'account.analytic.distribution'
_description = 'Analytic Account Distribution'
_rec_name = 'account_id'
account_id = fields.Many2one('account.analytic.account', string='Analytic Account', required=True)
percentage = fields.Float(string='Percentage', required=True, default=100.0)
name = fields.Char(string='Name', related='account_id.name', readonly=False)
tag_id = fields.Many2one('account.analytic.tag', string="Parent tag", required=True)
_sql_constraints = [
('check_percentage', 'CHECK(percentage >= 0 AND percentage <= 100)',
'The percentage of an analytic distribution should be between 0 and 100.')
]
class AccountAnalyticTag(models.Model):
_name = 'account.analytic.tag'
_description = 'Analytic Tags'
name = fields.Char(string='Analytic Tag', index='trigram', required=True)
color = fields.Integer('Color Index')
active = fields.Boolean(default=True, help="Set active to false to hide the Analytic Tag without removing it.")
active_analytic_distribution = fields.Boolean('Analytic Distribution')
analytic_distribution_ids = fields.One2many('account.analytic.distribution', 'tag_id', string="Analytic Accounts")
company_id = fields.Many2one('res.company', string='Company')
class AccountAnalyticGroup(models.Model):
_name = 'account.analytic.group'
_description = 'Analytic Categories'
_parent_store = True
_rec_name = 'complete_name'
name = fields.Char(required=True)
description = fields.Text(string='Description')
parent_id = fields.Many2one('account.analytic.group', string="Parent", ondelete='cascade', domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
parent_path = fields.Char(index='btree', unaccent=False)
children_ids = fields.One2many('account.analytic.group', 'parent_id', string="Childrens")
complete_name = fields.Char('Complete Name', compute='_compute_complete_name', recursive=True, store=True)
company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company)
@api.depends('name', 'parent_id.complete_name')
def _compute_complete_name(self):
for group in self:
if group.parent_id:
group.complete_name = '%s / %s' % (group.parent_id.complete_name, group.name)
else:
group.complete_name = group.name
class AccountAnalyticAccount(models.Model):
_name = 'account.analytic.account'
_inherit = ['mail.thread']
@@ -62,6 +14,118 @@ class AccountAnalyticAccount(models.Model):
_check_company_auto = True
_rec_names_search = ['name', 'code', 'partner_id']
name = fields.Char(
string='Analytic Account',
index='trigram',
required=True,
tracking=True,
)
code = fields.Char(
string='Reference',
index='btree',
tracking=True,
)
active = fields.Boolean(
'Active',
help="Deactivate the account.",
default=True,
tracking=True,
)
plan_id = fields.Many2one(
'account.analytic.plan',
string='Plan',
check_company=True,
required=True,
)
root_plan_id = fields.Many2one(
'account.analytic.plan',
string='Root Plan',
check_company=True,
compute="_compute_root_plan",
store=True,
)
color = fields.Integer(
'Color Index',
related='plan_id.color',
)
line_ids = fields.One2many(
'account.analytic.line',
'account_id',
string="Analytic Lines",
)
company_id = fields.Many2one(
'res.company',
string='Company',
default=lambda self: self.env.company,
)
# use auto_join to speed up name_search call
partner_id = fields.Many2one(
'res.partner',
string='Customer',
auto_join=True,
tracking=True,
check_company=True,
)
balance = fields.Monetary(
compute='_compute_debit_credit_balance',
string='Balance',
groups='account.group_account_readonly',
)
debit = fields.Monetary(
compute='_compute_debit_credit_balance',
string='Debit',
groups='account.group_account_readonly',
)
credit = fields.Monetary(
compute='_compute_debit_credit_balance',
string='Credit',
groups='account.group_account_readonly',
)
currency_id = fields.Many2one(
related="company_id.currency_id",
string="Currency",
)
@api.constrains('company_id')
def _check_company_consistency(self):
analytic_accounts = self.filtered('company_id')
if not analytic_accounts:
return
self.flush_recordset(['company_id'])
self.env['account.analytic.line'].flush_model(['account_id', 'company_id'])
self._cr.execute('''
SELECT line.id, line.company_id, line.account_id FROM account_analytic_line line''')
self._cr.execute('''
SELECT line.account_id
FROM account_analytic_line line
JOIN account_analytic_account account ON line.account_id = account.id
WHERE line.company_id != account.company_id and account.company_id IS NOT NULL
''')
if self._cr.fetchone():
raise UserError(_("You can't set a different company on your analytic account since there are some analytic items linked to it."))
def name_get(self):
res = []
for analytic in self:
name = analytic.name
if analytic.code:
name = f'[{analytic.code}]{name}'
if analytic.partner_id.commercial_partner_id.name:
name = f'{name} - {analytic.partner_id.commercial_partner_id.name} - '
res.append((analytic.id, name))
return res
@api.model
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
"""
@@ -92,9 +156,6 @@ class AccountAnalyticAccount(models.Model):
domain.append(('date', '>=', self._context['from_date']))
if self._context.get('to_date', False):
domain.append(('date', '<=', self._context['to_date']))
if self._context.get('tag_ids'):
tag_domain = expression.OR([[('tag_ids', 'in', [tag])] for tag in self._context['tag_ids']])
domain = expression.AND([domain, tag_domain])
user_currency = self.env.company.currency_id
credit_groups = analytic_line_obj.read_group(
@@ -124,64 +185,7 @@ class AccountAnalyticAccount(models.Model):
account.credit = data_credit.get(account.id, 0.0)
account.balance = account.credit - account.debit
name = fields.Char(string='Analytic Account', index='trigram', required=True, tracking=True)
code = fields.Char(string='Reference', index='btree', tracking=True)
active = fields.Boolean('Active', help="If the active field is set to False, it will allow you to hide the account without removing it.", default=True)
group_id = fields.Many2one('account.analytic.group', string='Group', check_company=True)
line_ids = fields.One2many('account.analytic.line', 'account_id', string="Analytic Lines")
company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company)
# use auto_join to speed up name_search call
partner_id = fields.Many2one('res.partner', string='Customer', auto_join=True, tracking=True, check_company=True)
balance = fields.Monetary(compute='_compute_debit_credit_balance', string='Balance', groups='account.group_account_readonly')
debit = fields.Monetary(compute='_compute_debit_credit_balance', string='Debit', groups='account.group_account_readonly')
credit = fields.Monetary(compute='_compute_debit_credit_balance', string='Credit', groups='account.group_account_readonly')
currency_id = fields.Many2one(related="company_id.currency_id", string="Currency", readonly=True)
def name_get(self):
res = []
for analytic in self:
name = analytic.name
if analytic.code:
name = '[' + analytic.code + '] ' + name
if analytic.partner_id.commercial_partner_id.name:
name = name + ' - ' + analytic.partner_id.commercial_partner_id.name
res.append((analytic.id, name))
return res
class AccountAnalyticLine(models.Model):
_name = 'account.analytic.line'
_description = 'Analytic Line'
_order = 'date desc, id desc'
_check_company_auto = True
@api.model
def _default_user(self):
return self.env.context.get('user_id', self.env.user.id)
name = fields.Char('Description', required=True)
date = fields.Date('Date', required=True, index=True, default=fields.Date.context_today)
amount = fields.Monetary('Amount', required=True, default=0.0)
unit_amount = fields.Float('Quantity', default=0.0)
product_uom_id = fields.Many2one('uom.uom', string='Unit of Measure', domain="[('category_id', '=', product_uom_category_id)]")
product_uom_category_id = fields.Many2one(related='product_uom_id.category_id', string='UoM Category', readonly=True)
account_id = fields.Many2one('account.analytic.account', 'Analytic Account', required=True, ondelete='restrict', index=True, check_company=True)
partner_id = fields.Many2one('res.partner', string='Partner', check_company=True)
user_id = fields.Many2one('res.users', string='User', default=_default_user, index=True)
tag_ids = fields.Many2many('account.analytic.tag', 'account_analytic_line_tag_rel', 'line_id', 'tag_id', string='Tags', copy=True, check_company=True)
company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, default=lambda self: self.env.company)
currency_id = fields.Many2one(related="company_id.currency_id", string="Currency", readonly=True, store=True, compute_sudo=True)
group_id = fields.Many2one('account.analytic.group', related='account_id.group_id', store=True, readonly=True, compute_sudo=True)
category = fields.Selection([('other', 'Other')], default='other')
@api.constrains('company_id', 'account_id')
def _check_company_id(self):
for line in self:
if line.account_id.company_id and line.company_id.id != line.account_id.company_id.id:
raise ValidationError(_('The selected account belongs to another company than the one you\'re trying to create an analytic item for'))
@api.depends('plan_id', 'plan_id.parent_path')
def _compute_root_plan(self):
for account in self:
account.root_plan_id = int(account.plan_id.parent_path[:-1].split('/')[0]) if account.plan_id.parent_path else None
@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
import json
class AccountAnalyticDistributionModel(models.Model):
_name = 'account.analytic.distribution.model'
_inherit = 'analytic.mixin'
_description = 'Analytic Distribution Model'
_rec_name = 'create_date'
_order = 'id desc'
partner_id = fields.Many2one(
'res.partner',
string='Partner',
ondelete='cascade',
help="Select a partner for which the analytic distribution will be used (e.g. create new customer invoice or Sales order if we select this partner, it will automatically take this as an analytic account)",
)
partner_category_id = fields.Many2one(
'res.partner.category',
string='Partner Category',
ondelete='cascade',
help="Select a partner category for which the analytic distribution will be used (e.g. create new customer invoice or Sales order if we select this partner, it will automatically take this as an analytic account)",
)
company_id = fields.Many2one(
'res.company',
string='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.model
def _get_distribution(self, vals):
""" Returns the distribution model that has the most fields that corresponds to the vals given
This method should be called to prefill analytic distribution field on several models """
domain = []
for fname, value in vals.items():
domain += self._create_domain(fname, value) or []
best_score = 0
res = {}
for rec in self.search(domain):
score = 0
for key, value in vals.items():
if value and rec[key]:
if rec._check_score(key, value) == 1:
score += 1
else:
score = -1
break
if score > best_score:
res = rec.analytic_distribution
best_score = score
return res
def _check_score(self, key, value):
self.ensure_one()
if key == 'partner_category_id':
if self[key].id in value:
return 1
if value == self[key].id:
return 1
else:
return -1
def _create_domain(self, fname, value):
if not value:
return False
if fname == 'partner_category_id':
value += [False]
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
+92
View File
@@ -0,0 +1,92 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
class AccountAnalyticLine(models.Model):
_name = 'account.analytic.line'
_description = 'Analytic Line'
_order = 'date desc, id desc'
_check_company_auto = True
name = fields.Char(
'Description',
required=True,
)
date = fields.Date(
'Date',
required=True,
index=True,
default=fields.Date.context_today,
)
amount = fields.Monetary(
'Amount',
required=True,
default=0.0,
)
unit_amount = fields.Float(
'Quantity',
default=0.0,
)
product_uom_id = fields.Many2one(
'uom.uom',
string='Unit of Measure',
domain="[('category_id', '=', product_uom_category_id)]",
)
product_uom_category_id = fields.Many2one(
related='product_uom_id.category_id',
string='UoM Category',
readonly=True,
)
account_id = fields.Many2one(
'account.analytic.account',
'Analytic Account',
required=True,
ondelete='restrict',
index=True,
check_company=True,
)
partner_id = fields.Many2one(
'res.partner',
string='Partner',
check_company=True,
)
user_id = fields.Many2one(
'res.users',
string='User',
default=lambda self: self.env.context.get('user_id', self.env.user.id),
index=True,
)
company_id = fields.Many2one(
'res.company',
string='Company',
required=True,
readonly=True,
default=lambda self: self.env.company,
)
currency_id = fields.Many2one(
related="company_id.currency_id",
string="Currency",
readonly=True,
store=True,
compute_sudo=True,
)
plan_id = fields.Many2one(
'account.analytic.plan',
related='account_id.plan_id',
store=True,
readonly=True,
compute_sudo=True,
)
category = fields.Selection(
[('other', 'Other')],
default='other',
)
@api.constrains('company_id', 'account_id')
def _check_company_id(self):
for line in self:
if line.account_id.company_id and line.company_id.id != line.account_id.company_id.id:
raise ValidationError(_('The selected account belongs to another company than the one you\'re trying to create an analytic item for'))
+49
View File
@@ -0,0 +1,49 @@
# -*- 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
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,
)
def _compute_analytic_distribution_stored_char(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):
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)
+222
View File
@@ -0,0 +1,222 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from random import randint
class AccountAnalyticPlan(models.Model):
_name = 'account.analytic.plan'
_description = 'Analytic Plans'
_parent_store = True
_rec_name = 'complete_name'
def _default_color(self):
return randint(1, 11)
name = fields.Char(required=True)
description = fields.Text(string='Description')
parent_id = fields.Many2one(
'account.analytic.plan',
string="Parent",
ondelete='cascade',
domain="[('id', '!=', id), ('company_id', 'in', [False, company_id])]",
)
parent_path = fields.Char(
index='btree',
unaccent=False,
)
children_ids = fields.One2many(
'account.analytic.plan',
'parent_id',
string="Childrens",
)
children_count = fields.Integer(
'Children Plans Count',
compute='_compute_children_count',
)
complete_name = fields.Char(
'Complete Name',
compute='_compute_complete_name',
recursive=True,
store=True,
)
company_id = fields.Many2one(
'res.company',
string='Company',
default=lambda self: self.env.company,
)
account_ids = fields.One2many(
'account.analytic.account',
'plan_id',
string="Accounts",
)
account_count = fields.Integer(
'Analytic Accounts Count',
compute='_compute_analytic_account_count',
)
all_account_count = fields.Integer(
'All Analytic Accounts Count',
compute='_compute_all_analytic_account_count',
)
color = fields.Integer(
'Color',
default=_default_color,
)
default_applicability = fields.Selection(
selection=[
('optional', 'Optional'),
('mandatory', 'Mandatory'),
('unavailable', 'Unavailable'),
],
string="Default Applicability",
required=True,
default='optional',
readonly=False,
)
applicability_ids = fields.One2many(
'account.analytic.applicability',
'analytic_plan_id',
string='Applicability',
)
@api.depends('name', 'parent_id.complete_name')
def _compute_complete_name(self):
for plan in self:
if plan.parent_id:
plan.complete_name = '%s / %s' % (plan.parent_id.complete_name, plan.name)
else:
plan.complete_name = plan.name
@api.depends('account_ids')
def _compute_analytic_account_count(self):
for plan in self:
plan.account_count = len(plan.account_ids)
@api.depends('account_ids', 'children_ids')
def _compute_all_analytic_account_count(self):
for plan in self:
plan.all_account_count = len(plan.account_ids) + len(plan.children_ids.account_ids)
@api.depends('children_ids')
def _compute_children_count(self):
for plan in self:
plan.children_count = len(plan.children_ids)
def action_view_analytical_accounts(self):
result = {
"type": "ir.actions.act_window",
"res_model": "account.analytic.account",
"domain": [('id', 'in', self.account_ids.ids)],
"context": {'default_plan_id': self.id},
"name": _("Analytical Accounts"),
'view_mode': 'list,form',
}
return result
def action_view_children_plans(self):
result = {
"type": "ir.actions.act_window",
"res_model": "account.analytic.plan",
"domain": [('id', 'in', self.children_ids.ids)],
"context": {'default_parent_id': self.id,
'default_color': self.color},
"name": _("Analytical Plans"),
'view_mode': 'list,form',
}
return result
@api.model
def get_relevant_plans(self, **kwargs):
""" Returns the list of plans that should be available.
This list is computed based on the applicabilities of root plans. """
list_plans = []
set_plan_ids = {}
all_plans = self.search([('parent_id', '=', False), '|', ('account_ids', '!=', False), ('children_ids.account_ids', '!=', False)])
for plan in all_plans:
applicability = plan._get_applicability(**kwargs)
if applicability != 'unavailable':
set_plan_ids[plan.id] = plan
list_plans.append(
{
"id": plan.id,
"name": plan.name,
"color": plan.color,
"applicability": applicability,
"all_account_count": plan.all_account_count
})
# If we have accounts that are already selected (before the applicability rules changed or from a model),
# we want the plans that were unavailable to be shown in the list (and in optional, because the previous
# percentage could be different from 0)
record_account_ids = kwargs.get('existing_account_ids', [])
forced_plans = self.env['account.analytic.account'].browse(record_account_ids).mapped('root_plan_id')
for plan in forced_plans.filtered(lambda plan: plan.id not in set_plan_ids):
list_plans.append({
"id": plan.id,
"name": plan.name,
"color": plan.color,
"applicability": 'optional',
"all_account_count": plan.all_account_count
})
return sorted(list_plans, key=lambda d: (d['applicability'], d['id']))
def _get_applicability(self, **kwargs):
""" Returns the applicability of the best applicability line or the default applicability """
self.ensure_one()
if 'applicability' in kwargs:
# For models for example, we want all plans to be visible, so we force the applicability
return kwargs['applicability']
else:
score = 0
applicability = self.default_applicability
for applicability_rule in self.applicability_ids:
score_rule = applicability_rule._get_score(**kwargs)
if score_rule > score:
applicability = applicability_rule.applicability
score = score_rule
return applicability
def _get_default(self):
plan = self.env['account.analytic.plan'].sudo().search(
['|', ('company_id', '=', False), ('company_id', '=', self.env.company.id)],
limit=1)
if plan:
return plan
else:
return self.env['account.analytic.plan'].create({
'name': 'Default',
'company_id': self.env.company.id,
})
class AccountAnalyticApplicability(models.Model):
_name = 'account.analytic.applicability'
_description = "Analytic Plan's Applicabilities"
analytic_plan_id = fields.Many2one('account.analytic.plan')
business_domain = fields.Selection(
selection=[
('sale', 'Sales'),
('purchase', 'Purchase'),
('general', 'Miscellaneous'),
],
required=True,
string='Domain',
)
applicability = fields.Selection([
('optional', 'Optional'),
('mandatory', 'Mandatory'),
('unavailable', 'Unavailable'),
],
required=True,
string="Applicability",
)
def _get_score(self, **kwargs):
""" Gives the score of an applicability with the parameters of kwargs """
self.ensure_one()
if not kwargs.get('business_domain'):
return 0
else:
return 1 if kwargs.get('business_domain') == self.business_domain else -1
+6 -11
View File
@@ -16,16 +16,16 @@
<field name="domain_force">['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
</record>
<record id="analytic_group_comp_rule" model="ir.rule">
<field name="name">Analytic line multi company rule</field>
<field name="model_id" ref="model_account_analytic_group"/>
<record id="analytic_plan_comp_rule" model="ir.rule">
<field name="name">Analytic plan multi company rule</field>
<field name="model_id" ref="model_account_analytic_plan"/>
<field eval="True" name="global"/>
<field name="domain_force">['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
</record>
<record id="analytic_tag_comp_rule" model="ir.rule">
<field name="name">Analytic line multi company rule</field>
<field name="model_id" ref="model_account_analytic_tag"/>
<record id="analytic_distribution_model_comp_rule" model="ir.rule">
<field name="name">Analytic distribution model multi company rule</field>
<field name="model_id" ref="model_account_analytic_distribution_model"/>
<field eval="True" name="global"/>
<field name="domain_force">['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
</record>
@@ -37,10 +37,5 @@
<field name="category_id" ref="base.module_category_hidden"/>
</record>
<record id="group_analytic_tags" model="res.groups">
<field name="name">Analytic Accounting Tags</field>
<field name="category_id" ref="base.module_category_hidden"/>
</record>
</data>
</odoo>
+3 -3
View File
@@ -1,6 +1,6 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_account_analytic_account,access_account_analytic_account,model_account_analytic_account,group_analytic_accounting,1,1,1,1
access_account_analytic_line,access_account_analytic_line,model_account_analytic_line,group_analytic_accounting,1,1,1,1
access_account_analytic_tag,access_account_analytic_tag,model_account_analytic_tag,group_analytic_accounting,1,1,1,1
access_account_analytic_group,access_account_analytic_group,model_account_analytic_group,group_analytic_accounting,1,1,1,1
access_account_analytic_distribution,access_account_analytic_distribution,model_account_analytic_distribution,group_analytic_accounting,1,1,1,1
access_account_analytic_plan,access_account_analytic_plan,model_account_analytic_plan,group_analytic_accounting,1,1,1,1
access_account_analytic_applicability,access_account_analytic_applicability,model_account_analytic_applicability,group_analytic_accounting,1,1,1,1
access_account_analytic_distribution_model,access_account_analytic_distribution_model,model_account_analytic_distribution_model,group_analytic_accounting,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_account_analytic_account access_account_analytic_account model_account_analytic_account group_analytic_accounting 1 1 1 1
3 access_account_analytic_line access_account_analytic_line model_account_analytic_line group_analytic_accounting 1 1 1 1
4 access_account_analytic_tag access_account_analytic_plan access_account_analytic_tag access_account_analytic_plan model_account_analytic_tag model_account_analytic_plan group_analytic_accounting 1 1 1 1
5 access_account_analytic_group access_account_analytic_applicability access_account_analytic_group access_account_analytic_applicability model_account_analytic_group model_account_analytic_applicability group_analytic_accounting 1 1 1 1
6 access_account_analytic_distribution access_account_analytic_distribution_model access_account_analytic_distribution access_account_analytic_distribution_model model_account_analytic_distribution model_account_analytic_distribution_model group_analytic_accounting 1 1 1 1
@@ -0,0 +1,603 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { evaluateExpr } from "@web/core/py_js/py";
import { getNextTabableElement, getPreviousTabableElement } from "@web/core/utils/ui";
import { usePosition } from "@web/core/position_hook";
import { getActiveHotkey } from "@web/core/hotkeys/hotkey_service";
import { AnalyticAutoComplete } from "../autocomplete/autocomplete";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
import { TagsList } from "@web/views/fields/many2many_tags/tags_list";
import { useOpenMany2XRecord } from "@web/views/fields/relational_utils";
import { parseFloat as oParseFloat } from "@web/views/fields/parsers";
import { formatPercentage } from "@web/views/fields/formatters";
const { Component, useState, useRef, useExternalListener, onWillUpdateProps, onWillStart, onPatched } = owl;
export class AnalyticDistribution extends Component {
setup(){
this.orm = useService("orm");
this.state = useState({
showDropdown: false,
list: {},
});
this.widgetRef = useRef("analyticDistribution");
this.dropdownRef = useRef("analyticDropdown");
this.mainRef = useRef("mainElement");
usePosition(() => this.widgetRef.el, {
popper: "analyticDropdown",
});
this.nextId = 1;
this.focusSelector = false;
this.activeGroup = false;
onWillStart(this.willStart);
onWillUpdateProps(this.willUpdate);
onPatched(this.patched);
useExternalListener(window, "click", this.onWindowClick, true);
this.openTemplate = useOpenMany2XRecord({
resModel: "account.analytic.distribution.model",
activeActions: {
canCreate: true,
canCreateEdit: false,
canWrite: true,
},
isToMany: false,
onRecordSaved: async (record) => {
this.mainRef.el.focus();
},
onClose: () => {
this.mainRef.el.focus();
},
fieldString: this.env._t("Analytic Distribution Template"),
});
}
// Lifecycle
async willStart() {
await this.fetchAllPlans(this.props);
await this.formatData(this.props);
}
async willUpdate(nextProps) {
// Unless force_applicability, Plans need to be retrieved again as the product or account might have changed
// and thus different applicabilities apply
// or a model applies that contains unavailable plans
// This should only execute when these fields have changed, therefore we use the `_field` props.
// (consider including the plans in the computed json, python side)
const valueChanged = JSON.stringify(this.props.value) !== JSON.stringify(nextProps.value);
if (this.applicabilityParamsChanged(nextProps) || valueChanged) {
await this.fetchAllPlans(nextProps);
await this.formatData(nextProps);
}
}
applicabilityParamsChanged(nextProps) {
if (this.props.force_applicability) {
return false;
}
if (this.props.account_field && this.props.record.data[this.props.account_field] !== nextProps.record.data[this.props.account_field] ||
this.props.product_field && this.props.record.data[this.props.product_field] !== nextProps.record.data[this.props.product_field]) {
return true;
}
return false;
}
async formatData(nextProps) {
const data = nextProps.value;
const analytic_account_ids = Object.keys(data);
const records = analytic_account_ids.length ? await this.fetchAnalyticAccounts([["id", "in", analytic_account_ids]]) : [];
if (records.length < data.length) {
console.log('removing tags... value should be updated');
}
let res = Object.assign({}, ...this.allPlans.map((plan) => ({[plan.id]: {...plan, distribution: []}})));
records.map((record) => {
res[record.root_plan_id[0]].distribution.push({
analytic_account_id: record.id,
percentage: data[record.id],
id: this.nextId++,
group_id: record.root_plan_id[0],
analytic_account_name: record.name,
color: record.color,
});
});
this.state.list = res;
}
patched() {
this.focusToSelector();
}
// ORM
fetchPlansArgs(nextProps) {
let args = {};
if (this.props.business_domain_compute) {
args['business_domain'] = evaluateExpr(this.props.business_domain_compute, this.props.record.evalContext);
}
if (this.props.business_domain) {
args['business_domain'] = this.props.business_domain;
}
if (this.props.product_field && this.props.record.data[this.props.product_field]) {
args['product'] = this.props.record.data[this.props.product_field][0];
}
if (this.props.account_field && this.props.record.data[this.props.account_field]) {
args['account'] = this.props.record.data[this.props.account_field][0];
}
if (this.props.force_applicability) {
args['applicability'] = this.props.force_applicability;
}
const existing_account_ids = Object.keys(nextProps.value).map((i) => parseInt(i));
if (existing_account_ids.length) {
args['existing_account_ids'] = existing_account_ids;
}
return args;
}
async fetchAllPlans(nextProps) {
// TODO: Optimize to execute once for all records when `force_applicability` is set
const argsPlan = this.fetchPlansArgs(nextProps);
this.allPlans = await this.orm.call("account.analytic.plan", "get_relevant_plans", [], argsPlan);
}
async fetchAnalyticAccounts(domain, limit=null) {
const args = {
domain: domain,
fields: ["id", "name", "root_plan_id", "color"],
context: [],
}
if (limit) {
args['limit'] = limit;
}
return await this.orm.call("account.analytic.account", "search_read", [], args);
}
// Autocomplete
get sourcesAnalyticAccount() {
return [this.optionsSourceAnalytic];
}
get optionsSourceAnalytic() {
return {
placeholder: this.env._t("Loading..."),
options: this.loadOptionsSourceAnalytic.bind(this),
};
}
async loadOptionsSourceAnalytic(request) {
let domain = [['id', 'not in', this.existingAnalyticAccountIDs]];
if (this.activeGroup) {
domain.push(['root_plan_id', '=', this.activeGroup]);
}
const records = await this.fetchAnalyticAccounts([...domain, ["name", "ilike", request]], 7);
let options = records.map((result) => ({
value: result.id,
label: result.name,
group_id: result.root_plan_id[0],
color: result.color,
}));
if (!options.length) {
options.push({
label: this.env._t("No Analytic Accounts for this plan"),
classList: "o_m2o_no_result",
unselectable: true,
});
}
return options;
}
groupAutocompleteFocus(group_id) {
this.activeGroup = group_id;
}
autoCompleteInput(dist_tag, {inputValue}) {
if (inputValue === "") {
dist_tag.analytic_account_id = "";
}
}
// Editing Distributions
async onSelect(option, params, tag) {
const selected_option = Object.getPrototypeOf(option);
tag.analytic_account_id = parseInt(selected_option.value);
tag.analytic_account_name = selected_option.label;
tag.color = selected_option.color;
this.setFocusSelector(`.tag_${tag.id} .o_analytic_percentage`);
}
async percentageChanged(dist_tag, ev) {
dist_tag.percentage = this.parse(ev.target.value);
if (this.remainderByGroup(dist_tag.group_id)) {
this.setFocusSelector(`#plan_${dist_tag.group_id} .incomplete .o_analytic_account_name`);
}
this.autoFill();
}
deleteTag(id) {
for (const group_id in this.list) {
this.list[group_id].distribution = this.list[group_id].distribution.filter((dist_tag) => dist_tag.id != id);
if (this.list[group_id].applicability === 'mandatory' && this.remainderByGroup(group_id)) this.addLineToGroup(group_id);
}
if (!this.isDropdownOpen){
this.save();
}
}
addLineToGroup(id) {
const tag_cnt_diff = this.list[id].distribution.length - this.listReadyByGroup(id).length;
if (!tag_cnt_diff) {
this.list[id].distribution.push(this.newTag(id));
} else {
//all tags are not ready - update the value of tag without analytic account or add
const tagToUpdate = this.list[id].distribution.filter((t) => !this.tagIsReady(t) && !t.analytic_account_id)[0];
if (!!tagToUpdate) {
tagToUpdate.percentage = this.remainderByGroup(id);
} else {
this.list[id].distribution.push(this.newTag(id));
}
}
}
// Getters
get tags() {
return this.listReady.map((dist_tag) => ({
id: dist_tag.id,
text: `${dist_tag.analytic_account_name}${dist_tag.percentage > 99.99 && dist_tag.percentage < 100.01 ? "" : " " + this.formatPercentage(dist_tag.percentage)}`,
colorIndex: dist_tag.color,
group_id: dist_tag.group_id,
onClick: (ev) => this.tagClicked(ev, dist_tag.id),
onDelete: this.editingRecord ? () => this.deleteTag(dist_tag.id) : undefined
}));
}
get listForJson() {
let res = {};
this.listReady.map(({analytic_account_id, percentage}) => {
res[parseInt(analytic_account_id)] = percentage;
});
return res;
}
get firstIncompletePlanId() {
for (const group_id in this.list) {
const group_status = this.groupStatus(group_id);
if (["orange", "red"].includes(group_status)) return group_id;
}
return 0;
}
get existingAnalyticAccountIDs() {
return this.listFlat.filter((i) => !!i.analytic_account_id).map((i) => i.analytic_account_id);
}
get listReady() {
return this.listFlat.filter((dist_tag) => this.tagIsReady(dist_tag));
}
get listFlat() {
return Object.values(this.list).flatMap((g) => g.distribution);
}
get list() {
return this.state.list;
}
get sortedList() {
return Object.values(this.list).sort((a, b) => {
const aApp = a.applicability,
bApp = b.applicability;
return aApp > bApp ? 1 : aApp < bApp ? -1 : 0;
});
}
get allowSave() {
for (const group_id in this.list) {
if (['orange', 'red'].includes(this.groupStatus(group_id))) return false;
}
return this.props.allow_save;
}
get editingRecord() {
return !this.props.readonly;
}
get isDropdownOpen() {
return this.state.showDropdown && !!this.dropdownRef.el;
}
applicabilityStatus(group_id) {
const group = this.list[group_id];
const status = this.groupStatus(group_id);
let description;
switch(status){
case "gray":
description = this.env._t("Editing (OK)");
break;
case "orange": {
description = this.env._t("Editing (Incomplete)");
break;
}
case "red": {
description = this.env._t("Invalid");
break;
}
case "green": {
description = this.env._t("OK");
break;
}
}
return `${group.applicability.charAt(0).toUpperCase()}${group.applicability.slice(1)} - ${description}`;
}
groupStatus(id) {
const group = this.list[id];
const ready_tags = this.listReadyByGroup(id);
if (group.distribution.length > ready_tags.length) {
return group.applicability === 'mandatory' ? 'orange' : 'gray';
}
const sum = this.sumByGroup(id);
if (group.applicability === 'mandatory' && (sum < 99.99 || sum >= 100.01)){
return 'red';
}
return 'green';
}
listReadyByGroup(id) {
return this.list[id].distribution.filter((tag) => this.tagIsReady(tag));
}
tagIsReady({analytic_account_id, percentage}) {
return !!analytic_account_id && !!percentage;
}
sumByGroup(id) {
return this.listReadyByGroup(id).reduce((prev, next) => prev + (parseFloat(next.percentage) || 0), 0);
}
remainderByGroup(id) {
return 100 - Math.min(this.sumByGroup(id), 100);
}
// actions
newTag(group_id) {
return {
id: this.nextId++,
group_id: group_id,
analytic_account_id: null,
analytic_account_name: "",
percentage: this.remainderByGroup(group_id),
color: this.list[group_id].color,
}
}
autoFill() {
for (const group of this.allPlans.filter((p) => p.all_account_count > 0)){
if (this.remainderByGroup(group.id)) {
this.addLineToGroup(group.id);
}
}
}
cleanUp() {
for (const group_id in this.list){
this.list[group_id].distribution = this.listReadyByGroup(group_id);
}
}
validate() {
for (const group_id in this.list) {
if (this.groupStatus(group_id) === 'red') {
this.invalidate();
return false;
}
}
return true;
}
invalidate() {
this.props.record.setInvalidField(this.props.name);
}
async save() {
const currentDistribution = this.listForJson;
const dataToSave = currentDistribution;
await this.props.update(dataToSave);
this.validate();
}
onSaveNew() {
this.openTemplate({ resId: false, context: {
'default_analytic_distribution': this.listForJson,
}});
this.closeAnalyticEditor();
}
forceCloseEditor() {
// focus to the main Element but the dropdown should not open
this.preventOpen = true;
this.closeAnalyticEditor();
this.mainRef.el.focus();
this.preventOpen = false;
}
closeAnalyticEditor() {
this.cleanUp();
this.save();
this.state.showDropdown = false;
}
openAnalyticEditor() {
this.autoFill();
const incompletePlan = this.firstIncompletePlanId;
this.setFocusSelector(incompletePlan ? `#plan_${incompletePlan} .incomplete`: ".analytic_json_popup");
this.state.showDropdown = true;
}
tagClicked(ev, id) {
if (this.editingRecord && !this.isDropdownOpen) {
this.openAnalyticEditor();
}
if (this.isDropdownOpen) {
this.setFocusSelector(`.tag_${id} .o_analytic_percentage`);
this.focusToSelector();
ev.stopPropagation();
}
}
// Focus
onMainElementFocus(ev) {
if (!this.isDropdownOpen && !this.preventOpen) {
this.openAnalyticEditor();
}
}
focusToSelector() {
if (!!this.focusSelector && this.isDropdownOpen) {
this.focus(this.adjacentElementToFocus("next", this.dropdownRef.el.querySelector(this.focusSelector)));
}
this.focusSelector = false;
}
setFocusSelector(selector) {
this.focusSelector = selector;
}
adjacentElementToFocus(direction, el = null) {
if (!this.isDropdownOpen) {
return null;
}
if (!el) {
el = this.dropdownRef.el;
}
return direction == "next" ? getNextTabableElement(el) : getPreviousTabableElement(el);
}
focusAdjacent(direction) {
const elementToFocus = this.adjacentElementToFocus(direction);
if (elementToFocus){
this.focus(elementToFocus);
return true;
}
return false;
}
focus(el) {
if (!el) return;
el.focus();
if (["INPUT", "TEXTAREA"].includes(el.tagName)) {
if (el.selectionStart) {
el.selectionStart = 0;
el.selectionEnd = el.value.length;
}
el.select();
}
}
// Keys and Clicks
onWidgetKeydown(ev) {
if (!this.editingRecord) {
return;
}
const hotkey = getActiveHotkey(ev);
switch (hotkey) {
case "enter":
case "tab": {
if (this.isDropdownOpen) {
this.focusAdjacent("next") || this.forceCloseEditor();
break;
};
return;
}
case "shift+tab": {
if (this.isDropdownOpen) {
this.focusAdjacent("previous") || this.forceCloseEditor();
break;
};
return;
}
case "escape": {
if (this.isDropdownOpen) {
this.forceCloseEditor();
break;
}
}
case "arrowdown": {
if (!this.isDropdownOpen) {
this.onMainElementFocus();
break;
}
return;
}
default: {
return;
}
}
ev.preventDefault();
ev.stopPropagation();
}
onWindowClick(ev) {
if (this.isDropdownOpen && this.dropdownRef.el && !this.dropdownRef.el.contains(ev.target) && !this.widgetRef.el.contains(ev.target)) {
this.closeAnalyticEditor();
}
}
// formatters and parsers
parse(value) {
try {
return typeof value === 'string' || value instanceof String ? oParseFloat(value.replace('%', '')) : value;
} catch (_error) {
return 0;
}
}
formatPercentage(value) {
return formatPercentage(value / 100, { digits: [false, 2] });
}
}
AnalyticDistribution.template = "analytic_distribution";
AnalyticDistribution.supportedTypes = ["char", "text"];
AnalyticDistribution.components = {
AnalyticAutoComplete,
TagsList,
}
AnalyticDistribution.props = {
...standardFieldProps,
business_domain: { type: String, optional: true },
account_field: { type: String, optional: true },
product_field: { type: String, optional: true },
business_domain_compute: { type: String, optional: true },
force_applicability: { type: String, optional: true },
allow_save: { type: Boolean },
}
AnalyticDistribution.extractProps = ({ field, attrs }) => {
return {
business_domain: attrs.options.business_domain,
account_field: attrs.options.account_field,
product_field: attrs.options.product_field,
business_domain_compute: attrs.business_domain_compute,
force_applicability: attrs.options.force_applicability,
allow_save: !Boolean(attrs.options.disable_save),
};
};
export class AnalyticDistributionForm extends AnalyticDistribution {}
AnalyticDistributionForm.template = "analytic_distribution_form";
registry.category("fields").add("analytic_distribution", AnalyticDistribution);
registry.category("fields").add("form.analytic_distribution", AnalyticDistributionForm);
@@ -0,0 +1,71 @@
.analytic_distribution {
.analytic_distribution_popup {
width: 400px;
cursor: default;
.o_input {
border-width: 0;
cursor: pointer;
}
.o_analytic_account_name {
width: 320px !important;
}
.o_analytic_percentage {
width: 45px !important;
}
.o_analytic_status_red {
background-color: $o-danger;
margin-left:8px;
}
.o_analytic_status_green {
background-color: $o-success;
margin-left:8px;
}
.o_analytic_status_orange {
background-color: $o-warning;
margin-left:8px;
}
.o_analytic_status_gray {
background-color: $gray-200;
margin-left:8px;
}
tr:hover {
outline: none !important;
}
}
.tag-list {
.badge {
margin: 1px 2px 1px 0;
border: none;
.o_delete {
padding-left: 4px;
}
}
}
.hidden-focus {
// focus to this element is required but we want to hide the blue outline
&:focus {
// z-index: -10;
outline: none;
}
}
}
.o_field_analytic_distribution {
&.o_field_invalid {
.o_tag {
border: 1px solid $o-danger !important;
background-color: white;
color: $o-danger;
}
}
}
.o_analytic_distribution_cell {
&.o_invalid_cell {
border: 1px solid $o-danger !important;
}
}
@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="analytic_distribution" owl="1">
<div class="analytic_distribution" t-ref="analyticDistribution" t-on-keydown="onWidgetKeydown">
<t t-call="analytic_distribution.tags"/>
<t t-if="!props.readonly">
<div class="o_input o_input_dropdown hidden-focus" tabindex="0" t-ref="mainElement" t-on-focus="onMainElementFocus">
<a role="button" class="o_dropdown_button" draggable="false" />
<t t-call="analytic_distribution.popup"/>
</div>
</t>
</div>
</t>
<t t-name="analytic_distribution_form" owl="1">
<div class="analytic_distribution" t-ref="analyticDistribution" t-on-keydown="onWidgetKeydown">
<t t-call="analytic_distribution.tags"/>
<t t-if="!props.readonly">
<div class="o_input o_input_dropdown hidden-focus" tabindex="0" t-ref="mainElement" t-on-focus="onMainElementFocus" t-on-click="onMainElementFocus">
<input class="invisible"/>
<a role="button" class="o_dropdown_button" draggable="false" />
<t t-call="analytic_distribution.popup"/>
</div>
</t>
</div>
</t>
<t t-name="analytic_distribution.tags" owl="1">
<div class="tag-list d-inline-flex flex-wrap">
<TagsList tags="tags"/>
</div>
</t>
<t t-name="analytic_distribution.popup" owl="1">
<div class="analytic_distribution_popup o-dropdown-menu show rounded py-0" t-if="state.showDropdown" t-ref="analyticDropdown">
<div class="popover-header">
<div class="d-flex">
<div class="h5 mt-2 me-auto">Analytic</div>
<div class="popupButtons">
<span t-if="tags.length and allowSave" class="btn o_button" t-on-click="onSaveNew"><i class="fa fa-save"/></span>
<span class="o_button ms-2 cursor-pointer" t-on-click.stop="() => this.closeAnalyticEditor()"><span class="fa fa-close"/></span>
</div>
</div>
</div>
<div class="p-2">
<span t-if="!sortedList.length">No plans available</span>
<t t-foreach="sortedList" t-as="plan" t-key="plan.id">
<table class="o_list_table table table-sm table-hover o_analytic_table mb-2 table-borderless" t-attf-id="plan_{{plan.id}}">
<tr class="border-bottom">
<th class="o_analytic_account_name">
<t t-esc="plan.name"/>
<t t-if="plan.account_count === 0"> (no accounts)</t>
<span t-attf-class="o_status d-inline-block o_analytic_status_{{groupStatus(plan.id)}}" t-att-title="applicabilityStatus(plan.id) + ' ' + formatPercentage(sumByGroup(plan.id))"/>
</th>
</tr>
<t t-foreach="plan.distribution" t-as="dist_tag" t-key="dist_tag.id">
<tr t-attf-class="{{tagIsReady(dist_tag) and 'ready' or !!dist_tag.analytic_account_id and 'to_remove' or 'incomplete'}} tag_{{dist_tag.id}}">
<td class="o_analytic_account_name">
<AnalyticAutoComplete
id="dist_tag.id.toString()"
placeholder="'Search Analytic Account'"
value="dist_tag.analytic_account_name"
sources="sourcesAnalyticAccount"
autoSelect="true"
onSelect.bind="(option, params) => this.onSelect(option, params, dist_tag)"
onFocus.bind="() => this.groupAutocompleteFocus(plan.id)"
onInput.bind="(inputVal) => this.autoCompleteInput(dist_tag, inputVal)"/>
</td>
<td class="o_analytic_percentage">
<input
class="o_input"
inputmode="numeric"
type="text"
t-att-value="formatPercentage(dist_tag.percentage)"
t-on-click.stop=""
t-on-change="(ev) => this.percentageChanged(dist_tag, ev)"/>
</td>
<td>
<span class="fa fa-trash-o cursor-pointer" t-on-click.stop="() => this.deleteTag(dist_tag.id)"/>
</td>
</tr>
</t>
<tr>
<td class="o_field_x2many_list_row_add" colspan="3">
<a t-if="plan.all_account_count" href="#" role="button" tabindex="-1" t-on-click.prevent.stop="() => this.addLineToGroup(plan.id)">Add a line</a>
</td>
</tr>
</table>
</t>
<div tabindex="0" class="hidden-focus"/>
</div>
</div>
</t>
</templates>
@@ -0,0 +1,14 @@
/** @odoo-module **/
import { AutoComplete } from "@web/core/autocomplete/autocomplete";
export class AnalyticAutoComplete extends AutoComplete {}
AnalyticAutoComplete.template = "analytic.AutoComplete";
AnalyticAutoComplete.props = {
...AutoComplete.props,
onFocus: { type: Function, optional: true },
}
AnalyticAutoComplete.defaultProps = {
...AutoComplete.defaultProps,
onFocus: () => {},
}
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="analytic.AutoComplete" t-inherit="web.AutoComplete" t-inherit-mode="primary" owl="1">
<xpath expr="//input" position="attributes">
<attribute name="t-on-click"></attribute>
<attribute name="t-on-click.stop">onInputClick</attribute>
<attribute name="t-on-focus">props.onFocus</attribute>
</xpath>
</t>
</templates>
@@ -0,0 +1,164 @@
/** @odoo-module **/
import {
click,
clickEdit,
getFixture,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
let serverData;
let target;
QUnit.module("Analytic", (hooks) => {
hooks.beforeEach(() => {
target = getFixture();
serverData = {
models: {
analytic_account: {
fields: {
plan_id: { string: "Plan", type: "many2one", relation: "plan" },
root_plan_id: { string: "Root Plan", type: "many2one", relation: "plan" },
color: { string: "Color", type: "integer" },
},
records: [
{id: 1, color: 1, root_plan_id: 1, plan_id: 1, name: "RD" },
{id: 2, color: 1, root_plan_id: 1, plan_id: 1, name: "HR" },
{id: 3, color: 1, root_plan_id: 1, plan_id: 1, name: "FI" },
{id: 4, color: 2, root_plan_id: 2, plan_id: 2, name: "Time Off" },
{id: 5, color: 2, root_plan_id: 2, plan_id: 2, name: "Operating Costs" },
{id: 6, color: 6, root_plan_id: 4, plan_id: 4, name: "Incognito" },
{id: 7, color: 5, root_plan_id: 5, plan_id: 5, name: "Belgium" },
{id: 8, color: 6, root_plan_id: 5, plan_id: 6, name: "Brussels" },
{id: 9, color: 6, root_plan_id: 5, plan_id: 6, name: "Namur" },
],
},
plan: {
fields: {
applicability: {
string: "Applicability",
type: "selection",
selection: [
["mandatory", "Mandatory"],
["optional", "Options"],
["unavailable", "Unavailable"],
],
},
color: { string: "Color", type: "integer" },
all_account_count: { type: "integer" },
},
records: [
{ id: 1, name: 'Departments', applicability: "mandatory" },
{ id: 2, name: 'Internal', applicability: "optional", all_account_count: 2 },
{ id: 3, name: 'Projects', applicability: "optional" },
{ id: 4, name: 'Hidden', applicability: "unavailable" },
{ id: 5, name: "Country", applicability: "optional"},
{ id: 6, name: "City", applicability: "optional"},
],
},
aml: {
fields: {
label: { string: "Label", type: "char" },
amount: { string: "Amount", type: "float" },
analytic_distribution: { string: "Analytic", type: "char" },
move_id: { string: "Account Move", type: "many2one", relation: "move" },
},
records: [
{ id: 1, label: "Developer Time", amount: 100.00, analytic_distribution: {"1": 30.3, "3": 69.7}},
{ id: 2, label: "Coke", amount: 100.00, analytic_distribution: '{}'},
{ id: 3, label: "Sprite", amount: 100.00, analytic_distribution: '{}'},
{ id: 4, label: "", amount: 100.00, analytic_distribution: '{}'},
],
},
move: {
fields: {
line_ids: { string: "Move Lines", type: "one2many", relation: "aml", relation_field: "move_line_id" },
},
records: [
{ id: 1, display_name: "INV0001", line_ids: [1, 2]},
{ id: 2, display_name: "INV0002", line_ids: [3, 4]},
],
}
},
};
setupViewRegistries();
});
QUnit.module("AnalyticDistribution");
QUnit.test("field in form view should open and close", async function (assert) {
await makeView({
type: "form",
resModel: "aml",
resId: 1,
serverData,
arch: `
<form>
<sheet>
<group>
<field name="label"/>
<field name="analytic_distribution" widget="analytic_distribution"/>
<field name="amount"/>
</group>
</sheet>
</form>`,
mockRPC(route, { kwargs, method, model }) {
if (method === "search_read" && model === "account.analytic.plan") {
return Promise.resolve(serverData.models['plan'].records);
} else if (method === "search_read" && model === "account.analytic.account") {
if (kwargs.domain[0][0] == "id" && kwargs.domain[0][1] == "in") {
const required_ids = kwargs.domain[0][2].map((id) => parseInt(id));
const analytic_accs = serverData.models['analytic_account'].records.filter((r) => required_ids.includes(r.id));
const accs_with_plan = analytic_accs.map(
(r) => (
{
...r,
root_plan_id: Object.values(serverData.models['plan'].records.find((p) => p.id === r.root_plan_id))
}
));
return Promise.resolve(accs_with_plan);
}
return Promise.resolve(serverData.models['analytic_account'].records);
} else if (method === "get_relevant_plans" && model === "account.analytic.plan") {
return Promise.resolve(serverData.models['plan'].records);
}
},
});
assert.containsOnce(target, ".analytic_distribution", "widget should be visible");
assert.containsN(target, ".badge", 2, "should contain 2 tags");
assert.strictEqual(
target.querySelector(".badge .o_tag_badge_text").textContent,
"RD 30.3%",
"should have rendered 'RD 30.3%'"
);
assert.strictEqual(
target.querySelectorAll(".badge .o_tag_badge_text")[1].textContent,
"FI 69.7%",
"should have rendered 'FI 69.7%'"
);
await clickEdit(target);
assert.containsN(
target,
".o_delete",
2,
"tags should contain a delete button"
);
const badge1 = target.querySelector('.badge');
await click(badge1, ".o_tag_badge_text");
assert.containsN(
target,
".analytic_distribution_popup",
1,
"popup should be visible"
);
await click(target, '.fa-close');
assert.containsNone(target, '.analytic_distribution_popup', "The popup should be closed");
});
});
+3
View File
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import test_analytic_account
@@ -0,0 +1,121 @@
# -*- coding: utf-8 -*-
from odoo.tests import tagged
from odoo.tests.common import TransactionCase
@tagged('post_install', '-at_install')
class TestAnalyticAccount(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.analytic_plan_1 = cls.env['account.analytic.plan'].create({
'name': 'Plan 1',
'default_applicability': 'unavailable',
'company_id': False,
})
cls.analytic_plan_child = cls.env['account.analytic.plan'].create({
'name': 'Plan Child',
'parent_id': cls.analytic_plan_1.id,
'company_id': False,
})
cls.analytic_plan_2 = cls.env['account.analytic.plan'].create({
'name': 'Plan 2',
'company_id': False,
})
# Create new user to avoid demo data.
user = cls.env['res.users'].create({
'name': 'The anal(ytic) expert!',
'login': 'analytic',
'password': 'analytic',
'groups_id': [
(6, 0, cls.env.user.groups_id.ids),
(4, cls.env.ref('analytic.group_analytic_accounting').id),
],
})
user.partner_id.email = 'analyticman@test.com'
# Shadow the current environment/cursor with one having the report user.
# This is mandatory to test access rights.
cls.env = cls.env(user=user)
cls.cr = cls.env.cr
cls.company_data = cls.env['res.company'].create({
'name': 'company_data',
})
cls.env.user.company_ids |= cls.company_data
user.write({
'company_ids': [(6, 0, cls.company_data.ids)],
'company_id': cls.company_data.id,
})
cls.partner_a = cls.env['res.partner'].create({'name': 'partner_a', 'company_id': False})
cls.partner_b = cls.env['res.partner'].create({'name': 'partner_b', 'company_id': False})
cls.analytic_account_1 = cls.env['account.analytic.account'].create({'name': 'Account 1', 'plan_id': cls.analytic_plan_1.id})
cls.analytic_account_2 = cls.env['account.analytic.account'].create({'name': 'Account 2', 'plan_id': cls.analytic_plan_child.id})
cls.analytic_account_3 = cls.env['account.analytic.account'].create({'name': 'Account 3', 'plan_id': cls.analytic_plan_2.id})
cls.distribution_1 = cls.env['account.analytic.distribution.model'].create({
'partner_id': cls.partner_a.id,
'analytic_distribution': {cls.analytic_account_3.id: 100}
})
cls.distribution_2 = cls.env['account.analytic.distribution.model'].create({
'partner_id': cls.partner_b.id,
'analytic_distribution': {cls.analytic_account_2.id: 100}
})
def test_get_plans_without_options(self):
""" Test that the plans with the good appliability are returned without if no options are given """
kwargs = {}
plans_json = self.env['account.analytic.plan'].get_relevant_plans(**kwargs)
self.assertEqual(1, len(plans_json), "Only the Default plan should be available")
self.analytic_plan_1.write({'default_applicability': 'mandatory'})
plans_json = self.env['account.analytic.plan'].get_relevant_plans(**kwargs)
self.assertEqual(2, len(plans_json), "All root plans should be available")
def test_get_plans_with_option(self):
""" Test the plans returned with applicability rules and options """
kwargs = {'business_domain': 'sale'}
plans_json = self.env['account.analytic.plan'].get_relevant_plans(**kwargs)
self.assertEqual(1, len(plans_json), "Only the Default plan should be available")
applicability = self.env['account.analytic.applicability'].create({
'business_domain': 'sale',
'analytic_plan_id': self.analytic_plan_1.id,
'applicability': 'mandatory'
})
plans_json = self.env['account.analytic.plan'].get_relevant_plans(**kwargs)
self.assertEqual(2, len(plans_json), "All root plans should be available")
self.analytic_plan_1.write({'default_applicability': 'mandatory'})
applicability.write({'applicability': 'unavailable'})
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'}
plans_json = self.env['account.analytic.plan'].get_relevant_plans(**kwargs)
self.assertEqual(2, len(plans_json), "Both plans should be available")
kwargs = {'applicability': 'optional'}
plans_json = self.env['account.analytic.plan'].get_relevant_plans(**kwargs)
self.assertEqual(2, len(plans_json), "All root plans should be available")
def test_analytic_distribution_model(self):
""" Test the distribution returned from the distribution model """
distribution_json = self.env['account.analytic.distribution.model']._get_distribution({})
self.assertEqual(distribution_json, {}, "No distribution should be given")
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")
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")
@@ -1,176 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="account_analytic_tag_tree_view" model="ir.ui.view">
<field name="name">account.analytic.tag.tree</field>
<field name="model">account.analytic.tag</field>
<field name="arch" type="xml">
<tree string="Analytic Tags">
<field name="name"/>
<field name="display_name" invisible="1"/>
<field name="company_id" groups="base.group_multi_company"/>
</tree>
</field>
</record>
<record id="account_analytic_tag_form_view" model="ir.ui.view">
<field name="name">account.analytic.tag.form</field>
<field name="model">account.analytic.tag</field>
<field name="arch" type="xml">
<form string="Analytic Tags">
<sheet>
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
<group>
<field name="active" invisible="1"/>
<field name="name"/>
<field name="active_analytic_distribution" groups="analytic.group_analytic_accounting"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
<field name="analytic_distribution_ids" nolabel="1" widget="one2many"
attrs="{'invisible': [('active_analytic_distribution', '=', False)]}" groups="analytic.group_analytic_accounting">
<tree string="Analytic Distribution" editable="bottom">
<field name="account_id"/>
<field name="percentage"/>
</tree>
</field>
</sheet>
</form>
</field>
</record>
<record id="account_analytic_tag_view_search" model="ir.ui.view">
<field name="name">account.analytic.tag.view.search</field>
<field name="model">account.analytic.tag</field>
<field name="arch" type="xml">
<search string="Search Analytic Tags">
<field name="name" />
<separator/>
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
</search>
</field>
</record>
<record id="account_analytic_tag_action" model="ir.actions.act_window">
<field name="name">Analytic Tags</field>
<field name="res_model">account.analytic.tag</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Add a new tag
</p>
</field>
</record>
<record id="account_analytic_group_form_view" model="ir.ui.view">
<field name="name">account.analytic.group.form</field>
<field name="model">account.analytic.group</field>
<field name="arch" type="xml">
<form string="Analytic Account Groups">
<field name="company_id" invisible="1"/>
<sheet>
<group>
<field name="name"/>
<field name="parent_id"/>
</group>
<group>
<field name="description"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="account_analytic_group_tree_view" model="ir.ui.view">
<field name="name">account.analytic.group.tree</field>
<field name="model">account.analytic.group</field>
<field name="arch" type="xml">
<tree string="Analytic Account Groups">
<field name="name"/>
<field name="company_id" groups="base.group_multi_company"/>
</tree>
</field>
</record>
<record id="account_analytic_group_action" model="ir.actions.act_window">
<field name="name">Analytic Account Groups</field>
<field name="res_model">account.analytic.group</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Click to add a new analytic account group.
</p>
<p>
This allows you to classify your analytic accounts.
</p>
</field>
</record>
<record id="view_account_analytic_line_tree" model="ir.ui.view">
<field name="name">account.analytic.line.tree</field>
<field name="model">account.analytic.line</field>
<field name="arch" type="xml">
<tree string="Analytic Entries">
<field name="date" optional="show"/>
<field name="name"/>
<field name="account_id"/>
<field name="currency_id" invisible="1"/>
<field name="unit_amount" sum="Quantity" optional="hide"/>
<field name="product_uom_id" optional="hide"/>
<field name="partner_id" optional="hide"/>
<field name="company_id" groups="base.group_multi_company" optional="show"/>
<field name="amount" sum="Total" optional="show"/>
<field name="tag_ids" optional="hide" widget="many2many_tags"/>
</tree>
</field>
</record>
<record id="view_account_analytic_line_filter" model="ir.ui.view">
<field name="name">account.analytic.line.select</field>
<field name="model">account.analytic.line</field>
<field name="arch" type="xml">
<search string="Search Analytic Lines">
<field name="name"/>
<field name="date"/>
<field name="account_id"/>
<field name="tag_ids"/>
<filter string="Date" name="date" date="date"/>
<group string="Group By..." expand="0" name="groupby">
<filter string="Analytic Account" name="account_id" context="{'group_by': 'account_id'}"/>
<filter string="Date" name="group_date" context="{'group_by': 'date'}"/>
<filter string="Category" name='category' context="{'group_by': 'category'}"/>
</group>
</search>
</field>
</record>
<record model="ir.actions.act_window" id="account_analytic_line_action">
<field name="context">{'search_default_group_date': 1, 'default_account_id': active_id}</field>
<field name="domain">[('account_id','=', active_id)]</field>
<field name="name">Gross Margin</field>
<field name="res_model">account.analytic.line</field>
<field name="view_mode">tree,form,graph,pivot</field>
<field name="view_id" ref="view_account_analytic_line_tree"/>
<field name="search_view_id" ref="view_account_analytic_line_filter"/>
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">
No activity yet on this account
</p><p>
In Odoo, sales orders and projects are implemented using
analytic accounts. You can track costs and revenues to analyse
your margins easily.
</p><p>
Costs will be created automatically when you register supplier
invoices, expenses or timesheets.
</p><p>
Revenues will be created automatically when you create customer
invoices. Customer invoices can be created based on sales orders
(fixed price invoices), on timesheets (based on the work done) or
on expenses (e.g. reinvoicing of travel costs).
</p>
</field>
</record>
<record id="view_account_analytic_account_form" model="ir.ui.view">
<field name="name">analytic.analytic.account.form</field>
<field name="model">account.analytic.account</field>
@@ -203,7 +32,7 @@
<field name="code"/>
</group>
<group>
<field name="group_id"/>
<field name="plan_id" options="{'no_quick_create': True}"/>
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
<field name="currency_id" options="{'no_create': True}" groups="base.group_multi_currency"/>
</group>
@@ -222,10 +51,11 @@
<field name="model">account.analytic.account</field>
<field eval="8" name="priority"/>
<field name="arch" type="xml">
<tree string="Analytic Accounts">
<tree string="Analytic Accounts" multi_edit="1">
<field name="name" string="Name"/>
<field name="code"/>
<field name="partner_id"/>
<field name="plan_id"/>
<field name="active" invisible="1"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="debit" sum="Debit"/>
@@ -307,120 +137,4 @@
</p>
</field>
</record>
<record id="view_account_analytic_line_form" model="ir.ui.view">
<field name="name">account.analytic.line.form</field>
<field name="model">account.analytic.line</field>
<field name="priority">1</field>
<field name="arch" type="xml">
<form string="Analytic Entry">
<field name="company_id" invisible="1"/>
<sheet>
<group>
<group name="analytic_entry" string="Analytic Entry">
<field name="name"/>
<field name="account_id"/>
<field name="tag_ids" widget="many2many_tags"/>
<field name="date"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
<group name="amount" string="Amount">
<field name="amount"/>
<field name="unit_amount"/>
<field name="product_uom_category_id" invisible="1"/>
<field name="product_uom_id" class="oe_inline"/>
<field name="currency_id" invisible="1"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_account_analytic_line_graph" model="ir.ui.view">
<field name="name">account.analytic.line.graph</field>
<field name="model">account.analytic.line</field>
<field name="arch" type="xml">
<graph string="Analytic Entries" sample="1">
<field name="account_id"/>
<field name="unit_amount" type="measure"/>
<field name="amount" type="measure"/>
</graph>
</field>
</record>
<record id="view_account_analytic_line_pivot" model="ir.ui.view">
<field name="name">account.analytic.line.pivot</field>
<field name="model">account.analytic.line</field>
<field name="arch" type="xml">
<pivot string="Analytic Entries" sample="1">
<field name="account_id" type="row"/>
<field name="unit_amount" type="measure"/>
<field name="amount" type="measure"/>
</pivot>
</field>
</record>
<record id="view_account_analytic_line_kanban" model="ir.ui.view">
<field name="name">account.analytic.line.kanban</field>
<field name="model">account.analytic.line</field>
<field name="arch" type="xml">
<kanban class="o_kanban_mobile">
<field name="date"/>
<field name="name"/>
<field name="account_id"/>
<field name="currency_id"/>
<field name="amount"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_card oe_kanban_global_click">
<div class="row">
<div class="col-6">
<strong><span><t t-esc="record.name.value"/></span></strong>
</div>
<div class="col-6 text-end">
<strong><t t-esc="record.date.value"/></strong>
</div>
</div>
<div class="row">
<div class="col-6 text-muted">
<span><t t-esc="record.account_id.value"/></span>
</div>
<div class="col-6">
<span class="float-end text-end">
<field name="amount" widget="monetary"/>
</span>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record model="ir.actions.act_window" id="account_analytic_line_action_entries">
<field name="name">Analytic Items</field>
<field name="res_model">account.analytic.line</field>
<field name="view_mode">tree,kanban,form,graph,pivot</field>
<field name="view_id" ref="view_account_analytic_line_tree"/>
<field name="search_view_id" ref="analytic.view_account_analytic_line_filter"/>
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">
No activity yet
</p><p>
In Odoo, sales orders and projects are implemented using
analytic accounts. You can track costs and revenues to analyse
your margins easily.
</p><p>
Costs will be created automatically when you register supplier
invoices, expenses or timesheets.
</p><p>
Revenues will be created automatically when you create customer
invoices. Customer invoices can be created based on sales orders
(fixed price invoices), on timesheets (based on the work done) or
on expenses (e.g. reinvoicing of travel costs).
</p>
</field>
</record>
</odoo>
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="account_analytic_distribution_model_tree_view" model="ir.ui.view">
<field name="name">account.analytic.distribution.model.tree</field>
<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="analytic_distribution" widget="analytic_distribution" nolabel="1"/>
<field name="partner_id"/>
<field name="partner_category_id"/>
<field name="company_id" groups="base.group_multi_company"/>
</tree>
</field>
</record>
<record id="account_analytic_distribution_model_form_view" model="ir.ui.view">
<field name="name">account.analytic.distribution.model.form</field>
<field name="model">account.analytic.distribution.model</field>
<field name="arch" type="xml">
<form string="Analytic Distribution Model">
<sheet>
<group>
<group>
<field name="partner_id"/>
<field name="partner_category_id"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
<group>
<field name="analytic_distribution_stored_char" invisible="1"/>
<field name="analytic_distribution" string="Analytic Distribution" widget="analytic_distribution"
options="{'force_applicability': 'optional', 'disable_save': true}"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_analytic_distribution_model" model="ir.actions.act_window">
<field name="name">Analytic Distribution Models</field>
<field name="res_model">account.analytic.distribution.model</field>
<field name="view_mode">tree,form</field>
</record>
</odoo>
@@ -0,0 +1,182 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_account_analytic_line_tree" model="ir.ui.view">
<field name="name">account.analytic.line.tree</field>
<field name="model">account.analytic.line</field>
<field name="arch" type="xml">
<tree string="Analytic Items" multi_edit="1">
<field name="date" optional="show"/>
<field name="name"/>
<field name="account_id"/>
<field name="plan_id"/>
<field name="currency_id" invisible="1"/>
<field name="unit_amount" sum="Quantity" optional="hide"/>
<field name="product_uom_id" optional="hide"/>
<field name="partner_id" optional="hide"/>
<field name="company_id" groups="base.group_multi_company" optional="show"/>
<field name="amount" sum="Total" optional="show"/>
</tree>
</field>
</record>
<record id="view_account_analytic_line_filter" model="ir.ui.view">
<field name="name">account.analytic.line.select</field>
<field name="model">account.analytic.line</field>
<field name="arch" type="xml">
<search string="Search Analytic Lines">
<field name="name"/>
<field name="date"/>
<field name="account_id"/>
<field name="plan_id"/>
<filter string="Date" name="date" date="date"/>
<filter string="Analytic Account" name="group_by_analytic_account" context="{'group_by': 'account_id'}"/>
<filter string="Analytic Plan" name="group_by_analytic_plan" context="{'group_by': 'plan_id'}"/>
<group string="Group By..." expand="0" name="groupby">
<filter string="Date" name="group_date" context="{'group_by': 'date'}"/>
</group>
</search>
</field>
</record>
<record model="ir.actions.act_window" id="account_analytic_line_action">
<field name="context">{'search_default_group_date': 1, 'default_account_id': active_id}</field>
<field name="domain">[('account_id','=', active_id)]</field>
<field name="name">Gross Margin</field>
<field name="res_model">account.analytic.line</field>
<field name="view_mode">tree,form,graph,pivot</field>
<field name="view_id" ref="view_account_analytic_line_tree"/>
<field name="search_view_id" ref="view_account_analytic_line_filter"/>
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">
No activity yet on this account
</p><p>
In Odoo, sales orders and projects are implemented using
analytic accounts. You can track costs and revenues to analyse
your margins easily.
</p><p>
Costs will be created automatically when you register supplier
invoices, expenses or timesheets.
</p><p>
Revenues will be created automatically when you create customer
invoices. Customer invoices can be created based on sales orders
(fixed price invoices), on timesheets (based on the work done) or
on expenses (e.g. reinvoicing of travel costs).
</p>
</field>
</record>
<record id="view_account_analytic_line_form" model="ir.ui.view">
<field name="name">account.analytic.line.form</field>
<field name="model">account.analytic.line</field>
<field name="priority">1</field>
<field name="arch" type="xml">
<form string="Analytic Item">
<field name="company_id" invisible="1"/>
<sheet>
<group>
<group name="analytic_item" string="Analytic Item">
<field name="name"/>
<field name="account_id"/>
<field name="date"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
<group name="amount" string="Amount">
<field name="amount"/>
<field name="unit_amount"/>
<field name="product_uom_category_id" invisible="1"/>
<field name="product_uom_id" class="oe_inline"/>
<field name="currency_id" invisible="1"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_account_analytic_line_graph" model="ir.ui.view">
<field name="name">account.analytic.line.graph</field>
<field name="model">account.analytic.line</field>
<field name="arch" type="xml">
<graph string="Analytic Items" sample="1">
<field name="account_id"/>
<field name="unit_amount" type="measure"/>
<field name="amount" type="measure"/>
</graph>
</field>
</record>
<record id="view_account_analytic_line_pivot" model="ir.ui.view">
<field name="name">account.analytic.line.pivot</field>
<field name="model">account.analytic.line</field>
<field name="arch" type="xml">
<pivot string="Analytic Items" sample="1">
<field name="account_id" type="row"/>
<field name="unit_amount" type="measure"/>
<field name="amount" type="measure"/>
</pivot>
</field>
</record>
<record id="view_account_analytic_line_kanban" model="ir.ui.view">
<field name="name">account.analytic.line.kanban</field>
<field name="model">account.analytic.line</field>
<field name="arch" type="xml">
<kanban class="o_kanban_mobile">
<field name="date"/>
<field name="name"/>
<field name="account_id"/>
<field name="currency_id"/>
<field name="amount"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_card oe_kanban_global_click">
<div class="row">
<div class="col-6">
<strong><span><t t-esc="record.name.value"/></span></strong>
</div>
<div class="col-6 text-end">
<strong><t t-esc="record.date.value"/></strong>
</div>
</div>
<div class="row">
<div class="col-6 text-muted">
<span><t t-esc="record.account_id.value"/></span>
</div>
<div class="col-6">
<span class="float-end text-end">
<field name="amount" widget="monetary"/>
</span>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record model="ir.actions.act_window" id="account_analytic_line_action_entries">
<field name="name">Analytic Items</field>
<field name="res_model">account.analytic.line</field>
<field name="view_mode">tree,kanban,form,graph,pivot</field>
<field name="view_id" ref="view_account_analytic_line_tree"/>
<field name="search_view_id" ref="analytic.view_account_analytic_line_filter"/>
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">
No activity yet
</p><p>
In Odoo, sales orders and projects are implemented using
analytic accounts. You can track costs and revenues to analyse
your margins easily.
</p><p>
Costs will be created automatically when you register supplier
invoices, expenses or timesheets.
</p><p>
Revenues will be created automatically when you create customer
invoices. Customer invoices can be created based on sales orders
(fixed price invoices), on timesheets (based on the work done) or
on expenses (e.g. reinvoicing of travel costs).
</p>
</field>
</record>
</odoo>
@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="account_analytic_plan_form_view" model="ir.ui.view">
<field name="name">account.analytic.plan.form</field>
<field name="model">account.analytic.plan</field>
<field name="arch" type="xml">
<form string="Analytic Plans">
<field name="company_id" invisible="1"/>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="action_view_children_plans" type="object" class="oe_stat_button" icon="fa-bars">
<field string="Subplans" name="children_count" widget="statinfo"/>
</button>
<button name="action_view_analytical_accounts" type="object" class="oe_stat_button" icon="fa-bars">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value"><field name="all_account_count"/></span>
<span class="o_stat_text">Analytic Accounts</span>
</div>
</button>
</div>
<div class="oe_title">
<h1>
<field name="name"/>
</h1>
</div>
<group>
<group>
<field name="parent_id"/>
<field name="default_applicability"
attrs="{'invisible': [('parent_id', '!=', False)]}"/>
<field name="color" widget="color_picker"/>
</group>
<group>
<field name="company_id" groups="base.group_multi_company"/>
</group>
</group>
<notebook>
<page string="Applicability" name="applicability"
attrs="{'invisible': [('parent_id', '!=', False)]}">
<field name="applicability_ids">
<tree editable="bottom">
<field name="business_domain"/>
<field name="applicability"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="account_analytic_plan_tree_view" model="ir.ui.view">
<field name="name">account.analytic.plan.tree</field>
<field name="model">account.analytic.plan</field>
<field name="arch" type="xml">
<tree string="Analytic Plans" multi_edit="True">
<field name="name"/>
<field name="default_applicability"/>
<field name="color" widget="color_picker"/>
<field name="company_id" groups="base.group_multi_company"/>
</tree>
</field>
</record>
<record id="account_analytic_plan_action" model="ir.actions.act_window">
<field name="name">Analytic Plans</field>
<field name="res_model">account.analytic.plan</field>
<field name="view_mode">tree,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree'}),
(0, 0, {'view_mode': 'form', 'view_id': ref('account_analytic_plan_form_view')})]"/>
<field name="domain">[('parent_id', '=', False)]</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Click to add a new analytic account plan.
</p>
</field>
</record>
</odoo>
+14 -15
View File
@@ -34,11 +34,10 @@
</record>
<!-- ++++++++++++++ Expense sheet for Admin ++++++++++++++-->
<record id="screen_expense" model="hr.expense">
<field name="name">Screen</field>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="analytic_account_id" ref="analytic.analytic_our_super_product"/>
<field name="analytic_distribution" eval="{ref('analytic.analytic_our_super_product'): 100}"/>
<field name="product_id" ref="product_product_no_cost"/>
<field eval="0.0" name="unit_amount"/>
<field eval="289.0" name="total_amount"/>
@@ -48,7 +47,7 @@
<record id="laptop_expense" model="hr.expense">
<field name="name">Laptop</field>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="analytic_account_id" ref="analytic.analytic_our_super_product"/>
<field name="analytic_distribution" eval="{ref('analytic.analytic_our_super_product'): 100}"/>
<field name="product_id" ref="product_product_no_cost"/>
<field eval="0.0" name="unit_amount"/>
<field eval="889.0" name="total_amount"/>
@@ -81,7 +80,7 @@
<record id="travel_by_air_expense" model="hr.expense">
<field name="name">Travel by Air</field>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="analytic_account_id" ref="analytic.analytic_our_super_product"/>
<field name="analytic_distribution" eval="{ref('analytic.analytic_our_super_product'): 100}"/>
<field name="product_id" ref="expense_product_travel_accommodation"/>
<field eval="700.0" name="unit_amount"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
@@ -93,7 +92,7 @@
<record id="hotel_bill_expense" model="hr.expense">
<field name="name">Hotel Expenses</field>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="analytic_distribution" eval="{ref('analytic.analytic_nebula'): 100}"/>
<field name="product_id" ref="expense_product_travel_accommodation"/>
<field eval="400.0" name="unit_amount"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
@@ -105,7 +104,7 @@
<record id="lunch_customer_bill_expense" model="hr.expense">
<field name="name">Lunch with Customer</field>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="analytic_distribution" eval="{ref('analytic.analytic_nebula'): 100}"/>
<field name="product_id" ref="expense_product_meal"/>
<field eval="152.8" name="unit_amount"/>
<field name="date" eval="time.strftime('%Y-%m')+'-13'"/>
@@ -116,7 +115,7 @@
<record id="lunch_bill_expense" model="hr.expense">
<field name="name">Lunch</field>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="analytic_distribution" eval="{ref('analytic.analytic_nebula'): 100}"/>
<field name="product_id" ref="expense_product_meal"/>
<field name="date" eval="time.strftime('%Y-%m')+'-15'"/>
<field eval="56.8" name="unit_amount"/>
@@ -134,7 +133,7 @@
<record id="travel_demo_by_car_expense" model="hr.expense">
<field name="name">Travel by Car</field>
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="analytic_account_id" ref="analytic.analytic_our_super_product"/>
<field name="analytic_distribution" eval="{ref('analytic.analytic_our_super_product'): 100}"/>
<field name="product_id" ref="expense_product_mileage"/>
<field eval="0.52" name="unit_amount"/>
<field name="product_uom_id" ref="uom.product_uom_km"/>
@@ -146,7 +145,7 @@
<record id="lunch_demo_customer_bill_expense" model="hr.expense">
<field name="name">Lunch with Customer</field>
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="analytic_distribution" eval="{ref('analytic.analytic_nebula'): 100}"/>
<field name="product_id" ref="product_product_no_cost"/>
<field name="date" eval="time.strftime('%Y')+'-01-15'"/>
<field eval="0.0" name="unit_amount"/>
@@ -164,7 +163,7 @@
<record id="pizzas_bill_expense" model="hr.expense">
<field name="name">Pizzas</field>
<field name="employee_id" ref="hr.employee_fme"/>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="analytic_distribution" eval="{ref('analytic.analytic_nebula'): 100}"/>
<field name="product_id" ref="expense_product_meal"/>
<field name="date" eval="time.strftime('%Y-%m')+'-05'"/>
<field eval="12.5" name="unit_amount"/>
@@ -175,7 +174,7 @@
<record id="drinks_bill_expense" model="hr.expense">
<field name="name">Drinks</field>
<field name="employee_id" ref="hr.employee_fme"/>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="analytic_distribution" eval="{ref('analytic.analytic_nebula'): 100}"/>
<field name="product_id" ref="expense_product_meal"/>
<field name="date" eval="time.strftime('%Y-%m')+'-05'"/>
<field eval="2.5" name="unit_amount"/>
@@ -186,7 +185,7 @@
<record id="paintball_bill_expense" model="hr.expense">
<field name="name">Paintball</field>
<field name="employee_id" ref="hr.employee_fme"/>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="analytic_distribution" eval="{ref('analytic.analytic_nebula'): 100}"/>
<field name="product_id" ref="product_product_no_cost"/>
<field name="date" eval="time.strftime('%Y-%m')+'-05'"/>
<field eval="0.0" name="unit_amount"/>
@@ -204,7 +203,7 @@
<record id="chair_bill_expense" model="hr.expense">
<field name="name">Chairs</field>
<field name="employee_id" ref="hr.employee_al"/>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="analytic_distribution" eval="{ref('analytic.analytic_nebula'): 100}"/>
<field name="product_id" ref="product_product_no_cost"/>
<field name="date" eval="time.strftime('%Y')+'-06-02'"/>
<field eval="0.0" name="unit_amount"/>
@@ -215,7 +214,7 @@
<record id="lamp_bill_expense" model="hr.expense">
<field name="name">Lamp</field>
<field name="employee_id" ref="hr.employee_al"/>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="analytic_distribution" eval="{ref('analytic.analytic_nebula'): 100}"/>
<field name="product_id" ref="product_product_no_cost"/>
<field name="date" eval="time.strftime('%Y')+'-06-02'"/>
<field eval="0.0" name="unit_amount"/>
@@ -227,7 +226,7 @@
<record id="afterwork_bill_expense" model="hr.expense">
<field name="name">Car tyres</field>
<field name="employee_id" ref="hr.employee_stw"/>
<field name="analytic_account_id" ref="analytic.analytic_nebula"/>
<field name="analytic_distribution" eval="{ref('analytic.analytic_nebula'): 100}"/>
<field name="product_id" ref="product_product_no_cost"/>
<field name="date" eval="time.strftime('%Y')+'-03-15'"/>
<field eval="0.0" name="unit_amount"/>
+15 -18
View File
@@ -13,7 +13,7 @@ from odoo.tools.misc import clean_context, format_date
class HrExpense(models.Model):
_name = "hr.expense"
_inherit = ['mail.thread', 'mail.activity.mixin']
_inherit = ['mail.thread', 'mail.activity.mixin', 'analytic.mixin']
_description = "Expense"
_order = "date desc, id desc"
_check_company_auto = True
@@ -78,8 +78,6 @@ class HrExpense(models.Model):
company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, default=lambda self: self.env.company)
currency_id = fields.Many2one('res.currency', string='Currency', required=True, readonly=False, store=True, states={'reported': [('readonly', True)], 'approved': [('readonly', True)], 'done': [('readonly', True)]}, compute='_compute_currency_id', default=lambda self: self.env.company.currency_id)
currency_rate = fields.Float(compute='_compute_currency_rate')
analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account', check_company=True)
analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags', states={'post': [('readonly', True)], 'done': [('readonly', True)]}, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
account_id = fields.Many2one('account.account', compute='_compute_from_product_id_company_id', store=True, readonly=False, precompute=True, string='Account',
domain="[('account_type', 'not in', ('asset_receivable','liability_payable','asset_cash','liability_credit_card')), ('company_id', '=', company_id)]", help="An expense account is expected")
description = fields.Text('Internal Notes', readonly=True, states={'draft': [('readonly', False)], 'reported': [('readonly', False)], 'refused': [('readonly', False)]})
@@ -308,17 +306,17 @@ class HrExpense(models.Model):
exp.duplicate_expense_ids = [(6, 0, ids)]
expenses = expenses - exp
# sgv todo replace with depends?
@api.onchange('product_id', 'date', 'account_id')
def _onchange_product_id_date_account_id(self):
rec = self.env['account.analytic.default'].sudo().account_get(
product_id=self.product_id.id,
account_id=self.account_id.id,
company_id=self.company_id.id,
date=self.date
)
self.analytic_account_id = self.analytic_account_id or rec.analytic_id.id
self.analytic_tag_ids = self.analytic_tag_ids or rec.analytic_tag_ids.ids
@api.depends('product_id', 'account_id')
def _compute_analytic_distribution_stored_char(self):
for expense in self:
distribution = self.env['account.analytic.distribution.model']._get_distributionjson({
'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()
@api.constrains('payment_mode')
def _check_payment_mode(self):
@@ -385,7 +383,7 @@ class HrExpense(models.Model):
raise UserError(_('You cannot delete a posted or approved expense.'))
def write(self, vals):
if 'tax_ids' in vals or 'analytic_account_id' in vals or 'account_id' in vals:
if 'tax_ids' in vals or 'analytic_distribution' in vals or 'account_id' in vals:
if any(not expense.is_editable for expense in self):
raise UserError(_('You are not authorized to edit this expense report.'))
if 'reference' in vals:
@@ -537,7 +535,7 @@ Or send your receipts at <a href="mailto:%(email)s?subject=Lunch%%20with%%20cust
'tax_ids': self.tax_ids.ids,
'currency_id': self.currency_id.id,
'company_id': self.company_id.id,
'analytic_account_id': self.analytic_account_id.id,
'analytic_distribution': self.analytic_distribution,
'employee_id': self.employee_id.id,
'expense_id': self.id,
} for price in [price_round_up, price_round_down]]
@@ -607,8 +605,7 @@ Or send your receipts at <a href="mailto:%(email)s?subject=Lunch%%20with%%20cust
'price_unit': expense.total_amount,
'product_id': expense.product_id.id,
'product_uom_id': expense.product_uom_id.id,
'analytic_account_id': expense.analytic_account_id.id,
'analytic_tag_ids': [(6, 0, expense.analytic_tag_ids.ids)],
'analytic_distribution': expense.analytic_distribution,
'expense_id': expense.id,
'partner_id': expense.employee_id.sudo().address_home_id.commercial_partner_id.id,
'tax_ids': [(6, 0, expense.tax_ids.ids)],
@@ -49,7 +49,11 @@
<td>
<span t-field="line.name"/>
<span t-field="line.description"/><br/>
<span t-field="line.analytic_account_id.name"/>
<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)"/>
</div>
</span>
</td>
<td style="text-center">
<span t-field="line.reference"/>
+3
View File
@@ -43,11 +43,14 @@ class TestExpenseCommon(AccountTestInvoicingCommon):
cls.env.user.groups_id |= group_expense_manager
# Create analytic account
cls.analytic_plan = cls.env['account.analytic.plan'].create({'name': 'Plan Test', 'company_id': False})
cls.analytic_account_1 = cls.env['account.analytic.account'].create({
'name': 'analytic_account_1',
'plan_id': cls.analytic_plan.id,
})
cls.analytic_account_2 = cls.env['account.analytic.account'].create({
'name': 'analytic_account_2',
'plan_id': cls.analytic_plan.id,
})
# Ensure products can be expensed.
+10 -12
View File
@@ -128,7 +128,7 @@ class TestExpenses(TestExpenseCommon):
'product_id': self.product_a.id,
'unit_amount': 1000.0,
'tax_ids': [(6, 0, self.company_data['default_tax_purchase'].ids)],
'analytic_account_id': self.analytic_account_1.id,
'analytic_distribution': {self.analytic_account_1.id: 100},
'employee_id': self.expense_employee.id,
}),
(0, 0, {
@@ -138,7 +138,7 @@ class TestExpenses(TestExpenseCommon):
'product_id': self.product_b.id,
'unit_amount': 1500.0,
'tax_ids': [(6, 0, self.company_data['default_tax_purchase'].ids)],
'analytic_account_id': self.analytic_account_2.id,
'analytic_distribution': {self.analytic_account_2.id: 100},
'currency_id': self.currency_data['currency'].id,
'employee_id': self.expense_employee.id,
}),
@@ -162,7 +162,7 @@ class TestExpenses(TestExpenseCommon):
'product_id': False,
'currency_id': self.company_data['currency'].id,
'tax_line_id': False,
'analytic_account_id': False,
'analytic_distribution': False,
},
# Receivable line (foreign currency):
{
@@ -173,7 +173,7 @@ class TestExpenses(TestExpenseCommon):
'product_id': False,
'currency_id': self.currency_data['currency'].id,
'tax_line_id': False,
'analytic_account_id': False,
'analytic_distribution': False,
},
# Tax line (foreign currency):
{
@@ -184,7 +184,7 @@ class TestExpenses(TestExpenseCommon):
'product_id': False,
'currency_id': self.currency_data['currency'].id,
'tax_line_id': self.company_data['default_tax_purchase'].id,
'analytic_account_id': False,
'analytic_distribution': False,
},
# Tax line (company currency):
{
@@ -195,7 +195,7 @@ class TestExpenses(TestExpenseCommon):
'product_id': False,
'currency_id': self.company_data['currency'].id,
'tax_line_id': self.company_data['default_tax_purchase'].id,
'analytic_account_id': False,
'analytic_distribution': False,
},
# Product line (foreign currency):
{
@@ -206,7 +206,7 @@ class TestExpenses(TestExpenseCommon):
'product_id': self.product_b.id,
'currency_id': self.currency_data['currency'].id,
'tax_line_id': False,
'analytic_account_id': self.analytic_account_2.id,
'analytic_distribution': {self.analytic_account_2.id: 100},
},
# Product line (company currency):
{
@@ -217,7 +217,7 @@ class TestExpenses(TestExpenseCommon):
'product_id': self.product_a.id,
'currency_id': self.company_data['currency'].id,
'tax_line_id': False,
'analytic_account_id': self.analytic_account_1.id,
'analytic_distribution': {self.analytic_account_1.id: 100},
},
])
@@ -258,7 +258,7 @@ class TestExpenses(TestExpenseCommon):
'unit_amount': 700.00,
'tax_ids': [(6, 0, tax.ids)],
'sheet_id': expense.id,
'analytic_account_id': self.analytic_account_1.id,
'analytic_distribution': {self.analytic_account_1.id: 100},
'currency_id': self.currency_data['currency'].id,
})
@@ -273,7 +273,6 @@ class TestExpenses(TestExpenseCommon):
# Create Expense Entries
expense.action_sheet_move_create()
self.assertEqual(expense.state, 'post', 'Expense is not in Waiting Payment state')
# Should get this result [(0.0, 350.0, -700.0), (318.18, 0.0, 636.36), (31.82, 0.0, 63.64)]
analytic_line = expense.account_move_id.line_ids.analytic_line_ids
self.assertEqual(len(analytic_line), 1)
@@ -327,9 +326,8 @@ class TestExpenses(TestExpenseCommon):
'unit_amount': 350.00,
'tax_ids': [(6, 0, [self.tax_purchase_a.id])],
'sheet_id': expense.id,
'analytic_account_id': self.analytic_account_1.id,
'analytic_distribution': {self.analytic_account_1.id: 100},
})
expense_line._onchange_product_id_date_account_id()
expense.action_submit_sheet()
expense.approve_expense_sheets()
+17 -8
View File
@@ -56,8 +56,10 @@
<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_account_id" optional="show" groups="analytic.group_analytic_accounting"/>
<field name="analytic_tag_ids" optional="hide" widget="many2many_tags" groups="analytic.group_analytic_tags"/>
<field name="analytic_distribution_stored_char" invisible="1"/>
<field name="analytic_distribution" widget="analytic_distribution"
optional="show"
groups="analytic.group_analytic_accounting"/>
<field name="account_id" optional="hide" groups="account.group_account_readonly"/>
<field name="company_id" optional="show" groups="base.group_multi_company" readonly="1"/>
<field name="unit_amount_display" string="Unit Price" optional="hide" widget="monetary" options="{'currency_field': 'company_currency_id'}"/>
@@ -185,8 +187,13 @@
<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_account_id" domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]" groups="analytic.group_analytic_accounting" attrs="{'readonly': [('is_editable', '=', False)]}"/>
<field name="analytic_tag_ids" widget="many2many_tags" groups="analytic.group_analytic_tags" attrs="{'readonly': [('is_editable', '=', 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', 'account_field': 'account_id'}"
business_domain_compute="'purchase'"
attrs="{'readonly': [('is_editable', '=', False)]}"
/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="employee_id" groups="hr_expense.group_hr_expense_team_approver" context="{'default_company_id': company_id}" widget="many2one_avatar_employee"/>
<label id="lo" for="payment_mode" attrs="{'invisible': [('product_has_cost', '=', True)]}"/>
@@ -330,7 +337,6 @@
<field string="Expense" name="name" filter_domain="['|', '|', ('employee_id', 'ilike', self), ('name', 'ilike', self), ('product_id', 'ilike', self)]"/>
<field name="date"/>
<field name="employee_id"/>
<field name="analytic_account_id" groups="analytic.group_analytic_accounting"/>
<filter string="My Expenses" name="my_expenses" domain="[('employee_id.user_id', '=', uid)]"/>
<filter string="My Team" name="my_team_expenses" domain="[('employee_id.parent_id.user_id', '=', uid)]" groups="hr_expense.group_hr_expense_team_approver" help="Expenses of Your Team Member"/>
<separator />
@@ -351,7 +357,6 @@
<group expand="0" string="Group By">
<filter string="Employee" name="employee" domain="[]" context="{'group_by': 'employee_id'}"/>
<filter string="Category" name="product" domain="[]" context="{'group_by': 'product_id'}"/>
<filter string="Analytic Account" name="analyticacc" domain="[]" context="{'group_by': 'analytic_account_id'}" groups="analytic.group_analytic_accounting"/>
<filter string="Status" name="status" domain="[]" context="{'group_by': 'state'}"/>
<filter string="Expense Date" name="expensesmonth" domain="[]" context="{'group_by': 'date'}" help="Expense Date"/>
<filter string="Company" name="company" domain="[]" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/>
@@ -743,8 +748,12 @@
<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_account_id" optional="show" domain="['|', ('company_id', '=', parent.company_id), ('company_id', '=', False)]" groups="analytic.group_analytic_accounting"/>
<field name="analytic_tag_ids" optional="hide" widget="many2many_tags" groups="analytic.group_analytic_tags"/>
<field name="analytic_distribution_stored_char" invisible="1"/>
<field name="analytic_distribution" widget="analytic_distribution"
groups="analytic.group_analytic_accounting"
optional="show"
options="{'product_field': 'product_id', 'account_field': 'account_id'}"
business_domain_compute="'purchase'"/>
<field name="account_id" optional="hide" readonly="True"/>
<field name="product_has_cost" invisible="True"/>
<field name="unit_amount" optional="hide" widget="monetary" attrs="{'readonly': [('product_has_cost', '=', False)]}" options="{'currency_field': 'currency_id'}"/>
@@ -22,7 +22,6 @@
<field name="date" eval="(DateTime.now() + relativedelta(days=-1)).strftime('%Y-%m-%d')"/>
<field name="unit_amount">2.00</field>
<field name="project_id" ref='project.project_project_2'/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_analysis'))]"/>
<field name="amount">-60.00</field>
</record>
@@ -50,7 +49,6 @@
<field name="date" eval="(DateTime.now() + relativedelta(days=-1)).strftime('%Y-%m-%d')"/>
<field name="unit_amount">1.00</field>
<field name="project_id" ref='project.project_project_2'/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_analysis'))]"/>
<field name="amount">-30.00</field>
</record>
@@ -70,7 +68,6 @@
<field name="unit_amount">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_1"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_analysis'))]"/>
<field name="amount">-30.0</field>
</record>
@@ -91,7 +88,6 @@
<field name="unit_amount">3</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_1"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_training'))]"/>
<field name="amount">-90.0</field>
</record>
@@ -122,7 +118,6 @@
<field name="unit_amount">2</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_1"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_analysis'))]"/>
<field name="amount">-60.0</field>
</record>
@@ -183,7 +178,6 @@
<field name="unit_amount">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_1"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_training'))]"/>
<field name="amount">-30.0</field>
</record>
@@ -244,7 +238,6 @@
<field name="unit_amount">2</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_1"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_training'))]"/>
<field name="amount">-60.0</field>
</record>
@@ -305,7 +298,6 @@
<field name="unit_amount">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_1"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_analysis'))]"/>
<field name="amount">-30.0</field>
</record>
@@ -396,7 +388,6 @@
<field name="unit_amount">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_2"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_training'))]"/>
<field name="amount">-30.0</field>
</record>
@@ -417,7 +408,6 @@
<field name="unit_amount">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_2"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_training'))]"/>
<field name="amount">-30.0</field>
</record>
@@ -448,7 +438,6 @@
<field name="unit_amount">3</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_2"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_analysis'))]"/>
<field name="amount">-90.0</field>
</record>
@@ -479,7 +468,6 @@
<field name="unit_amount">3</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_2"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_training'))]"/>
<field name="amount">-90.0</field>
</record>
@@ -500,7 +488,6 @@
<field name="unit_amount">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_2"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_training'))]"/>
<field name="amount">-30.0</field>
</record>
@@ -511,7 +498,6 @@
<field name="unit_amount">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_2"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_training'))]"/>
<field name="amount">-30.0</field>
</record>
@@ -552,7 +538,6 @@
<field name="unit_amount">2</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_2"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_analysis'))]"/>
<field name="amount">-60.0</field>
</record>
@@ -623,7 +608,6 @@
<field name="unit_amount">3</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_5"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_analysis'))]"/>
<field name="amount">-90.0</field>
</record>
@@ -644,7 +628,6 @@
<field name="unit_amount">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_5"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_analysis'))]"/>
<field name="amount">-30.0</field>
</record>
@@ -655,7 +638,6 @@
<field name="unit_amount">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_5"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_training'))]"/>
<field name="amount">-30.0</field>
</record>
@@ -686,7 +668,6 @@
<field name="unit_amount">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_5"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_analysis'))]"/>
<field name="amount">-30.0</field>
</record>
@@ -697,7 +678,6 @@
<field name="unit_amount">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_5"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_analysis'))]"/>
<field name="amount">-30.0</field>
</record>
@@ -708,7 +688,6 @@
<field name="unit_amount">2</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_5"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_analysis'))]"/>
<field name="amount">-60.0</field>
</record>
@@ -729,7 +708,6 @@
<field name="unit_amount">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_5"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_training'))]"/>
<field name="amount">-30.0</field>
</record>
@@ -750,7 +728,6 @@
<field name="unit_amount">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_5"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_training'))]"/>
<field name="amount">-30.0</field>
</record>
@@ -771,7 +748,6 @@
<field name="unit_amount">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_5"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_analysis'))]"/>
<field name="amount">-30.0</field>
</record>
@@ -892,7 +868,6 @@
<field name="unit_amount">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_6"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_analysis'))]"/>
<field name="amount">-30.0</field>
</record>
@@ -903,7 +878,6 @@
<field name="unit_amount">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_6"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_analysis'))]"/>
<field name="amount">-30.0</field>
</record>
@@ -954,7 +928,6 @@
<field name="unit_amount">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_6"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_analysis'))]"/>
<field name="amount">-30.0</field>
</record>
@@ -995,7 +968,6 @@
<field name="unit_amount">2</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_6"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_analysis'))]"/>
<field name="amount">-60.0</field>
</record>
@@ -1026,7 +998,6 @@
<field name="unit_amount">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_6"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_training'))]"/>
<field name="amount">-30.0</field>
</record>
@@ -1037,7 +1008,6 @@
<field name="unit_amount">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_6"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_training'))]"/>
<field name="amount">-30.0</field>
</record>
@@ -1058,7 +1028,6 @@
<field name="unit_amount">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_6"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_training'))]"/>
<field name="amount">-30.0</field>
</record>
@@ -1090,7 +1059,6 @@
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_6"/>
<field name="amount">-60.0</field>
<field name="tag_ids" eval="[Command.link(ref('project.tag_analysis'))]"/>
</record>
<record id="project_1_task_6_account_analytic_line_21" model="account.analytic.line">
@@ -1170,7 +1138,6 @@
<field name="unit_amount">3</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_7"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_analysis'))]"/>
<field name="amount">-90.0</field>
</record>
@@ -1191,7 +1158,6 @@
<field name="unit_amount">2</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_7"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_analysis'))]"/>
<field name="amount">-60.0</field>
</record>
@@ -1202,7 +1168,6 @@
<field name="unit_amount">3</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_7"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_training'))]"/>
<field name="amount">-90.0</field>
</record>
@@ -1213,7 +1178,6 @@
<field name="unit_amount">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_7"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_analysis'))]"/>
<field name="amount">-30.0</field>
</record>
@@ -1264,7 +1228,6 @@
<field name="unit_amount">2</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_7"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_analysis'))]"/>
<field name="amount">-60.0</field>
</record>
@@ -1435,7 +1398,6 @@
<field name="unit_amount">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_8"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_training'))]"/>
<field name="amount">-30.0</field>
</record>
@@ -1566,7 +1528,6 @@
<field name="unit_amount">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_8"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_training'))]"/>
<field name="amount">-30.0</field>
</record>
@@ -1647,7 +1608,6 @@
<field name="unit_amount">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_8"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_training'))]"/>
<field name="amount">-30.0</field>
</record>
@@ -1688,7 +1648,6 @@
<field name="unit_amount">2</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="task_id" ref="project.project_1_task_8"/>
<field name="tag_ids" eval="[Command.link(ref('project.tag_training'))]"/>
<field name="amount">-60.0</field>
</record>
+4 -9
View File
@@ -115,7 +115,7 @@ class AccountAnalyticLine(models.Model):
@api.depends('employee_id')
def _compute_user_id(self):
for line in self:
line.user_id = line.employee_id.user_id if line.employee_id else line._default_user()
line.user_id = line.employee_id.user_id if line.employee_id else self._default_user()
@api.depends('employee_id')
def _compute_department_id(self):
@@ -272,20 +272,12 @@ class AccountAnalyticLine(models.Model):
task_analytic_account_id = task._get_task_analytic_account_id()
vals['account_id'] = task_analytic_account_id.id
vals['company_id'] = task_analytic_account_id.company_id.id or task.company_id.id
if vals.get('tag_ids'):
vals['tag_ids'] += [Command.link(tag_id.id) for tag_id in task.analytic_tag_ids]
else:
vals['tag_ids'] = [Command.set(task.analytic_tag_ids.ids)]
if not task_analytic_account_id.active:
raise UserError(_('You cannot add timesheets to a project or a task linked to an inactive analytic account.'))
# project implies analytic account
elif project and not vals.get('account_id'):
vals['account_id'] = project.analytic_account_id.id
vals['company_id'] = project.analytic_account_id.company_id.id or project.company_id.id
if vals.get('tag_ids'):
vals['tag_ids'] += [Command.link(tag_id.id) for tag_id in project.analytic_tag_ids]
else:
vals['tag_ids'] = [Command.set(project.analytic_tag_ids.ids)]
if not project.analytic_account_id.active:
raise UserError(_('You cannot add timesheets to a project linked to an inactive analytic account.'))
# force customer partner, from the task or the project
@@ -351,3 +343,6 @@ class AccountAnalyticLine(models.Model):
if len(task_ids) == 1:
return _('Timesheets - %s', task_ids.name)
return _('Timesheets')
def _default_user(self):
return self.env.context.get('user_id', self.env.user.id)
+10 -38
View File
@@ -26,9 +26,14 @@ class TestCommonTimesheet(TransactionCase):
'phone': '42',
})
cls.analytic_plan = cls.env['account.analytic.plan'].create({
'name': 'Plan Test',
'company_id': False,
})
cls.analytic_account = cls.env['account.analytic.account'].create({
'name': 'Analytic Account for Test Customer',
'partner_id': cls.partner.id,
'plan_id': cls.analytic_plan.id,
'code': 'TEST'
})
@@ -359,8 +364,13 @@ class TestTimesheet(TestCommonTimesheet):
company_2 = self.env['res.company'].create({'name': 'Company 2'})
company_3 = self.env['res.company'].create({'name': 'Company 3'})
analytic_plan = self.env['account.analytic.plan'].create({
'name': 'Plan Test',
'company_id': company_3.id
})
analytic_account = self.env['account.analytic.account'].create({
'name': 'Aa Aa',
'plan_id': analytic_plan.id,
'company_id': company_3.id,
})
project = self.env['project.project'].create({
@@ -438,44 +448,6 @@ class TestTimesheet(TestCommonTimesheet):
self.assertEqual(self.task1.subtask_effective_hours, 8, 'Hours Spent on Sub-tasks should be 8 hours in Parent Task')
self.task1.child_ids = [Command.clear()]
def test_log_timesheet_with_analytic_tags(self):
""" Test whether the analytic tag of the project or task is set on the timesheet.
Test Case:
----------
1) Create analytic tags
2) Add analytic tag in project
3) Create timesheet
4) Check the default analytic tag of the project and timesheet
5) Add analytic tag in task
6) Check the analytic tag of the timesheet and task
"""
Timesheet = self.env['account.analytic.line'].with_user(self.user_employee)
share_capital_tag, office_furn_tag = self.env['account.analytic.tag'].create([
{'name': 'Share capital'},
{'name': 'Office Furniture'},
])
self.project_customer.analytic_tag_ids = [Command.set((share_capital_tag + office_furn_tag).ids)]
timesheet = Timesheet.create({
'project_id': self.project_customer.id,
'name': 'my first timesheet',
'unit_amount': 4,
})
self.assertEqual(timesheet.tag_ids, self.project_customer.analytic_tag_ids)
self.task2.analytic_tag_ids = [Command.set((share_capital_tag + office_furn_tag).ids)]
timesheet1 = Timesheet.create({
'project_id': self.project_customer.id,
'task_id': self.task2.id,
'name': 'my first timesheet',
'unit_amount': 4,
})
self.assertEqual(timesheet1.tag_ids, self.task2.analytic_tag_ids)
def test_ensure_product_uom_set_in_timesheet(self):
self.assertFalse(self.project_customer.timesheet_ids, 'No timesheet should be recorded in this project')
self.assertFalse(self.project_customer.total_timesheet_time, 'The total time recorded should be equal to 0 since no timesheet is recorded.')
@@ -28,7 +28,6 @@
<field name="project_id" required="1" options="{'no_create_edit': True, 'no_open': 1}"/>
<field name="task_id" optional="show" options="{'no_create_edit': True, 'no_open': True}" widget="task_with_hours" context="{'default_project_id': project_id}"/>
<field name="name" optional="show" required="0"/>
<field name="tag_ids" groups="analytic.group_analytic_tags" widget="many2many_tags" optional="hide"/>
<field name="unit_amount" optional="show" widget="timesheet_uom" sum="Total" decoration-danger="unit_amount &gt; 24 or unit_amount &lt; 0"/>
<field name="company_id" invisible="1"/>
<field name="user_id" invisible="1"/>
@@ -140,7 +139,6 @@
<group>
<field name="project_id" required="1"/>
<field name="task_id" widget="task_with_hours" context="{'default_project_id': project_id}"/>
<field name="tag_ids" groups="analytic.group_analytic_tags" widget="many2many_tags"/>
<field name="name"/>
<field name="company_id" groups="base.group_multi_company" invisible="1"/>
</group>
@@ -181,7 +179,6 @@
<field name="project_id"/>
<field name="ancestor_task_id" groups="project.group_subtask_project"/>
<field name="task_id"/>
<field name="tag_ids" groups="analytic.group_analytic_tags"/>
<field name="name"/>
<field name="department_id"/>
<field name="manager_id"/>
@@ -152,7 +152,6 @@
<field name="user_id" invisible="1"/>
<field name="employee_id" required="1" widget="many2one_avatar_employee" context="{'active_test': True}"/>
<field name="name" required="0"/>
<field name="tag_ids" groups="analytic.group_analytic_tags" widget="many2many_tags" optional="hide"/>
<field name="unit_amount" widget="timesheet_uom" decoration-danger="unit_amount &gt; 24 or unit_amount &lt; 0"/>
<field name="project_id" invisible="1"/>
<field name="task_id" invisible="1"/>
@@ -12,7 +12,14 @@ class TestAnalyticAccount(TransactionCase):
super().setUpClass()
cls.env.user.write({'groups_id': [(4, cls.env.ref('analytic.group_analytic_accounting').id),]})
cls.analytic_account = cls.env['account.analytic.account'].create({'name': 'test_analytic_account'})
cls.analytic_plan = cls.env['account.analytic.plan'].create({
'name': 'Plan',
'company_id': False,
})
cls.analytic_account = cls.env['account.analytic.account'].create({
'name': 'test_analytic_account',
'plan_id': cls.analytic_plan.id,
})
cls.workcenter = cls.env['mrp.workcenter'].create({
'name': 'Workcenter',
'default_capacity': 1,
@@ -152,7 +159,8 @@ class TestAnalyticAccount(TransactionCase):
# Required for `workorder_ids` to be visible in the view
self.env.user.groups_id += self.env.ref('mrp.group_mrp_routings')
# set wc analytic account to be different from the one on the bom
wc_analytic_account = self.env['account.analytic.account'].create({'name': 'wc_analytic_account'})
analytic_plan = self.env['account.analytic.plan'].create({'name': 'Plan Test', 'company_id': False})
wc_analytic_account = self.env['account.analytic.account'].create({'name': 'wc_analytic_account', 'plan_id': analytic_plan.id})
self.workcenter.costs_hour_account_id = wc_analytic_account
# create a mo
@@ -216,7 +224,8 @@ class TestAnalyticAccount(TransactionCase):
self.assertEqual(len(mo.move_raw_ids.analytic_account_line_id), 1)
# Create a new analytic account
new_analytic_account = self.env['account.analytic.account'].create({'name': 'test_analytic_account_2'})
analytic_plan = self.env['account.analytic.plan'].create({'name': 'Plan Test', 'company_id': False})
new_analytic_account = self.env['account.analytic.account'].create({'name': 'test_analytic_account_2', 'plan_id': analytic_plan.id})
# Change the MO analytic account
mo.analytic_account_id = new_analytic_account
self.assertEqual(mo.move_raw_ids.analytic_account_line_id.account_id.id, new_analytic_account.id)
+3 -43
View File
@@ -40,50 +40,17 @@
<!-- Needed so that we can have the same analytic accounts on hr_timesheet and project_account_budget -->
<record id="analytic_office_design" model="account.analytic.account">
<field name="name">Office Design</field>
<field name="plan_id" ref="analytic.analytic_plan_projects"/>
</record>
<record id="analytic_research_development" model="account.analytic.account">
<field name="name">Research &amp; Development</field>
<field name="plan_id" ref="analytic.analytic_plan_projects"/>
</record>
<record id="analytic_renovations" model="account.analytic.account">
<field name="name">Renovations</field>
</record>
<!-- Analytic Tags -->
<record id="tag_analysis" model="account.analytic.tag">
<field name="name">Analysis</field>
<field name="color">1</field>
</record>
<record id="tag_design" model="account.analytic.tag">
<field name="name">Design</field>
<field name="color">2</field>
</record>
<record id="tag_training" model="account.analytic.tag">
<field name="name">Training</field>
<field name="color">3</field>
</record>
<record id="tag_research" model="account.analytic.tag">
<field name="name">Research</field>
<field name="color">4</field>
</record>
<record id="tag_development" model="account.analytic.tag">
<field name="name">Development</field>
<field name="color">5</field>
</record>
<record id="tag_modification" model="account.analytic.tag">
<field name="name">Modification</field>
<field name="color">6</field>
</record>
<record id="tag_decoration" model="account.analytic.tag">
<field name="name">Decoration</field>
<field name="color">7</field>
<field name="plan_id" ref="analytic.analytic_plan_projects"/>
</record>
<!-- Stage templates -->
@@ -129,7 +96,6 @@
<field name="tag_ids" eval="[Command.link(ref('project.project_tags_05'))]"/>
<field name="stage_id" ref="project.project_project_stage_1"/>
<field name="analytic_account_id" ref="project.analytic_office_design"/>
<field name="analytic_tag_ids" eval="[Command.link(ref('tag_design'))]"/>
</record>
<record id="project_1_follower_admin" model="mail.followers">
<field name="res_model">project.project</field>
@@ -146,7 +112,6 @@
<field name="tag_ids" eval="[Command.link(ref('project.project_tags_04'))]"/>
<field name="stage_id" ref="project.project_project_stage_1"/>
<field name="analytic_account_id" ref="project.analytic_research_development"/>
<field name="analytic_tag_ids" eval="[Command.link(ref('tag_research')), Command.link(ref('tag_development'))]"/>
</record>
<record id="project_2_activity_1" model="mail.activity">
<field name="res_id" ref="project_project_2"/>
@@ -394,7 +359,6 @@
<field name="tag_ids" eval="[Command.set([ref('project_tags_00')])]"/>
<field name="create_date" eval="DateTime.now() - relativedelta(months=5)"/>
<field name="milestone_id" ref="project.project_1_milestone_2" />
<field name="analytic_tag_ids" eval="[Command.link(ref('tag_modification'))]"/>
</record>
<record id="project_1_task_4_mail_message_1" model="mail.message">
<field name="model">project.task</field>
@@ -488,7 +452,6 @@
<field name="create_date" eval="DateTime.now() - relativedelta(months=5)"/>
<field name="color">11</field>
<field name="milestone_id" ref="project.project_1_milestone_3" />
<field name="analytic_tag_ids" eval="[Command.link(ref('tag_modification'))]"/>
</record>
<record id="project_1_task_6_mail_message_1" model="mail.message">
<field name="model">project.task</field>
@@ -529,7 +492,6 @@
<field name="date_deadline" eval="DateTime.now() + relativedelta(days=6)"/>
<field name="color">9</field>
<field name="milestone_id" ref="project.project_1_milestone_3" />
<field name="analytic_tag_ids" eval="[Command.link(ref('tag_modification'))]"/>
</record>
<record id="project_1_task_7_mail_message_1" model="mail.message">
<field name="model">project.task</field>
@@ -701,7 +663,6 @@
<field name="parent_id" ref="project.project_1_task_12"/>
<field name="name">Preparation</field>
<field name="stage_id" ref="project_stage_1"/>
<field name="analytic_tag_ids" eval="[Command.link(ref('tag_analysis'))]"/>
</record>
<record id="project_1_task_15" model="project.task">
<field name="sequence">30</field>
@@ -725,7 +686,6 @@
<field name="name">Customer analysis + Architecture</field>
<field name="color">7</field>
<field name="create_date" eval="DateTime.now() - relativedelta(months=5)"/>
<field name="analytic_tag_ids" eval="[Command.link(ref('tag_analysis')), Command.link(ref('tag_design'))]"/>
</record>
<record id="project_2_task_1_mail_message_1" model="mail.message">
<field name="model">project.task</field>
+1 -1
View File
@@ -2,7 +2,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import analytic_account
from . import analytic_account_tag
from . import project_milestone
from . import project_project_stage
from . import project_task_recurrence
@@ -11,6 +10,7 @@ from . import project_task_stage_personal
from . import project
from . import project_collaborator
from . import project_update
from . import company
from . import res_config_settings
from . import res_partner
from . import digest
@@ -1,10 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class AccountAnalyticTag(models.Model):
_inherit = 'account.analytic.tag'
task_ids = fields.Many2many('project.task', string='Tasks')
project_ids = fields.Many2many('project.project', string='Projects')
+19
View File
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from odoo import fields, models
class ResCompany(models.Model):
_name = "res.company"
_inherit = "res.company"
analytic_plan_id = fields.Many2one(
'account.analytic.plan',
string="Default Plan",
check_company=True,
readonly=False,
compute="_compute_analytic_plan_id",
help="Default Plan for a new analytic account for projects")
def _compute_analytic_plan_id(self):
for company in self:
company.analytic_plan_id = self.env['account.analytic.plan'].with_company(company)._get_default()
+4 -7
View File
@@ -303,7 +303,6 @@ class Project(models.Model):
help="Analytic account to which this project is linked for financial management. "
"Use an analytic account to record cost and revenue on your project.")
analytic_account_balance = fields.Monetary(related="analytic_account_id.balance")
analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags')
favorite_user_ids = fields.Many2many(
'res.users', 'project_favorite_user_rel', 'project_id', 'user_id',
@@ -945,11 +944,12 @@ class Project(models.Model):
@api.model
def _create_analytic_account_from_values(self, values):
company = self.env['res.company'].browse(values.get('company_id')) if values.get('company_id') else self.env.company
analytic_account = self.env['account.analytic.account'].create({
'name': values.get('name', _('Unknown Analytic Account')),
'company_id': values.get('company_id') or self.env.company.id,
'company_id': company.id,
'partner_id': values.get('partner_id'),
'active': True,
'plan_id': company.analytic_plan_id.id,
})
return analytic_account
@@ -959,6 +959,7 @@ class Project(models.Model):
'name': project.name,
'company_id': project.company_id.id,
'partner_id': project.partner_id.id,
'plan_id': project.company_id.analytic_plan_id.id,
'active': True,
})
project.write({'analytic_account_id': analytic_account.id})
@@ -1307,8 +1308,6 @@ class Task(models.Model):
"If empty, the analytic account of the project will be used.")
is_analytic_account_id_changed = fields.Boolean('Is Analytic Account Manually Changed', compute='_compute_is_analytic_account_id_changed', store=True)
project_analytic_account_id = fields.Many2one('account.analytic.account', string='Project Analytic Account', related='project_id.analytic_account_id')
analytic_tag_ids = fields.Many2many('account.analytic.tag', string="Analytic Tags",
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", check_company=True)
@property
def SELF_READABLE_FIELDS(self):
@@ -1835,8 +1834,6 @@ class Task(models.Model):
project = self.env['project.project'].browse(project_id)
if project.analytic_account_id:
vals['analytic_account_id'] = project.analytic_account_id.id
if project.analytic_tag_ids:
vals['analytic_tag_ids'] = [Command.set(project.analytic_tag_ids.ids)]
else:
vals['user_ids'] = [Command.link(self.env.user.id)]
@@ -16,6 +16,14 @@ class ResConfigSettings(models.TransientModel):
group_project_task_dependencies = fields.Boolean("Task Dependencies", implied_group="project.group_project_task_dependencies")
group_project_milestone = fields.Boolean('Milestones', implied_group='project.group_project_milestone', group='base.group_portal,base.group_user')
# Analytic Accounting
analytic_plan_id = fields.Many2one(
comodel_name='account.analytic.plan',
string="Default Plan",
readonly=False,
related='company_id.analytic_plan_id',
)
@api.model
def _get_basic_project_domain(self):
return []
@@ -14,9 +14,14 @@ class TestProjectProfitabilityCommon(TransactionCase):
'name': 'Georges',
'email': 'georges@project-profitability.com'})
cls.analytic_plan = cls.env['account.analytic.plan'].create({
'name': 'Plan A',
'company_id': False,
})
cls.analytic_account = cls.env['account.analytic.account'].create({
'name': 'Project - AA',
'code': 'AA-1234',
'plan_id': cls.analytic_plan.id,
})
cls.project = cls.env['project.project'].with_context({'mail_create_nolog': True}).create({
'name': 'Project',
-2
View File
@@ -519,7 +519,6 @@
<group>
<group>
<field name="analytic_account_id" domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]" context="{'default_partner_id': partner_id}" groups="analytic.group_analytic_accounting"/>
<field name="analytic_tag_ids" groups="analytic.group_analytic_tags" widget="many2many_tags"/>
<field name="privacy_visibility" widget="radio"/>
<span colspan="2" class="text-muted" attrs="{'invisible':[('access_instruction_message', '=', '')]}">
<i class="fa fa-lightbulb-o"/>&amp;nbsp;<field name="access_instruction_message" nolabel="1"/>
@@ -1319,7 +1318,6 @@
<field name="is_analytic_account_id_changed" invisible="1"/>
<field name="parent_id" attrs="{'invisible': [('allow_subtasks', '=', False)]}" groups="base.group_no_one"/>
<field name="analytic_account_id" groups="analytic.group_analytic_accounting" context="{'default_partner_id': partner_id}"/>
<field name="analytic_tag_ids" groups="analytic.group_analytic_tags" widget="many2many_tags"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
<field name="sequence" groups="base.group_no_one"/>
<field name="email_from" invisible="1"/>
@@ -118,6 +118,10 @@
</div>
</div>
</div>
<div class="col-12 col-lg-6" groups="analytic.group_analytic_accounting">
<label for="analytic_plan_id" class="col-lg-5"/>
<field name="analytic_plan_id"/>
</div>
</div>
</div>
</xpath>
@@ -2,10 +2,10 @@
<odoo>
<data noupdate="1">
<record id="hr_expense.travel_admin_by_car_expense" model="hr.expense">
<field name="analytic_account_id" ref="project.analytic_office_design"/>
<field name="analytic_distribution" eval="{ref('project.analytic_office_design'): 100}"/>
</record>
<record id="hr_expense.travel_demo_by_car_expense" model="hr.expense">
<field name="analytic_account_id" ref="project.analytic_office_design"/>
<field name="analytic_distribution" eval="{ref('project.analytic_office_design'): 100}"/>
</record>
</data>
</odoo>
+7 -10
View File
@@ -16,14 +16,11 @@ class Project(models.Model):
if not self.analytic_account_id:
self.expenses_count = 0
return
expenses_data = self.env['hr.expense']._read_group([
('analytic_account_id', '!=', False),
('analytic_account_id', 'in', self.analytic_account_id.ids)
],
['analytic_account_id'], ['analytic_account_id'])
mapped_data = {data['analytic_account_id'][0]: data['analytic_account_id_count'] for data in expenses_data}
for project in self:
project.expenses_count = mapped_data.get(project.analytic_account_id.id, 0)
expenses = self.env['hr.expense'].search([
('analytic_distribution_stored_char', '=ilike', f'%"{project.analytic_account_id.id}":%')
])
project.expenses_count = len(expenses)
# ----------------------------
# Actions
@@ -36,7 +33,7 @@ class Project(models.Model):
action.update({
'display_name': _('Expenses'),
'views': [[False, 'tree'], [False, 'form'], [False, 'kanban'], [False, 'graph'], [False, 'pivot']],
'context': {'default_analytic_account_id': self.analytic_account_id.id},
'context': {'default_analytic_distribution': {self.analytic_account_id.id: 100}},
'domain': domain or [('id', 'in', expense_ids)],
})
if len(expense_ids) == 1:
@@ -68,7 +65,7 @@ class Project(models.Model):
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_account_id', 'in', self.analytic_account_id.ids),
[('analytic_distribution_stored_char', '=ilike', f'%"{self.analytic_account_id.id}":%'),
('is_refused', '=', False),
('state', 'in', ['approved', 'done'])],
['untaxed_amount', 'ids:array_agg(id)'],
@@ -92,7 +89,7 @@ class Project(models.Model):
def _get_profitability_aal_domain(self):
return expression.AND([
super()._get_profitability_aal_domain(),
['|', ('move_id', '=', False), ('move_id.expense_id', '=', False)],
['|', ('move_line_id', '=', False), ('move_line_id.expense_id', '=', False)],
])
def _get_profitability_items(self, with_action=True):
@@ -42,7 +42,7 @@ class TestProjectHrExpenseProfitability(TestProjectProfitabilityCommon, TestProj
'product_id': self.product_a.id,
'unit_amount': 350.00,
'company_id': self.project.company_id.id,
'analytic_account_id': self.project.analytic_account_id.id,
'analytic_distribution': {self.project.analytic_account_id.id: 100},
})
expense_sheet = self.check_project_profitability_before_creating_and_approving_expense_sheet(
@@ -3,7 +3,7 @@
<data noupdate="1">
<function model="purchase.order.line" name="write">
<value model="purchase.order.line" search="[('product_id', 'in', [ref('product.product_delivery_01'), ref('product.product_product_27')]), ('order_id', '=', ref('purchase.purchase_order_1'))]"/>
<value eval="{'account_analytic_id': ref('project.analytic_office_design')}"/>
<value eval="{'analytic_distribution': {ref('analytic.analytic_our_super_product'): 100}}"/>
</function>
</data>
</odoo>
</odoo>
+7 -9
View File
@@ -17,12 +17,11 @@ class Project(models.Model):
if not self.analytic_account_id:
self.purchase_orders_count = 0
return
purchase_orders_data = self.env['purchase.order.line']._read_group([
('account_analytic_id', 'in', self.analytic_account_id.ids)
], ['account_analytic_id', 'order_id:count_distinct'], ['account_analytic_id'])
mapped_data = dict([(data['account_analytic_id'][0], data['order_id']) for data in purchase_orders_data])
for project in self:
project.purchase_orders_count = mapped_data.get(project.analytic_account_id.id, 0)
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)
# ----------------------------
# Actions
@@ -30,8 +29,7 @@ class Project(models.Model):
def action_open_project_purchase_orders(self):
purchase_orders = self.env['purchase.order'].search([
('order_line.account_analytic_id', '!=', False),
('order_line.account_analytic_id', 'in', self.analytic_account_id.ids)
('order_line.analytic_distribution_stored_char', '=ilike', f'%"{self.analytic_account_id.id}":%')
])
action_window = {
'name': _('Purchase Orders'),
@@ -94,7 +92,7 @@ class Project(models.Model):
def _get_profitability_aal_domain(self):
return expression.AND([
super()._get_profitability_aal_domain(),
['|', ('move_id', '=', False), ('move_id.purchase_line_id', '=', False)],
['|', ('move_line_id', '=', False), ('move_line_id.purchase_line_id', '=', False)],
])
def _get_profitability_labels(self):
@@ -111,7 +109,7 @@ class Project(models.Model):
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([
('account_analytic_id', 'in', self.analytic_account_id.ids),
('analytic_distribution_stored_char', '=ilike', f'%"{self.analytic_account_id.id}":%'),
('state', 'in', ['purchase', 'done']),
'|',
('qty_invoiced', '>', 0),
@@ -1,15 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo import models
class PurchaseOrderLine(models.Model):
_inherit = 'purchase.order.line'
def _default_account_analytic_id(self):
def _compute_analytic_distribution(self):
super()._compute_analytic_distribution()
if self._context.get('project_id'):
return self.env['project.project'].browse(self._context['project_id']).analytic_account_id
return False
account_analytic_id = fields.Many2one(default=_default_account_analytic_id)
self.analytic_distribution = {self.env['project.project'].browse(self._context['project_id']).analytic_account_id: 100}
@@ -15,7 +15,7 @@ class Project(models.Model):
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_account_id', 'in', self.analytic_account_id.ids),
('analytic_distribution_stored_char', '=ilike', f'%"{self.analytic_account_id.id}":%'),
('is_refused', '=', False),
('state', 'in', ['approved', 'done']),
],
@@ -16,7 +16,7 @@ class TestProjectSaleExpenseProfitability(TestProjectProfitabilityCommon, TestPr
'product_id': self.company_data['product_order_sales_price'].id,
'unit_amount': self.company_data['product_order_sales_price'].list_price,
'employee_id': self.expense_employee.id,
'analytic_account_id': self.project.analytic_account_id.id,
'analytic_distribution': {self.project.analytic_account_id.id: 100},
'sale_order_id': self.sale_order.id,
})
+16 -30
View File
@@ -2,7 +2,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, time
from dateutil.relativedelta import relativedelta
import json
from markupsafe import escape, Markup
from pytz import timezone, UTC
@@ -909,6 +908,7 @@ class PurchaseOrder(models.Model):
class PurchaseOrderLine(models.Model):
_name = 'purchase.order.line'
_inherit = 'analytic.mixin'
_description = 'Purchase Order Line'
_order = 'order_id, sequence, id'
@@ -936,8 +936,7 @@ class PurchaseOrderLine(models.Model):
price_tax = fields.Float(compute='_compute_amount', string='Tax', store=True)
order_id = fields.Many2one('purchase.order', string='Order Reference', index=True, required=True, ondelete='cascade')
account_analytic_id = fields.Many2one('account.analytic.account', store=True, string='Analytic Account', compute='_compute_account_analytic_id', readonly=False)
analytic_tag_ids = fields.Many2many('account.analytic.tag', store=True, string='Analytic Tags', compute='_compute_analytic_tag_ids', readonly=False)
company_id = fields.Many2one('res.company', related='order_id.company_id', string='Company', store=True, readonly=True)
state = fields.Selection(related='order_id.state', store=True)
@@ -1131,31 +1130,19 @@ class PurchaseOrderLine(models.Model):
else:
return datetime.today() + relativedelta(days=seller.delay if seller else 0)
@api.depends('product_id', 'date_order')
def _compute_account_analytic_id(self):
for rec in self:
if not rec.display_type:
default_analytic_account = rec.env['account.analytic.default'].sudo().account_get(
product_id=rec.product_id.id,
partner_id=rec.order_id.partner_id.id,
user_id=rec.env.uid,
date=rec.date_order,
company_id=rec.company_id.id,
)
rec.account_analytic_id = default_analytic_account.analytic_id
@api.depends('product_id', 'date_order')
def _compute_analytic_tag_ids(self):
for rec in self:
if not rec.display_type:
default_analytic_account = rec.env['account.analytic.default'].sudo().account_get(
product_id=rec.product_id.id,
partner_id=rec.order_id.partner_id.id,
user_id=rec.env.uid,
date=rec.date_order,
company_id=rec.company_id.id,
)
rec.analytic_tag_ids = default_analytic_account.analytic_tag_ids
@api.depends('product_id', 'order_id.partner_id')
def _compute_analytic_distribution_stored_char(self):
for line in self:
if not line.display_type:
distribution = self.env['account.analytic.distribution.model']._get_distributionjson({
"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()
@api.onchange('product_id')
def onchange_product_id(self):
@@ -1352,8 +1339,7 @@ class PurchaseOrderLine(models.Model):
'quantity': self.qty_to_invoice,
'price_unit': self.currency_id._convert(self.price_unit, aml_currency, self.company_id, date, round=False),
'tax_ids': [(6, 0, self.taxes_id.ids)],
'analytic_account_id': self.account_analytic_id.id,
'analytic_tag_ids': [(6, 0, self.analytic_tag_ids.ids)],
'analytic_distribution': self.analytic_distribution,
'purchase_line_id': self.id,
}
@@ -48,7 +48,6 @@ class PurchaseReport(models.Model):
product_tmpl_id = fields.Many2one('product.template', 'Product Template', readonly=True)
country_id = fields.Many2one('res.country', 'Partner Country', readonly=True)
fiscal_position_id = fields.Many2one('account.fiscal.position', string='Fiscal Position', readonly=True)
account_analytic_id = fields.Many2one('account.analytic.account', 'Analytic Account', readonly=True)
commercial_partner_id = fields.Many2one('res.partner', 'Commercial Entity', readonly=True)
weight = fields.Float('Gross Weight', readonly=True)
volume = fields.Float('Volume', readonly=True)
@@ -90,7 +89,6 @@ class PurchaseReport(models.Model):
(sum(l.product_qty * l.price_unit / COALESCE(po.currency_rate, 1.0))/NULLIF(sum(l.product_qty/line_uom.factor*product_uom.factor),0.0))::decimal(16,2) * currency_table.rate as price_average,
partner.country_id as country_id,
partner.commercial_partner_id as commercial_partner_id,
analytic_account.id as account_analytic_id,
sum(p.weight * l.product_qty/line_uom.factor*product_uom.factor) as weight,
sum(p.volume * l.product_qty/line_uom.factor*product_uom.factor) as volume,
sum(l.price_subtotal / COALESCE(po.currency_rate, 1.0))::decimal(16,2) * currency_table.rate as untaxed_total,
@@ -114,7 +112,6 @@ class PurchaseReport(models.Model):
left join product_template t on (p.product_tmpl_id=t.id)
left join uom_uom line_uom on (line_uom.id=l.product_uom)
left join uom_uom product_uom on (product_uom.id=t.uom_id)
left join account_analytic_account analytic_account on (l.account_analytic_id = analytic_account.id)
left join currency_rate cr on (cr.currency_id = po.currency_id and
cr.company_id = po.company_id and
cr.date_start <= coalesce(po.date_order, now()) and
@@ -152,7 +149,6 @@ class PurchaseReport(models.Model):
product_uom.factor,
partner.country_id,
partner.commercial_partner_id,
analytic_account.id,
po.id,
currency_table.rate
"""
+24 -24
View File
@@ -313,19 +313,21 @@ class TestPurchaseToInvoice(AccountTestInvoicingCommon):
self.assertEqual(move.amount_total, 0.01)
def test_vendor_bill_analytic_account_default_change(self):
def test_vendor_bill_analytic_account_model_change(self):
""" Tests whether, when an analytic account rule is set, and user changes manually the analytic account on
the po, it is the same that is mentioned in the bill.
"""
# Required for `analytic.group_analytic_accounting` to be visible in the view
self.env.user.groups_id += self.env.ref('analytic.group_analytic_accounting')
analytic_account_default = self.env['account.analytic.account'].create({'name': 'default'})
analytic_account_manual = self.env['account.analytic.account'].create({'name': 'manual'})
analytic_plan = self.env['account.analytic.plan'].create({'name': 'Plan Test', 'company_id': False})
analytic_account_default = self.env['account.analytic.account'].create({'name': 'default', 'plan_id': analytic_plan.id})
analytic_account_manual = self.env['account.analytic.account'].create({'name': 'manual', 'plan_id': analytic_plan.id})
self.env['account.analytic.default'].create({
'analytic_id': analytic_account_default.id,
self.env['account.analytic.distribution.model'].create({
'analytic_distribution': {analytic_account_default.id: 100},
'product_id': self.product_order.id,
})
analytic_distribution_manual = {analytic_account_manual.id: 100}
po_form = Form(self.env['purchase.order'].with_context(tracking_disable=True))
po_form.partner_id = self.partner_a
@@ -334,32 +336,32 @@ class TestPurchaseToInvoice(AccountTestInvoicingCommon):
po_line_form.product_id = self.product_order
po_line_form.product_qty = 1.0
po_line_form.price_unit = 10
po_line_form.account_analytic_id = analytic_account_manual
po_line_form.analytic_distribution = analytic_distribution_manual
purchase_order = po_form.save()
purchase_order.button_confirm()
purchase_order.action_create_invoice()
aml = self.env['account.move.line'].search([('purchase_line_id', '=', purchase_order.order_line.id)])
self.assertRecordValues(aml, [{'analytic_account_id': analytic_account_manual.id}])
self.assertRecordValues(aml, [{'analytic_distribution': analytic_distribution_manual}])
def test_vendor_bill_analytic_account_product_change(self):
def test_purchase_order_analytic_account_product_change(self):
self.env.user.groups_id += self.env.ref('account.group_account_readonly')
self.env.user.groups_id += self.env.ref('analytic.group_analytic_accounting')
analytic_account_super = self.env['account.analytic.account'].create({'name': 'Super Account'})
analytic_account_great = self.env['account.analytic.account'].create({'name': 'Great Account'})
analytic_plan = self.env['account.analytic.plan'].create({'name': 'Plan Test', 'company_id': False})
analytic_account_super = self.env['account.analytic.account'].create({'name': 'Super Account', 'plan_id': analytic_plan.id})
analytic_account_great = self.env['account.analytic.account'].create({'name': 'Great Account', 'plan_id': analytic_plan.id})
super_product = self.env['product.product'].create({'name': 'Super Product'})
great_product = self.env['product.product'].create({'name': 'Great Product'})
product_no_account = self.env['product.product'].create({'name': 'Product No Account'})
self.env['account.analytic.default'].create([
self.env['account.analytic.distribution.model'].create([
{
'analytic_id': analytic_account_super.id,
'analytic_distribution': {analytic_account_super.id: 100},
'product_id': super_product.id,
},
{
'analytic_id': analytic_account_great.id,
'analytic_distribution': {analytic_account_great.id: 100},
'product_id': great_product.id,
},
])
@@ -371,23 +373,21 @@ class TestPurchaseToInvoice(AccountTestInvoicingCommon):
purchase_order = po_form.save()
purchase_order_line = purchase_order.order_line
self.assertEqual(purchase_order_line.account_analytic_id.id, analytic_account_super.id, "The analytic account should be set to 'Super Account'")
self.assertEqual(purchase_order_line.analytic_distribution, {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.account_analytic_id.id, analytic_account_great.id, "The analytic account should be set to 'Great Account'")
purchase_order_line.write({'product_id': product_no_account.id})
self.assertFalse(purchase_order_line.account_analytic_id.id, "The analytic account should not be set")
self.assertEqual(purchase_order_line.analytic_distribution, {analytic_account_great.id: 100}, "The analytic account should be set to 'Great Account'")
po_no_analytic_account = self.env['purchase.order'].create({
po_no_analytic_distribution = self.env['purchase.order'].create({
'partner_id': self.env.ref('base.res_partner_1').id,
})
pol_no_analytic_account = self.env['purchase.order.line'].create({
pol_no_analytic_distribution = self.env['purchase.order.line'].create({
'name': super_product.name,
'product_id': super_product.id,
'order_id': po_no_analytic_account.id,
'account_analytic_id': False,
'order_id': po_no_analytic_distribution.id,
'analytic_distribution': False,
})
po_no_analytic_account.button_confirm()
self.assertFalse(pol_no_analytic_account.account_analytic_id.id, "The compute should not overwrite what the user has set.")
po_no_analytic_distribution.button_confirm()
self.assertFalse(pol_no_analytic_distribution.analytic_distribution, "The compute should not overwrite what the user has set.")
def test_sequence_invoice_lines_from_multiple_purchases(self):
"""Test if the invoice lines are sequenced by purchase order when creating an invoice
+19 -5
View File
@@ -237,8 +237,13 @@
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="account_analytic_id" optional="hide" context="{'default_partner_id':parent.partner_id}" groups="analytic.group_analytic_accounting" domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]"/>
<field name="analytic_tag_ids" optional="hide" groups="analytic.group_analytic_tags" widget="many2many_tags" options="{'color_field': 'color'}" domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]"/>
<field name="analytic_distribution_stored_char" invisible="1"/>
<!-- TODO widget="analytic_distribution" -->
<field name="analytic_distribution"
optional="hide" invisible="1"
groups="analytic.group_analytic_accounting"
options="{'product_field': 'product_id'}"
business_domain_compute="'purchase'"/>
<field name="product_qty"/>
<field name="qty_received_manual" invisible="1"/>
<field name="qty_received_method" invisible="1"/>
@@ -282,8 +287,12 @@
</group>
<group>
<field name="date_planned" widget="date" attrs="{'required': [('display_type', '=', False)]}"/>
<field name="account_analytic_id" colspan="2" domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]" groups="analytic.group_analytic_accounting"/>
<field name="analytic_tag_ids" groups="analytic.group_analytic_tags" domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="analytic_distribution_stored_char" invisible="1"/>
<!-- TODO widget="analytic_distribution" -->
<field name="analytic_distribution"
groups="analytic.group_analytic_accounting" invisible="1"
options="{'product_field': 'product_id'}"
business_domain_compute="'purchase'"/>
</group>
<group colspan="12">
<notebook>
@@ -733,7 +742,12 @@
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="account_analytic_id" colspan="4" groups="analytic.group_analytic_accounting"/>
<field name="analytic_distribution_stored_char" invisible="1"/>
<!-- TODO widget="analytic_distribution" -->
<field name="analytic_distribution"
groups="analytic.group_analytic_accounting" invisible="1"
options="{'product_field': 'product_id'}"
business_domain_compute="'purchase'"/>
</group>
</group>
<field name="name"/>
@@ -163,6 +163,7 @@ class PurchaseRequisition(models.Model):
class PurchaseRequisitionLine(models.Model):
_name = "purchase.requisition.line"
_inherit = 'analytic.mixin'
_description = "Purchase Requisition Line"
_rec_name = 'product_id'
@@ -175,8 +176,6 @@ class PurchaseRequisitionLine(models.Model):
qty_ordered = fields.Float(compute='_compute_ordered_qty', string='Ordered Quantities')
requisition_id = fields.Many2one('purchase.requisition', required=True, string='Purchase Agreement', ondelete='cascade')
company_id = fields.Many2one('res.company', related='requisition_id.company_id', string='Company', store=True, readonly=True)
account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account')
analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags')
schedule_date = fields.Date(string='Scheduled Date')
supplier_info_ids = fields.One2many('product.supplierinfo', 'purchase_requisition_line_id')
@@ -266,6 +265,5 @@ class PurchaseRequisitionLine(models.Model):
'price_unit': price_unit,
'taxes_id': [(6, 0, taxes_ids)],
'date_planned': date_planned,
'account_analytic_id': self.account_analytic_id.id,
'analytic_tag_ids': self.analytic_tag_ids.ids,
'analytic_distribution': self.analytic_distribution,
}
@@ -161,8 +161,13 @@
<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="account_analytic_id" optional="hide" domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]" groups="analytic.group_analytic_accounting"/>
<field name="analytic_tag_ids" optional="hide" domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]" groups="analytic.group_analytic_tags" widget="many2many_tags"/>
<field name="analytic_distribution_stored_char" invisible="1"/>
<!-- TODO widget="analytic_distribution" -->
<field name="analytic_distribution"
optional="hide" invisible="1"
groups="analytic.group_analytic_accounting"
options="{'product_field': 'product_id'}"
business_domain_compute="'purchase'"/>
<field name="price_unit"/>
</tree>
<form string="Products">
@@ -174,7 +179,12 @@
<field name="product_uom_category_id" invisible="1"/>
<field name="product_uom_id" />
<field name="schedule_date"/>
<field name="account_analytic_id" domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]" groups="analytic.group_analytic_accounting"/>
<field name="analytic_distribution_stored_char" invisible="1"/>
<!-- TODO widget="analytic_distribution" -->
<field name="analytic_distribution"
groups="analytic.group_analytic_accounting" invisible="1"
options="{'product_field': 'product_id'}"
business_domain_compute="'purchase'"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
</group>
</form>
@@ -52,7 +52,7 @@
<field name="invoice_status" position="before">
<field name="receipt_status" attrs="{'invisible': [('state', 'not in', ('purchase', 'done'))]}"/>
</field>
<xpath expr="//field[@name='order_line']/form//field[@name='account_analytic_id']" position="before">
<xpath expr="//field[@name='order_line']/form//field[@name='analytic_distribution']" position="before">
<field name="propagate_cancel" groups="base.group_no_one"/>
</xpath>
<xpath expr="//field[@name='order_line']/tree//field[@name='qty_received']" position="attributes">
+21 -24
View File
@@ -21,26 +21,26 @@ class AccountMoveLine(models.Model):
super(AccountMoveLine, self)._copy_data_extend_business_fields(values)
values['sale_line_ids'] = [(6, None, self.sale_line_ids.ids)]
def _prepare_analytic_line(self):
""" Note: This method is called only on the move.line that having an analytic account, and
def _prepare_analytic_lines(self):
""" Note: This method is called only on the move.line that having an analytic distribution, and
so that should create analytic entries.
"""
values_list = super(AccountMoveLine, self)._prepare_analytic_line()
values_list = super(AccountMoveLine, self)._prepare_analytic_lines()
# filter the move lines that can be reinvoiced: a cost (negative amount) analytic line without SO line but with a product can be reinvoiced
move_to_reinvoice = self.env['account.move.line']
for index, move_line in enumerate(self):
values = values_list[index]
if 'so_line' not in values:
if move_line._sale_can_be_reinvoice():
move_to_reinvoice |= move_line
if len(values_list) > 0:
for index, move_line in enumerate(self):
values = values_list[index]
if 'so_line' not in values:
if move_line._sale_can_be_reinvoice():
move_to_reinvoice |= move_line
# insert the sale line in the create values of the analytic entries
if move_to_reinvoice:
map_sale_line_per_move = move_to_reinvoice._sale_create_reinvoice_sale_line()
for values in values_list:
sale_line = map_sale_line_per_move.get(values.get('move_id'))
sale_line = map_sale_line_per_move.get(values.get('move_line_id'))
if sale_line:
values['so_line'] = sale_line.id
@@ -133,23 +133,20 @@ class AccountMoveLine(models.Model):
""" Get the mapping of move.line with the sale.order record on which its analytic entries should be reinvoiced
:return a dict where key is the move line id, and value is sale.order record (or None).
"""
analytic_accounts = self.mapped('analytic_account_id')
# link the analytic account with its open SO by creating a map: {AA.id: sale.order}, if we find some analytic accounts
mapping = {}
if analytic_accounts: # first, search for the open sales order
sale_orders = self.env['sale.order'].search([('analytic_account_id', 'in', analytic_accounts.ids), ('state', '=', 'sale')], order='create_date DESC')
for sale_order in sale_orders:
mapping[sale_order.analytic_account_id.id] = sale_order
analytic_accounts_without_open_order = analytic_accounts.filtered(lambda account: not mapping.get(account.id))
if analytic_accounts_without_open_order: # then, fill the blank with not open sales orders
sale_orders = self.env['sale.order'].search([('analytic_account_id', 'in', analytic_accounts_without_open_order.ids)], order='create_date DESC')
for sale_order in sale_orders:
mapping[sale_order.analytic_account_id.id] = sale_order
for move_line in self:
if move_line.analytic_distribution:
distribution_json = move_line.analytic_distribution
sale_order = self.env['sale.order'].search([('analytic_account_id', 'in', list(int(account_id) for account_id in distribution_json.keys())),
('state', '=', 'sale')], order='create_date ASC', limit=1)
if sale_order:
mapping[move_line.id] = sale_order
else:
sale_order = self.env['sale.order'].search([('analytic_account_id', 'in', list(int(account_id) for account_id in distribution_json.keys()))], order='create_date ASC', limit=1)
mapping[move_line.id] = sale_order
# map of AAL index with the SO on which it needs to be reinvoiced. Maybe be None if no SO found
return {move_line.id: mapping.get(move_line.analytic_account_id.id) for move_line in self}
return mapping
def _sale_prepare_sale_line_values(self, order, price):
""" Generate the sale.line creation value from the current move line """
+7 -13
View File
@@ -250,7 +250,6 @@ class SaleOrder(models.Model):
analytic_account_id = fields.Many2one(
comodel_name='account.analytic.account',
string="Analytic Account",
compute='_compute_analytic_account_id', store=True, readonly=False,
copy=False, check_company=True, # Unrequired company
states=READONLY_FIELD_STATES,
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
@@ -554,18 +553,6 @@ class SaleOrder(models.Model):
else:
record.tax_country_id = record.company_id.account_fiscal_country_id
@api.depends('partner_id', 'date_order')
def _compute_analytic_account_id(self):
for order in self:
if not order.analytic_account_id:
default_analytic_account = order.env['account.analytic.default'].sudo().account_get(
partner_id=order.partner_id.id,
user_id=order.env.uid,
date=order.date_order,
company_id=order.company_id.id,
)
order.analytic_account_id = default_analytic_account.analytic_id
@api.depends('company_id', 'partner_id', 'amount_total')
def _compute_partner_credit_warning(self):
for order in self:
@@ -1391,10 +1378,17 @@ class SaleOrder(models.Model):
name = self.name
if prefix:
name = prefix + ": " + self.name
plan = self.env['account.analytic.plan'].search(['|', ('company_id', '=', self.company_id.id), ('company_id', '=', False)], limit=1)
if not plan:
plan = self.env['account.analytic.plan'].create({
'name': 'Default',
'company_id': self.company_id.id
})
return {
'name': name,
'code': self.client_order_ref,
'company_id': self.company_id.id,
'plan_id': plan.id,
'partner_id': self.partner_id.id
}
+21 -19
View File
@@ -13,6 +13,7 @@ from odoo.tools import float_is_zero, float_compare, float_round
class SaleOrderLine(models.Model):
_name = 'sale.order.line'
_inherit = 'analytic.mixin'
_description = "Sales Order Line"
_rec_names_search = ['name', 'order_id.name']
_order = 'order_id, sequence, id'
@@ -217,11 +218,6 @@ class SaleOrderLine(models.Model):
digits='Product Unit of Measure',
store=True)
analytic_tag_ids = fields.Many2many(
comodel_name='account.analytic.tag',
string="Analytic Tags",
compute='_compute_analytic_tag_ids', store=True, readonly=False,
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
analytic_line_ids = fields.One2many(
comodel_name='account.analytic.line', inverse_name='so_line',
string="Analytic lines")
@@ -879,18 +875,19 @@ class SaleOrderLine(models.Model):
line.untaxed_amount_to_invoice = amount_to_invoice
@api.depends('product_id', 'order_id.date_order', 'order_id.partner_id')
def _compute_analytic_tag_ids(self):
@api.depends('order_id.partner_id', 'product_id')
def _compute_analytic_distribution_stored_char(self):
for line in self:
if not line.display_type and line.state == 'draft':
default_analytic_account = line.env['account.analytic.default'].sudo().account_get(
product_id=line.product_id.id,
partner_id=line.order_id.partner_id.id,
user_id=self.env.uid,
date=line.order_id.date_order,
company_id=line.company_id.id,
)
line.analytic_tag_ids = default_analytic_account.analytic_tag_ids
distribution = line.env['account.analytic.distribution.model']._get_distributionjson({
"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()
@api.depends('product_id', 'state', 'qty_invoiced', 'qty_delivered')
def _compute_product_updatable(self):
@@ -1000,7 +997,7 @@ class SaleOrderLine(models.Model):
def _get_protected_fields(self):
return [
'product_id', 'name', 'price_unit', 'product_uom', 'product_uom_qty',
'tax_id', 'analytic_tag_ids'
'tax_id', 'analytic_distribution_stored_char'
]
def _update_line_quantity(self, values):
@@ -1077,12 +1074,17 @@ class SaleOrderLine(models.Model):
'discount': self.discount,
'price_unit': self.price_unit,
'tax_ids': [Command.set(self.tax_id.ids)],
'analytic_tag_ids': [Command.set(self.analytic_tag_ids.ids)],
'analytic_distribution': self.analytic_distribution,
'sale_line_ids': [Command.link(self.id)],
'is_downpayment': self.is_downpayment,
}
if self.order_id.analytic_account_id:
res['analytic_account_id'] = self.order_id.analytic_account_id.id
analytic_account_id = self.order_id.analytic_account_id.id
if analytic_account_id:
res['analytic_distribution'] = res['analytic_distribution'] or {}
if self.analytic_distribution:
res['analytic_distribution'][analytic_account_id] = self.analytic_distribution.get(analytic_account_id, 0) + 100
else:
res['analytic_distribution'][analytic_account_id] = 100
if optional_values:
res.update(optional_values)
if self.display_type:
-1
View File
@@ -1,7 +1,6 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_account_account_salesman,account_account salesman,account.model_account_account,sales_team.group_sale_salesman,1,0,0,0
access_account_analytic_account_salesman,account_analytic_account salesman,analytic.model_account_analytic_account,sales_team.group_sale_salesman,1,1,1,0
access_account_analytic_tag_sale_salesman,account.analytic.tag.sale.salesman,analytic.model_account_analytic_tag,sales_team.group_sale_salesman,1,0,0,0
access_account_account_tag_sale_salesman,account.account.tag.sale.salesman,account.model_account_account_tag,sales_team.group_sale_salesman,1,0,0,0
access_account_invoice_send_salesman,access.account.invoice.send.salesman,account.model_account_invoice_send,sales_team.group_sale_salesman,1,1,1,0
access_sale_account_journal,account.journal sale order.user,account.model_account_journal,sales_team.group_sale_salesman,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_account_account_salesman account_account salesman account.model_account_account sales_team.group_sale_salesman 1 0 0 0
3 access_account_analytic_account_salesman account_analytic_account salesman analytic.model_account_analytic_account sales_team.group_sale_salesman 1 1 1 0
access_account_analytic_tag_sale_salesman account.analytic.tag.sale.salesman analytic.model_account_analytic_tag sales_team.group_sale_salesman 1 0 0 0
4 access_account_account_tag_sale_salesman account.account.tag.sale.salesman account.model_account_account_tag sales_team.group_sale_salesman 1 0 0 0
5 access_account_invoice_send_salesman access.account.invoice.send.salesman account.model_account_invoice_send sales_team.group_sale_salesman 1 1 1 0
6 access_sale_account_journal account.journal sale order.user account.model_account_journal sales_team.group_sale_salesman 1 0 0 0
+15 -9
View File
@@ -12,10 +12,16 @@ class TestReInvoice(TestSaleCommon):
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.analytic_plan = cls.env['account.analytic.plan'].create({
'name': 'Plan',
'company_id': cls.partner_a.company_id.id,
})
cls.analytic_account = cls.env['account.analytic.account'].create({
'name': 'Test AA',
'code': 'TESTSALE_REINVOICE',
'company_id': cls.partner_a.company_id.id,
'plan_id': cls.analytic_plan.id,
'partner_id': cls.partner_a.id
})
@@ -60,11 +66,11 @@ class TestReInvoice(TestSaleCommon):
with move_form.invoice_line_ids.new() as line_form:
line_form.product_id = self.company_data['product_order_cost']
line_form.quantity = 3.0
line_form.analytic_account_id = self.analytic_account
line_form.analytic_distribution = {self.analytic_account.id: 100}
with move_form.invoice_line_ids.new() as line_form:
line_form.product_id = self.company_data['product_delivery_cost']
line_form.quantity = 3.0
line_form.analytic_account_id = self.analytic_account
line_form.analytic_distribution = {self.analytic_account.id: 100}
invoice_a = move_form.save()
invoice_a.action_post()
@@ -88,11 +94,11 @@ class TestReInvoice(TestSaleCommon):
with move_form.invoice_line_ids.new() as line_form:
line_form.product_id = self.company_data['product_order_cost']
line_form.quantity = 2.0
line_form.analytic_account_id = self.analytic_account
line_form.analytic_distribution = {self.analytic_account.id: 100}
with move_form.invoice_line_ids.new() as line_form:
line_form.product_id = self.company_data['product_delivery_cost']
line_form.quantity = 2.0
line_form.analytic_account_id = self.analytic_account
line_form.analytic_distribution = {self.analytic_account.id: 100}
invoice_b = move_form.save()
invoice_b.action_post()
@@ -171,11 +177,11 @@ class TestReInvoice(TestSaleCommon):
with move_form.invoice_line_ids.new() as line_form:
line_form.product_id = self.company_data['product_delivery_sales_price']
line_form.quantity = 3.0
line_form.analytic_account_id = self.analytic_account
line_form.analytic_distribution = {self.analytic_account.id: 100}
with move_form.invoice_line_ids.new() as line_form:
line_form.product_id = self.company_data['product_order_sales_price']
line_form.quantity = 3.0
line_form.analytic_account_id = self.analytic_account
line_form.analytic_distribution = {self.analytic_account.id: 100}
invoice_a = move_form.save()
invoice_a.action_post()
@@ -199,11 +205,11 @@ class TestReInvoice(TestSaleCommon):
with move_form.invoice_line_ids.new() as line_form:
line_form.product_id = self.company_data['product_delivery_sales_price']
line_form.quantity = 2.0
line_form.analytic_account_id = self.analytic_account
line_form.analytic_distribution = {self.analytic_account.id: 100}
with move_form.invoice_line_ids.new() as line_form:
line_form.product_id = self.company_data['product_order_sales_price']
line_form.quantity = 2.0
line_form.analytic_account_id = self.analytic_account
line_form.analytic_distribution = {self.analytic_account.id: 100}
invoice_b = move_form.save()
invoice_b.action_post()
@@ -237,7 +243,7 @@ class TestReInvoice(TestSaleCommon):
with move_form.invoice_line_ids.new() as line_form:
line_form.product_id = self.company_data['product_delivery_no']
line_form.quantity = 3.0
line_form.analytic_account_id = self.analytic_account
line_form.analytic_distribution = {self.analytic_account.id: 100}
invoice_a = move_form.save()
invoice_a.action_post()
+6
View File
@@ -31,9 +31,15 @@ class TestSaleFlow(TestSaleCommonBase):
'company_id': False,
})
cls.analytic_plan = cls.env['account.analytic.plan'].create({
'name': 'Plan',
'company_id': cls.company.id,
})
cls.analytic_account = cls.env['account.analytic.account'].create({
'name': 'Test analytic_account',
'code': 'analytic_account',
'plan_id': cls.analytic_plan.id,
'company_id': cls.company.id,
'partner_id': cls.partner_a.id
})
+11 -17
View File
@@ -374,27 +374,23 @@ class TestSalesTeam(SaleCommon):
sale_order.user_id = self.user_not_in_team
self.assertEqual(sale_order.team_id.id, self.sale_team_2.id, 'Should not reset the team to default')
def test_sale_order_analytic_tag_change(self):
def test_sale_order_analytic_distribution_change(self):
self.env.user.groups_id += self.env.ref('analytic.group_analytic_accounting')
self.env.user.groups_id += self.env.ref('analytic.group_analytic_tags')
analytic_account_super = self.env['account.analytic.account'].create({'name': 'Super Account'})
analytic_account_great = self.env['account.analytic.account'].create({'name': 'Great Account'})
analytic_tag_super = self.env['account.analytic.tag'].create({'name': 'Super Tag'})
analytic_tag_great = self.env['account.analytic.tag'].create({'name': 'Great Tag'})
analytic_plan = self.env['account.analytic.plan'].create({'name': 'Plan Test', 'company_id': False})
analytic_account_super = self.env['account.analytic.account'].create({'name': 'Super Account', 'plan_id': analytic_plan.id})
analytic_account_great = self.env['account.analytic.account'].create({'name': 'Great Account', 'plan_id': analytic_plan.id})
super_product = self.env['product.product'].create({'name': 'Super Product'})
great_product = self.env['product.product'].create({'name': 'Great Product'})
product_no_account = self.env['product.product'].create({'name': 'Product No Account'})
self.env['account.analytic.default'].create([
self.env['account.analytic.distribution.model'].create([
{
'analytic_id': analytic_account_super.id,
'analytic_distribution': {analytic_account_super.id: 100},
'product_id': super_product.id,
'analytic_tag_ids': [analytic_tag_super.id],
},
{
'analytic_id': analytic_account_great.id,
'analytic_distribution': {analytic_account_great.id: 100},
'product_id': great_product.id,
'analytic_tag_ids': [analytic_tag_great.id],
},
])
sale_order = self.env['sale.order'].create({
@@ -406,11 +402,9 @@ class TestSalesTeam(SaleCommon):
'order_id': sale_order.id,
})
self.assertEqual(sol.analytic_tag_ids.id, analytic_tag_super.id, "The analytic tag should be set to 'Super Tag'")
self.assertEqual(sol.analytic_distribution, {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_tag_ids.id, analytic_tag_great.id, "The analytic tag should be set to 'Great Tag'")
sol.write({'product_id': product_no_account.id})
self.assertFalse(sol.analytic_tag_ids.id, "The analytic account should not be set")
self.assertEqual(sol.analytic_distribution, {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,
@@ -419,7 +413,7 @@ class TestSalesTeam(SaleCommon):
'name': super_product.name,
'product_id': super_product.id,
'order_id': so_no_analytic_account.id,
'analytic_tag_ids': False,
'analytic_distribution': False,
})
so_no_analytic_account.action_confirm()
self.assertFalse(sol_no_analytic_account.analytic_tag_ids.id, "The compute should not overwrite what the user has set.")
self.assertFalse(sol_no_analytic_account.analytic_distribution, "The compute should not overwrite what the user has set.")
+14 -12
View File
@@ -414,14 +414,15 @@ class TestSaleToInvoice(TestSaleCommon):
downpayment.create_invoices()
self.assertEqual(so_for_downpayment.invoice_ids[0].company_id.id, so_company_id, "The company of the downpayment invoice should be the same as the one from the SO")
def test_invoice_analytic_account_default(self):
def test_invoice_analytic_distribution_model(self):
""" Tests whether, when an analytic account rule is set and the so has no analytic account,
the default analytic acount is correctly computed in the invoice.
the default analytic account is correctly computed in the invoice.
"""
analytic_account_default = self.env['account.analytic.account'].create({'name': 'default'})
analytic_plan_default = self.env['account.analytic.plan'].create({'name': 'default'})
analytic_account_default = self.env['account.analytic.account'].create({'name': 'default', 'plan_id': analytic_plan_default.id})
self.env['account.analytic.default'].create({
'analytic_id': analytic_account_default.id,
self.env['account.analytic.distribution.model'].create({
'analytic_distribution': {analytic_account_default.id: 100},
'product_id': self.product_a.id,
})
@@ -446,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_account_id': analytic_account_default.id}])
self.assertRecordValues(aml, [{'analytic_distribution': {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,
@@ -454,11 +455,12 @@ class TestSaleToInvoice(TestSaleCommon):
"""
# Required for `analytic_account_id` to be visible in the view
self.env.user.groups_id += self.env.ref('analytic.group_analytic_accounting')
analytic_account_default = self.env['account.analytic.account'].create({'name': 'default'})
analytic_account_so = self.env['account.analytic.account'].create({'name': 'so'})
analytic_plan_default = self.env['account.analytic.plan'].create({'name': 'default'})
analytic_account_default = self.env['account.analytic.account'].create({'name': 'default', 'plan_id': analytic_plan_default.id})
analytic_account_so = self.env['account.analytic.account'].create({'name': 'so', 'plan_id': analytic_plan_default.id})
self.env['account.analytic.default'].create({
'analytic_id': analytic_account_default.id,
self.env['account.analytic.distribution.model'].create({
'analytic_distribution': {analytic_account_default.id: 100},
'product_id': self.product_a.id,
})
@@ -484,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_account_id': analytic_account_so.id}])
self.assertRecordValues(aml, [{'analytic_distribution': {analytic_account_default.id: 100, analytic_account_so.id: 100}}])
def test_invoice_after_product_return_price_not_default(self):
so = self.env['sale.order'].create({
@@ -598,7 +600,7 @@ class TestSaleToInvoice(TestSaleCommon):
'product_uom_id': serv_cost.uom_id.id,
'quantity': 2,
'price_unit': serv_cost.standard_price,
'analytic_account_id': so.analytic_account_id.id,
'analytic_distribution': {so.analytic_account_id.id: 100},
}),
],
})
+13 -9
View File
@@ -362,7 +362,12 @@
<div name="lead">
<field name="customer_lead" class="oe_inline"/> days
</div>
<field name="analytic_tag_ids" widget="many2many_tags" groups="analytic.group_analytic_tags" options="{'color_field': 'color'}" domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]"/>
<field name="analytic_distribution_stored_char" invisible="1"/>
<!-- TODO widget="analytic_distribution" -->
<field name="analytic_distribution"
groups="analytic.group_analytic_accounting" invisible="1"
options="{'product_field': 'product_id'}"
business_domain_compute="'sale'"/>
</group>
</group>
<label for="name" string="Description" attrs="{'invisible': [('display_type', '!=', False)]}"/>
@@ -430,14 +435,13 @@
domain="[('sale_ok', '=', True), '|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]"
widget="product_configurator"/>
<field name="name" widget="section_and_note_text" optional="show"/>
<field
name="analytic_tag_ids"
optional="hide"
groups="analytic.group_analytic_tags"
widget="many2many_tags"
options="{'color_field': 'color'}"
domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]"
/>
<field name="analytic_distribution_stored_char" invisible="1"/>
<!-- TODO widget="analytic_distribution" -->
<field name="analytic_distribution"
optional="hide" invisible="1"
groups="analytic.group_analytic_accounting"
options="{'product_field': 'product_id'}"
business_domain_compute="'sale'"/>
<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,6 +6,7 @@ import time
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.fields import Command
from odoo.tools import float_is_zero
class SaleAdvancePaymentInv(models.TransientModel):
@@ -220,6 +221,15 @@ class SaleAdvancePaymentInv(models.TransientModel):
def _prepare_so_line_values(self, order):
self.ensure_one()
analytic_distribution = {}
amount_total = sum(order.order_line.mapped("price_total"))
if not float_is_zero(amount_total, precision_rounding=self.currency_id.rounding):
for line in order.order_line:
distrib_dict = line.analytic_distribution or {}
for account, distribution in distrib_dict.items():
analytic_distribution[account] = distribution * line.price_total + analytic_distribution.get(account, 0)
for account, distribution_amount in analytic_distribution.items():
analytic_distribution[account] = distribution_amount/amount_total
context = {'lang': order.partner_id.lang}
so_values = {
'name': _('Down Payment: %s (Draft)', time.strftime('%m %Y')),
@@ -228,11 +238,7 @@ class SaleAdvancePaymentInv(models.TransientModel):
'order_id': order.id,
'discount': 0.0,
'product_id': self.product_id.id,
'analytic_tag_ids': [
Command.link(analytic_tag.id)
for line in order.order_line
for analytic_tag in line.analytic_tag_ids
],
'analytic_distribution': analytic_distribution,
'is_downpayment': True,
'sequence': order.order_line and order.order_line[-1].sequence + 1 or 10,
}

Some files were not shown because too many files have changed in this diff Show More