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 _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 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): try: 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) except UserError: if ml.lot_id: self.env['stock.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 moves = self.mapped('move_id') res = super(StockMoveLine, self).unlink() if moves: moves._recompute_state() return res
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 _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)): uninvoiced_timesheet_lines = self.env[ 'account.analytic.line'].sudo().search([ ('so_line', 'in', invoice_line.sale_line_ids.ids), ('project_id', '!=', False), ('timesheet_invoice_id', '=', False), ('timesheet_invoice_type', 'in', ['billable_time', 'billable_fixed']) ]) # 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: total_revenue_per_currency[ timesheet_line.company_currency_id. id] += timesheet_line.timesheet_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 no_quantities_done = all(float_is_zero(move_line.qty_done, precision_rounding=move_line.product_uom_id.rounding) for move_line in self.move_line_ids) 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 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 = self.product_id.uom_id._compute_quantity( new_product_qty, self.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 _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) 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, 0, precision_rounding=rounding) > 0 and 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.' ) % (', '.join( quants.mapped('product_id').mapped('display_name')))) elif float_compare(quantity, 0, precision_rounding=rounding) < 0 and float_compare( abs(quantity), sum(quants.mapped('reserved_quantity')), precision_rounding=rounding) > 0: raise UserError( _('It is not possible to unreserve more products of %s than you have in stock.' ) % (', '.join( quants.mapped('product_id').mapped('display_name')))) 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 _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)