1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
|
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.tools.float_utils import float_compare, float_is_zero
from odoo.exceptions import UserError
class AccountMove(models.Model):
_inherit = 'account.move'
def _stock_account_prepare_anglo_saxon_in_lines_vals(self):
''' Prepare values used to create the journal items (account.move.line) corresponding to the price difference
lines for vendor bills.
Example:
Buy a product having a cost of 9 and a supplier price of 10 and being a storable product and having a perpetual
valuation in FIFO. The vendor bill's journal entries looks like:
Account | Debit | Credit
---------------------------------------------------------------
101120 Stock Interim Account (Received) | 10.0 |
---------------------------------------------------------------
101100 Account Payable | | 10.0
---------------------------------------------------------------
This method computes values used to make two additional journal items:
---------------------------------------------------------------
101120 Stock Interim Account (Received) | | 1.0
---------------------------------------------------------------
xxxxxx Price Difference Account | 1.0 |
---------------------------------------------------------------
:return: A list of Python dictionary to be passed to env['account.move.line'].create.
'''
lines_vals_list = []
price_unit_prec = self.env['decimal.precision'].precision_get('Product Price')
for move in self:
if move.move_type not in ('in_invoice', 'in_refund', 'in_receipt') or not move.company_id.anglo_saxon_accounting:
continue
move = move.with_company(move.company_id)
for line in move.invoice_line_ids.filtered(lambda line: line.product_id.type == 'product' and line.product_id.valuation == 'real_time'):
# Filter out lines being not eligible for price difference.
if line.product_id.type != 'product' or line.product_id.valuation != 'real_time':
continue
# Retrieve accounts needed to generate the price difference.
debit_pdiff_account = line.product_id.property_account_creditor_price_difference \
or line.product_id.categ_id.property_account_creditor_price_difference_categ
debit_pdiff_account = move.fiscal_position_id.map_account(debit_pdiff_account)
if not debit_pdiff_account:
continue
if line.product_id.cost_method != 'standard' and line.purchase_line_id:
po_currency = line.purchase_line_id.currency_id
po_company = line.purchase_line_id.company_id
# Retrieve stock valuation moves.
valuation_stock_moves = self.env['stock.move'].search([
('purchase_line_id', '=', line.purchase_line_id.id),
('state', '=', 'done'),
('product_qty', '!=', 0.0),
])
if move.move_type == 'in_refund':
valuation_stock_moves = valuation_stock_moves.filtered(lambda stock_move: stock_move._is_out())
else:
valuation_stock_moves = valuation_stock_moves.filtered(lambda stock_move: stock_move._is_in())
if valuation_stock_moves:
valuation_price_unit_total = 0
valuation_total_qty = 0
for val_stock_move in valuation_stock_moves:
# In case val_stock_move is a return move, its valuation entries have been made with the
# currency rate corresponding to the original stock move
valuation_date = val_stock_move.origin_returned_move_id.date or val_stock_move.date
svl = val_stock_move.with_context(active_test=False).mapped('stock_valuation_layer_ids').filtered(lambda l: l.quantity)
layers_qty = sum(svl.mapped('quantity'))
layers_values = sum(svl.mapped('value'))
valuation_price_unit_total += line.company_currency_id._convert(
layers_values, move.currency_id,
move.company_id, valuation_date, round=False,
)
valuation_total_qty += layers_qty
if float_is_zero(valuation_total_qty, precision_rounding=line.product_uom_id.rounding or line.product_id.uom_id.rounding):
raise UserError(_('Odoo is not able to generate the anglo saxon entries. The total valuation of %s is zero.') % line.product_id.display_name)
valuation_price_unit = valuation_price_unit_total / valuation_total_qty
valuation_price_unit = line.product_id.uom_id._compute_price(valuation_price_unit, line.product_uom_id)
elif line.product_id.cost_method == 'fifo':
# In this condition, we have a real price-valuated product which has not yet been received
valuation_price_unit = po_currency._convert(
line.purchase_line_id.price_unit, move.currency_id,
po_company, move.date, round=False,
)
else:
# For average/fifo/lifo costing method, fetch real cost price from incoming moves.
price_unit = line.purchase_line_id.product_uom._compute_price(line.purchase_line_id.price_unit, line.product_uom_id)
valuation_price_unit = po_currency._convert(
price_unit, move.currency_id,
po_company, move.date, round=False
)
else:
# Valuation_price unit is always expressed in invoice currency, so that it can always be computed with the good rate
price_unit = line.product_id.uom_id._compute_price(line.product_id.standard_price, line.product_uom_id)
valuation_price_unit = line.company_currency_id._convert(
price_unit, move.currency_id,
move.company_id, fields.Date.today(), round=False
)
price_unit = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
if line.tax_ids and line.quantity:
# We do not want to round the price unit since :
# - It does not follow the currency precision
# - It may include a discount
# Since compute_all still rounds the total, we use an ugly workaround:
# multiply then divide the price unit.
price_unit *= line.quantity
price_unit = line.tax_ids.with_context(round=False, force_sign=move._get_tax_force_sign()).compute_all(
price_unit, currency=move.currency_id, quantity=1.0, is_refund=move.move_type == 'in_refund')['total_excluded']
price_unit /= line.quantity
price_unit_val_dif = price_unit - valuation_price_unit
price_subtotal = line.quantity * price_unit_val_dif
# We consider there is a price difference if the subtotal is not zero. In case a
# discount has been applied, we can't round the price unit anymore, and hence we
# can't compare them.
if (
not move.currency_id.is_zero(price_subtotal)
and float_compare(line["price_unit"], line.price_unit, precision_digits=price_unit_prec) == 0
):
# Add price difference account line.
vals = {
'name': line.name[:64],
'move_id': move.id,
'currency_id': line.currency_id.id,
'product_id': line.product_id.id,
'product_uom_id': line.product_uom_id.id,
'quantity': line.quantity,
'price_unit': price_unit_val_dif,
'price_subtotal': line.quantity * price_unit_val_dif,
'account_id': debit_pdiff_account.id,
'analytic_account_id': line.analytic_account_id.id,
'analytic_tag_ids': [(6, 0, line.analytic_tag_ids.ids)],
'exclude_from_invoice_tab': True,
'is_anglo_saxon_line': True,
'partner_id': line.partner_id.id,
}
vals.update(line._get_fields_onchange_subtotal(price_subtotal=vals['price_subtotal']))
lines_vals_list.append(vals)
# Correct the amount of the current line.
vals = {
'name': line.name[:64],
'move_id': move.id,
'currency_id': line.currency_id.id,
'product_id': line.product_id.id,
'product_uom_id': line.product_uom_id.id,
'quantity': line.quantity,
'price_unit': -price_unit_val_dif,
'price_subtotal': line.quantity * -price_unit_val_dif,
'account_id': line.account_id.id,
'analytic_account_id': line.analytic_account_id.id,
'analytic_tag_ids': [(6, 0, line.analytic_tag_ids.ids)],
'exclude_from_invoice_tab': True,
'is_anglo_saxon_line': True,
'partner_id': line.partner_id.id,
}
vals.update(line._get_fields_onchange_subtotal(price_subtotal=vals['price_subtotal']))
lines_vals_list.append(vals)
return lines_vals_list
def _post(self, soft=True):
# OVERRIDE
# Create additional price difference lines for vendor bills.
if self._context.get('move_reverse_cancel'):
return super()._post(soft)
self.env['account.move.line'].create(self._stock_account_prepare_anglo_saxon_in_lines_vals())
return super()._post(soft)
def _stock_account_get_last_step_stock_moves(self):
""" Overridden from stock_account.
Returns the stock moves associated to this invoice."""
rslt = super(AccountMove, self)._stock_account_get_last_step_stock_moves()
for invoice in self.filtered(lambda x: x.move_type == 'in_invoice'):
rslt += invoice.mapped('invoice_line_ids.purchase_line_id.move_ids').filtered(lambda x: x.state == 'done' and x.location_id.usage == 'supplier')
for invoice in self.filtered(lambda x: x.move_type == 'in_refund'):
rslt += invoice.mapped('invoice_line_ids.purchase_line_id.move_ids').filtered(lambda x: x.state == 'done' and x.location_dest_id.usage == 'supplier')
return rslt
|