[MERGE][ADD] project_expense: setting project on expense

This merge commit provide the possibility for project user to select
a project on an expense. Selectable project are only the one with an
analytic account set. Setting the project will set the analytic
account, so posting the expense will create analytic entries, and will
finally appear on the project overview.
If the analytic account still match an open sale order (and if the
product is reinvoicable), a new SO line will be added on the SO (this
flow does not change here).

Task-1843241 (previsouly Task-1911581)

closes odoo/odoo#29638

Signed-off-by: Jérome Maes (jem) <jem@openerp.com>
This commit is contained in:
Robot Odoo
2019-08-21 14:53:47 +02:00
committed by GitHub
12 changed files with 79 additions and 28 deletions
@@ -78,6 +78,7 @@ class AccountAnalyticAccount(models.Model):
line['credit'] = sum(accounts.mapped('credit'))
return res
@api.depends('line_ids.amount')
def _compute_debit_credit_balance(self):
Curr = self.env['res.currency']
analytic_line_obj = self.env['account.analytic.line']
+1 -2
View File
@@ -22,5 +22,4 @@ access_mail_alias,mail.alias,mail.model_mail_alias,project.group_project_manager
access_mail_activity_type_project_manager,mail.activity.type.project.manager,mail.model_mail_activity_type,project.group_project_manager,1,1,1,1
access_account_analytic_account_user,account.analytic.account,analytic.model_account_analytic_account,project.group_project_user,1,0,0,0
access_account_analytic_account_manager,account.analytic.account,analytic.model_account_analytic_account,project.group_project_manager,1,1,1,1
access_account_analytic_account_portal,account_analytic_account,analytic.model_account_analytic_account,base.group_portal,1,0,0,0
access_account_analytic_line_project,account.analytic.line project,analytic.model_account_analytic_line,project.group_project_manager,1,1,1,1
access_account_analytic_line_project,account.analytic.line project,analytic.model_account_analytic_line,project.group_project_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
22 access_mail_activity_type_project_manager mail.activity.type.project.manager mail.model_mail_activity_type project.group_project_manager 1 1 1 1
23 access_account_analytic_account_user account.analytic.account analytic.model_account_analytic_account project.group_project_user 1 0 0 0
24 access_account_analytic_account_manager account.analytic.account analytic.model_account_analytic_account project.group_project_manager 1 1 1 1
25 access_account_analytic_account_portal access_account_analytic_line_project account_analytic_account account.analytic.line project analytic.model_account_analytic_account analytic.model_account_analytic_line base.group_portal project.group_project_manager 1 0 1 0 1 0 1
access_account_analytic_line_project account.analytic.line project analytic.model_account_analytic_line project.group_project_manager 1 1 1 1
+11 -4
View File
@@ -26,14 +26,13 @@ class AccountMoveLine(models.Model):
"""
values_list = super(AccountMoveLine, self)._prepare_analytic_line()
uom_precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure')
# 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 and float_compare(move_line.credit or 0.0, move_line.debit or 0.0, precision_digits=uom_precision_digits) != 1 and move_line.product_id.expense_policy not in [False, 'no']:
move_to_reinvoice |= move_line
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:
@@ -46,6 +45,14 @@ class AccountMoveLine(models.Model):
return values_list
def _sale_can_be_reinvoice(self):
""" determine if the generated analytic line should be reinvoiced or not.
For Vendor Bill flow, if the product has a 'erinvoice policy' and is a cost, then we will find the SO on which reinvoice the AAL
"""
self.ensure_one()
uom_precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure')
return float_compare(self.credit or 0.0, self.debit or 0.0, precision_digits=uom_precision_digits) != 1 and self.product_id.expense_policy not in [False, 'no']
def _sale_create_reinvoice_sale_line(self):
sale_order_map = self._sale_determine_order()
+1 -1
View File
@@ -445,7 +445,7 @@ class SaleOrder(models.Model):
['|', ('name', operator, name), ('partner_id.name', operator, name)]
])
order_ids = self._search(domain, limit=limit, access_rights_uid=name_get_uid)
return self.browse(order_ids).name_get()
return models.lazy_name_get(self.browse(order_ids).with_user(name_get_uid))
return super(SaleOrder, self)._name_search(name, args=args, operator=operator, limit=limit, name_get_uid=name_get_uid)
def _prepare_invoice(self):
-2
View File
@@ -16,8 +16,6 @@ This module allow to reinvoice employee expense, by setting the SO directly on t
'depends': ['sale_management', 'hr_expense'],
'data': [
'data/digest_data.xml',
'security/ir.model.access.csv',
'security/sale_expense_security.xml',
'views/product_view.xml',
'views/hr_expense_views.xml',
'views/sale_order_views.xml',
+10 -2
View File
@@ -7,6 +7,15 @@ from odoo import api, models
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
def _sale_can_be_reinvoice(self):
""" determine if the generated analytic line should be reinvoiced or not.
For Expense flow, if the product has a 'reinvoice policy' and a Sales Order is set on the expense, then we will reinvoice the AAL
"""
self.ensure_one()
if self.expense_id: # expense flow is different from vendor bill reinvoice flow
return self.expense_id.product_id.expense_policy in ['sales_price', 'cost'] and self.expense_id.sale_order_id
return super(AccountMoveLine, self)._sale_can_be_reinvoice()
def _sale_determine_order(self):
""" For move lines created from expense, we override the normal behavior.
Note: if no SO but an AA is given on the expense, we will determine anyway the SO from the AA, using the same
@@ -16,8 +25,7 @@ class AccountMoveLine(models.Model):
mapping_from_expense = {}
for move_line in self.filtered(lambda move_line: move_line.expense_id):
if move_line.expense_id.sale_order_id:
mapping_from_expense[move_line.id] = move_line.expense_id.sale_order_id or None
mapping_from_expense[move_line.id] = move_line.expense_id.sale_order_id or None
mapping_from_invoice.update(mapping_from_expense)
return mapping_from_invoice
+19 -6
View File
@@ -10,18 +10,31 @@ class Expense(models.Model):
sale_order_id = fields.Many2one(
'sale.order', string='Reinvoice Customer', readonly=True,
states={'draft': [('readonly', False)], 'reported': [('readonly', False)]},
domain="[('state', '=', 'sale'), ('company_id', '=', company_id)]")
domain="[('state', '=', 'sale'), ('company_id', '=', company_id)]",
help="If the product has an expense policy, it will be reinvoiced on this sales order")
can_be_reinvoiced = fields.Boolean("Can be reinvoiced", compute='_compute_can_be_reinvoiced')
@api.depends('product_id')
def _compute_can_be_reinvoiced(self):
for expense in self:
expense.can_be_reinvoiced = expense.product_id.expense_policy in ['sales_price', 'cost']
@api.onchange('product_id')
def _onchange_product_id(self):
super(Expense, self)._onchange_product_id()
if not self.can_be_reinvoiced:
self.sale_order_id = False
@api.onchange('sale_order_id')
def _onchange_sale_order(self):
if self.sale_order_id:
self.analytic_account_id = self.sale_order_id.analytic_account_id
self.analytic_account_id = self.sale_order_id.sudo().analytic_account_id # `sudo` required for normal employee without sale access rights
def action_move_create(self):
""" When posting expense, if a SO is set, this means you want to reinvoice. To do so, we
have to set an Analytic Account on the expense. We choose the one from the SO, and
if it does not exist, we generate it. Create AA even for product with no expense policy
to keep track of the analytic.
""" When posting expense, if the AA is given, we will track cost in that
If a SO is set, this means we want to reinvoice the expense. But to do so, we
need the analytic entries to be generated, so a AA is required to reinvoice. So,
we ensure the AA if a SO is given.
"""
for expense in self.filtered(lambda expense: expense.sale_order_id and not expense.analytic_account_id):
if not expense.sale_order_id.analytic_account_id:
+6
View File
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo import SUPERUSER_ID
class SaleOrder(models.Model):
@@ -10,6 +11,11 @@ class SaleOrder(models.Model):
expense_ids = fields.One2many('hr.expense', 'sale_order_id', string='Expenses', domain=[('state', '=', 'done')], readonly=True, copy=False)
expense_count = fields.Integer("# of Expenses", compute='_compute_expense_count', compute_sudo=True)
@api.model
def _name_search(self, name='', args=None, operator='ilike', limit=100, name_get_uid=None):
name_get_uid = SUPERUSER_ID if self.env.user.has_group('base.group_user') else self.env.user.id
return super(SaleOrder, self)._name_search(name=name, args=args, operator=operator, limit=limit, name_get_uid=name_get_uid)
@api.depends('expense_ids')
def _compute_expense_count(self):
expense_data = self.env['hr.expense'].read_group([('sale_order_id', 'in', self.ids), ('state', '=', 'done')], ['sale_order_id'], ['sale_order_id'])
+8 -5
View File
@@ -197,6 +197,9 @@ class TestReInvoice(TestExpenseCommon, TestCommonSaleNoChart):
def test_no_expense(self):
""" Test invoicing expenses with no policy. Check nothing happen. """
analytic_account = self.env['account.analytic.account'].create({
'name': "AA to track expense",
})
# confirm SO
sale_order_line = self.env['sale.order.line'].create({
'name': self.product_no_expense.name,
@@ -220,18 +223,18 @@ class TestReInvoice(TestExpenseCommon, TestCommonSaleNoChart):
'quantity': 3,
'sheet_id': self.expense_sheet.id,
'sale_order_id': self.sale_order.id,
'analytic_account_id': self.sale_order.analytic_account_id.id,
'analytic_account_id': analytic_account.id,
})
expense1._onchange_product_id()
expense1._onchange_product_id() # will reset the SO field to NULL
# approve and generate entries
self.expense_sheet.approve_expense_sheets()
self.expense_sheet.action_sheet_move_create()
self.assertTrue(self.sale_order.analytic_account_id, "Posting expense with an expense product (even with no expense pilocy) should trigger the analytic account creation")
self.assertEquals(self.sale_order.analytic_account_id, expense1.analytic_account_id, "SO analytic account should be the same for the expense")
self.assertEquals(len(self.sale_order.order_line), 1, "No SO line should have been created (or removed) on expense report posting")
self.assertFalse(expense1.sale_order_id, "None reinvoicable expense can not be linked to SO")
self.assertFalse(self.sale_order.analytic_account_id, "Posting expense with an non expense product should not trigger analytic Account creation from SO, since non reinvoicable expense can not be linked to SO")
self.assertEquals(len(self.sale_order.order_line), 1, "No SO line should have been created (or removed) on expense report posting")
self.assertEquals(sale_order_line.price_unit, self.product_no_expense.list_price, "The unit price of SO line should be the same")
self.assertEquals(sale_order_line.product_uom_qty, 2, "The ordered quantity of SO line should be two")
self.assertEquals(sale_order_line.qty_delivered, 0, "The delivered quantity of SO line should have been incremented")
@@ -45,7 +45,8 @@ class TestSaleExpense(TestSale):
'analytic_account_id': so.analytic_account_id.id,
'unit_amount': 621.54,
'employee_id': employee.id,
'sheet_id': sheet.id
'sheet_id': sheet.id,
'sale_order_id': so.id,
})
# Approve
sheet.approve_expense_sheets()
@@ -74,7 +75,8 @@ class TestSaleExpense(TestSale):
'unit_amount': 0.15,
'quantity': 100,
'employee_id': employee.id,
'sheet_id': sheet.id
'sheet_id': sheet.id,
'sale_order_id': so.id,
})
# Approve
sheet.approve_expense_sheets()
+17 -3
View File
@@ -4,12 +4,25 @@
<field name="name">hr.expense.form.inherit.sale.expense</field>
<field name="model">hr.expense</field>
<field name="inherit_id" ref="hr_expense.hr_expense_view_form"/>
<field name="priority">30</field>
<field name="arch" type="xml">
<xpath expr="//field[@name='employee_id']" position="after">
<field name="sale_order_id" context="{'sale_show_partner_name': True}" groups="sales_team.group_sale_salesman"/>
<xpath expr="//field[@name='analytic_account_id']" position="before">
<field name="sale_order_id" attrs="{'invisible': [('can_be_reinvoiced', '=', False)]}" options="{'no_create_edit': True, 'no_create': True,}" context="{'sale_show_partner_name': True}"/>
<field name="can_be_reinvoiced" invisible="1"/>
</xpath>
</field>
</record>
<record id="hr_expense_form_view_inherit_account_manager" model="ir.ui.view">
<field name="name">hr.expense.form.inherit.sale.expense</field>
<field name="model">hr.expense</field>
<field name="inherit_id" ref="sale_expense.hr_expense_form_view_inherit_sale_expense"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='sale_order_id']" position="attributes">
<attribute name="attrs">{'invisible':[['can_be_reinvoiced','=',False]],'readonly':[['state','in',['done']]]}</attribute>
</xpath>
</field>
<field name="groups_id" eval="[(6, 0, [ref('account.group_account_manager')])]"/>
</record>
<record id="hr_expense_sheet_form_view_inherit_sale_expense" model="ir.ui.view">
<field name="name">hr.expense.sheet.form.inherit.sale.expense</field>
@@ -17,7 +30,8 @@
<field name="inherit_id" ref="hr_expense.view_hr_expense_sheet_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='expense_line_ids']/tree/field[@name='name']" position="after">
<field name="sale_order_id" context="{'sale_show_partner_name': True}" groups="sales_team.group_sale_salesman"/>
<field name="sale_order_id" attrs="{'invisible': [('can_be_reinvoiced', '=', False)]}" options="{'no_create_edit': True, 'no_create': True}" context="{'sale_show_partner_name': True}"/>
<field name="can_be_reinvoiced" invisible="1"/>
</xpath>
</field>
</record>
@@ -30,7 +30,7 @@ class Project(models.Model):
'count': len(account_invoices),
'icon': 'fa pencil-square-o',
'action': _to_action_data(
action=self.env.ref('purchase.action_invoice_pending'),
action=self.env.ref('account.action_move_in_invoice_type'),
domain=[('id', 'in', account_invoices.ids)],
context={'create': False, 'edit': False, 'delete': False}
)})