Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Master clean reinvoice jem #28939

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
52 changes: 31 additions & 21 deletions addons/account/models/account_move.py
Expand Up @@ -1350,38 +1350,48 @@ def _get_analytic_tag_ids(self):
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']
for obj_line in self:
for tag in obj_line.analytic_tag_ids.filtered('active_analytic_distribution'):
for distribution in tag.analytic_distribution_ids:
vals_line = obj_line._prepare_analytic_distribution_line(distribution)
self.env['account.analytic.line'].create(vals_line)
if obj_line.analytic_account_id:
vals_line = obj_line._prepare_analytic_line()[0]
self.env['account.analytic.line'].create(vals_line)
lines_to_create_analytic_entries |= obj_line

@api.one
# create analytic entries in batch
if lines_to_create_analytic_entries:
values_list = lines_to_create_analytic_entries._prepare_analytic_line()
self.env['account.analytic.line'].create(values_list)

@api.multi
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
"""
amount = (self.credit or 0.0) - (self.debit or 0.0)
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': self.analytic_account_id.id,
'tag_ids': [(6, 0, 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,
'user_id': self.invoice_id.user_id.id or self._uid,
'partner_id': self.partner_id.id,
'company_id': self.analytic_account_id.company_id.id or self.env.user.company_id.id,
}
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 '/'))
result.append({
'name': default_name,
'date': move_line.date,
'account_id': move_line.analytic_account_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.invoice_id.user_id.id or self._uid,
'partner_id': move_line.partner_id.id,
'company_id': move_line.analytic_account_id.company_id.id or self.env.user.company_id.id,
})
return result

def _prepare_analytic_distribution_line(self, distribution):
""" Prepare the values used to create() an account.analytic.line upon validation of an account.move.line having
Expand Down
1 change: 1 addition & 0 deletions addons/sale/models/__init__.py
Expand Up @@ -4,6 +4,7 @@
from . import analytic
from . import account_invoice
from . import account_reconciliation_widget
from . import account_move
from . import product_product
from . import product_template
from . import res_company
Expand Down
185 changes: 185 additions & 0 deletions addons/sale/models/account_move.py
@@ -0,0 +1,185 @@
# -*- 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 UserError


class AccountMoveLine(models.Model):
_inherit = 'account.move.line'

@api.multi
def _prepare_analytic_line(self):
""" Note: This method is called only on the move.line that having an analytic account, and
so that should create analytic entries.
"""
values_list = super(AccountMoveLine, self)._prepare_analytic_line()

# 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 (move_line.credit or 0.0) <= (move_line.debit or 0.0) and move_line.product_id.expense_policy not in [False, 'no']:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we use float compare ?

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'))
if sale_line:
values['so_line'] = sale_line.id

return values_list

@api.multi
def _sale_create_reinvoice_sale_line(self):

sale_order_map = self._sale_determine_order()

sale_line_values_to_create = [] # the list of creation values of sale line to create.
existing_sale_line_cache = {} # in the sales_price-delivery case, we can reuse the same sale line. This cache will avoid doing a search each time the case happen
# `map_move_sale_line` is map where
# - key is the move line identifier
# - value is either a sale.order.line record (existing case), or an integer representing the index of the sale line to create in
# the `sale_line_values_to_create` (not existing case, which will happen more often than the first one).
map_move_sale_line = {}

for move_line in self:
sale_order = sale_order_map.get(move_line.id)

# no reinvoice as no sales order was found
if not sale_order:
continue

# raise if the sale order is not currenlty open
if sale_order.state != 'sale':
message_unconfirmed = _('The Sales Order %s linked to the Analytic Account %s must be validated before registering expenses.')
messages = {
'draft': message_unconfirmed,
'sent': message_unconfirmed,
'done': _('The Sales Order %s linked to the Analytic Account %s is currently locked. You cannot register an expense on a locked Sales Order. Please create a new SO linked to this Analytic Account.'),
'cancel': _('The Sales Order %s linked to the Analytic Account %s is cancelled. You cannot register an expense on a cancelled Sales Order.'),
}
raise UserError(messages[sale_order.state] % (sale_order.name, sale_order.analytic_account_id.name))

price = move_line._sale_get_invoice_price(sale_order)

# find the existing sale.line or keep its creation values to process this in batch
sale_line = None
if move_line.product_id.expense_policy == 'sales_price' and move_line.product_id.invoice_policy == 'delivery': # for those case only, we can try to reuse one
map_entry_key = (sale_order.id, move_line.product_id.id, price) # cache entry to limit the call to search
sale_line = existing_sale_line_cache.get(map_entry_key)
if sale_line: # already search, so reuse it. sale_line can be sale.order.line record or index of a "to create values" in `sale_line_values_to_create`
map_move_sale_line[move_line.id] = sale_line
existing_sale_line_cache[map_entry_key] = sale_line
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you can easily understand with the comment, you can directly assign both values
map_move_sale_line[move_line.id] = existing_sale_line_cache[map_entry_key] = sale_line

else: # search for existing sale line
sale_line = self.env['sale.order.line'].search([
('order_id', '=', sale_order.id),
('price_unit', '=', price),
('product_id', '=', move_line.product_id.id),
('is_expense', '=', True),
], limit=1)
if sale_line: # found existing one, so keep the browse record
map_move_sale_line[move_line.id] = existing_sale_line_cache[map_entry_key] = sale_line
else: # should be create, so use the index of creation values instead of browse record
# save value to create it
sale_line_values_to_create.append(move_line._sale_prepare_sale_line_values(sale_order, price))
# store it in the cache of existing ones
existing_sale_line_cache[map_entry_key] = len(sale_line_values_to_create) - 1 # save the index of the value to create sale line
# store it in the map_move_sale_line map
map_move_sale_line[move_line.id] = len(sale_line_values_to_create) - 1 # save the index of the value to create sale line
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same principle


else: # save its value to create it anyway
sale_line_values_to_create.append(move_line._sale_prepare_sale_line_values(sale_order, price))
map_move_sale_line[move_line.id] = len(sale_line_values_to_create) - 1 # save the index of the value to create sale line

# create the sale lines in batch
new_sale_lines = self.env['sale.order.line'].create(sale_line_values_to_create)

# build result map by replacing index with newly created record of sale.order.line
result = {}
for move_line_id, unknown_sale_line in map_move_sale_line.items():
if isinstance(unknown_sale_line, int): # index of newly created sale line
result[move_line_id] = new_sale_lines[unknown_sale_line]
elif isinstance(unknown_sale_line, models.BaseModel): # already record of sale.order.line
result[move_line_id] = unknown_sale_line
return result

@api.multi
def _sale_determine_order(self):
""" 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use dict comprehension as you dict is empty


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

# 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}

@api.multi
def _sale_prepare_sale_line_values(self, order, price):
""" Generate the sale.line creation value from the current move line """
self.ensure_one()
last_so_line = self.env['sale.order.line'].search([('order_id', '=', order.id)], order='sequence desc', limit=1)
last_sequence = last_so_line.sequence + 1 if last_so_line else 100

fpos = order.fiscal_position_id or order.partner_id.property_account_position_id
taxes = fpos.map_tax(self.product_id.taxes_id, self.product_id, order.partner_id)

return {
'order_id': order.id,
'name': self.name,
'sequence': last_sequence,
'price_unit': price,
'tax_id': [x.id for x in taxes],
'discount': 0.0,
'product_id': self.product_id.id,
'product_uom': self.product_uom_id.id,
'product_uom_qty': 0.0,
'is_expense': True,
}

@api.multi
def _sale_get_invoice_price(self, order):
""" Based on the current move line, compute the price to reinvoice the analytic line that is going to be created (so the
price of the sale line).
"""
self.ensure_one()

unit_amount = self.quantity
amount = (self.credit or 0.0) - (self.debit or 0.0)

if self.product_id.expense_policy == 'sales_price':
return self.product_id.with_context(
partner=order.partner_id.id,
date_order=order.date_order,
pricelist=order.pricelist_id.id,
uom=self.product_uom_id.id
).price
if unit_amount == 0.0:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Float compare ? (or float is zero that I think exists)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably, but with which precision ?
As the product uom is not required on account.move.line, should we use the rounding of the UoM and fallback on dp.get_precision('Product Unit of Measure')?

return 0.0

# Prevent unnecessary currency conversion that could be impacted by exchange rate
# fluctuations
if self.company_id.currency_id and amount and self.company_id.currency_id == order.currency_id:
return abs(amount / unit_amount)

price_unit = abs(amount / unit_amount)
currency_id = self.company_id.currency_id
if currency_id and currency_id != order.currency_id:
price_unit = currency_id._convert(price_unit, order.currency_id, order.company_id, order.date_order or fields.Date.today())
return price_unit