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 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
|
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import deque
from odoo import api, Command, fields, models, _
from odoo.tools.float_utils import float_round, float_is_zero, float_compare
from odoo.exceptions import UserError
class StockMove(models.Model):
_inherit = 'stock.move'
purchase_line_id = fields.Many2one(
'purchase.order.line', 'Purchase Order Line',
ondelete='set null', index='btree_not_null', readonly=True)
created_purchase_line_ids = fields.Many2many(
'purchase.order.line', 'stock_move_created_purchase_line_rel',
'move_id', 'created_purchase_line_id', 'Created Purchase Order Lines', copy=False)
@api.model
def _prepare_merge_moves_distinct_fields(self):
distinct_fields = super(StockMove, self)._prepare_merge_moves_distinct_fields()
distinct_fields += ['purchase_line_id', 'created_purchase_line_ids']
return distinct_fields
@api.model
def _prepare_merge_negative_moves_excluded_distinct_fields(self):
excluded_fields = super()._prepare_merge_negative_moves_excluded_distinct_fields() + ['created_purchase_line_id']
if self.env['ir.config_parameter'].sudo().get_param('purchase_stock.merge_different_procurement'):
excluded_fields += ['procure_method']
return excluded_fields
def _compute_partner_id(self):
# dropshipped moves should have their partner_ids directly set
not_dropshipped_moves = self.filtered(lambda m: not m._is_dropshipped())
super(StockMove, not_dropshipped_moves)._compute_partner_id()
def _should_ignore_pol_price(self):
self.ensure_one()
return self.origin_returned_move_id or not self.purchase_line_id or not self.product_id.id
def _get_price_unit(self):
""" Returns the unit price for the move"""
self.ensure_one()
if self._should_ignore_pol_price():
return super(StockMove, self)._get_price_unit()
price_unit_prec = self.env['decimal.precision'].precision_get('Product Price')
line = self.purchase_line_id
order = line.order_id
received_qty = line.qty_received
if self.state == 'done':
received_qty -= self.product_uom._compute_quantity(self.quantity, line.product_uom, rounding_method='HALF-UP')
if line.product_id.purchase_method == 'purchase' and float_compare(line.qty_invoiced, received_qty, precision_rounding=line.product_uom.rounding) > 0:
move_layer = line.move_ids.sudo().stock_valuation_layer_ids
invoiced_layer = line.sudo().invoice_lines.stock_valuation_layer_ids
# value on valuation layer is in company's currency, while value on invoice line is in order's currency
receipt_value = 0
if move_layer:
receipt_value += sum(move_layer.mapped(lambda l: l.currency_id._convert(
l.value, order.currency_id, order.company_id, l.create_date, round=False)))
if invoiced_layer:
receipt_value += sum(invoiced_layer.mapped(lambda l: l.currency_id._convert(
l.value, order.currency_id, order.company_id, l.create_date, round=False)))
total_invoiced_value = 0
invoiced_qty = 0
for invoice_line in line.sudo().invoice_lines:
if invoice_line.move_id.state != 'posted':
continue
# Adjust unit price to account for discounts before adding taxes.
adjusted_unit_price = invoice_line.price_unit * (1 - (invoice_line.discount / 100)) if invoice_line.discount else invoice_line.price_unit
if invoice_line.tax_ids:
invoice_line_value = invoice_line.tax_ids.compute_all(
adjusted_unit_price,
currency=invoice_line.currency_id,
quantity=invoice_line.quantity,
rounding_method="round_globally",
)['total_void']
else:
invoice_line_value = adjusted_unit_price * invoice_line.quantity
total_invoiced_value += invoice_line.currency_id._convert(
invoice_line_value, order.currency_id, order.company_id, invoice_line.move_id.invoice_date, round=False)
invoiced_qty += invoice_line.product_uom_id._compute_quantity(invoice_line.quantity, line.product_id.uom_id)
# TODO currency check
remaining_value = total_invoiced_value - receipt_value
# TODO qty_received in product uom
remaining_qty = invoiced_qty - line.product_uom._compute_quantity(received_qty, line.product_id.uom_id)
if order.currency_id != order.company_id.currency_id and remaining_value and remaining_qty:
# will be rounded during currency conversion
price_unit = remaining_value / remaining_qty
elif remaining_value and remaining_qty:
price_unit = float_round(remaining_value / remaining_qty, precision_digits=price_unit_prec)
else:
price_unit = line._get_gross_price_unit()
else:
price_unit = line._get_gross_price_unit()
if order.currency_id != order.company_id.currency_id:
# The date must be today, and not the date of the move since the move move is still
# in assigned state. However, the move date is the scheduled date until move is
# done, then date of actual move processing. See:
# https://github.com/odoo/odoo/blob/2f789b6863407e63f90b3a2d4cc3be09815f7002/addons/stock/models/stock_move.py#L36
price_unit = order.currency_id._convert(
price_unit, order.company_id.currency_id, order.company_id, fields.Date.context_today(self), round=False)
if self.product_id.lot_valuated:
return dict.fromkeys(self.lot_ids, price_unit)
return {self.env['stock.lot']: price_unit}
def _generate_valuation_lines_data(self, partner_id, qty, debit_value, credit_value, debit_account_id, credit_account_id, svl_id, description):
""" Overridden from stock_account to support amount_currency on valuation lines generated from po
"""
self.ensure_one()
rslt = super(StockMove, self)._generate_valuation_lines_data(partner_id, qty, debit_value, credit_value, debit_account_id, credit_account_id, svl_id, description)
purchase_currency = self.purchase_line_id.currency_id
company_currency = self.company_id.currency_id
if not self.purchase_line_id or purchase_currency == company_currency:
return rslt
svl = self.env['stock.valuation.layer'].browse(svl_id)
if not svl.account_move_line_id:
rslt['credit_line_vals']['amount_currency'] = company_currency._convert(
rslt['credit_line_vals']['balance'],
purchase_currency,
self.company_id,
self.date
)
rslt['debit_line_vals']['amount_currency'] = company_currency._convert(
rslt['debit_line_vals']['balance'],
purchase_currency,
self.company_id,
self.date
)
rslt['debit_line_vals']['currency_id'] = purchase_currency.id
rslt['credit_line_vals']['currency_id'] = purchase_currency.id
else:
rslt['credit_line_vals']['amount_currency'] = 0
rslt['debit_line_vals']['amount_currency'] = 0
rslt['debit_line_vals']['currency_id'] = purchase_currency.id
rslt['credit_line_vals']['currency_id'] = purchase_currency.id
if not svl.price_diff_value:
return rslt
# The idea is to force using the company currency during the reconciliation process
rslt['debit_line_vals_curr'] = {
'name': _("Currency exchange rate difference"),
'product_id': self.product_id.id,
'quantity': 0,
'product_uom_id': self.product_id.uom_id.id,
'partner_id': partner_id,
'balance': 0,
'account_id': debit_account_id,
'currency_id': purchase_currency.id,
'amount_currency': -svl.price_diff_value,
}
rslt['credit_line_vals_curr'] = {
'name': _("Currency exchange rate difference"),
'product_id': self.product_id.id,
'quantity': 0,
'product_uom_id': self.product_id.uom_id.id,
'partner_id': partner_id,
'balance': 0,
'account_id': credit_account_id,
'currency_id': purchase_currency.id,
'amount_currency': svl.price_diff_value,
}
return rslt
def _account_entry_move(self, qty, description, svl_id, cost):
"""
In case of a PO return, if the value of the returned product is
different from the purchased one, we need to empty the stock_in account
with the difference
"""
am_vals_list = super()._account_entry_move(qty, description, svl_id, cost)
returned_move = self.origin_returned_move_id
pdiff_exists = bool((self | returned_move).stock_valuation_layer_ids.stock_valuation_layer_ids.account_move_line_id)
if not am_vals_list or not self.purchase_line_id or pdiff_exists or float_is_zero(qty, precision_rounding=self.product_id.uom_id.rounding):
return am_vals_list
layer = self.env['stock.valuation.layer'].browse(svl_id)
returned_move = self.origin_returned_move_id
if returned_move and self._is_out() and self._is_returned(valued_type='out'):
returned_layer = returned_move.stock_valuation_layer_ids.filtered(lambda svl: not svl.stock_valuation_layer_id)[:1]
unit_diff = layer._get_layer_price_unit() - returned_layer._get_layer_price_unit() if returned_layer else 0
elif returned_move and returned_move._is_out() and returned_move._is_returned(valued_type='out'):
returned_layer = returned_move.stock_valuation_layer_ids.filtered(lambda svl: not svl.stock_valuation_layer_id)[:1]
unit_diff = returned_layer._get_layer_price_unit() - self.purchase_line_id._get_gross_price_unit()
else:
return am_vals_list
diff = unit_diff * qty
company = self.purchase_line_id.company_id
if company.currency_id.is_zero(diff):
return am_vals_list
sm = self.with_company(company).with_context(is_returned=True)
accounts = sm.product_id.product_tmpl_id.get_product_accounts()
acc_exp_id = accounts['expense'].id
acc_stock_in_id = accounts['stock_input'].id
journal_id = accounts['stock_journal'].id
vals = sm._prepare_account_move_vals(acc_exp_id, acc_stock_in_id, journal_id, qty, description, False, diff)
am_vals_list.append(vals)
return am_vals_list
def _prepare_move_split_vals(self, uom_qty):
vals = super(StockMove, self)._prepare_move_split_vals(uom_qty)
vals['purchase_line_id'] = self.purchase_line_id.id
return vals
def _clean_merged(self):
super(StockMove, self)._clean_merged()
self.write({'created_purchase_line_ids': [Command.clear()]})
def _get_upstream_documents_and_responsibles(self, visited):
created_pl = self.created_purchase_line_ids.filtered(lambda cpl: cpl.state not in ('done', 'cancel') and (cpl.state != 'draft' or self._context.get('include_draft_documents')))
if created_pl:
return [(pl.order_id, pl.order_id.user_id, visited) for pl in created_pl]
elif self.purchase_line_id and self.purchase_line_id.state not in ('done', 'cancel'):
return[(self.purchase_line_id.order_id, self.purchase_line_id.order_id.user_id, visited)]
else:
return super(StockMove, self)._get_upstream_documents_and_responsibles(visited)
def _get_related_invoices(self):
""" Overridden to return the vendor bills related to this stock move.
"""
rslt = super(StockMove, self)._get_related_invoices()
rslt += self.mapped('picking_id.purchase_id.invoice_ids').filtered(lambda x: x.state == 'posted')
return rslt
def _get_source_document(self):
res = super()._get_source_document()
return self.purchase_line_id.order_id or res
def _get_valuation_price_and_qty(self, related_aml, to_curr):
valuation_price_unit_total = 0
valuation_total_qty = 0
for val_stock_move in self:
# 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 += related_aml.company_currency_id._convert(
layers_values, to_curr, related_aml.company_id, valuation_date, round=False,
)
valuation_total_qty += layers_qty
if float_is_zero(valuation_total_qty, precision_rounding=related_aml.product_uom_id.rounding or related_aml.product_id.uom_id.rounding):
raise UserError(
_('Odoo is not able to generate the anglo saxon entries. The total valuation of %s is zero.',
related_aml.product_id.display_name))
return valuation_price_unit_total, valuation_total_qty
def _is_purchase_return(self):
self.ensure_one()
return self.location_dest_id.usage == "supplier" or (self.origin_returned_move_id and self.location_dest_id == self.env.ref('stock.stock_location_inter_company', raise_if_not_found=False))
def _get_all_related_aml(self):
# The back and for between account_move and account_move_line is necessary to catch the
# additional lines from a cogs correction
return super()._get_all_related_aml() | self.purchase_line_id.invoice_lines.move_id.line_ids.filtered(
lambda aml: aml.product_id == self.purchase_line_id.product_id)
def _get_all_related_sm(self, product):
return super()._get_all_related_sm(product) | self.filtered(lambda m: m.purchase_line_id.product_id == product)
def _get_purchase_line_and_partner_from_chain(self):
moves_to_check = deque(self)
seen_moves = set()
while moves_to_check:
current_move = moves_to_check.popleft()
if current_move.purchase_line_id:
return current_move.purchase_line_id.id, current_move.picking_id.partner_id.id
seen_moves.add(current_move)
moves_to_check.extend(
[move for move in current_move.move_orig_ids if move not in moves_to_check and move not in seen_moves]
)
return None, None
|