def _update_reserved_quantity(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None, strict=False): """ Increase the reserved quantity, i.e. increase `reserved_quantity` for the set of quants sharing the combination of `product_id, location_id` if `strict` is set to False or sharing the *exact same characteristics* otherwise. Typically, this method is called when reserving a move or updating a reserved move line. When reserving a chained move, the strict flag should be enabled (to reserve exactly what was brought). When the move is MTS,it could take anything from the stock, so we disable the flag. When editing a move line, we naturally enable the flag, to reflect the reservation according to the edition. :return: a list of tuples (quant, quantity_reserved) showing on which quant the reservation was done and how much the system was able to reserve on it """ self = self.sudo() rounding = product_id.uom_id.rounding quants = self._gather(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict) reserved_quants = [] if float_compare(quantity, 0, precision_rounding=rounding) > 0: # if we want to reserve available_quantity = self._get_available_quantity(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict) if float_compare(quantity, available_quantity, precision_rounding=rounding) > 0: raise UserError(_('It is not possible to reserve more products of %s than you have in stock.') % product_id.display_name) elif float_compare(quantity, 0, precision_rounding=rounding) < 0: # if we want to unreserve available_quantity = sum(quants.mapped('reserved_quantity')) if float_compare(abs(quantity), available_quantity, precision_rounding=rounding) > 0: raise UserError(_('It is not possible to unreserve more products of %s than you have in stock.') % product_id.display_name) else: return reserved_quants for quant in quants: if float_compare(quantity, 0, precision_rounding=rounding) > 0: max_quantity_on_quant = quant.quantity - quant.reserved_quantity if float_compare(max_quantity_on_quant, 0, precision_rounding=rounding) <= 0: continue max_quantity_on_quant = min(max_quantity_on_quant, quantity) quant.reserved_quantity += max_quantity_on_quant reserved_quants.append((quant, max_quantity_on_quant)) quantity -= max_quantity_on_quant available_quantity -= max_quantity_on_quant else: max_quantity_on_quant = min(quant.reserved_quantity, abs(quantity)) quant.reserved_quantity -= max_quantity_on_quant reserved_quants.append((quant, -max_quantity_on_quant)) quantity += max_quantity_on_quant available_quantity += max_quantity_on_quant if float_is_zero(quantity, precision_rounding=rounding) or float_is_zero(available_quantity, precision_rounding=rounding): break return reserved_quants
def action_done(self): self.ensure_one() self._check_company() pickings = self.mapped('picking_ids').filtered(lambda picking: picking.state not in ('cancel', 'done')) if any(picking.state not in ('assigned', 'confirmed') for picking in pickings): raise UserError(_('Some transfers are still waiting for goods. Please check or force their availability before setting this batch to done.')) empty_pickings = set() for picking in pickings: if all(float_is_zero(line.qty_done, precision_rounding=line.product_uom_id.rounding) for line in picking.move_line_ids if line.state not in ('done', 'cancel')): empty_pickings.add(picking.id) picking.message_post( body="<b>%s:</b> %s <a href=#id=%s&view_type=form&model=stock.picking.batch>%s</a>" % ( _("Transferred by"), _("Batch Transfer"), picking.batch_id.id, picking.batch_id.name)) if len(empty_pickings) == len(pickings): return pickings.button_validate() else: res = pickings.with_context(skip_immediate=True).button_validate() if empty_pickings and res.get('context'): res['context']['pickings_to_detach'] = list(empty_pickings) return 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( _('Flectra 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 _action_confirm(self, merge=True, merge_into=False): subcontract_details_per_picking = defaultdict(list) move_to_not_merge = self.env['stock.move'] for move in self: if move.location_id.usage != 'supplier' or move.location_dest_id.usage == 'supplier': continue if move.move_orig_ids.production_id: continue bom = move._get_subcontract_bom() if not bom: continue if float_is_zero(move.product_qty, precision_rounding=move.product_uom.rounding) and\ move.picking_id.immediate_transfer is True: raise UserError(_("To subcontract, use a planned transfer.")) subcontract_details_per_picking[move.picking_id].append((move, bom)) move.write({ 'is_subcontract': True, 'location_id': move.picking_id.partner_id.with_company(move.company_id).property_stock_subcontractor.id }) move_to_not_merge |= move for picking, subcontract_details in subcontract_details_per_picking.items(): picking._subcontracted_produce(subcontract_details) # We avoid merging move due to complication with stock.rule. res = super(StockMove, move_to_not_merge)._action_confirm(merge=False) res |= super(StockMove, self - move_to_not_merge)._action_confirm(merge=merge, merge_into=merge_into) if subcontract_details_per_picking: self.env['stock.picking'].concat(*list(subcontract_details_per_picking.keys())).action_assign() return res
def _compute_draft_quantity_count(self, product_template_ids, product_variant_ids, wh_location_ids): """ Overrides to computes the valuations of the stock. """ res = super()._compute_draft_quantity_count(product_template_ids, product_variant_ids, wh_location_ids) domain = self._product_domain(product_template_ids, product_variant_ids) company = self.env['stock.location'].browse(wh_location_ids).mapped( 'company_id') svl = self.env['stock.valuation.layer'].search(domain + [('company_id', '=', company.id)]) currency = svl.currency_id or self.env.company.currency_id total_quantity = sum(svl.mapped('quantity')) # Because we can have negative quantities, `total_quantity` may be equal to zero even if the warehouse's `quantity` is positive. if svl and not float_is_zero( total_quantity, precision_rounding=svl.product_id.uom_id.rounding): def filter_on_locations(layer): return layer.stock_move_id.location_dest_id.id in wh_location_ids or layer.stock_move_id.location_id.id in wh_location_ids quantity = sum( svl.filtered(filter_on_locations).mapped('quantity')) value = sum(svl.mapped('value')) * (quantity / total_quantity) else: value = 0 value = float_repr(value, precision_digits=currency.decimal_places) if currency.position == 'after': value = '%s %s' % (value, currency.symbol) else: value = '%s %s' % (currency.symbol, value) res['value'] = value return res
def _auto_balance_opening_move(self): """ Checks the opening_move of this company. If it has not been posted yet and is unbalanced, balances it with a automatic account.move.line in the current year earnings account. """ if self.account_opening_move_id and self.account_opening_move_id.state == 'draft': debit_diff, credit_diff = self.get_opening_move_differences(self.account_opening_move_id.line_ids) currency = self.currency_id balancing_move_line = self.account_opening_move_id.line_ids.filtered(lambda x: x.account_id == self.get_unaffected_earnings_account()) if float_is_zero(debit_diff + credit_diff, precision_rounding=currency.rounding): if balancing_move_line: # zero difference and existing line : delete the line balancing_move_line.unlink() else: if balancing_move_line: # Non-zero difference and existing line : edit the line balancing_move_line.write({'debit': credit_diff, 'credit': debit_diff}) else: # Non-zero difference and no existing line : create a new line balancing_account = self.get_unaffected_earnings_account() self.env['account.move.line'].create({ 'name': _('Automatic Balancing Line'), 'move_id': self.account_opening_move_id.id, 'account_id': balancing_account.id, 'debit': credit_diff, 'credit': debit_diff, })
def _compute_value(self): """ For standard and AVCO valuation, compute the current accounting valuation of the quants by multiplying the quantity by the standard price. Instead for FIFO, use the quantity times the average cost (valuation layers are not manage by location so the average cost is the same for all location and the valuation field is a estimation more than a real value). """ for quant in self: quant.currency_id = quant.company_id.currency_id # If the user didn't enter a location yet while enconding a quant. if not quant.location_id: quant.value = 0 return if not quant.location_id._should_be_valued() or\ (quant.owner_id and quant.owner_id != quant.company_id.partner_id): quant.value = 0 continue if quant.product_id.cost_method == 'fifo': quantity = quant.product_id.with_company( quant.company_id).quantity_svl if float_is_zero( quantity, precision_rounding=quant.product_id.uom_id.rounding): quant.value = 0.0 continue average_cost = quant.product_id.with_company( quant.company_id).value_svl / quantity quant.value = quant.quantity * average_cost else: quant.value = quant.quantity * quant.product_id.with_company( quant.company_id).standard_price
def subcontracting_record_component(self): self.ensure_one() assert self.env.context.get('subcontract_move_id') if float_is_zero(self.qty_producing, precision_rounding=self.product_uom_id.rounding): return {'type': 'ir.actions.act_window_close'} if self.product_tracking != 'none' and not self.lot_producing_id: raise UserError( _('You must enter a serial number for %s') % self.product_id.name) for sml in self.move_raw_ids.move_line_ids: if sml.tracking != 'none' and not sml.lot_id: raise UserError( _('You must enter a serial number for each line of %s') % sml.product_id.display_name) self._update_finished_move() quantity_issues = self._get_quantity_produced_issues() if quantity_issues: backorder = self._generate_backorder_productions(close_mo=False) # No qty to consume to avoid propagate additional move # TODO avoid : stock move created in backorder with 0 as qty backorder.move_raw_ids.filtered( lambda m: m.additional).product_uom_qty = 0.0 backorder.qty_producing = backorder.product_qty backorder._set_qty_producing() self.product_qty = self.qty_producing subcontract_move_id = self.env['stock.move'].browse( self.env.context.get('subcontract_move_id')) action = subcontract_move_id._action_record_components() action.update({'res_id': backorder.id}) return action return {'type': 'ir.actions.act_window_close'}
def _update_available_quantity(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None, in_date=None): """ Increase or decrease `reserved_quantity` of a set of quants for a given set of product_id/location_id/lot_id/package_id/owner_id. :param product_id: :param location_id: :param quantity: :param lot_id: :param package_id: :param owner_id: :param datetime in_date: Should only be passed when calls to this method are done in order to move a quant. When creating a tracked quant, the current datetime will be used. :return: tuple (available_quantity, in_date as a datetime) """ self = self.sudo() quants = self._gather(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=True) rounding = product_id.uom_id.rounding if lot_id: incoming_dates = quants.mapped('in_date') # `mapped` already filtered out falsy items incoming_dates = [fields.Datetime.from_string(incoming_date) for incoming_date in incoming_dates] if in_date: incoming_dates += [in_date] # If multiple incoming dates are available for a given lot_id/package_id/owner_id, we # consider only the oldest one as being relevant. if incoming_dates: in_date = fields.Datetime.to_string(min(incoming_dates)) else: in_date = fields.Datetime.now() for quant in quants: try: with self._cr.savepoint(): self._cr.execute("SELECT 1 FROM stock_quant WHERE id = %s FOR UPDATE NOWAIT", [quant.id], log_exceptions=False) quant.write({ 'quantity': quant.quantity + quantity, 'in_date': in_date, }) # cleanup empty quants if float_is_zero(quant.quantity, precision_rounding=rounding) and float_is_zero(quant.reserved_quantity, precision_rounding=rounding): quant.unlink() break except OperationalError as e: if e.pgcode == '55P03': # could not obtain the lock continue else: raise else: self.create({ 'product_id': product_id.id, 'location_id': location_id.id, 'quantity': quantity, 'lot_id': lot_id and lot_id.id, 'package_id': package_id and package_id.id, 'owner_id': owner_id and owner_id.id, 'in_date': in_date, }) return self._get_available_quantity(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=False, allow_negative=True), fields.Datetime.from_string(in_date)
def _compute_show_subcontracting_details_visible(self): """ Compute if the action button in order to see moves raw is visible """ for move in self: if move.is_subcontract and move._has_tracked_subcontract_components() and\ not float_is_zero(move.quantity_done, precision_rounding=move.product_uom.rounding): move.show_subcontracting_details_visible = True else: move.show_subcontracting_details_visible = False
def _free_reservation(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None, ml_to_ignore=None): """ When editing a done move line or validating one with some forced quantities, it is possible to impact quants that were not reserved. It is therefore necessary to edit or unlink the move lines that reserved a quantity now unavailable. :param ml_to_ignore: recordset of `stock.move.line` that should NOT be unreserved """ self.ensure_one() if ml_to_ignore is None: ml_to_ignore = self.env['stock.move.line'] ml_to_ignore |= self # Check the available quantity, with the `strict` kw set to `True`. If the available # quantity is greather than the quantity now unavailable, there is nothing to do. available_quantity = self.env['stock.quant']._get_available_quantity( product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=True ) if quantity > available_quantity: # We now have to find the move lines that reserved our now unavailable quantity. We # take care to exclude ourselves and the move lines were work had already been done. oudated_move_lines_domain = [ ('move_id.state', 'not in', ['done', 'cancel']), ('product_id', '=', product_id.id), ('lot_id', '=', lot_id.id if lot_id else False), ('location_id', '=', location_id.id), ('owner_id', '=', owner_id.id if owner_id else False), ('package_id', '=', package_id.id if package_id else False), ('product_qty', '>', 0.0), ('id', 'not in', ml_to_ignore.ids), ] oudated_candidates = self.env['stock.move.line'].search(oudated_move_lines_domain) # As the move's state is not computed over the move lines, we'll have to manually # recompute the moves which we adapted their lines. move_to_recompute_state = self.env['stock.move'] rounding = self.product_uom_id.rounding for candidate in oudated_candidates: if float_compare(candidate.product_qty, quantity, precision_rounding=rounding) <= 0: quantity -= candidate.product_qty move_to_recompute_state |= candidate.move_id if candidate.qty_done: candidate.product_uom_qty = 0.0 else: candidate.unlink() if float_is_zero(quantity, precision_rounding=rounding): break else: # split this move line and assign the new part to our extra move quantity_split = float_round( candidate.product_qty - quantity, precision_rounding=self.product_uom_id.rounding, rounding_method='UP') candidate.product_uom_qty = self.product_id.uom_id._compute_quantity(quantity_split, candidate.product_uom_id, rounding_method='HALF-UP') move_to_recompute_state |= candidate.move_id break move_to_recompute_state._recompute_state()
def check_reserved_done_quantity(self): for move_line in self: if move_line.state == 'done' and not float_is_zero( move_line.product_uom_qty, precision_digits=self.env['decimal.precision']. precision_get('Product Unit of Measure')): raise ValidationError( _('A done move line should never have a reserved quantity.' ))
def _check_move_lines_map_quant_package(self, package): """ This method checks that all product of the package (quant) are well present in the move_line_ids of the picking. """ all_in = True pack_move_lines = self.move_line_ids.filtered(lambda ml: ml.package_id == package) keys = ['product_id', 'lot_id'] precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure') grouped_quants = {} for k, g in groupby(sorted(package.quant_ids, key=itemgetter(*keys)), key=itemgetter(*keys)): grouped_quants[k] = sum(self.env['stock.quant'].concat(*list(g)).mapped('quantity')) grouped_ops = {} for k, g in groupby(sorted(pack_move_lines, key=itemgetter(*keys)), key=itemgetter(*keys)): grouped_ops[k] = sum(self.env['stock.move.line'].concat(*list(g)).mapped('product_qty')) if any(not float_is_zero(grouped_quants.get(key, 0) - grouped_ops.get(key, 0), precision_digits=precision_digits) for key in grouped_quants) \ or any(not float_is_zero(grouped_ops.get(key, 0) - grouped_quants.get(key, 0), precision_digits=precision_digits) for key in grouped_ops): all_in = False return all_in
def filter_in(mo): if mo.state in ('done', 'cancel'): return False if float_is_zero(mo.qty_producing, precision_rounding=mo.product_uom_id.rounding): return False if not all(line.lot_id for line in mo.move_raw_ids.filtered( lambda sm: sm.has_tracking != 'none').move_line_ids): return False return True
def _auto_balance_opening_move(self): """ Checks the opening_move of this company. If it has not been posted yet and is unbalanced, balances it with a automatic account.move.line in the current year earnings account. """ if self.account_opening_move_id and self.account_opening_move_id.state == 'draft': balancing_account = self.get_unaffected_earnings_account() currency = self.currency_id balancing_move_line = self.account_opening_move_id.line_ids.filtered( lambda x: x.account_id == balancing_account) # There could be multiple lines if we imported the balance from unaffected earnings account too if len(balancing_move_line) > 1: self.with_context( check_move_validity=False ).account_opening_move_id.line_ids -= balancing_move_line[1:] balancing_move_line = balancing_move_line[0] debit_diff, credit_diff = self.get_opening_move_differences( self.account_opening_move_id.line_ids) if float_is_zero(debit_diff + credit_diff, precision_rounding=currency.rounding): if balancing_move_line: # zero difference and existing line : delete the line self.account_opening_move_id.line_ids -= balancing_move_line else: if balancing_move_line: # Non-zero difference and existing line : edit the line balancing_move_line.write({ 'debit': credit_diff, 'credit': debit_diff }) else: # Non-zero difference and no existing line : create a new line self.env['account.move.line'].create({ 'name': _('Automatic Balancing Line'), 'move_id': self.account_opening_move_id.id, 'account_id': balancing_account.id, 'debit': credit_diff, 'credit': debit_diff, })
def _quantity_in_progress(self): bom_kit_orderpoints = { orderpoint: bom for orderpoint in self for bom in self.env['mrp.bom']._bom_find( product=orderpoint.product_id, bom_type='phantom') if bom } res = super(StockWarehouseOrderpoint, self.filtered(lambda p: p not in bom_kit_orderpoints) )._quantity_in_progress() for orderpoint in bom_kit_orderpoints: boms, bom_sub_lines = bom_kit_orderpoints[orderpoint].explode( orderpoint.product_id, 1) ratios_qty_available = [] # total = qty_available + in_progress ratios_total = [] for bom_line, bom_line_data in bom_sub_lines: component = bom_line.product_id if component.type != 'product' or float_is_zero( bom_line_data['qty'], precision_rounding=bom_line.product_uom_id.rounding): continue uom_qty_per_kit = bom_line_data['qty'] / bom_line_data[ 'original_qty'] qty_per_kit = bom_line.product_uom_id._compute_quantity( uom_qty_per_kit, bom_line.product_id.uom_id, raise_if_failure=False) if not qty_per_kit: continue qty_by_product_location, dummy = component._get_quantity_in_progress( orderpoint.location_id.ids) qty_in_progress = qty_by_product_location.get( (component.id, orderpoint.location_id.id), 0.0) qty_available = component.qty_available / qty_per_kit ratios_qty_available.append(qty_available) ratios_total.append(qty_available + (qty_in_progress / qty_per_kit)) # For a kit, the quantity in progress is : # (the quantity if we have received all in-progress components) - (the quantity using only available components) product_qty = min(ratios_total or [0]) - min(ratios_qty_available or [0]) res[orderpoint. id] = orderpoint.product_id.uom_id._compute_quantity( product_qty, orderpoint.product_uom, round=False) return res
def unlink(self): precision = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') for ml in self: if ml.state in ('done', 'cancel'): raise UserError( _('You can not delete product moves if the picking is done. You can only correct the done quantities.' )) # Unlinking a move line should unreserve. if ml.product_id.type == 'product' and not ml.location_id.should_bypass_reservation( ) and not float_is_zero(ml.product_qty, precision_digits=precision): self.env['stock.quant']._update_reserved_quantity( ml.product_id, ml.location_id, -ml.product_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) return super(StockMoveLine, self).unlink()
def write(self, vals): res = super(Picking, self).write(vals) # Change locations of moves if those of the picking change after_vals = {} if vals.get('location_id'): after_vals['location_id'] = vals['location_id'] if vals.get('location_dest_id'): after_vals['location_dest_id'] = vals['location_dest_id'] if after_vals: self.mapped('move_lines').filtered(lambda move: not move.scrapped).write(after_vals) if vals.get('move_lines'): # Do not run autoconfirm if any of the moves has an initial demand. If an initial demand # is present in any of the moves, it means the picking was created through the "planned # transfer" mechanism. pickings_to_not_autoconfirm = self.env['stock.picking'] for picking in self: if picking.state != 'draft': continue for move in picking.move_lines: if not float_is_zero(move.product_uom_qty, precision_rounding=move.product_uom.rounding): pickings_to_not_autoconfirm |= picking break (self - pickings_to_not_autoconfirm)._autoconfirm_picking() return res
def _update_finished_move(self): """ After producing, set the move line on the subcontract picking. """ self.ensure_one() subcontract_move_id = self.env.context.get('subcontract_move_id') if subcontract_move_id: subcontract_move_id = self.env['stock.move'].browse( subcontract_move_id) quantity = self.qty_producing if self.lot_producing_id: move_lines = subcontract_move_id.move_line_ids.filtered( lambda ml: ml.lot_id == self.lot_producing_id or not ml. lot_id) else: move_lines = subcontract_move_id.move_line_ids.filtered( lambda ml: not ml.lot_id) # Update reservation and quantity done for ml in move_lines: rounding = ml.product_uom_id.rounding if float_compare(quantity, 0, precision_rounding=rounding) <= 0: break quantity_to_process = min(quantity, ml.product_uom_qty - ml.qty_done) quantity -= quantity_to_process new_quantity_done = (ml.qty_done + quantity_to_process) # on which lot of finished product if float_compare(new_quantity_done, ml.product_uom_qty, precision_rounding=rounding) >= 0: ml.write({ 'qty_done': new_quantity_done, 'lot_id': self.lot_producing_id and self.lot_producing_id.id, }) else: new_qty_reserved = ml.product_uom_qty - new_quantity_done default = { 'product_uom_qty': new_quantity_done, 'qty_done': new_quantity_done, 'lot_id': self.lot_producing_id and self.lot_producing_id.id, } ml.copy(default=default) ml.with_context(bypass_reservation_update=True).write({ 'product_uom_qty': new_qty_reserved, 'qty_done': 0 }) if float_compare( quantity, 0, precision_rounding=self.product_uom_id.rounding) > 0: self.env['stock.move.line'].create({ 'move_id': subcontract_move_id.id, 'picking_id': subcontract_move_id.picking_id.id, 'product_id': self.product_id.id, 'location_id': subcontract_move_id.location_id.id, 'location_dest_id': subcontract_move_id.location_dest_id.id, 'product_uom_qty': 0, 'product_uom_id': self.product_uom_id.id, 'qty_done': quantity, 'lot_id': self.lot_producing_id and self.lot_producing_id.id, }) if not self._get_quantity_to_backorder(): ml_reserved = subcontract_move_id.move_line_ids.filtered( lambda ml: float_is_zero(ml.qty_done, precision_rounding=ml. product_uom_id.rounding) and not float_is_zero(ml.product_uom_qty, precision_rounding=ml.product_uom_id. rounding)) ml_reserved.unlink() for ml in subcontract_move_id.move_line_ids: ml.product_uom_qty = ml.qty_done subcontract_move_id._recompute_state()
def _update_reserved_quantity(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None, strict=False): """ Increase the reserved quantity, i.e. increase `reserved_quantity` for the set of quants sharing the combination of `product_id, location_id` if `strict` is set to False or sharing the *exact same characteristics* otherwise. Typically, this method is called when reserving a move or updating a reserved move line. When reserving a chained move, the strict flag should be enabled (to reserve exactly what was brought). When the move is MTS,it could take anything from the stock, so we disable the flag. When editing a move line, we naturally enable the flag, to reflect the reservation according to the edition. :return: a list of tuples (quant, quantity_reserved) showing on which quant the reservation was done and how much the system was able to reserve on it """ self = self.sudo() rounding = product_id.uom_id.rounding quants = self._gather(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict) reserved_quants = [] if float_compare(quantity, 0, precision_rounding=rounding) > 0: # if we want to reserve available_quantity = sum( quants.filtered(lambda q: float_compare( q.quantity, 0, precision_rounding=rounding) > 0).mapped( 'quantity')) - sum(quants.mapped('reserved_quantity')) if float_compare(quantity, available_quantity, precision_rounding=rounding) > 0: raise UserError( _( 'It is not possible to reserve more products of %s than you have in stock.', product_id.display_name)) elif float_compare(quantity, 0, precision_rounding=rounding) < 0: # if we want to unreserve available_quantity = sum(quants.mapped('reserved_quantity')) if float_compare(abs(quantity), available_quantity, precision_rounding=rounding) > 0: action_fix_unreserve = self.env.ref( 'stock.stock_quant_stock_move_line_desynchronization', raise_if_not_found=False) if action_fix_unreserve and self.user_has_groups( 'base.group_system'): raise RedirectWarning( _( """It is not possible to unreserve more products of %s than you have in stock. The correction could unreserve some operations with problematics products.""", product_id.display_name), action_fix_unreserve.id, _('Automated action to fix it')) else: raise UserError( _( 'It is not possible to unreserve more products of %s than you have in stock. Contact an administrator.', product_id.display_name)) else: return reserved_quants for quant in quants: if float_compare(quantity, 0, precision_rounding=rounding) > 0: max_quantity_on_quant = quant.quantity - quant.reserved_quantity if float_compare(max_quantity_on_quant, 0, precision_rounding=rounding) <= 0: continue max_quantity_on_quant = min(max_quantity_on_quant, quantity) quant.reserved_quantity += max_quantity_on_quant reserved_quants.append((quant, max_quantity_on_quant)) quantity -= max_quantity_on_quant available_quantity -= max_quantity_on_quant else: max_quantity_on_quant = min(quant.reserved_quantity, abs(quantity)) quant.reserved_quantity -= max_quantity_on_quant reserved_quants.append((quant, -max_quantity_on_quant)) quantity += max_quantity_on_quant available_quantity += max_quantity_on_quant if float_is_zero( quantity, precision_rounding=rounding) or float_is_zero( available_quantity, precision_rounding=rounding): break return reserved_quants
def write(self, vals): """ Through the interface, we allow users to change the charateristics of a move line. If a quantity has been reserved for this move line, we impact the reservation directly to free the old quants and allocate the new ones. """ if self.env.context.get('bypass_reservation_update'): return super(StockMoveLine, self).write(vals) Quant = self.env['stock.quant'] precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') # We forbid to change the reserved quantity in the interace, but it is needed in the # case of stock.move's split. # TODO Move me in the update if 'product_uom_qty' in vals: for ml in self.filtered(lambda m: m.state in ('partially_available', 'assigned') and m.product_id.type == 'product'): if not ml.location_id.should_bypass_reservation(): qty_to_decrease = ml.product_qty - ml.product_uom_id._compute_quantity(vals['product_uom_qty'], ml.product_id.uom_id, rounding_method='HALF-UP') try: Quant._update_reserved_quantity(ml.product_id, ml.location_id, -qty_to_decrease, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) except UserError: if ml.lot_id: Quant._update_reserved_quantity(ml.product_id, ml.location_id, -qty_to_decrease, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) else: raise triggers = [ ('location_id', 'stock.location'), ('location_dest_id', 'stock.location'), ('lot_id', 'stock.production.lot'), ('package_id', 'stock.quant.package'), ('result_package_id', 'stock.quant.package'), ('owner_id', 'res.partner') ] updates = {} for key, model in triggers: if key in vals: updates[key] = self.env[model].browse(vals[key]) if updates: for ml in self.filtered(lambda ml: ml.state in ['partially_available', 'assigned'] and ml.product_id.type == 'product'): if not ml.location_id.should_bypass_reservation(): try: Quant._update_reserved_quantity(ml.product_id, ml.location_id, -ml.product_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) except UserError: if ml.lot_id: Quant._update_reserved_quantity(ml.product_id, ml.location_id, -ml.product_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) else: raise if not updates.get('location_id', ml.location_id).should_bypass_reservation(): new_product_qty = 0 try: q = Quant._update_reserved_quantity(ml.product_id, updates.get('location_id', ml.location_id), ml.product_qty, lot_id=updates.get('lot_id', ml.lot_id), package_id=updates.get('package_id', ml.package_id), owner_id=updates.get('owner_id', ml.owner_id), strict=True) new_product_qty = sum([x[1] for x in q]) except UserError: if updates.get('lot_id'): # If we were not able to reserve on tracked quants, we can use untracked ones. try: q = Quant._update_reserved_quantity(ml.product_id, updates.get('location_id', ml.location_id), ml.product_qty, lot_id=False, package_id=updates.get('package_id', ml.package_id), owner_id=updates.get('owner_id', ml.owner_id), strict=True) new_product_qty = sum([x[1] for x in q]) except UserError: pass if new_product_qty != ml.product_qty: new_product_uom_qty = ml.product_id.uom_id._compute_quantity(new_product_qty, ml.product_uom_id, rounding_method='HALF-UP') ml.with_context(bypass_reservation_update=True).product_uom_qty = new_product_uom_qty # When editing a done move line, the reserved availability of a potential chained move is impacted. Take care of running again `_action_assign` on the concerned moves. next_moves = self.env['stock.move'] if updates or 'qty_done' in vals: for ml in self.filtered(lambda ml: ml.move_id.state == 'done' and ml.product_id.type == 'product'): # undo the original move line qty_done_orig = ml.move_id.product_uom._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP') in_date = Quant._update_available_quantity(ml.product_id, ml.location_dest_id, -qty_done_orig, lot_id=ml.lot_id, package_id=ml.result_package_id, owner_id=ml.owner_id)[1] Quant._update_available_quantity(ml.product_id, ml.location_id, qty_done_orig, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, in_date=in_date) # move what's been actually done product_id = ml.product_id location_id = updates.get('location_id', ml.location_id) location_dest_id = updates.get('location_dest_id', ml.location_dest_id) qty_done = vals.get('qty_done', ml.qty_done) lot_id = updates.get('lot_id', ml.lot_id) package_id = updates.get('package_id', ml.package_id) result_package_id = updates.get('result_package_id', ml.result_package_id) owner_id = updates.get('owner_id', ml.owner_id) quantity = ml.move_id.product_uom._compute_quantity(qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP') if not location_id.should_bypass_reservation(): ml._free_reservation(product_id, location_id, quantity, lot_id=lot_id, package_id=package_id, owner_id=owner_id) if not float_is_zero(quantity, precision_digits=precision): available_qty, in_date = Quant._update_available_quantity(product_id, location_id, -quantity, lot_id=lot_id, package_id=package_id, owner_id=owner_id) if available_qty < 0 and lot_id: # see if we can compensate the negative quants with some untracked quants untracked_qty = Quant._get_available_quantity(product_id, location_id, lot_id=False, package_id=package_id, owner_id=owner_id, strict=True) if untracked_qty: taken_from_untracked_qty = min(untracked_qty, abs(available_qty)) Quant._update_available_quantity(product_id, location_id, -taken_from_untracked_qty, lot_id=False, package_id=package_id, owner_id=owner_id) Quant._update_available_quantity(product_id, location_id, taken_from_untracked_qty, lot_id=lot_id, package_id=package_id, owner_id=owner_id) if not location_id.should_bypass_reservation(): ml._free_reservation(ml.product_id, location_id, untracked_qty, lot_id=False, package_id=package_id, owner_id=owner_id) Quant._update_available_quantity(product_id, location_dest_id, quantity, lot_id=lot_id, package_id=result_package_id, owner_id=owner_id, in_date=in_date) # Unreserve and reserve following move in order to have the real reserved quantity on move_line. next_moves |= ml.move_id.move_dest_ids.filtered(lambda move: move.state not in ('done', 'cancel')) # Log a note if ml.picking_id: ml._log_message(ml.picking_id, ml, 'stock.track_move_template', vals) res = super(StockMoveLine, self).write(vals) # Update scrap object linked to move_lines to the new quantity. if 'qty_done' in vals: for move in self.mapped('move_id'): if move.scrapped: move.scrap_ids.write({'scrap_qty': move.quantity_done}) # As stock_account values according to a move's `product_uom_qty`, we consider that any # done stock move should have its `quantity_done` equals to its `product_uom_qty`, and # this is what move's `action_done` will do. So, we replicate the behavior here. if updates or 'qty_done' in vals: moves = self.filtered(lambda ml: ml.move_id.state == 'done').mapped('move_id') for move in moves: move.product_uom_qty = move.quantity_done next_moves._do_unreserve() next_moves._action_assign() return res
def _compute_timesheet_revenue(self): for invoice in self: for invoice_line in invoice.invoice_line_ids.filtered( lambda line: line.product_id.type == 'service' ).sorted(key=lambda inv_line: (inv_line.invoice_id, inv_line.id)): domain = self._get_compute_timesheet_revenue_domain( invoice_line.sale_line_ids.ids) uninvoiced_timesheet_lines = self.env[ 'account.analytic.line'].sudo().search(domain) # NOTE JEM : changing quantity (or unit price) of invoice line does not impact the revenue calculation. (FP specs) if uninvoiced_timesheet_lines: # delivered : update revenue with the prorata of number of hours on the timesheet line if invoice_line.product_id.invoice_policy == 'delivery': invoiced_price_per_hour = invoice_line.currency_id.round( invoice_line.price_subtotal / float( sum( uninvoiced_timesheet_lines.mapped( 'unit_amount')))) # invoicing analytic lines of different currency total_revenue_per_currency = dict.fromkeys( uninvoiced_timesheet_lines.mapped( 'company_currency_id').ids, 0.0) for index, timesheet_line in enumerate( uninvoiced_timesheet_lines.sorted( key=lambda ts: (ts.date, ts.id))): if index + 1 != len(uninvoiced_timesheet_lines): line_revenue = invoice_line.currency_id.compute( invoiced_price_per_hour, timesheet_line.company_currency_id ) * timesheet_line.unit_amount total_revenue_per_currency[ timesheet_line.company_currency_id. id] += line_revenue else: # last line: add the difference to avoid rounding problem total_revenue = sum([ self.env['res.currency'].browse( currency_id).compute( amount, timesheet_line.company_currency_id) for currency_id, amount in total_revenue_per_currency.items() ]) line_revenue = invoice_line.currency_id.compute( invoice_line.price_subtotal, timesheet_line .company_currency_id) - total_revenue timesheet_line.write({ 'timesheet_invoice_id': invoice.id, 'timesheet_revenue': timesheet_line.company_currency_id.round( line_revenue), }) # ordered : update revenue with the prorata of theorical revenue elif invoice_line.product_id.invoice_policy == 'order': zero_timesheet_revenue = uninvoiced_timesheet_lines.filtered( lambda line: line.timesheet_revenue == 0.0) no_zero_timesheet_revenue = uninvoiced_timesheet_lines.filtered( lambda line: line.timesheet_revenue != 0.0) # timesheet with zero theorical revenue keep the same revenue, but become invoiced (invoice_id set) zero_timesheet_revenue.write( {'timesheet_invoice_id': invoice.id}) # invoicing analytic lines of different currency total_revenue_per_currency = dict.fromkeys( no_zero_timesheet_revenue.mapped( 'company_currency_id').ids, 0.0) for index, timesheet_line in enumerate( no_zero_timesheet_revenue.sorted( key=lambda ts: (ts.date, ts.id))): if index + 1 != len(no_zero_timesheet_revenue): price_subtotal_inv = invoice_line.currency_id.compute( invoice_line.price_subtotal, timesheet_line.company_currency_id) price_subtotal_sol = timesheet_line.so_line.currency_id.compute( timesheet_line.so_line.price_subtotal, timesheet_line.company_currency_id) if not float_is_zero( price_subtotal_sol, precision_rounding=timesheet_line. company_currency_id.rounding): line_revenue = timesheet_line.timesheet_revenue * price_subtotal_inv / price_subtotal_sol total_revenue_per_currency[ timesheet_line.company_currency_id. id] += line_revenue else: line_revenue = timesheet_line.timesheet_revenue total_revenue_per_currency[ timesheet_line.company_currency_id. id] += line_revenue else: # last line: add the difference to avoid rounding problem last_price_subtotal_inv = invoice_line.currency_id.compute( invoice_line.price_subtotal, timesheet_line.company_currency_id) total_revenue = sum([ self.env['res.currency'].browse( currency_id).compute( amount, timesheet_line.company_currency_id) for currency_id, amount in total_revenue_per_currency.items() ]) line_revenue = last_price_subtotal_inv - total_revenue timesheet_line.write({ 'timesheet_invoice_id': invoice.id, 'timesheet_revenue': timesheet_line.company_currency_id.round( line_revenue), })
def button_validate(self): self.ensure_one() if not self.move_lines and not self.move_line_ids: raise UserError(_('Please add some lines to move')) # If no lots when needed, raise error picking_type = self.picking_type_id precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure') no_quantities_done = all(float_is_zero(move_line.qty_done, precision_digits=precision_digits) for move_line in self.move_line_ids.filtered(lambda m: m.state not in ('done', 'cancel'))) no_reserved_quantities = all(float_is_zero(move_line.product_qty, precision_rounding=move_line.product_uom_id.rounding) for move_line in self.move_line_ids) if no_reserved_quantities and no_quantities_done: raise UserError(_('You cannot validate a transfer if you have not processed any quantity. You should rather cancel the transfer.')) if picking_type.use_create_lots or picking_type.use_existing_lots: lines_to_check = self.move_line_ids if not no_quantities_done: lines_to_check = lines_to_check.filtered( lambda line: float_compare(line.qty_done, 0, precision_rounding=line.product_uom_id.rounding) ) for line in lines_to_check: product = line.product_id if product and product.tracking != 'none': if not line.lot_name and not line.lot_id: raise UserError(_('You need to supply a lot/serial number for %s.') % product.display_name) elif line.qty_done == 0: raise UserError(_('You cannot validate a transfer if you have not processed any quantity for %s.') % product.display_name) if no_quantities_done: view = self.env.ref('stock.view_immediate_transfer') wiz = self.env['stock.immediate.transfer'].create({'pick_ids': [(4, self.id)]}) return { 'name': _('Immediate Transfer?'), 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': 'stock.immediate.transfer', 'views': [(view.id, 'form')], 'view_id': view.id, 'target': 'new', 'res_id': wiz.id, 'context': self.env.context, } if self._get_overprocessed_stock_moves() and not self._context.get('skip_overprocessed_check'): view = self.env.ref('stock.view_overprocessed_transfer') wiz = self.env['stock.overprocessed.transfer'].create({'picking_id': self.id}) return { 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': 'stock.overprocessed.transfer', 'views': [(view.id, 'form')], 'view_id': view.id, 'target': 'new', 'res_id': wiz.id, 'context': self.env.context, } # Check backorder should check for other barcodes if self._check_backorder(): return self.action_generate_backorder_wizard() self.action_done() return
def button_validate(self): self._check_can_validate() cost_without_adjusment_lines = self.filtered(lambda c: not c.valuation_adjustment_lines) if cost_without_adjusment_lines: cost_without_adjusment_lines.compute_landed_cost() if not self._check_sum(): raise UserError(_('Cost and adjustments lines do not match. You should maybe recompute the landed costs.')) for cost in self: cost = cost.with_company(cost.company_id) move = self.env['account.move'] move_vals = { 'journal_id': cost.account_journal_id.id, 'date': cost.date, 'ref': cost.name, 'line_ids': [], 'move_type': 'entry', } valuation_layer_ids = [] cost_to_add_byproduct = defaultdict(lambda: 0.0) for line in cost.valuation_adjustment_lines.filtered(lambda line: line.move_id): remaining_qty = sum(line.move_id.stock_valuation_layer_ids.mapped('remaining_qty')) linked_layer = line.move_id.stock_valuation_layer_ids[:1] # Prorate the value at what's still in stock cost_to_add = (remaining_qty / line.move_id.product_qty) * line.additional_landed_cost if not cost.company_id.currency_id.is_zero(cost_to_add): valuation_layer = self.env['stock.valuation.layer'].create({ 'value': cost_to_add, 'unit_cost': 0, 'quantity': 0, 'remaining_qty': 0, 'stock_valuation_layer_id': linked_layer.id, 'description': cost.name, 'stock_move_id': line.move_id.id, 'product_id': line.move_id.product_id.id, 'stock_landed_cost_id': cost.id, 'company_id': cost.company_id.id, }) linked_layer.remaining_value += cost_to_add valuation_layer_ids.append(valuation_layer.id) # Update the AVCO product = line.move_id.product_id if product.cost_method == 'average': cost_to_add_byproduct[product] += cost_to_add # Products with manual inventory valuation are ignored because they do not need to create journal entries. if product.valuation != "real_time": continue # `remaining_qty` is negative if the move is out and delivered proudcts that were not # in stock. qty_out = 0 if line.move_id._is_in(): qty_out = line.move_id.product_qty - remaining_qty elif line.move_id._is_out(): qty_out = line.move_id.product_qty move_vals['line_ids'] += line._create_accounting_entries(move, qty_out) # batch standard price computation avoid recompute quantity_svl at each iteration products = self.env['product.product'].browse(p.id for p in cost_to_add_byproduct.keys()) for product in products: # iterate on recordset to prefetch efficiently quantity_svl if not float_is_zero(product.quantity_svl, precision_rounding=product.uom_id.rounding): product.with_company(cost.company_id).sudo().with_context(disable_auto_svl=True).standard_price += cost_to_add_byproduct[product] / product.quantity_svl move_vals['stock_valuation_layer_ids'] = [(6, None, valuation_layer_ids)] # We will only create the accounting entry when there are defined lines (the lines will be those linked to products of real_time valuation category). cost_vals = {'state': 'done'} if move_vals.get("line_ids"): move = move.create(move_vals) cost_vals.update({'account_move_id': move.id}) cost.write(cost_vals) if cost.account_move_id: move._post() if cost.vendor_bill_id and cost.vendor_bill_id.state == 'posted' and cost.company_id.anglo_saxon_accounting: all_amls = cost.vendor_bill_id.line_ids | cost.account_move_id.line_ids for product in cost.cost_lines.product_id: accounts = product.product_tmpl_id.get_product_accounts() input_account = accounts['stock_input'] all_amls.filtered(lambda aml: aml.account_id == input_account and not aml.reconciled).reconcile() return True
def _compute_quantities_dict(self, lot_id, owner_id, package_id, from_date=False, to_date=False): """ When the product is a kit, this override computes the fields : - 'virtual_available' - 'qty_available' - 'incoming_qty' - 'outgoing_qty' - 'free_qty' This override is used to get the correct quantities of products with 'phantom' as BoM type. """ bom_kits = self.env['mrp.bom']._get_product2bom(self, bom_type='phantom') kits = self.filtered(lambda p: bom_kits.get(p)) res = super(ProductProduct, self - kits)._compute_quantities_dict(lot_id, owner_id, package_id, from_date=from_date, to_date=to_date) qties = self.env.context.get("mrp_compute_quantities", {}) qties.update(res) for product in bom_kits: boms, bom_sub_lines = bom_kits[product].explode(product, 1) ratios_virtual_available = [] ratios_qty_available = [] ratios_incoming_qty = [] ratios_outgoing_qty = [] ratios_free_qty = [] for bom_line, bom_line_data in bom_sub_lines: component = bom_line.product_id.with_context( mrp_compute_quantities=qties) if component.type != 'product' or float_is_zero( bom_line_data['qty'], precision_rounding=bom_line.product_uom_id.rounding): # As BoMs allow components with 0 qty, a.k.a. optionnal components, we simply skip those # to avoid a division by zero. The same logic is applied to non-storable products as those # products have 0 qty available. continue uom_qty_per_kit = bom_line_data['qty'] / bom_line_data[ 'original_qty'] qty_per_kit = bom_line.product_uom_id._compute_quantity( uom_qty_per_kit, bom_line.product_id.uom_id, round=False, raise_if_failure=False) if not qty_per_kit: continue rounding = component.uom_id.rounding component_res = res.get( component.id, { "virtual_available": float_round(component.virtual_available, precision_rounding=rounding), "qty_available": float_round(component.qty_available, precision_rounding=rounding), "incoming_qty": float_round(component.incoming_qty, precision_rounding=rounding), "outgoing_qty": float_round(component.outgoing_qty, precision_rounding=rounding), "free_qty": float_round(component.free_qty, precision_rounding=rounding), }) ratios_virtual_available.append( component_res["virtual_available"] / qty_per_kit) ratios_qty_available.append(component_res["qty_available"] / qty_per_kit) ratios_incoming_qty.append(component_res["incoming_qty"] / qty_per_kit) ratios_outgoing_qty.append(component_res["outgoing_qty"] / qty_per_kit) ratios_free_qty.append(component_res["free_qty"] / qty_per_kit) if bom_sub_lines and ratios_virtual_available: # Guard against all cnsumable bom: at least one ratio should be present. res[product.id] = { 'virtual_available': min(ratios_virtual_available) // 1, 'qty_available': min(ratios_qty_available) // 1, 'incoming_qty': min(ratios_incoming_qty) // 1, 'outgoing_qty': min(ratios_outgoing_qty) // 1, 'free_qty': min(ratios_free_qty) // 1, } else: res[product.id] = { 'virtual_available': 0, 'qty_available': 0, 'incoming_qty': 0, 'outgoing_qty': 0, 'free_qty': 0, } return res
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( _('Flectra 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, } 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, } vals.update( line._get_fields_onchange_subtotal( price_subtotal=vals['price_subtotal'])) lines_vals_list.append(vals) return lines_vals_list