[ADD]:Added Upstream Patch of stock_account
This commit is contained in:
parent
fea006c468
commit
a861b436a1
@ -26,17 +26,6 @@
|
||||
<field name="value">manual_periodic</field>
|
||||
<field name="type">selection</field>
|
||||
</record>
|
||||
<record id="action_stock_account_valuation_report" model="ir.actions.server">
|
||||
<field name="name">Stock Valuation Report</field>
|
||||
<field name="type">ir.actions.server</field>
|
||||
<field name="state">code</field>
|
||||
<field name="model_id" ref="product.model_product_product"></field>
|
||||
<field name="binding_model_id" ref="product.model_product_product"></field>
|
||||
<field name="code">
|
||||
env['stock.move']._run_fifo_vacuum()
|
||||
action = env.ref('stock_account.product_valuation_action').read()[0]
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</flectra>
|
||||
|
||||
|
@ -55,10 +55,10 @@
|
||||
'fields_id': field_id,
|
||||
'value': value,
|
||||
}
|
||||
properties = PropertyObj.search([('name', '=', record), ('company_id', '=', company.id)])
|
||||
if properties:
|
||||
# the property exist: modify it
|
||||
properties.write(vals)
|
||||
else:
|
||||
properties = PropertyObj.search([('name', '=', record), ('company_id', '=', company.id)], limit=1)
|
||||
if not properties:
|
||||
# create the property
|
||||
PropertyObj.create(vals)
|
||||
elif not properties.value_reference:
|
||||
# update the property if False
|
||||
properties.write(vals)
|
||||
|
@ -63,7 +63,7 @@ class ProductTemplate(models.Model):
|
||||
if self.property_cost_method == 'fifo' and self.cost_method in ['average', 'standard']:
|
||||
# Cannot use the `stock_value` computed field as it's already invalidated when
|
||||
# entering this method.
|
||||
valuation = sum([variant._sum_remaining_values() for variant in self.product_variant_ids])
|
||||
valuation = sum([variant._sum_remaining_values()[0] for variant in self.product_variant_ids])
|
||||
qty_available = self.with_context(company_owned=True).qty_available
|
||||
if qty_available:
|
||||
self.standard_price = valuation / qty_available
|
||||
@ -85,16 +85,7 @@ class ProductTemplate(models.Model):
|
||||
|
||||
@api.multi
|
||||
def action_open_product_moves(self):
|
||||
self.ensure_one()
|
||||
action = self.env.ref('stock_account.stock_move_valuation_action').read()[0]
|
||||
action['domain'] = [('product_tmpl_id', '=', self.id)]
|
||||
action['context'] = {
|
||||
'search_default_outgoing': True,
|
||||
'search_default_incoming': True,
|
||||
'search_default_done': True,
|
||||
'is_avg': self.cost_method == 'average',
|
||||
}
|
||||
return action
|
||||
pass
|
||||
|
||||
@api.multi
|
||||
def get_product_accounts(self, fiscal_pos=None):
|
||||
@ -111,6 +102,12 @@ class ProductProduct(models.Model):
|
||||
|
||||
stock_value = fields.Float(
|
||||
'Value', compute='_compute_stock_value')
|
||||
qty_at_date = fields.Float(
|
||||
'Quantity', compute='_compute_stock_value')
|
||||
stock_fifo_real_time_aml_ids = fields.Many2many(
|
||||
'account.move.line', compute='_compute_stock_value')
|
||||
stock_fifo_manual_move_ids = fields.Many2many(
|
||||
'stock.move', compute='_compute_stock_value')
|
||||
|
||||
@api.multi
|
||||
def do_change_standard_price(self, new_price, account_id):
|
||||
@ -148,11 +145,13 @@ class ProductProduct(models.Model):
|
||||
'account_id': debit_account_id,
|
||||
'debit': abs(diff * qty_available),
|
||||
'credit': 0,
|
||||
'product_id': product.id,
|
||||
}), (0, 0, {
|
||||
'name': _('Standard Price changed - %s') % (product.display_name),
|
||||
'account_id': credit_account_id,
|
||||
'debit': 0,
|
||||
'credit': abs(diff * qty_available),
|
||||
'product_id': product.id,
|
||||
})],
|
||||
}
|
||||
move = AccountMove.create(move_vals)
|
||||
@ -173,29 +172,98 @@ class ProductProduct(models.Model):
|
||||
StockMove = self.env['stock.move']
|
||||
domain = [('product_id', '=', self.id)] + StockMove._get_all_base_domain()
|
||||
moves = StockMove.search(domain)
|
||||
return sum(moves.mapped('remaining_value'))
|
||||
return sum(moves.mapped('remaining_value')), moves
|
||||
|
||||
@api.multi
|
||||
@api.depends('stock_move_ids.product_qty', 'stock_move_ids.state', 'product_tmpl_id.cost_method')
|
||||
@api.depends('stock_move_ids.product_qty', 'stock_move_ids.state', 'stock_move_ids.remaining_value', 'product_tmpl_id.cost_method', 'product_tmpl_id.standard_price', 'product_tmpl_id.property_valuation', 'product_tmpl_id.categ_id.property_valuation')
|
||||
def _compute_stock_value(self):
|
||||
StockMove = self.env['stock.move']
|
||||
to_date = self.env.context.get('to_date')
|
||||
|
||||
self.env['account.move.line'].check_access_rights('read')
|
||||
fifo_automated_values = {}
|
||||
query = """SELECT aml.product_id, aml.account_id, sum(aml.debit) - sum(aml.credit), sum(quantity), array_agg(aml.id)
|
||||
FROM account_move_line AS aml
|
||||
WHERE aml.product_id IS NOT NULL AND aml.company_id=%%s %s
|
||||
GROUP BY aml.product_id, aml.account_id"""
|
||||
params = (self.env.user.company_id.id,)
|
||||
if to_date:
|
||||
query = query % ('AND aml.date <= %s',)
|
||||
params = params + (to_date,)
|
||||
else:
|
||||
query = query % ('',)
|
||||
self.env.cr.execute(query, params=params)
|
||||
|
||||
res = self.env.cr.fetchall()
|
||||
for row in res:
|
||||
fifo_automated_values[(row[0], row[1])] = (row[2], row[3], list(row[4]))
|
||||
|
||||
for product in self:
|
||||
if product.cost_method in ['standard', 'average']:
|
||||
product.stock_value = product.standard_price * product.with_context(company_owned=True).qty_available
|
||||
qty_available = product.with_context(company_owned=True, owner_id=False).qty_available
|
||||
price_used = product.standard_price
|
||||
if to_date:
|
||||
price_used = product.get_history_price(
|
||||
self.env.user.company_id.id,
|
||||
date=to_date,
|
||||
)
|
||||
product.stock_value = price_used * qty_available
|
||||
product.qty_at_date = qty_available
|
||||
elif product.cost_method == 'fifo':
|
||||
product.stock_value = product._sum_remaining_values()
|
||||
if to_date:
|
||||
if product.product_tmpl_id.valuation == 'manual_periodic':
|
||||
domain = [('product_id', '=', product.id), ('date', '<=', to_date)] + StockMove._get_all_base_domain()
|
||||
moves = StockMove.search(domain)
|
||||
product.stock_value = sum(moves.mapped('value'))
|
||||
product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available
|
||||
product.stock_fifo_manual_move_ids = StockMove.browse(moves.ids)
|
||||
elif product.product_tmpl_id.valuation == 'real_time':
|
||||
valuation_account_id = product.categ_id.property_stock_valuation_account_id.id
|
||||
value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, [])
|
||||
product.stock_value = value
|
||||
product.qty_at_date = quantity
|
||||
product.stock_fifo_real_time_aml_ids = self.env['account.move.line'].browse(aml_ids)
|
||||
else:
|
||||
product.stock_value, moves = product._sum_remaining_values()
|
||||
product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available
|
||||
if product.product_tmpl_id.valuation == 'manual_periodic':
|
||||
product.stock_fifo_manual_move_ids = moves
|
||||
elif product.product_tmpl_id.valuation == 'real_time':
|
||||
valuation_account_id = product.categ_id.property_stock_valuation_account_id.id
|
||||
value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, [])
|
||||
product.stock_fifo_real_time_aml_ids = self.env['account.move.line'].browse(aml_ids)
|
||||
|
||||
def action_valuation_at_date_details(self):
|
||||
""" Returns an action with either a list view of all the valued stock moves of `self` if the
|
||||
valuation is set as manual or a list view of all the account move lines if the valuation is
|
||||
set as automated.
|
||||
"""
|
||||
self.ensure_one()
|
||||
to_date = self.env.context.get('to_date')
|
||||
action = {
|
||||
'name': _('Valuation at date'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_type': 'form',
|
||||
'view_mode': 'tree,form',
|
||||
'context': self.env.context,
|
||||
}
|
||||
if self.valuation == 'real_time':
|
||||
action['res_model'] = 'account.move.line'
|
||||
action['domain'] = [('id', 'in', self.with_context(to_date=to_date).stock_fifo_real_time_aml_ids.ids)]
|
||||
tree_view_ref = self.env.ref('stock_account.view_stock_account_aml')
|
||||
form_view_ref = self.env.ref('account.view_move_line_form')
|
||||
action['views'] = [(tree_view_ref.id, 'tree'), (form_view_ref.id, 'form')]
|
||||
else:
|
||||
action['res_model'] = 'stock.move'
|
||||
action['domain'] = [('id', 'in', self.with_context(to_date=to_date).stock_fifo_manual_move_ids.ids)]
|
||||
tree_view_ref = self.env.ref('stock_account.view_move_tree_valuation_at_date')
|
||||
form_view_ref = self.env.ref('stock.view_move_form')
|
||||
action['views'] = [(tree_view_ref.id, 'tree'), (form_view_ref.id, 'form')]
|
||||
return action
|
||||
|
||||
@api.multi
|
||||
def action_open_product_moves(self):
|
||||
self.ensure_one()
|
||||
action = self.env.ref('stock_account.stock_move_valuation_action').read()[0]
|
||||
action['domain'] = [('product_id', '=', self.id)]
|
||||
action['context'] = {
|
||||
'search_default_outgoing': True,
|
||||
'search_default_incoming': True,
|
||||
'search_default_done': True,
|
||||
'is_avg': self.cost_method == 'average',
|
||||
}
|
||||
return action
|
||||
pass
|
||||
|
||||
@api.model
|
||||
def _anglo_saxon_sale_move_lines(self, name, product, uom, qty, price_unit, currency=False, amount_currency=False, fiscal_position=False, account_analytic=False, analytic_tags=False):
|
||||
@ -260,6 +328,28 @@ class ProductProduct(models.Model):
|
||||
return price or 0.0
|
||||
return self.uom_id._compute_price(price, uom)
|
||||
|
||||
def _compute_average_price(self, qty_done, quantity, moves):
|
||||
average_price_unit = 0
|
||||
qty_delivered = 0
|
||||
invoiced_qty = 0
|
||||
for move in moves:
|
||||
if move.state != 'done':
|
||||
continue
|
||||
invoiced_qty += move.product_qty
|
||||
if invoiced_qty <= qty_done:
|
||||
continue
|
||||
qty_to_consider = move.product_qty
|
||||
if invoiced_qty - move.product_qty < qty_done:
|
||||
qty_to_consider = invoiced_qty - qty_done
|
||||
qty_to_consider = min(qty_to_consider, quantity - qty_delivered)
|
||||
qty_delivered += qty_to_consider
|
||||
# `move.price_unit` is negative if the move is out and positive if the move is
|
||||
# dropshipped. Use its absolute value to compute the average price unit.
|
||||
average_price_unit = (average_price_unit * (qty_delivered - qty_to_consider) + abs(move.price_unit) * qty_to_consider) / qty_delivered
|
||||
if qty_delivered == quantity:
|
||||
break
|
||||
return average_price_unit
|
||||
|
||||
|
||||
class ProductCategory(models.Model):
|
||||
_inherit = 'product.category'
|
||||
@ -311,4 +401,3 @@ class ProductCategory(models.Model):
|
||||
'message': _("Changing your cost method is an important change that will impact your inventory valuation. Are you sure you want to make that change?"),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,34 +74,42 @@ class StockMoveLine(models.Model):
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
""" When editing a done stock.move.line, we impact the valuation. Users may increase or
|
||||
decrease the `qty_done` field. There are three cost method available: standard, average
|
||||
and fifo. We implement the logic in a similar way for standard and average: increase
|
||||
or decrease the original value with the standard or average price of today. In fifo, we
|
||||
have a different logic wheter the move is incoming or outgoing. If the move is incoming, we
|
||||
update the value and remaining_value/qty with the unit price of the move. If the move is
|
||||
outgoing and the user increases qty_done, we call _run_fifo and it'll consume layer(s) in
|
||||
the stack the same way a new outgoing move would have done. If the move is outoing and the
|
||||
user decreases qty_done, we either increase the last receipt candidate if one is found or
|
||||
we decrease the value with the last fifo price.
|
||||
"""
|
||||
if 'qty_done' in vals:
|
||||
# We need to update the `value`, `remaining_value` and `remaining_qty` on the linked
|
||||
# stock move.
|
||||
moves_to_update = {}
|
||||
for move_line in self.filtered(lambda ml: ml.state == 'done' and (ml.move_id._is_in() or ml.move_id._is_out())):
|
||||
moves_to_update[move_line.move_id] = vals['qty_done'] - move_line.qty_done
|
||||
|
||||
for move_id, qty_difference in moves_to_update.items():
|
||||
# more/less units are available, update `remaining_value` and
|
||||
# `remaining_qty` on the linked stock move.
|
||||
move_vals = {'remaining_qty': move_id.remaining_qty + qty_difference}
|
||||
new_remaining_value = 0
|
||||
move_vals = {}
|
||||
if move_id.product_id.cost_method in ['standard', 'average']:
|
||||
correction_value = qty_difference * move_id.product_id.standard_price
|
||||
if move_id._is_in():
|
||||
move_vals['value'] = move_id.value + correction_value
|
||||
elif move_id._is_out():
|
||||
move_vals['value'] = move_id.value - correction_value
|
||||
move_vals.pop('remaining_qty')
|
||||
else:
|
||||
# FIFO handling
|
||||
if move_id._is_in():
|
||||
correction_value = qty_difference * move_id.price_unit
|
||||
new_remaining_value = move_id.remaining_value + correction_value
|
||||
move_vals['value'] = move_id.value + correction_value
|
||||
move_vals['remaining_qty'] = move_id.remaining_qty + qty_difference
|
||||
move_vals['remaining_value'] = move_id.remaining_value + correction_value
|
||||
elif move_id._is_out() and qty_difference > 0:
|
||||
# send more, run fifo again
|
||||
correction_value = self.env['stock.move']._run_fifo(move_id, quantity=qty_difference)
|
||||
new_remaining_value = move_id.remaining_value + correction_value
|
||||
move_vals.pop('remaining_qty')
|
||||
# no need to adapt `remaining_qty` and `remaining_value` as `_run_fifo` took care of it
|
||||
move_vals['value'] = move_id.value - correction_value
|
||||
elif move_id._is_out() and qty_difference < 0:
|
||||
# fake return, find the last receipt and augment its qties
|
||||
candidates_receipt = self.env['stock.move'].search(move_id._get_in_domain(), order='date, id desc', limit=1)
|
||||
if candidates_receipt:
|
||||
candidates_receipt.write({
|
||||
@ -111,15 +119,11 @@ class StockMoveLine(models.Model):
|
||||
correction_value = qty_difference * candidates_receipt.price_unit
|
||||
else:
|
||||
correction_value = qty_difference * move_id.product_id.standard_price
|
||||
move_vals.pop('remaining_qty')
|
||||
if move_id._is_out():
|
||||
move_vals['remaining_value'] = new_remaining_value if new_remaining_value < 0 else 0
|
||||
else:
|
||||
move_vals['remaining_value'] = new_remaining_value
|
||||
move_vals['value'] = move_id.value - correction_value
|
||||
move_id.write(move_vals)
|
||||
|
||||
if move_id.product_id.valuation == 'real_time':
|
||||
move_id.with_context(force_valuation_amount=correction_value)._account_entry_move()
|
||||
move_id.with_context(force_valuation_amount=correction_value, forced_quantity=qty_difference)._account_entry_move()
|
||||
if qty_difference > 0:
|
||||
move_id.product_price_update_before_done(forced_qty=qty_difference)
|
||||
return super(StockMoveLine, self).write(vals)
|
||||
@ -210,13 +214,23 @@ class StockMove(models.Model):
|
||||
|
||||
@api.model
|
||||
def _run_fifo(self, move, quantity=None):
|
||||
""" Value `move` according to the FIFO rule, meaning we consume the
|
||||
oldest receipt first. Candidates receipts are marked consumed or free
|
||||
thanks to their `remaining_qty` and `remaining_value` fields.
|
||||
By definition, `move` should be an outgoing stock move.
|
||||
|
||||
:param quantity: quantity to value instead of `move.product_qty`
|
||||
:returns: valued amount in absolute
|
||||
"""
|
||||
move.ensure_one()
|
||||
# Find back incoming stock moves (called candidates here) to value this move.
|
||||
|
||||
# Deal with possible move lines that do not impact the valuation.
|
||||
valued_move_lines = move.move_line_ids.filtered(lambda ml: ml.location_id._should_be_valued() and not ml.location_dest_id._should_be_valued() and not ml.owner_id)
|
||||
valued_quantity = 0
|
||||
for valued_move_line in valued_move_lines:
|
||||
valued_quantity += valued_move_line.product_uom_id._compute_quantity(valued_move_line.qty_done, move.product_id.uom_id)
|
||||
|
||||
# Find back incoming stock moves (called candidates here) to value this move.
|
||||
qty_to_take_on_candidates = quantity or valued_quantity
|
||||
candidates = move.product_id._get_fifo_candidates_in_move()
|
||||
new_standard_price = 0
|
||||
@ -246,7 +260,7 @@ class StockMove(models.Model):
|
||||
|
||||
# Update the standard price with the price of the last used candidate, if any.
|
||||
if new_standard_price and move.product_id.cost_method == 'fifo':
|
||||
move.product_id.standard_price = new_standard_price
|
||||
move.product_id.sudo().standard_price = new_standard_price
|
||||
|
||||
# If there's still quantity to value but we're out of candidates, we fall in the
|
||||
# negative stock use case. We chose to value the out move at the price of the
|
||||
@ -259,11 +273,12 @@ class StockMove(models.Model):
|
||||
elif qty_to_take_on_candidates > 0:
|
||||
last_fifo_price = new_standard_price or move.product_id.standard_price
|
||||
negative_stock_value = last_fifo_price * -qty_to_take_on_candidates
|
||||
tmp_value += abs(negative_stock_value)
|
||||
vals = {
|
||||
'remaining_qty': move.remaining_qty + -qty_to_take_on_candidates,
|
||||
'remaining_value': move.remaining_value + negative_stock_value,
|
||||
'value': -tmp_value + negative_stock_value,
|
||||
'price_unit': (-tmp_value + negative_stock_value) / (move.product_qty or quantity),
|
||||
'value': -tmp_value,
|
||||
'price_unit': -1 * last_fifo_price,
|
||||
}
|
||||
move.write(vals)
|
||||
return tmp_value
|
||||
@ -297,7 +312,9 @@ class StockMove(models.Model):
|
||||
self.write(vals)
|
||||
elif self._is_out():
|
||||
valued_move_lines = self.move_line_ids.filtered(lambda ml: ml.location_id._should_be_valued() and not ml.location_dest_id._should_be_valued() and not ml.owner_id)
|
||||
valued_quantity = sum(valued_move_lines.mapped('qty_done'))
|
||||
valued_quantity = 0
|
||||
for valued_move_line in valued_move_lines:
|
||||
valued_quantity += valued_move_line.product_uom_id._compute_quantity(valued_move_line.qty_done, self.product_id.uom_id)
|
||||
self.env['stock.move']._run_fifo(self, quantity=quantity)
|
||||
if self.product_id.cost_method in ['standard', 'average']:
|
||||
curr_rounding = self.company_id.currency_id.rounding
|
||||
@ -355,6 +372,7 @@ class StockMove(models.Model):
|
||||
product_tot_qty_available = move.product_id.qty_available + tmpl_dict[move.product_id.id]
|
||||
rounding = move.product_id.uom_id.rounding
|
||||
|
||||
qty_done = 0.0
|
||||
if float_is_zero(product_tot_qty_available, precision_rounding=rounding):
|
||||
new_std_price = move._get_price_unit()
|
||||
elif float_is_zero(product_tot_qty_available + move.product_qty, precision_rounding=rounding):
|
||||
@ -362,10 +380,11 @@ class StockMove(models.Model):
|
||||
else:
|
||||
# Get the standard price
|
||||
amount_unit = std_price_update.get((move.company_id.id, move.product_id.id)) or move.product_id.standard_price
|
||||
qty = forced_qty or move.product_qty
|
||||
new_std_price = ((amount_unit * product_tot_qty_available) + (move._get_price_unit() * qty)) / (product_tot_qty_available + move.product_qty)
|
||||
qty_done = move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id)
|
||||
qty = forced_qty or qty_done
|
||||
new_std_price = ((amount_unit * product_tot_qty_available) + (move._get_price_unit() * qty)) / (product_tot_qty_available + qty_done)
|
||||
|
||||
tmpl_dict[move.product_id.id] += move.product_qty
|
||||
tmpl_dict[move.product_id.id] += qty_done
|
||||
# Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products
|
||||
move.product_id.with_context(force_company=move.company_id.id).sudo().write({'standard_price': new_std_price})
|
||||
std_price_update[move.company_id.id, move.product_id.id] = new_std_price
|
||||
@ -388,12 +407,14 @@ class StockMove(models.Model):
|
||||
if not candidates:
|
||||
continue
|
||||
qty_to_take_on_candidates = abs(move.remaining_qty)
|
||||
qty_taken_on_candidates = 0
|
||||
tmp_value = 0
|
||||
for candidate in candidates:
|
||||
if candidate.remaining_qty <= qty_to_take_on_candidates:
|
||||
qty_taken_on_candidate = candidate.remaining_qty
|
||||
else:
|
||||
qty_taken_on_candidate = qty_to_take_on_candidates
|
||||
qty_taken_on_candidates += qty_taken_on_candidate
|
||||
|
||||
value_taken_on_candidate = qty_taken_on_candidate * candidate.price_unit
|
||||
candidate_vals = {
|
||||
@ -407,27 +428,22 @@ class StockMove(models.Model):
|
||||
if qty_to_take_on_candidates == 0:
|
||||
break
|
||||
|
||||
remaining_value_before_vacuum = move.remaining_value
|
||||
|
||||
# If `remaining_qty` should be updated to 0, we wipe `remaining_value`. If it was set
|
||||
# it was only used to infer the correction entry anyway.
|
||||
new_remaining_qty = -qty_to_take_on_candidates
|
||||
new_remaining_value = 0 if not new_remaining_qty else move.remaining_value + tmp_value
|
||||
# When working with `price_unit`, beware that out move are negative.
|
||||
move_price_unit = move.price_unit if move._is_out() else -1 * move.price_unit
|
||||
# Get the estimated value we will correct.
|
||||
remaining_value_before_vacuum = qty_taken_on_candidates * move_price_unit
|
||||
new_remaining_qty = move.remaining_qty + qty_taken_on_candidates
|
||||
new_remaining_value = new_remaining_qty * abs(move.price_unit)
|
||||
|
||||
corrected_value = remaining_value_before_vacuum + tmp_value
|
||||
move.write({
|
||||
'remaining_value': new_remaining_value,
|
||||
'remaining_qty': new_remaining_qty,
|
||||
'value': move.value - corrected_value,
|
||||
})
|
||||
|
||||
if move.product_id.valuation == 'real_time':
|
||||
# If `move.remaining_value` is negative, it means that we initially valued this move at
|
||||
# an estimated price *and* posted an entry. `tmp_value` is the real value we took to
|
||||
# compensate and should always be positive, but if the remaining value is still negative
|
||||
# we have to take care to not overvalue by decreasing the correction entry by what's
|
||||
# already been posted.
|
||||
corrected_value = tmp_value
|
||||
if remaining_value_before_vacuum < 0:
|
||||
corrected_value += remaining_value_before_vacuum
|
||||
|
||||
# If `corrected_value` is 0, absolutely do *not* call `_account_entry_move`. We
|
||||
# force the amount in the context, but in the case it is 0 it'll create an entry
|
||||
# for the entire cost of the move. This case happens when the candidates moves
|
||||
@ -441,9 +457,9 @@ class StockMove(models.Model):
|
||||
# The correction should behave as a return too. As `_account_entry_move`
|
||||
# will post the natural values for an IN move (credit IN account, debit
|
||||
# OUT one), we inverse the sign to create the correct entries.
|
||||
move.with_context(force_valuation_amount=-corrected_value)._account_entry_move()
|
||||
move.with_context(force_valuation_amount=-corrected_value, forced_quantity=0)._account_entry_move()
|
||||
else:
|
||||
move.with_context(force_valuation_amount=corrected_value)._account_entry_move()
|
||||
move.with_context(force_valuation_amount=corrected_value, forced_quantity=0)._account_entry_move()
|
||||
|
||||
@api.model
|
||||
def _run_fifo_vacuum(self):
|
||||
@ -502,6 +518,11 @@ class StockMove(models.Model):
|
||||
else:
|
||||
valuation_amount = cost
|
||||
|
||||
if self._context.get('forced_ref'):
|
||||
ref = self._context['forced_ref']
|
||||
else:
|
||||
ref = self.picking_id.name
|
||||
|
||||
# the standard_price of the product may be in another decimal precision, or not compatible with the coinage of
|
||||
# the company currency... so we need to use round() before creating the accounting entries.
|
||||
debit_value = self.company_id.currency_id.round(valuation_amount)
|
||||
@ -511,24 +532,13 @@ class StockMove(models.Model):
|
||||
raise UserError(_("The cost of %s is currently equal to 0. Change the cost or the configuration of your product to avoid an incorrect valuation.") % (self.product_id.name,))
|
||||
credit_value = debit_value
|
||||
|
||||
if self.product_id.cost_method == 'average' and self.company_id.anglo_saxon_accounting:
|
||||
# in case of a supplier return in anglo saxon mode, for products in average costing method, the stock_input
|
||||
# account books the real purchase price, while the stock account books the average price. The difference is
|
||||
# booked in the dedicated price difference account.
|
||||
if self.location_dest_id.usage == 'supplier' and self.origin_returned_move_id and self.origin_returned_move_id.purchase_line_id:
|
||||
debit_value = self.origin_returned_move_id.price_unit * qty
|
||||
# in case of a customer return in anglo saxon mode, for products in average costing method, the stock valuation
|
||||
# is made using the original average price to negate the delivery effect.
|
||||
if self.location_id.usage == 'customer' and self.origin_returned_move_id:
|
||||
debit_value = self.origin_returned_move_id.price_unit * qty
|
||||
credit_value = debit_value
|
||||
partner_id = (self.picking_id.partner_id and self.env['res.partner']._find_accounting_partner(self.picking_id.partner_id).id) or False
|
||||
debit_line_vals = {
|
||||
'name': self.name,
|
||||
'product_id': self.product_id.id,
|
||||
'quantity': qty,
|
||||
'product_uom_id': self.product_id.uom_id.id,
|
||||
'ref': self.picking_id.name,
|
||||
'ref': ref,
|
||||
'partner_id': partner_id,
|
||||
'debit': debit_value if debit_value > 0 else 0,
|
||||
'credit': -debit_value if debit_value < 0 else 0,
|
||||
@ -539,7 +549,7 @@ class StockMove(models.Model):
|
||||
'product_id': self.product_id.id,
|
||||
'quantity': qty,
|
||||
'product_uom_id': self.product_id.uom_id.id,
|
||||
'ref': self.picking_id.name,
|
||||
'ref': ref,
|
||||
'partner_id': partner_id,
|
||||
'credit': credit_value if credit_value > 0 else 0,
|
||||
'debit': -credit_value if credit_value < 0 else 0,
|
||||
@ -559,7 +569,7 @@ class StockMove(models.Model):
|
||||
'product_id': self.product_id.id,
|
||||
'quantity': qty,
|
||||
'product_uom_id': self.product_id.uom_id.id,
|
||||
'ref': self.picking_id.name,
|
||||
'ref': ref,
|
||||
'partner_id': partner_id,
|
||||
'credit': diff_amount > 0 and diff_amount or 0,
|
||||
'debit': diff_amount < 0 and -diff_amount or 0,
|
||||
@ -571,14 +581,26 @@ class StockMove(models.Model):
|
||||
def _create_account_move_line(self, credit_account_id, debit_account_id, journal_id):
|
||||
self.ensure_one()
|
||||
AccountMove = self.env['account.move']
|
||||
move_lines = self._prepare_account_move_line(self.product_qty, abs(self.value), credit_account_id, debit_account_id)
|
||||
quantity = self.env.context.get('forced_quantity', self.product_qty)
|
||||
quantity = quantity if self._is_in() else -1 * quantity
|
||||
|
||||
# Make an informative `ref` on the created account move to differentiate between classic
|
||||
# movements, vacuum and edition of past moves.
|
||||
ref = self.picking_id.name
|
||||
if self.env.context.get('force_valuation_amount'):
|
||||
if self.env.context.get('forced_quantity') == 0:
|
||||
ref = 'Revaluation of %s (negative inventory)' % ref
|
||||
elif self.env.context.get('forced_quantity') is not None:
|
||||
ref = 'Correction of %s (modification of past move)' % ref
|
||||
|
||||
move_lines = self.with_context(forced_ref=ref)._prepare_account_move_line(quantity, abs(self.value), credit_account_id, debit_account_id)
|
||||
if move_lines:
|
||||
date = self._context.get('force_period_date', fields.Date.context_today(self))
|
||||
new_account_move = AccountMove.create({
|
||||
new_account_move = AccountMove.sudo().create({
|
||||
'journal_id': journal_id,
|
||||
'line_ids': move_lines,
|
||||
'date': date,
|
||||
'ref': self.picking_id.name,
|
||||
'ref': ref,
|
||||
'stock_move_id': self.id,
|
||||
})
|
||||
new_account_move.post()
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -87,96 +87,20 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_valuation_tree" model="ir.ui.view">
|
||||
<field name="name">product.valuation.tree</field>
|
||||
<field name="model">product.product</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="display_name" string="Product"/>
|
||||
<field name="qty_available" context="{'company_owned': True}" string="Quantity on Hand"/>
|
||||
<field name="uom_id" groups="product.group_uom"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="stock_value" sum="Stock Valuation" widget="monetary" string="Total Value"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="product_valuation_action" model="ir.actions.act_window">
|
||||
<field name="name">Product Valuation</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">product.product</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_id" ref="product_valuation_tree"/>
|
||||
<field name="view_id" ref="view_stock_product_tree2"/>
|
||||
<field name="domain">[('type', '=', 'product'), ('qty_available', '!=', 0)]</field>
|
||||
<field name="context">{}</field>
|
||||
<field name="context">{'company_owned': True}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
If there are products, you will see its name and valuation.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_valuation_form_view" model="ir.ui.view">
|
||||
<field name="name">product.product</field>
|
||||
<field name="model">product.product</field>
|
||||
<field name="inherit_id" ref="stock.product_form_view_procurement_button"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='action_view_stock_move_lines']" position="after">
|
||||
<button string="Inventory Valuation" type="object" name="action_open_product_moves" class="oe_stat_button" icon="fa-cubes" attrs="{'invisible': ['|', ('cost_method', '=', 'standard'), ('id', '=', False)]}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_template_valuation_form_view" model="ir.ui.view">
|
||||
<field name="name">product.template</field>
|
||||
<field name="model">product.template</field>
|
||||
<field name="inherit_id" ref="stock.product_template_form_view_procurement_button"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='action_view_stock_move_lines']" position="after">
|
||||
<button string="Inventory Valuation" type="object" name="action_open_product_moves" class="oe_stat_button" icon="fa-cubes" attrs="{'invisible': [('cost_method', '=', 'standard')]}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- stock move valuation view -->
|
||||
<record id="view_move_tree_valuation" model="ir.ui.view">
|
||||
<field name="name">stock.move.tree.valuation</field>
|
||||
<field name="model">stock.move</field>
|
||||
<field eval="8" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<tree decoration-muted="state == 'cancel'" decoration-danger="(state not in ('cancel','done')) and date > current_date" string="Moves" create="0">
|
||||
<field name="name"/>
|
||||
<field name="picking_id" string="Reference"/>
|
||||
<field name="origin"/>
|
||||
<field name="picking_type_id" invisible="1"/>
|
||||
<field name="create_date" invisible="1" groups="base.group_no_one"/>
|
||||
<field name="product_id"/>
|
||||
<field name="location_id" groups="stock.group_stock_multi_locations"/>
|
||||
<field name="location_dest_id" groups="stock.group_stock_multi_locations"/>
|
||||
<field name="date" groups="base.group_no_one"/>
|
||||
<field name="state" invisible="1"/>
|
||||
<field name="product_uom_qty" string="Qty"/>
|
||||
<field name="product_uom" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="product.group_uom"/>
|
||||
<field name="price_unit"/>
|
||||
<field name="value" sum="Stock Valuation"/>
|
||||
<field name="remaining_qty" invisible="context.get('is_avg')"/>
|
||||
<field name="remaining_value" sum="Stock Valuation" invisible="context.get('is_avg')"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="stock_move_valuation_action" model="ir.actions.act_window">
|
||||
<field name="name">Stock Moves</field>
|
||||
<field name="res_model">stock.move</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_id" ref="view_move_tree_valuation"/>
|
||||
<field name="search_view_id" ref="stock.view_move_search"/>
|
||||
<field name="context">{'search_default_outgoing': 1, 'search_default_incoming': 1, 'search_default_done': 1, 'search_default_group_by_product': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
Click to create a stock movement.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</flectra>
|
||||
|
@ -49,6 +49,82 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_valuation" name="Inventory Valuation" parent="stock.menu_warehouse_report" sequence="110" action="stock_account.action_stock_account_valuation_report"/>
|
||||
<!-- valuation wizard: current or at date -->
|
||||
<record id="view_stock_quantity_history" model="ir.ui.view">
|
||||
<field name="name">Valuation Report</field>
|
||||
<field name="model">stock.quantity.history</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Choose your date">
|
||||
<group>
|
||||
<group>
|
||||
<field name="compute_at_date" widget="radio"/>
|
||||
<field name="date" attrs="{'invisible': [('compute_at_date', '=', 0)]}"/>
|
||||
</group>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="open_table" string="Retrieve the inventory valuation" type="object" class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-default" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_stock_product_tree2" model="ir.ui.view">
|
||||
<field name="name">product.stock.tree.2</field>
|
||||
<field name="model">product.product</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="qty_at_date"/>
|
||||
<field name="uom_id" groups="product.group_uom"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="stock_value" sum="Stock Valuation" widget="monetary"/>
|
||||
<field name="cost_method" invisible="1"/>
|
||||
<button name="action_valuation_at_date_details" type="object" icon="fa-info-circle" attrs="{'invisible': [('cost_method', '!=', 'fifo')]}" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_stock_inventory_valuation" model="ir.actions.act_window">
|
||||
<field name="name">Valuation Report</field>
|
||||
<field name="res_model">stock.quantity.history</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="stock_account.view_stock_quantity_history"/>
|
||||
<field name="target">new</field>
|
||||
<field name="context">{'default_compute_at_date': 0, 'valuation': True}</field>
|
||||
</record>
|
||||
<menuitem id="menu_valuation" name="Inventory Valuation" parent="stock.menu_warehouse_report" sequence="110" action="action_stock_inventory_valuation"/>
|
||||
|
||||
<!-- AML specialized tree view -->
|
||||
<record id="view_stock_account_aml" model="ir.ui.view">
|
||||
<field name="name">stock.account.aml</field>
|
||||
<field name="model">account.move.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="date" />
|
||||
<field name="ref" />
|
||||
<field name="product_id" />
|
||||
<field name="quantity" />
|
||||
<field name="product_uom_id" groups="product.group_uom" />
|
||||
<field name="balance" string="Value"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- stock move specialized tree view -->
|
||||
<record id="view_move_tree_valuation_at_date" model="ir.ui.view">
|
||||
<field name="name">stock.move.tree.valuation.at.date</field>
|
||||
<field name="model">stock.move</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="date" />
|
||||
<field name="picking_id" string="Reference"/>
|
||||
<field name="state" invisible="1"/>
|
||||
<field name="product_id"/>
|
||||
<field name="product_uom_qty" string="Quantity"/>
|
||||
<field name="product_uom" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="product.group_uom"/>
|
||||
<field name="value" sum="Stock Valuation"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</flectra>
|
||||
|
@ -2,4 +2,5 @@
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import stock_change_standard_price
|
||||
from . import stock_quantity_history
|
||||
|
||||
|
32
addons/stock_account/wizard/stock_quantity_history.py
Normal file
32
addons/stock_account/wizard/stock_quantity_history.py
Normal file
@ -0,0 +1,32 @@
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from flectra import models, _
|
||||
|
||||
|
||||
class StockQuantityHistory(models.TransientModel):
|
||||
_inherit = 'stock.quantity.history'
|
||||
|
||||
def open_table(self):
|
||||
if not self.env.context.get('valuation'):
|
||||
return super(StockQuantityHistory, self).open_table()
|
||||
|
||||
self.env['stock.move']._run_fifo_vacuum()
|
||||
|
||||
if self.compute_at_date:
|
||||
tree_view_id = self.env.ref('stock_account.view_stock_product_tree2').id
|
||||
form_view_id = self.env.ref('stock.product_form_view_procurement_button').id
|
||||
# We pass `to_date` in the context so that `qty_available` will be computed across
|
||||
# moves until date.
|
||||
action = {
|
||||
'type': 'ir.actions.act_window',
|
||||
'views': [(tree_view_id, 'tree'), (form_view_id, 'form')],
|
||||
'view_mode': 'tree,form',
|
||||
'name': _('Products'),
|
||||
'res_model': 'product.product',
|
||||
'domain': "[('type', '=', 'product'), ('qty_available', '!=', 0)]",
|
||||
'context': dict(self.env.context, to_date=self.date, company_owned=True),
|
||||
}
|
||||
return action
|
||||
else:
|
||||
return self.env.ref('stock_account.product_valuation_action').read()[0]
|
||||
|
Loading…
Reference in New Issue
Block a user