[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:
committed by
william
co-authored by
Habib
parent
20b8f4c7e8
commit
7e3403068f
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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': [],
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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,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>
|
||||
+10
-20
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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', '>=', context.get('date_from')), ('date', '<=', 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"/>
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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 & 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 & Development</field>
|
||||
<field name="group_id" ref="analytic.analytic_group_departments"/>
|
||||
<field name="plan_id" ref="analytic.analytic_plan_departments"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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'))
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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);
|
||||
+71
@@ -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");
|
||||
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 > 24 or unit_amount < 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 > 24 or unit_amount < 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)
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
@@ -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')
|
||||
@@ -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()
|
||||
@@ -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',
|
||||
|
||||
@@ -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"/>&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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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 """
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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},
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user