def _search_quants_to_reconcile(self): """ Searches negative quants to reconcile for where the quant to reconcile is put """ dom = [ "&", "&", "&", "&", ("qty", "<", 0), ("location_id", "child_of", self.location_id.id), ("product_id", "=", self.product_id.id), ("owner_id", "=", self.owner_id.id), # Do not let the quant eat itself, or it will kill its history (e.g. returns / Stock -> Stock) ("id", "!=", self.propagated_from_id.id), ] if self.package_id.id: dom = ["&"] + dom + [("package_id", "=", self.package_id.id)] if self.lot_id: dom = ["&"] + dom + ["|", ("lot_id", "=", False), ("lot_id", "=", self.lot_id.id)] order = "lot_id, in_date" else: order = "in_date" rounding = self.product_id.uom_id.rounding quants = [] quantity = self.qty for quant in self.search(dom, order=order): if float_compare(quantity, abs(quant.qty), precision_rounding=rounding) >= 0: quants += [(quant, abs(quant.qty))] quantity -= abs(quant.qty) elif float_compare(quantity, 0.0, precision_rounding=rounding) != 0: quants += [(quant, quantity)] quantity = 0 break return quants
def _create_or_update_picking(self): for line in self: if line.product_id.type in ('product', 'consu'): # Prevent decreasing below received quantity if float_compare(line.product_qty, line.qty_received, line.product_uom.rounding) < 0: raise UserError(_('You cannot decrease the ordered quantity below the received quantity.\n' 'Create a return first.')) if float_compare(line.product_qty, line.qty_invoiced, line.product_uom.rounding) == -1: # If the quantity is now below the invoiced quantity, create an activity on the vendor bill # inviting the user to create a refund. activity = self.env['mail.activity'].sudo().create({ 'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id, 'note': _('The quantities on your purchase order indicate less than billed. You should ask for a refund. '), 'res_id': line.invoice_lines[0].invoice_id.id, 'res_model_id': self.env.ref('account.model_account_invoice').id, }) activity._onchange_activity_type_id() # If the user increased quantity of existing line or created a new line pickings = line.order_id.picking_ids.filtered(lambda x: x.state not in ('done', 'cancel') and x.location_dest_id.usage in ('internal', 'transit')) picking = pickings and pickings[0] or False if not picking: res = line.order_id._prepare_picking() picking = self.env['stock.picking'].create(res) move_vals = line._prepare_stock_moves(picking) for move_val in move_vals: self.env['stock.move']\ .create(move_val)\ ._action_confirm()\ ._action_assign()
def _paypal_form_get_invalid_parameters(self, data): invalid_parameters = [] _logger.info('Received a notification from Paypal with IPN version %s', data.get('notify_version')) if data.get('test_ipn'): _logger.warning( 'Received a notification from Paypal using sandbox' ), # TODO: txn_id: shoudl be false at draft, set afterwards, and verified with txn details if self.acquirer_reference and data.get('txn_id') != self.acquirer_reference: invalid_parameters.append(('txn_id', data.get('txn_id'), self.acquirer_reference)) # check what is buyed if float_compare(float(data.get('mc_gross', '0.0')), (self.amount + self.fees), 2) != 0: invalid_parameters.append(('mc_gross', data.get('mc_gross'), '%.2f' % self.amount)) # mc_gross is amount + fees if data.get('mc_currency') != self.currency_id.name: invalid_parameters.append(('mc_currency', data.get('mc_currency'), self.currency_id.name)) if 'handling_amount' in data and float_compare(float(data.get('handling_amount')), self.fees, 2) != 0: invalid_parameters.append(('handling_amount', data.get('handling_amount'), self.fees)) # check buyer if self.payment_token_id and data.get('payer_id') != self.payment_token_id.acquirer_ref: invalid_parameters.append(('payer_id', data.get('payer_id'), self.payment_token_id.acquirer_ref)) # check seller if data.get('receiver_id') and self.acquirer_id.paypal_seller_account and data['receiver_id'] != self.acquirer_id.paypal_seller_account: invalid_parameters.append(('receiver_id', data.get('receiver_id'), self.acquirer_id.paypal_seller_account)) if not data.get('receiver_id') or not self.acquirer_id.paypal_seller_account: # Check receiver_email only if receiver_id was not checked. # In Paypal, this is possible to configure as receiver_email a different email than the business email (the login email) # In Odoo, there is only one field for the Paypal email: the business email. This isn't possible to set a receiver_email # different than the business email. Therefore, if you want such a configuration in your Paypal, you are then obliged to fill # the Merchant ID in the Paypal payment acquirer in Odoo, so the check is performed on this variable instead of the receiver_email. # At least one of the two checks must be done, to avoid fraudsters. if data.get('receiver_email') != self.acquirer_id.paypal_email_account: invalid_parameters.append(('receiver_email', data.get('receiver_email'), self.acquirer_id.paypal_email_account)) return invalid_parameters
def _search_quants_to_reconcile(self): """ Searches negative quants to reconcile for where the quant to reconcile is put """ dom = ['&', '&', '&', '&', ('qty', '<', 0), ('location_id', 'child_of', self.location_id.id), ('product_id', '=', self.product_id.id), ('owner_id', '=', self.owner_id.id), # Do not let the quant eat itself, or it will kill its history (e.g. returns / Stock -> Stock) ('id', '!=', self.propagated_from_id.id)] if self.package_id.id: dom = ['&'] + dom + [('package_id', '=', self.package_id.id)] if self.lot_id: dom = ['&'] + dom + ['|', ('lot_id', '=', False), ('lot_id', '=', self.lot_id.id)] order = 'lot_id, in_date' else: order = 'in_date' rounding = self.product_id.uom_id.rounding quants = [] quantity = self.qty for quant in self.search(dom, order=order): if float_compare(quantity, abs(quant.qty), precision_rounding=rounding) >= 0: quants += [(quant, abs(quant.qty))] quantity -= abs(quant.qty) elif float_compare(quantity, 0.0, precision_rounding=rounding) != 0: quants += [(quant, quantity)] quantity = 0 break return quants
def quants_reserve(self, quants, move, link=False): ''' This function reserves quants for the given move and optionally given link. If the total of quantity reserved is enough, the move state is also set to 'assigned' :param quants: list of tuple(quant browse record or None, qty to reserve). If None is given as first tuple element, the item will be ignored. Negative quants should not be received as argument :param move: browse record :param link: browse record (stock.move.operation.link) ''' # TDE CLEANME: use ids + quantities dict # TDE CLEANME: check use of sudo quants_to_reserve_sudo = self.env['stock.quant'].sudo() reserved_availability = move.reserved_availability # split quants if needed for quant, qty in quants: if qty <= 0.0 or (quant and quant.qty <= 0.0): raise UserError(_('You can not reserve a negative quantity or a negative quant.')) if not quant: continue quant._quant_split(qty) quants_to_reserve_sudo |= quant reserved_availability += quant.qty # reserve quants if quants_to_reserve_sudo: quants_to_reserve_sudo.write({'reservation_id': move.id}) # check if move state needs to be set as 'assigned' # TDE CLEANME: should be moved as a move model method IMO rounding = move.product_id.uom_id.rounding if float_compare(reserved_availability, move.product_qty, precision_rounding=rounding) == 0 and move.state in ('confirmed', 'waiting'): move.write({'state': 'assigned'}) elif float_compare(reserved_availability, 0, precision_rounding=rounding) > 0 and not move.partially_available: move.write({'partially_available': True})
def _get_invoiced(self): precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') for order in self: if order.state not in ('purchase', 'done'): order.invoice_status = 'no' continue if any(float_compare(line.qty_invoiced, line.product_qty if line.product_id.purchase_method == 'purchase' else line.qty_received, precision_digits=precision) == -1 for line in order.order_line): order.invoice_status = 'to invoice' elif all(float_compare(line.qty_invoiced, line.product_qty if line.product_id.purchase_method == 'purchase' else line.qty_received, precision_digits=precision) >= 0 for line in order.order_line) and order.invoice_ids: order.invoice_status = 'invoiced' else: order.invoice_status = 'no'
def _prepare_invoice_line_from_po_line(self, line): if line.product_id.purchase_method == 'purchase': qty = line.product_qty - line.qty_invoiced else: qty = line.qty_received - line.qty_invoiced if float_compare(qty, 0.0, precision_rounding=line.product_uom.rounding) <= 0: qty = 0.0 taxes = line.taxes_id invoice_line_tax_ids = line.order_id.fiscal_position_id.map_tax(taxes) invoice_line = self.env['account.invoice.line'] data = { 'purchase_line_id': line.id, 'name': line.order_id.name+': '+line.name, 'origin': line.order_id.origin, 'uom_id': line.product_uom.id, 'product_id': line.product_id.id, 'account_id': invoice_line.with_context({'journal_id': self.journal_id.id, 'type': 'in_invoice'})._default_account(), 'price_unit': line.order_id.currency_id.compute(line.price_unit, self.currency_id, round=False), 'quantity': qty, 'discount': 0.0, 'account_analytic_id': line.account_analytic_id.id, 'analytic_tag_ids': line.analytic_tag_ids.ids, 'invoice_line_tax_ids': invoice_line_tax_ids.ids } account = invoice_line.get_invoice_line_account('in_invoice', line.product_id, line.order_id.fiscal_position_id, self.env.user.company_id) if account: data['account_id'] = account.id return data
def add_payment(self, data): statement_id = super(PosOrder, self).add_payment(data) statement_lines = self.env['account.bank.statement.line'].search([('statement_id', '=', statement_id), ('pos_statement_id', '=', self.id), ('journal_id', '=', data['journal'])]) statement_lines = statement_lines.filtered(lambda line: float_compare(line.amount, data['amount'], precision_rounding=line.journal_currency_id.rounding) == 0) # we can get multiple statement_lines when there are >1 credit # card payments with the same amount. In that case it doesn't # matter which statement line we pick, just pick one that # isn't already used. for line in statement_lines: if not line.mercury_card_brand: line.mercury_card_brand = data.get('card_brand') line.mercury_card_number = data.get('card_number') line.mercury_card_owner_name = data.get('card_owner_name') line.mercury_ref_no = data.get('ref_no') line.mercury_record_no = data.get('record_no') line.mercury_invoice_no = data.get('invoice_no') break return statement_id
def action_done(self): """ This method will finalize the work with a move line by "moving" quants to the destination location. """ for ml in self: if ml.product_id.type != 'consu': Quant = self.env['stock.quant'] rounding = ml.product_uom_id.rounding # if this move line is force assigned, unreserve elsewhere if needed if float_compare(ml.qty_done, ml.product_qty, precision_rounding=rounding) > 0: extra_qty = ml.qty_done - ml.product_qty ml._free_reservation(ml.product_id, ml.location_id, extra_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id) # unreserve what's been reserved if ml.location_id.should_impact_quants() and ml.product_id.type == 'product' and ml.product_qty: try: Quant._decrease_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) except UserError: Quant._decrease_reserved_quantity(ml.product_id, ml.location_id, ml.product_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id) # move what's been actually done quantity = ml.product_uom_id._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP') if ml.location_id.should_impact_quants() and ml.product_id.type == 'product': available_qty = Quant._decrease_available_quantity(ml.product_id, ml.location_id, quantity, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id) if available_qty < 0 and ml.lot_id: # see if we can compensate the negative quants with some untracked quants untracked_qty = Quant._get_available_quantity(ml.product_id, ml.location_id, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) if untracked_qty: taken_from_untracked_qty = min(untracked_qty, abs(quantity)) Quant._decrease_available_quantity(ml.product_id, ml.location_id, taken_from_untracked_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id) Quant._increase_available_quantity(ml.product_id, ml.location_id, taken_from_untracked_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id) if ml.location_dest_id.should_impact_quants() and ml.product_id.type == 'product': Quant._increase_available_quantity(ml.product_id, ml.location_dest_id, quantity, lot_id=ml.lot_id, package_id=ml.result_package_id, owner_id=ml.owner_id)
def quants_get_reservation( self, qty, move, pack_operation_id=False, lot_id=False, company_id=False, domain=None, preferred_domain_list=None, ): """ This function tries to find quants for the given domain and move/ops, by trying to first limit the choice on the quants that match the first item of preferred_domain_list as well. But if the qty requested is not reached it tries to find the remaining quantity by looping on the preferred_domain_list (tries with the second item and so on). Make sure the quants aren't found twice => all the domains of preferred_domain_list should be orthogonal """ # TDE FIXME: clean me reservations = [(None, qty)] pack_operation = self.env["stock.pack.operation"].browse(pack_operation_id) location = pack_operation.location_id if pack_operation else move.location_id # don't look for quants in location that are of type production, supplier or inventory. if location.usage in ["inventory", "production", "supplier"]: return reservations # return self._Reservation(reserved_quants, qty, qty, move, None) restrict_lot_id = lot_id if pack_operation else move.restrict_lot_id.id or lot_id removal_strategy = move.get_removal_strategy() domain = self._quants_get_reservation_domain( move, pack_operation_id=pack_operation_id, lot_id=lot_id, company_id=company_id, initial_domain=domain ) if not restrict_lot_id and not preferred_domain_list: meta_domains = [[]] elif restrict_lot_id and not preferred_domain_list: meta_domains = [[("lot_id", "=", restrict_lot_id)], [("lot_id", "=", False)]] elif restrict_lot_id and preferred_domain_list: lot_list = [] no_lot_list = [] for inner_domain in preferred_domain_list: lot_list.append(inner_domain + [("lot_id", "=", restrict_lot_id)]) no_lot_list.append(inner_domain + [("lot_id", "=", False)]) meta_domains = lot_list + no_lot_list else: meta_domains = preferred_domain_list res_qty = qty while float_compare(res_qty, 0, precision_rounding=move.product_id.uom_id.rounding) and meta_domains: additional_domain = meta_domains.pop(0) reservations.pop() new_reservations = self._quants_get_reservation( res_qty, move, ops=pack_operation, domain=domain + additional_domain, removal_strategy=removal_strategy ) for quant in new_reservations: if quant[0]: res_qty -= quant[1] reservations += new_reservations return reservations
def _generate_moves(self): moves = self.env['stock.move'] Quant = self.env['stock.quant'] for line in self: line._fixup_negative_quants() if float_utils.float_compare(line.theoretical_qty, line.product_qty, precision_rounding=line.product_id.uom_id.rounding) == 0: continue diff = line.theoretical_qty - line.product_qty if diff < 0: # found more than expected vals = self._get_move_values(abs(diff), line.product_id.property_stock_inventory.id, line.location_id.id) else: vals = self._get_move_values(abs(diff), line.location_id.id, line.product_id.property_stock_inventory.id) move = moves.create(vals) if diff > 0: domain = [('qty', '>', 0.0), ('package_id', '=', line.package_id.id), ('lot_id', '=', line.prod_lot_id.id), ('location_id', '=', line.location_id.id)] preferred_domain_list = [[('reservation_id', '=', False)], [('reservation_id.inventory_id', '!=', line.inventory_id.id)]] quants = Quant.quants_get_preferred_domain(move.product_qty, move, domain=domain, preferred_domain_list=preferred_domain_list) Quant.quants_reserve(quants, move) elif line.package_id: move.action_done() move.quant_ids.write({'package_id': line.package_id.id}) quants = Quant.search([('qty', '<', 0.0), ('product_id', '=', move.product_id.id), ('location_id', '=', move.location_dest_id.id), ('package_id', '!=', False)], limit=1) if quants: for quant in move.quant_ids: if quant.location_id.id == move.location_dest_id.id: #To avoid we take a quant that was reconcile already quant._quant_reconcile_negative(move) return moves
def _put_in_pack(self): package = False for pick in self: operations = pick.move_line_ids.filtered(lambda o: o.qty_done > 0 and not o.result_package_id) operation_ids = self.env['stock.move.line'] if operations: package = self.env['stock.quant.package'].create({}) for operation in operations: if float_compare(operation.qty_done, operation.product_uom_qty, precision_rounding=operation.product_uom_id.rounding) >= 0: operation_ids |= operation else: quantity_left_todo = float_round( operation.product_uom_qty - operation.qty_done, precision_rounding=operation.product_uom_id.rounding, rounding_method='UP') done_to_keep = operation.qty_done new_operation = operation.copy( default={'product_uom_qty': 0, 'qty_done': operation.qty_done}) operation.write({'product_uom_qty': quantity_left_todo, 'qty_done': 0.0}) new_operation.write({'product_uom_qty': done_to_keep}) operation_ids |= new_operation operation_ids.write({'result_package_id': package.id}) else: raise UserError(_('Please process some quantities to put in the pack first!')) return package
def _compute_show_check_availability(self): for picking in self: has_moves_to_reserve = any( move.state in ('waiting', 'confirmed', 'partially_available') and float_compare(move.product_uom_qty, 0, precision_rounding=move.product_uom.rounding) for move in picking.move_lines ) picking.show_check_availability = picking.is_locked and picking.state in ('confirmed', 'waiting', 'assigned') and has_moves_to_reserve
def split_quantities(self): for operation in self: if float_compare(operation.product_qty, operation.qty_done, precision_rounding=operation.product_uom_id.rounding) == 1: operation.copy(default={'qty_done': 0.0, 'product_qty': operation.product_qty - operation.qty_done}) operation.write({'product_qty': operation.qty_done}) else: raise UserError(_('The quantity to split should be smaller than the quantity To Do. ')) return True
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() 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') quantity -= quantity_split move_to_recompute_state |= candidate.move_id if quantity == 0.0: break move_to_recompute_state._recompute_state()
def _ccavenue_form_get_invalid_parameters(self, data): invalid_parameters = [] if self.acquirer_reference and data.get('order_id') != self.acquirer_reference: invalid_parameters.append( ('Transaction Id', data.get('order_id'), self.acquirer_reference)) # check what is buyed if float_compare(float(data.get('amount', '0.0')), self.amount, precision_digits=2) != 0: invalid_parameters.append(('Amount', data.get('amount'), '%.2f' % self.amount)) return invalid_parameters
def _authorize_form_get_invalid_parameters(self, data): invalid_parameters = [] if self.acquirer_reference and data.get('x_trans_id') != self.acquirer_reference: invalid_parameters.append(('Transaction Id', data.get('x_trans_id'), self.acquirer_reference)) # check what is buyed if float_compare(float(data.get('x_amount', '0.0')), self.amount, 2) != 0: invalid_parameters.append(('Amount', data.get('x_amount'), '%.2f' % self.amount)) return invalid_parameters
def _transfer_form_get_invalid_parameters(self, data): invalid_parameters = [] if float_compare(float(data.get('amount', '0.0')), self.amount, 2) != 0: invalid_parameters.append(('amount', data.get('amount'), '%.2f' % self.amount)) if data.get('currency') != self.currency_id.name: invalid_parameters.append(('currency', data.get('currency'), self.currency_id.name)) return invalid_parameters
def _process(self, cancel_backorder=False): if cancel_backorder: for pick_id in self.pick_ids: moves_to_log = {} for move in pick_id.move_lines: if float_compare(move.product_uom_qty, move.quantity_done, move.product_uom.rounding) > 0: moves_to_log[move] = (move.quantity_done, move.product_uom_qty) pick_id._log_less_quantities_than_expected(moves_to_log) self.pick_ids.with_context({'cancel_backorder': cancel_backorder}).action_done()
def _onchange_qty_done(self): """ When the user is encoding a move line for a tracked product, we apply some logic to help him. This onchange will warn him if he set `qty_done` to a non-supported value. """ res = {} if self.qty_done and self.product_id.tracking == 'serial': if float_compare(self.qty_done, 1.0, precision_rounding=self.move_id.product_id.uom_id.rounding) != 0: message = _('You can only process 1.0 %s of products with unique serial number.') % self.product_id.uom_id.name res['warning'] = {'title': _('Warning'), 'message': message} return res
def _payulatam_form_get_invalid_parameters(self, data): invalid_parameters = [] if self.acquirer_reference and data.get('referenceCode') != self.acquirer_reference: invalid_parameters.append(('Reference code', data.get('referenceCode'), self.acquirer_reference)) if float_compare(float(data.get('TX_VALUE', '0.0')), self.amount, 2) != 0: invalid_parameters.append(('Amount', data.get('TX_VALUE'), '%.2f' % self.amount)) if data.get('merchantId') != self.acquirer_id.payulatam_merchant_id: invalid_parameters.append(('Merchant Id', data.get('merchantId'), self.acquirer_id.payulatam_merchant_id)) return invalid_parameters
def _default_shipping_weight(self): picking = self.env['stock.picking'].browse(self.env.context.get('default_picking_id')) move_line_ids = picking.move_line_ids.filtered(lambda m: float_compare(m.qty_done, 0.0, precision_rounding=m.product_uom_id.rounding) > 0 and not m.result_package_id ) total_weight = 0.0 for ml in move_line_ids: qty = ml.product_uom_id._compute_quantity(ml.qty_done, ml.product_id.uom_id) total_weight += qty * ml.product_id.weight return total_weight
def _copy_remaining_pack_lot_ids(self, new_operation): for op in self: for lot in op.pack_lot_ids: new_qty_todo = lot.qty_todo - lot.qty if float_compare(new_qty_todo, 0, precision_rounding=op.product_uom_id.rounding) > 0: lot.copy({ 'operation_id': new_operation.id, 'qty_todo': new_qty_todo, 'qty': 0, })
def put_in_pack(self): move_line_ids = self.picking_id.move_line_ids.filtered(lambda ml: float_compare(ml.qty_done, 0.0, precision_rounding=ml.product_uom_id.rounding) > 0 and not ml.result_package_id ) delivery_package = self.picking_id._put_in_pack(move_line_ids) # write shipping weight and product_packaging on 'stock_quant_package' if needed if self.delivery_packaging_id: delivery_package.packaging_id = self.delivery_packaging_id if self.shipping_weight: delivery_package.shipping_weight = self.shipping_weight
def _payumoney_form_get_invalid_parameters(self, transaction, data): invalid_parameters = [] if transaction.acquirer_reference and data.get('mihpayid') != transaction.acquirer_reference: invalid_parameters.append( ('Transaction Id', data.get('mihpayid'), transaction.acquirer_reference)) #check what is buyed if float_compare(float(data.get('amount', '0.0')), transaction.amount, 2) != 0: invalid_parameters.append( ('Amount', data.get('amount'), '%.2f' % transaction.amount)) return invalid_parameters
def _alipay_form_get_invalid_parameters(self, data): invalid_parameters = [] if float_compare(float(data.get('total_fee', '0.0')), (self.amount + self.fees), 2) != 0: invalid_parameters.append(('total_fee', data.get('total_fee'), '%.2f' % (self.amount + self.fees))) # mc_gross is amount + fees if self.acquirer_id.alipay_payment_method == 'standard_checkout': if data.get('currency') != self.currency_id.name: invalid_parameters.append(('currency', data.get('currency'), self.currency_id.name)) else: if data.get('seller_email') != self.acquirer_id.alipay_seller_email: invalid_parameters.append(('seller_email', data.get('seller_email'), self.acquirer_id.alipay_seller_email)) return invalid_parameters
def _generate_moves(self): moves = self.env['stock.move'] for line in self: if float_utils.float_compare(line.theoretical_qty, line.product_qty, precision_rounding=line.product_id.uom_id.rounding) == 0: continue diff = line.theoretical_qty - line.product_qty if diff < 0: # found more than expected vals = line._get_move_values(abs(diff), line.product_id.property_stock_inventory.id, line.location_id.id, False) else: vals = line._get_move_values(abs(diff), line.location_id.id, line.product_id.property_stock_inventory.id, True) moves |= self.env['stock.move'].create(vals) return moves
def _buckaroo_form_get_invalid_parameters(self, data): invalid_parameters = [] data = normalize_keys_upper(data) if self.acquirer_reference and data.get('BRQ_TRANSACTIONS') != self.acquirer_reference: invalid_parameters.append(('Transaction Id', data.get('BRQ_TRANSACTIONS'), self.acquirer_reference)) # check what is buyed if float_compare(float(data.get('BRQ_AMOUNT', '0.0')), self.amount, 2) != 0: invalid_parameters.append(('Amount', data.get('BRQ_AMOUNT'), '%.2f' % self.amount)) if data.get('BRQ_CURRENCY') != self.currency_id.name: invalid_parameters.append(('Currency', data.get('BRQ_CURRENCY'), self.currency_id.name)) return invalid_parameters
def _quant_split(self, qty): self.ensure_one() rounding = self.product_id.uom_id.rounding if float_compare(abs(self.qty), abs(qty), precision_rounding=rounding) <= 0: # if quant <= qty in abs, take it entirely return False qty_round = float_round(qty, precision_rounding=rounding) new_qty_round = float_round(self.qty - qty, precision_rounding=rounding) # Fetch the history_ids manually as it will not do a join with the stock moves then (=> a lot faster) self._cr.execute("""SELECT move_id FROM stock_quant_move_rel WHERE quant_id = %s""", (self.id,)) res = self._cr.fetchall() new_quant = self.sudo().copy(default={'qty': new_qty_round, 'history_ids': [(4, x[0]) for x in res]}) self.sudo().write({'qty': qty_round}) return new_quant
def _sips_form_get_invalid_parameters(self, data): invalid_parameters = [] data = self._sips_data_to_object(data.get('Data')) # TODO: txn_id: should be false at draft, set afterwards, and verified with txn details if self.acquirer_reference and data.get('transactionReference') != self.acquirer_reference: invalid_parameters.append(('transactionReference', data.get('transactionReference'), self.acquirer_reference)) # check what is bought if float_compare(float(data.get('amount', '0.0')) / 100, self.amount, 2) != 0: invalid_parameters.append(('amount', data.get('amount'), '%.2f' % self.amount)) return invalid_parameters
def action_apply_quant(self): quant_ids = self.quant_ids.filtered(lambda x: x.new_quantity > 0.00) if not quant_ids: return precision_digits = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') self.move_id.do_unreserve_for_pda() route_vals = self.move_id.update_info_route_vals() moves = self.env['stock.move'] for quant_id in quant_ids: quant = quant_id.quant_id ##SI CAMBIA EL PICKING_TYPE_ID DEL MOVMIMIENTOS SEGÚN LA NUEVA UBICACION if self.move_id.picking_type_id.code == 'incoming': field = 'location_dest_id' else: field = 'location_id' new_location = quant.location_id new_move_id = self.move_id._split(quant_id.new_quantity) new_move = self.env['stock.move'].browse(new_move_id) if new_location.picking_type_id and new_location.picking_type_id != self.move_id.location_id.picking_type_id: ##tengo que cambiarlod e albarán new_loc_vals = { field: new_location.id, 'picking_type_id': new_location.picking_type_id.id, 'picking_id': False } new_loc_vals.update(route_vals) new_move.write(new_loc_vals) new_move.check_new_location() if float_compare(quant_id.new_quantity, 0.0, precision_digits=precision_digits) > 0: available_quantity = quant._get_available_quantity( new_move.product_id, quant[field], lot_id=quant.lot_id, package_id=quant.package_id, owner_id=quant.owner_id, ) if float_compare(available_quantity, 0.0, precision_digits=precision_digits) <= 0: return new_move._update_reserved_quantity(quant_id.new_quantity, available_quantity, quant[field], lot_id=quant.lot_id, package_id=quant.package_id, owner_id=quant.owner_id, strict=True) moves |= new_move else: if float_compare(quant_id.new_quantity, 0.0, precision_digits=precision_digits) > 0: available_quantity = quant._get_available_quantity( new_move.product_id, quant[field], lot_id=quant.lot_id, package_id=quant.package_id, owner_id=quant.owner_id, ) if float_compare(available_quantity, 0.0, precision_digits=precision_digits) <= 0: return new_move._update_reserved_quantity(quant_id.new_quantity, available_quantity, quant[field], lot_id=quant.lot_id, package_id=quant.package_id, owner_id=quant.owner_id, strict=True) moves |= new_move if moves and self.move_id not in moves: self.move_id.action_cancel_for_pda() moves._action_assign() moves.move_sel_assign_picking() self.move_id._recompute_state() moves._recompute_state() return self.env['stock.picking.type'].return_action_show_moves( domain=[('id', 'in', moves.ids)])
def _prepare_stock_moves(self, picking): """ Prepare the stock moves data for one order line. This function returns a list of dictionary ready to be used in stock.move's create() """ self.ensure_one() res = [] if self.product_id.type not in ['product', 'consu']: return res qty = 0.0 price_unit = self._get_stock_move_price_unit() outgoing_moves, incoming_moves = self._get_outgoing_incoming_moves() for move in outgoing_moves: qty -= move.product_uom._compute_quantity( move.product_uom_qty, self.product_uom, rounding_method='HALF-UP') for move in incoming_moves: qty += move.product_uom._compute_quantity( move.product_uom_qty, self.product_uom, rounding_method='HALF-UP') move_dests = self.move_dest_ids if not move_dests: move_dests = self.move_ids.move_dest_ids.filtered( lambda m: m.state != 'cancel' and not m.location_dest_id.usage == 'supplier') if not move_dests: qty_to_attach = 0 qty_to_push = self.product_qty - qty else: move_dests_initial_demand = self.product_id.uom_id._compute_quantity( sum( move_dests.filtered(lambda m: m.state != 'cancel' and not m .location_dest_id.usage == 'supplier'). mapped('product_qty')), self.product_uom, rounding_method='HALF-UP') qty_to_attach = move_dests_initial_demand - qty qty_to_push = self.product_qty - move_dests_initial_demand if float_compare(qty_to_attach, 0.0, precision_rounding=self.product_uom.rounding) > 0: product_uom_qty, product_uom = self.product_uom._adjust_uom_quantities( qty_to_attach, self.product_id.uom_id) res.append( self._prepare_stock_move_vals(picking, price_unit, product_uom_qty, product_uom)) if float_compare(qty_to_push, 0.0, precision_rounding=self.product_uom.rounding) > 0: product_uom_qty, product_uom = self.product_uom._adjust_uom_quantities( qty_to_push, self.product_id.uom_id) extra_move_vals = self._prepare_stock_move_vals( picking, price_unit, product_uom_qty, product_uom) extra_move_vals['move_dest_ids'] = False # don't attach res.append(extra_move_vals) return res
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) if lot_id and quantity > 0: quants = quants.filtered(lambda q: q.lot_id) if location_id.should_bypass_reservation(): incoming_dates = [] else: incoming_dates = [quant.in_date for quant in quants if quant.in_date and float_compare(quant.quantity, 0, precision_rounding=quant.product_uom_id.rounding) > 0] 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(flush=False): # Avoid flush compute store of package 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, }) break except OperationalError as e: if e.pgcode == '55P03': # could not obtain the lock continue else: # Because savepoint doesn't flush, we need to invalidate the cache # when there is a error raise from the write (other than lock-error) self.clear_caches() 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 _prepare_stock_moves(self, picking): """ Prepare the stock moves data for one order line. This function returns a list of dictionary ready to be used in stock.move's create() """ self.ensure_one() res = [] if self.product_id.type not in ['product', 'consu']: return res qty = 0.0 price_unit = self._get_stock_move_price_unit() for move in self.move_ids.filtered( lambda x: x.state != 'cancel' and not x.location_dest_id.usage == "supplier"): qty += move.product_uom._compute_quantity( move.product_uom_qty, self.product_uom, rounding_method='HALF-UP') template = { # truncate to 2000 to avoid triggering index limit error # TODO: remove index in master? 'name': (self.name or '')[:2000], 'product_id': self.product_id.id, 'product_uom': self.product_uom.id, 'date': self.order_id.date_order, 'date_expected': self.date_planned, 'location_id': self.order_id.partner_id.property_stock_supplier.id, 'location_dest_id': self.order_id._get_destination_location(), 'picking_id': picking.id, 'partner_id': self.order_id.dest_address_id.id, 'move_dest_ids': [(4, x) for x in self.move_dest_ids.ids], 'state': 'draft', 'purchase_line_id': self.id, 'company_id': self.order_id.company_id.id, 'price_unit': price_unit, 'picking_type_id': self.order_id.picking_type_id.id, 'group_id': self.order_id.group_id.id, 'origin': self.order_id.name, 'propagate_date': self.propagate_date, 'propagate_date_minimum_delta': self.propagate_date_minimum_delta, 'description_picking': self.product_id._get_description(self.order_id.picking_type_id), 'propagate_cancel': self.propagate_cancel, 'route_ids': self.order_id.picking_type_id.warehouse_id and [(6, 0, [ x.id for x in self.order_id.picking_type_id.warehouse_id.route_ids ])] or [], 'warehouse_id': self.order_id.picking_type_id.warehouse_id.id, } diff_quantity = self.product_qty - qty if float_compare(diff_quantity, 0.0, precision_rounding=self.product_uom.rounding) > 0: po_line_uom = self.product_uom quant_uom = self.product_id.uom_id product_uom_qty, product_uom = po_line_uom._adjust_uom_quantities( diff_quantity, quant_uom) template['product_uom_qty'] = product_uom_qty template['product_uom'] = product_uom.id res.append(template) return res
def _action_assign(self, mo_obj, lot_name): move_ids = mo_obj.move_raw_ids assigned_moves = self.env['stock.move'] partially_available_moves = self.env['stock.move'] # Read the `reserved_availability` field of the moves out of the loop to prevent unwanted # cache invalidation when actually reserving the move. reserved_availability = { move: move.reserved_availability for move in move_ids } roundings = { move: move.product_id.uom_id.rounding for move in move_ids } move_line_vals_list = [] not_available_list = [] for move in move_ids.filtered(lambda m: m.state != 'cancel'): rounding = roundings[move] missing_reserved_uom_quantity = move.product_uom_qty - reserved_availability[ move] missing_reserved_quantity = move.product_uom._compute_quantity( missing_reserved_uom_quantity, move.product_id.uom_id, rounding_method='HALF-UP') if move._should_bypass_reservation(): # create the move line(s) but do not impact quants if move.product_id.tracking == 'serial' and ( move.picking_type_id.use_create_lots or move.picking_type_id.use_existing_lots): for i in range(0, int(missing_reserved_quantity)): move_line_vals_list.append( move._prepare_move_line_vals(quantity=1)) else: to_update = move.move_line_ids.filtered( lambda ml: ml.product_uom_id == move.product_uom and ml .location_id == move.location_id and ml. location_dest_id == move.location_dest_id and ml. picking_id == move.picking_id and not ml.lot_id and not ml.package_id and not ml.owner_id) if to_update: to_update[ 0].product_uom_qty += missing_reserved_uom_quantity else: move_line_vals_list.append( move._prepare_move_line_vals( quantity=missing_reserved_quantity)) assigned_moves |= move else: if float_is_zero(move.product_uom_qty, precision_rounding=move.product_uom.rounding): assigned_moves |= move elif not move.move_orig_ids: if move.procure_method == 'make_to_order': continue # If we don't need any quantity, consider the move assigned. need = missing_reserved_quantity lot_id = None if move.product_id.tracking == 'none': if float_is_zero(need, precision_rounding=rounding): assigned_moves |= move continue else: move_line = move.move_line_ids if move_line and move_line.filtered( lambda m: m.lot_id.name == lot_name or m. lot_name == lot_name): if float_is_zero(need, precision_rounding=rounding): assigned_moves |= move continue lot_id = move_line[0].lot_id else: move_line.unlink() need = move.product_uom_qty # Reserve new quants and create move lines accordingly. if not lot_id and move.product_id.tracking != 'none': lot_id = self.env['stock.production.lot'].search([ ('product_id', '=', move.product_id.id), ('name', '=', lot_name), ('product_qty', '>=', need) ]) if not lot_id: not_available_list.append(lot_name) continue forced_package_id = move.package_level_id.package_id or None available_quantity = move._get_available_quantity( move.location_id, lot_id=lot_id, package_id=forced_package_id) if available_quantity < need: not_available_list.append(lot_name) continue taken_quantity = move._update_reserved_quantity( need, available_quantity, move.location_id, lot_id=lot_id, package_id=forced_package_id, strict=False) if float_is_zero(taken_quantity, precision_rounding=rounding): continue if float_compare(need, taken_quantity, precision_rounding=rounding) == 0: assigned_moves |= move else: partially_available_moves |= move else: # Check what our parents brought and what our siblings took in order to # determine what we can distribute. # `qty_done` is in `ml.product_uom_id` and, as we will later increase # the reserved quantity on the quants, convert it here in # `product_id.uom_id` (the UOM of the quants is the UOM of the product). move_lines_in = move.move_orig_ids.filtered( lambda m: m.state == 'done').mapped('move_line_ids') keys_in_groupby = [ 'location_dest_id', 'lot_id', 'result_package_id', 'owner_id' ] def _keys_in_sorted(ml): return (ml.location_dest_id.id, ml.lot_id.id, ml.result_package_id.id, ml.owner_id.id) grouped_move_lines_in = {} for k, g in groupby(sorted(move_lines_in, key=_keys_in_sorted), key=itemgetter(*keys_in_groupby)): qty_done = 0 for ml in g: qty_done += ml.product_uom_id._compute_quantity( ml.qty_done, ml.product_id.uom_id) grouped_move_lines_in[k] = qty_done move_lines_out_done = (move.move_orig_ids.mapped('move_dest_ids') - move) \ .filtered(lambda m: m.state in ['done']) \ .mapped('move_line_ids') # As we defer the write on the stock.move's state at the end of the loop, there # could be moves to consider in what our siblings already took. moves_out_siblings = move.move_orig_ids.mapped( 'move_dest_ids') - move moves_out_siblings_to_consider = moves_out_siblings & ( assigned_moves + partially_available_moves) reserved_moves_out_siblings = moves_out_siblings.filtered( lambda m: m.state in ['partially_available', 'assigned']) move_lines_out_reserved = (reserved_moves_out_siblings | moves_out_siblings_to_consider ).mapped('move_line_ids') keys_out_groupby = [ 'location_id', 'lot_id', 'package_id', 'owner_id' ] def _keys_out_sorted(ml): return (ml.location_id.id, ml.lot_id.id, ml.package_id.id, ml.owner_id.id) grouped_move_lines_out = {} for k, g in groupby(sorted(move_lines_out_done, key=_keys_out_sorted), key=itemgetter(*keys_out_groupby)): qty_done = 0 for ml in g: qty_done += ml.product_uom_id._compute_quantity( ml.qty_done, ml.product_id.uom_id) grouped_move_lines_out[k] = qty_done for k, g in groupby(sorted(move_lines_out_reserved, key=_keys_out_sorted), key=itemgetter(*keys_out_groupby)): grouped_move_lines_out[k] = sum( self.env['stock.move.line'].concat( *list(g)).mapped('product_qty')) available_move_lines = { key: grouped_move_lines_in[key] - grouped_move_lines_out.get(key, 0) for key in grouped_move_lines_in.keys() } # pop key if the quantity available amount to 0 available_move_lines = dict( (k, v) for k, v in available_move_lines.items() if v) if not available_move_lines: continue for move_line in move.move_line_ids.filtered( lambda m: m.product_qty): if available_move_lines.get( (move_line.location_id, move_line.lot_id, move_line.result_package_id, move_line.owner_id)): available_move_lines[( move_line.location_id, move_line.lot_id, move_line.result_package_id, move_line.owner_id)] -= move_line.product_qty for (location_id, lot_id, package_id, owner_id), quantity in available_move_lines.items(): need = move.product_qty - sum( move.move_line_ids.mapped('product_qty')) # `quantity` is what is brought by chained done move lines. We double check # here this quantity is available on the quants themselves. If not, this # could be the result of an inventory adjustment that removed totally of # partially `quantity`. When this happens, we chose to reserve the maximum # still available. This situation could not happen on MTS move, because in # this case `quantity` is directly the quantity on the quants themselves. available_quantity = move._get_available_quantity( location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=True) if float_is_zero(available_quantity, precision_rounding=rounding): continue taken_quantity = move._update_reserved_quantity( need, min(quantity, available_quantity), location_id, lot_id, package_id, owner_id) if float_is_zero(taken_quantity, precision_rounding=rounding): continue if float_is_zero(need - taken_quantity, precision_rounding=rounding): assigned_moves |= move break partially_available_moves |= move if move.product_id.tracking == 'serial': move.next_serial_count = move.product_uom_qty if not_available_list: raise UserError( _(' Lot %s added in uploaded file is not available in stock.', (", ".join(a for a in set(not_available_list))))) self.env['stock.move.line'].create(move_line_vals_list) partially_available_moves.write({'state': 'partially_available'}) assigned_moves.write({'state': 'assigned'}) move_ids.mapped('picking_id')._check_entire_pack()
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 _get_available_quantity(self, product_id, location_id, lot_id=None, package_id=None, owner_id=None, strict=False, allow_negative=False): """ Return the available quantity, i.e. the sum of `quantity` minus the sum of `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. This method is called in the following usecases: - when a stock move checks its availability - when a stock move actually assign - when editing a move line, to check if the new value is forced or not - when validating a move line with some forced values and have to potentially unlink an equivalent move line in another picking In the two first usecases, `strict` should be set to `False`, as we don't know what exact quants we'll reserve, and the characteristics are meaningless in this context. In the last ones, `strict` should be set to `True`, as we work on a specific set of characteristics. :return: available quantity as a float """ self = self.sudo() quants = self._gather(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict) rounding = product_id.uom_id.rounding if product_id.tracking == 'none': available_quantity = sum(quants.mapped('quantity')) - sum( quants.mapped('reserved_quantity')) if allow_negative: return available_quantity else: return available_quantity if float_compare( available_quantity, 0.0, precision_rounding=rounding) >= 0.0 else 0.0 else: availaible_quantities = { lot_id: 0.0 for lot_id in list(set(quants.mapped('lot_id'))) + ['untracked'] } for quant in quants: if not quant.lot_id: availaible_quantities[ 'untracked'] += quant.quantity - quant.reserved_quantity else: availaible_quantities[ quant. lot_id] += quant.quantity - quant.reserved_quantity if allow_negative: return sum(availaible_quantities.values()) else: return sum([ available_quantity for available_quantity in availaible_quantities.values() if float_compare( available_quantity, 0, precision_rounding=rounding) > 0 ])
def test_01_compute_price_operation_cost(self): """Test calcuation of bom cost with operations.""" workcenter_from1 = Form(self.env['mrp.workcenter']) workcenter_from1.name = 'Workcenter' workcenter_from1.time_efficiency = 100 workcenter_from1.capacity = 2 workcenter_from1.oee_target = 100 workcenter_from1.time_start = 0 workcenter_from1.time_stop = 0 workcenter_from1.costs_hour = 100 workcenter_1 = workcenter_from1.save() self.bom_1.write({ 'operation_ids': [ (0, 0, { 'name': 'Cutting', 'workcenter_id': workcenter_1.id, 'time_mode': 'manual', 'time_cycle_manual': 20, 'sequence': 1, }), (0, 0, { 'name': 'Drilling', 'workcenter_id': workcenter_1.id, 'time_mode': 'manual', 'time_cycle_manual': 25, 'sequence': 2, }), (0, 0, { 'name': 'Fitting', 'workcenter_id': workcenter_1.id, 'time_mode': 'manual', 'time_cycle_manual': 30, 'sequence': 3, }), ], }), self.bom_2.write({ 'operation_ids': [ (0, 0, { 'name': 'Cutting', 'workcenter_id': workcenter_1.id, 'time_mode': 'manual', 'time_cycle_manual': 20, 'sequence': 1, }), (0, 0, { 'name': 'Drilling', 'workcenter_id': workcenter_1.id, 'time_mode': 'manual', 'time_cycle_manual': 25, 'sequence': 2, }), (0, 0, { 'name': 'Fitting', 'workcenter_id': workcenter_1.id, 'time_mode': 'manual', 'time_cycle_manual': 30, 'sequence': 3, }), ], }), # ----------------------------------------------------------------- # Dinning Table Operation Cost(1 Unit) # ----------------------------------------------------------------- # Operation cost calculate for 1 units # Cutting (20 / 60) * 100 = 33.33 # Drilling (25 / 60) * 100 = 41.67 # Fitting (30 / 60) * 100 = 50.00 # ---------------------------------------- # Operation Cost 1 unit = 125 # ----------------------------------------------------------------- # -------------------------------------------------------------------------- # Table Head Operation Cost (1 Dozen) # -------------------------------------------------------------------------- # Operation cost calculate for 1 dozens # Cutting (20 * 1 / 60) * 100 = 33,33 # Drilling (25 * 1 / 60) * 100 = 41,67 # Fitting (30 * 1 / 60) * 100 = 50 # ---------------------------------------- # Operation Cost 1 dozen (125 per dozen) and 10.42 for 1 Unit # -------------------------------------------------------------------------- self.assertEqual(self.dining_table.standard_price, 1000, "Initial price of the Product should be 1000") self.dining_table.button_bom_cost() # Total cost of Dining Table = (550) + Total cost of operations (125) = 675.0 self.assertEqual( float_round(self.dining_table.standard_price, precision_digits=2), 675.0, "After computing price from BoM price should be 612.5") self.Product.browse([self.dining_table.id, self.table_head.id]).action_bom_cost() # Total cost of Dining Table = (718.75) + Total cost of all operations (125 + 10.42) = 854.17 self.assertEqual( float_compare(self.dining_table.standard_price, 854.17, precision_digits=2), 0, "After computing price from BoM price should be 786.46")
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. outdated_move_lines_domain = [ ('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), ] # We take the current picking first, then the pickings with the latest scheduled date current_picking_first = lambda cand: ( cand.picking_id != self.move_id.picking_id, -(cand.picking_id.scheduled_date or cand.move_id.date_expected) .timestamp() if cand.picking_id or cand.move_id else -cand.id, ) outdated_candidates = self.env['stock.move.line'].search( outdated_move_lines_domain).sorted(current_picking_first) # 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 outdated_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_quantity(self): for quant in self: if float_compare(quant.quantity, 1, precision_rounding=quant.product_uom_id.rounding) > 0 and quant.lot_id and quant.product_id.tracking == 'serial': raise ValidationError(_('The serial number has already been assigned: \n Product: %s, Serial Number: %s') % (quant.product_id.display_name, quant.lot_id.name))
def write(self, vals): if self.env.context.get('bypass_reservation_update'): return super(StockMoveLine, self).write(vals) if 'product_id' in vals and any( vals.get('state', ml.state) != 'draft' and vals['product_id'] != ml.product_id.id for ml in self): raise UserError( _("Changing the product is only allowed in 'Draft' state.")) moves_to_recompute_state = self.env['stock.move'] Quant = self.env['stock.quant'] precision = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') 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 'result_package_id' in updates: for ml in self.filtered(lambda ml: ml.package_level_id): if updates.get('result_package_id'): ml.package_level_id.package_id = updates.get( 'result_package_id') else: # TODO: make package levels less of a pain and fix this package_level = ml.package_level_id ml.package_level_id = False package_level.unlink() # When we try to write on a reserved move line any fields from `triggers` or directly # `product_uom_qty` (the actual reserved quantity), we need to make sure the associated # quants are correctly updated in order to not make them out of sync (i.e. the sum of the # move lines `product_uom_qty` should always be equal to the sum of `reserved_quantity` on # the quants). If the new charateristics are not available on the quants, we chose to # reserve the maximum possible. if updates or 'product_uom_qty' in vals: for ml in self.filtered( lambda ml: ml.state in ['partially_available', 'assigned'] and ml.product_id.type == 'product'): if 'product_uom_qty' in vals: new_product_uom_qty = ml.product_uom_id._compute_quantity( vals['product_uom_qty'], ml.product_id.uom_id, rounding_method='HALF-UP') # Make sure `product_uom_qty` is not negative. if float_compare(new_product_uom_qty, 0, precision_rounding=ml.product_id.uom_id. rounding) < 0: raise UserError( _('Reserving a negative quantity is not allowed.')) else: new_product_uom_qty = ml.product_qty # Unreserve the old charateristics of the move line. if not ml._should_bypass_reservation(ml.location_id): 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) # Reserve the maximum available of the new charateristics of the move line. if not ml._should_bypass_reservation( updates.get('location_id', ml.location_id)): reserved_qty = 0 try: q = Quant._update_reserved_quantity( ml.product_id, updates.get('location_id', ml.location_id), new_product_uom_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) reserved_qty = sum([x[1] for x in q]) except UserError: pass if reserved_qty != new_product_uom_qty: new_product_uom_qty = ml.product_id.uom_id._compute_quantity( reserved_qty, ml.product_uom_id, rounding_method='HALF-UP') moves_to_recompute_state |= ml.move_id 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: mls = self.filtered(lambda ml: ml.move_id.state == 'done' and ml. product_id.type == 'product') if not updates: # we can skip those where qty_done is already good up to UoM rounding mls = mls.filtered(lambda ml: not float_is_zero( ml.qty_done - vals['qty_done'], precision_rounding=ml.product_uom_id.rounding)) for ml in mls: # 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 ml._should_bypass_reservation(location_id): 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 ml._should_bypass_reservation(location_id): 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') moves |= self.filtered( lambda ml: ml.move_id.state not in ('done', 'cancel') and ml.move_id.picking_id.immediate_transfer and not ml.product_uom_qty).mapped('move_id') for move in moves: move.product_uom_qty = move.quantity_done next_moves._do_unreserve() next_moves._action_assign() if moves_to_recompute_state: moves_to_recompute_state._recompute_state() return res
def check_backorder(self): need_rereserve, all_op_processed = self.picking_recompute_remaining_quantities(done_qtys=True) for move in self.move_lines: if float_compare(move.remaining_qty, 0, precision_rounding=move.product_id.uom_id.rounding) != 0: return True return False
def generate_stock_valuation_layer(env): openupgrade.logged_query( env.cr, """ ALTER TABLE stock_valuation_layer ADD COLUMN old_product_price_history_id integer""", ) company_obj = env["res.company"] product_obj = env["product.product"] # Needed to modify global variable global precision_price precision_price = env["decimal.precision"].precision_get("Product Price") precision_uom = env["decimal.precision"].precision_get( "Product Unit of Measure") companies = company_obj.search([]) products = product_obj.with_context(active_test=False).search([ ("type", "in", ("product", "consu")) ]) all_svl_list = [] for company in companies: _logger.info("Doing svl for company_id {}".format(company.id)) for product in products: history_lines = [] if product.cost_method != "fifo": history_lines = get_product_price_history( env, company.id, product.id) moves = get_stock_moves(env, company.id, product.id) svl_in_vals_list = [] svl_out_vals_list = [] svl_man_vals_list = [] svl_in_index = 0 h_index = 0 previous_price = 0.0 previous_qty = 0.0 for move in moves: is_dropship = True if move["move_type"] in ( "dropship", "dropship_return") else False if product.cost_method in ("average", "standard"): # useless for Fifo because we have price unit in stock.move # Add manual adjusts have_qty = not float_is_zero( previous_qty, precision_digits=precision_uom) while h_index < len(history_lines) and history_lines[ h_index]["datetime"] < move["date"]: price_history_rec = history_lines[h_index] if float_compare(price_history_rec["cost"], previous_price, precision_digits=precision_price): if have_qty: svl_vals = _prepare_man_svl_vals( price_history_rec, previous_price, previous_qty, company, product) svl_man_vals_list.append(svl_vals) previous_price = price_history_rec["cost"] h_index += 1 # Add in svl if move["move_type"] == "in" or is_dropship: total_qty = previous_qty + move["product_qty"] # TODO: is needed vaccum if total_qty is negative? if float_is_zero(total_qty, precision_digits=precision_uom): previous_price = move["price_unit"] else: previous_price = float_round( (previous_price * previous_qty + move["price_unit"] * move["product_qty"]) / total_qty, precision_digits=precision_price) svl_vals = _prepare_in_svl_vals(move, move["product_qty"], move["price_unit"], product, is_dropship) svl_in_vals_list.append(svl_vals) previous_qty = total_qty # Add out svl if move["move_type"] == "out" or is_dropship: qty = move["product_qty"] if product.cost_method in ("average", "fifo") and not is_dropship: # Reduce remaininig qty in svl of type "in" while qty > 0 and svl_in_index < len(svl_in_vals_list): if svl_in_vals_list[svl_in_index][ "remaining_qty"] >= qty: candidate_cost = ( svl_in_vals_list[svl_in_index] ["remaining_value"] / svl_in_vals_list[svl_in_index] ["remaining_qty"]) svl_in_vals_list[svl_in_index][ "remaining_qty"] -= qty svl_in_vals_list[svl_in_index][ "remaining_value"] = float_round( candidate_cost * svl_in_vals_list[svl_in_index] ["remaining_qty"], precision_digits=precision_price) qty = 0 elif svl_in_vals_list[svl_in_index][ "remaining_qty"]: qty -= svl_in_vals_list[svl_in_index][ "remaining_qty"] svl_in_vals_list[svl_in_index][ "remaining_qty"] = 0.0 svl_in_vals_list[svl_in_index][ "remaining_value"] = 0.0 svl_in_index += 1 else: svl_in_index += 1 if product.cost_method == 'fifo': svl_vals = _prepare_out_svl_vals( move, move["product_qty"], abs(move["price_unit"]), product) else: svl_vals = _prepare_out_svl_vals( move, move["product_qty"], previous_price, product) svl_out_vals_list.append(svl_vals) previous_qty -= move["product_qty"] # Add manual adjusts after last move if product.cost_method in ( "average", "standard") and not float_is_zero( previous_qty, precision_digits=precision_uom): # useless for Fifo because we have price unit on product form while h_index < len(history_lines): price_history_rec = history_lines[h_index] if float_compare(price_history_rec["cost"], previous_price, precision_digits=precision_price): svl_vals = _prepare_man_svl_vals( price_history_rec, previous_price, previous_qty, company, product) svl_man_vals_list.append(svl_vals) previous_price = price_history_rec["cost"] h_index += 1 all_svl_list.extend(svl_in_vals_list + svl_out_vals_list + svl_man_vals_list) if all_svl_list: all_svl_list = sorted(all_svl_list, key=lambda k: (k["create_date"])) _logger.info("To create {} svl records".format(len(all_svl_list))) query_insert(env.cr, "stock_valuation_layer", all_svl_list)
def _prepare_stock_moves(self, picking): """ Prepare the stock moves data for one order line. This function returns a list of dictionary ready to be used in stock.move's create() """ self.ensure_one() res = [] if self.product_id.type not in ['product', 'consu']: return res qty = 0.0 price_unit = self._get_stock_move_price_unit() for move in self.move_ids.filtered( lambda x: x.state != 'cancel' and not x.location_dest_id.usage == "supplier"): qty += move.product_qty template = { 'name': self.name or '', 'product_id': self.product_id.id, 'product_uom': self.product_uom.id, 'date': self.order_id.date_order, 'date_expected': self.date_planned, 'location_id': self.order_id.partner_id.property_stock_supplier.id, 'location_dest_id': self.order_id._get_destination_location(), 'picking_id': picking.id, 'partner_id': self.order_id.dest_address_id.id, 'move_dest_ids': [(4, x) for x in self.move_dest_ids.ids], 'state': 'draft', 'purchase_line_id': self.id, 'company_id': self.order_id.company_id.id, 'price_unit': price_unit, 'picking_type_id': self.order_id.picking_type_id.id, 'group_id': self.order_id.group_id.id, 'origin': self.order_id.name, 'route_ids': self.order_id.picking_type_id.warehouse_id and [(6, 0, [ x.id for x in self.order_id.picking_type_id.warehouse_id.route_ids ])] or [], 'warehouse_id': self.order_id.picking_type_id.warehouse_id.id, 'branch_id': self.order_id.branch_id.id } diff_quantity = self.product_qty - qty if float_compare(diff_quantity, 0.0, precision_rounding=self.product_uom.rounding) > 0: template['product_uom_qty'] = diff_quantity res.append(template) return res
def _schedule_hours(self, hours, day_dt, compute_leaves=False, resource_id=None, domain=None): """ Schedule hours of work, using a calendar and an optional resource to compute working and leave days. This method can be used backwards, i.e. scheduling days before a deadline. For compute_leaves, resource_id: see _get_day_work_intervals. This method does not use rrule because rrule does not allow backwards computation. :param int hours: number of hours to schedule. Use a negative number to compute a backwards scheduling. :param datetime day_dt: reference date to compute working days. If days is > 0 date is the starting date. If days is < 0 date is the ending date. :return list intervals: list of time intervals in naive UTC """ self.ensure_one() backwards = (hours < 0) intervals = [] remaining_hours, iterations = abs(hours * 1.0), 0 day_dt_tz = to_naive_user_tz(day_dt, self.env.user) current_datetime = day_dt_tz call_args = dict(compute_leaves=compute_leaves, resource_id=resource_id, domain=domain) while float_compare( remaining_hours, 0.0, precision_digits=2) in (1, 0) and iterations < 1000: if backwards: call_args['end_time'] = current_datetime.time() else: call_args['start_time'] = current_datetime.time() working_intervals = self._get_day_work_intervals( current_datetime.date(), **call_args) if working_intervals: new_working_intervals = self._interval_schedule_hours( working_intervals, remaining_hours, backwards=backwards) res = timedelta() for interval in working_intervals: res += interval[1] - interval[0] remaining_hours -= res.total_seconds() / 3600.0 intervals = intervals + new_working_intervals if not backwards else new_working_intervals + intervals # get next day if backwards: current_datetime = datetime.datetime.combine( self._get_previous_work_day(current_datetime), datetime.time(23, 59, 59)) else: current_datetime = datetime.datetime.combine( self._get_next_work_day(current_datetime), datetime.time()) # avoid infinite loops iterations += 1 return intervals
def action_done(self): """ Process completely the moves given and if all moves are done, it will finish the picking. """ self.filtered(lambda move: move.state == 'draft').action_confirm() Uom = self.env['product.uom'] Quant = self.env['stock.quant'] pickings = self.env['stock.picking'] procurements = self.env['procurement.order'] operations = self.env['stock.pack.operation'] remaining_move_qty = {} for move in self: if move.picking_id: pickings |= move.picking_id remaining_move_qty[move.id] = move.product_qty for link in move.linked_move_operation_ids: operations |= link.operation_id pickings |= link.operation_id.picking_id # Sort operations according to entire packages first, then package + lot, package only, lot only operations = operations.sorted( key=lambda x: ((x.package_id and not x.product_id) and -4 or 0) + (x.package_id and -2 or 0) + (x.pack_lot_ids and -1 or 0)) for operation in operations: # product given: result put immediately in the result package (if False: without package) # but if pack moved entirely, quants should not be written anything for the destination package quant_dest_package_id = operation.product_id and operation.result_package_id.id or False entire_pack = not operation.product_id and True or False # compute quantities for each lot + check quantities match lot_quantities = dict( (pack_lot.lot_id.id, operation.product_uom_id._compute_quantity( pack_lot.qty, operation.product_id.uom_id)) for pack_lot in operation.pack_lot_ids) qty = operation.product_qty if operation.product_uom_id and operation.product_uom_id != operation.product_id.uom_id: qty = operation.product_uom_id._compute_quantity( qty, operation.product_id.uom_id) if operation.pack_lot_ids and float_compare( sum(lot_quantities.values()), qty, precision_rounding=operation.product_id.uom_id.rounding ) != 0.0: raise UserError( _('You have a difference between the quantity on the operation and the quantities specified for the lots. ' )) quants_taken = [] false_quants = [] lot_move_qty = {} prout_move_qty = {} for link in operation.linked_move_operation_ids: prout_move_qty[link.move_id] = prout_move_qty.get( link.move_id, 0.0) + link.qty # Process every move only once for every pack operation for move in prout_move_qty.keys(): # TDE FIXME: do in batch ? move.check_tracking(operation) # TDE FIXME: I bet the message error is wrong if not remaining_move_qty.get(move.id): raise UserError( _("The roundings of your unit of measure %s on the move vs. %s on the product don't allow to do these operations or you are not transferring the picking at once. " ) % (move.product_uom.name, move.product_id.uom_id.name)) if not operation.pack_lot_ids: preferred_domain_list = [[('reservation_id', '=', move.id) ], [('reservation_id', '=', False)], [ '&', ('reservation_id', '!=', move.id), ('reservation_id', '!=', False) ]] quants = Quant.quants_get_preferred_domain( prout_move_qty[move], move, ops=operation, domain=[('qty', '>', 0)], preferred_domain_list=preferred_domain_list) Quant.quants_move(quants, move, operation.location_dest_id, location_from=operation.location_id, lot_id=False, owner_id=operation.owner_id.id, src_package_id=operation.package_id.id, dest_package_id=quant_dest_package_id, entire_pack=entire_pack) else: # Check what you can do with reserved quants already qty_on_link = prout_move_qty[move] rounding = operation.product_id.uom_id.rounding for reserved_quant in move.reserved_quant_ids: if (reserved_quant.owner_id.id != operation.owner_id.id) or (reserved_quant.location_id.id != operation.location_id.id) or \ (reserved_quant.package_id.id != operation.package_id.id): continue if not reserved_quant.lot_id: false_quants += [reserved_quant] elif float_compare(lot_quantities.get( reserved_quant.lot_id.id, 0), 0, precision_rounding=rounding) > 0: if float_compare( lot_quantities[reserved_quant.lot_id.id], reserved_quant.qty, precision_rounding=rounding) >= 0: lot_quantities[reserved_quant.lot_id. id] -= reserved_quant.qty quants_taken += [(reserved_quant, reserved_quant.qty)] qty_on_link -= reserved_quant.qty else: quants_taken += [ (reserved_quant, lot_quantities[reserved_quant.lot_id.id]) ] lot_quantities[reserved_quant.lot_id.id] = 0 qty_on_link -= lot_quantities[ reserved_quant.lot_id.id] lot_move_qty[move.id] = qty_on_link remaining_move_qty[move.id] -= prout_move_qty[move] # Handle lots separately if operation.pack_lot_ids: # TDE FIXME: fix call to move_quants_by_lot to ease understanding self._move_quants_by_lot(operation, lot_quantities, quants_taken, false_quants, lot_move_qty, quant_dest_package_id) # Handle pack in pack if not operation.product_id and operation.package_id and operation.result_package_id.id != operation.package_id.parent_id.id: operation.package_id.sudo().write( {'parent_id': operation.result_package_id.id}) # Check for remaining qtys and unreserve/check move_dest_id in move_dest_ids = set() for move in self: if float_compare(remaining_move_qty[move.id], 0, precision_rounding=move.product_id.uom_id.rounding ) > 0: # In case no pack operations in picking move.check_tracking( False) # TDE: do in batch ? redone ? check this preferred_domain_list = [[('reservation_id', '=', move.id)], [('reservation_id', '=', False)], [ '&', ('reservation_id', '!=', move.id), ('reservation_id', '!=', False) ]] quants = Quant.quants_get_preferred_domain( remaining_move_qty[move.id], move, domain=[('qty', '>', 0)], preferred_domain_list=preferred_domain_list) Quant.quants_move(quants, move, move.location_dest_id, lot_id=move.restrict_lot_id.id, owner_id=move.restrict_partner_id.id) # If the move has a destination, add it to the list to reserve if move.move_dest_id and move.move_dest_id.state in ('waiting', 'confirmed'): move_dest_ids.add(move.move_dest_id.id) if move.procurement_id: procurements |= move.procurement_id # unreserve the quants and make them available for other operations/moves move.quants_unreserve() # Check the packages have been placed in the correct locations self.mapped('quant_ids').filtered( lambda quant: quant.package_id and quant.qty > 0).mapped( 'package_id')._check_location_constraint() # set the move as done # setting force_date into stock moves f_date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) for move in self: if move.picking_id.force_date: f_date = move.picking_id.force_date self.write({'state': 'done', 'date': f_date}) # self.write({'state': 'done', 'date': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}) procurements.check() # assign destination moves if move_dest_ids: # TDE FIXME: record setise me self.browse(list(move_dest_ids)).action_assign() pickings.filtered(lambda picking: picking.state == 'done' and not picking.date_done).write({'date_done': f_date}) # pickings.filtered(lambda picking: picking.state == 'done' and not picking.date_done).write({'date_done': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}) return True
def button_validate(self): self.ensure_one() if self.purchase_id.requisition_id.id != False: picking_id = self.search([('purchase_requisition_id', '=', self.purchase_id.requisition_id.id)]) if picking_id: picking_id.state = "waiting" 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) 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 _anglo_saxon_purchase_move_lines(self, i_line, res): """Return the additional move lines for purchase invoices and refunds. i_line: An account.invoice.line object. res: The move line entries produced so far by the parent move_line_get. """ inv = i_line.invoice_id company_currency = inv.company_id.currency_id if i_line.product_id and i_line.product_id.valuation == 'real_time' and i_line.product_id.type == 'product': # get the fiscal position fpos = i_line.invoice_id.fiscal_position_id # get the price difference account at the product acc = i_line.product_id.property_account_creditor_price_difference if not acc: # if not found on the product get the price difference account at the category acc = i_line.product_id.categ_id.property_account_creditor_price_difference_categ acc = fpos.map_account(acc).id # reference_account_id is the stock input account reference_account_id = i_line.product_id.product_tmpl_id.get_product_accounts(fiscal_pos=fpos)['stock_input'].id diff_res = [] # calculate and write down the possible price difference between invoice price and product price for line in res: if line.get('invl_id', 0) == i_line.id and reference_account_id == line['account_id']: valuation_price_unit = i_line.product_id.uom_id._compute_price(i_line.product_id.standard_price, i_line.uom_id) line_quantity = line['quantity'] if i_line.product_id.cost_method != 'standard' and i_line.purchase_line_id: #for average/fifo/lifo costing method, fetch real cost price from incomming moves valuation_price_unit = i_line.purchase_line_id.product_uom._compute_price(i_line.purchase_line_id.price_unit, i_line.uom_id) stock_move_obj = self.env['stock.move'] valuation_stock_move = stock_move_obj.search([('purchase_line_id', '=', i_line.purchase_line_id.id), ('state', '=', 'done')]) if valuation_stock_move: valuation_price_unit_total = 0 valuation_total_qty = 0 for val_stock_move in valuation_stock_move: valuation_price_unit_total += abs(val_stock_move.price_unit) * val_stock_move.product_qty valuation_total_qty += val_stock_move.product_qty valuation_price_unit = valuation_price_unit_total / valuation_total_qty valuation_price_unit = i_line.product_id.uom_id._compute_price(valuation_price_unit, i_line.uom_id) line_quantity = valuation_total_qty elif i_line.product_id.cost_method == 'real': # In this condition, we have a real price-valuated product which has not yet been received valuation_price_unit = i_line.purchase_line_id.price_unit interim_account_price = valuation_price_unit * line_quantity if inv.currency_id.id != company_currency.id: # We express everyhting in the invoice currency valuation_price_unit = company_currency._convert(valuation_price_unit, inv.currency_id, inv.company_id, inv.date_invoice or fields.Date.today(), round=False) interim_account_price = company_currency._convert(interim_account_price, inv.currency_id, inv.company_id, inv.date_invoice or fields.Date.today(), round=False) invoice_cur_prec = inv.currency_id.decimal_places if float_compare(valuation_price_unit, i_line.price_unit, precision_digits=invoice_cur_prec) != 0 and float_compare(line['price_unit'], i_line.price_unit, precision_digits=invoice_cur_prec) == 0: # price with discount and without tax included price_unit = i_line.price_unit * (1 - (i_line.discount or 0.0) / 100.0) tax_ids = [] if line['tax_ids']: #line['tax_ids'] is like [(4, tax_id, None), (4, tax_id2, None)...] taxes = self.env['account.tax'].browse([x[1] for x in line['tax_ids']]) price_unit = taxes.compute_all(price_unit, currency=inv.currency_id, quantity=1.0)['total_excluded'] for tax in taxes: tax_ids.append((4, tax.id, None)) for child in tax.children_tax_ids: if child.type_tax_use != 'none': tax_ids.append((4, child.id, None)) price_before = line.get('price', 0.0) price_unit_val_dif = price_unit - valuation_price_unit price_val_dif = price_before - interim_account_price if inv.currency_id.compare_amounts(i_line.price_unit, i_line.purchase_line_id.price_unit) != 0 and acc: # If the unit prices have not changed and we have a # valuation difference, it means this difference is due to exchange rates, # so we don't create anything, the exchange rate entries will # be processed automatically by the rest of the code. diff_res.append({ 'type': 'src', 'name': i_line.name[:64], 'price_unit': inv.currency_id.round(price_unit_val_dif), 'quantity': line_quantity, 'price': inv.currency_id.round(price_val_dif), 'account_id': acc, 'product_id': line['product_id'], 'uom_id': line['uom_id'], 'account_analytic_id': line['account_analytic_id'], 'tax_ids': tax_ids, }) return diff_res return []
def rebuild_moves(self): company = self.env.user.company_id.id # warehouse = self.env['stock.warehouse'].search([('company_id', '=', company)], limit=1) # force remove quant's for product in self.product_variant_ids: rounding = product.uom_id.rounding for warehouse in self.env['stock.warehouse'].search([('company_id', '=', company)]): location = warehouse.lot_stock_id location |= warehouse.wh_input_stock_loc_id location |= warehouse.wh_qc_stock_loc_id location |= warehouse.wh_output_stock_loc_id location |= warehouse.wh_pack_stock_loc_id quants = self.env['stock.quant'].sudo()._gather(product, location, strict=False) # _logger.info("QUINTS FOR PRODUCT IN LOCATIONS %s:%s:%s:%s" % (product.default_code, product, location.name, quants)) 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': 0, 'reserved_quantity': 0, }) # 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 history = self.env['product.price.history'].search([ ('company_id', '=', company), ('product_id', 'in', self.product_variant_ids.ids)], order='datetime desc,id desc') if history: history.unlink() # quants = self.env['stock.quant'].sudo()._gather(product, location, strict=False) # _logger.info("QUINTS FOR PRODUCT IN LOCATIONS %s:%s:%s:%s" % (product.default_code, product, location.name, quants)) moves = self.env['stock.move'].search([('product_id', '=', product.id), ('state', '=', 'done')]) if self._uid == SUPERUSER_ID: moves = moves.filtered(lambda r: r.company_id == self.env.user.company_id) landed_cost = self.env['stock.valuation.adjustment.lines'].search([('move_id', 'in', moves.ids)]) # try: # _logger.info("MOVES %s" % moves) date_move = False for move in moves.sorted(lambda r: r.date): # move.write({"state": 'assigned'}) # move.move_line_ids.write({"state": 'assigned'}) # # !!!! need to move special module if you are mrp is ok but is not this logic is incorrect !!!! # if move.production_id: # production = move.production_id # production.with_context(dict(self._context, force_only_production=True)).rebuild_account_move() # continue move.remaining_value = 0.0 move.remaining_qty = 0.0 move.value = 0.0 move.price_unit = 0.0 if move.quantity_done == 0: continue if move.inventory_id: prod_inventory = move.inventory_id.line_ids.filtered(lambda r: r.product_id == move.product_id) if prod_inventory and prod_inventory[0].price_unit != 0: move.price_unit = prod_inventory[0].price_unit # if move.raw_material_production_id: # force_accounting_date = move.raw_material_production_id.date_finished # product = move.product_id.with_context( # dict(self._context, to_date=force_accounting_date)) # if product.qty_at_date != 0: # move.price_unit = product.account_value / product.qty_at_date correction_value = move._run_valuation(move.quantity_done) for move_line in move.move_line_ids.filtered( lambda r: float_compare(r.qty_done, 0, precision_rounding=r.product_uom_id.rounding) > 0): move_line._action_done() if self.only_quants: continue # now to regenerate account moves acc_moves = False for acc_move in move.account_move_ids: # if acc_move.state == 'posted': if not acc_moves: acc_moves = acc_move else: acc_moves |= acc_move if acc_moves: for acc_move in acc_moves: if acc_move.state == 'draft': acc_move.unlink() continue ret = acc_move.button_cancel() if ret: acc_move.unlink() # _logger.info("MOVE %s" % move.reference) if move.picking_id: move.write({'date': move.picking_id.date_done, 'accounting_date': move.picking_id.date_done}) move.move_line_ids.write({'date': move.picking_id.date_done}) if move.inventory_id: move.write({'date': move.inventory_id.accounting_date or move.inventory_id.date, 'accounting_date': move.inventory_id.accounting_date or move.inventory_id.date}) move.move_line_ids.write({'date': move.inventory_id.accounting_date or move.inventory_id.date}) move.with_context(dict(self._context, force_valuation_amount=correction_value, force_date=move.date, rebuld_try=True, force_re_calculate=True)).rebuild_account_move() # move_landed_cost = landed_cost.filtered(lambda r: r.move_id == move) # for landed_cost_adjustment in move_landed_cost: # landed_cost_adjustment.former_cost = move.value # if not date_move: # date_move = move.date # _logger.info("move_landed_cost %s <> %s" % (date_move, move.date)) # move_landed_cost = landed_cost.filtered(lambda r: r.move_id == move and (date_move > r.cost_id.date <= move.date)) # if move_landed_cost: # _logger.info("move_landed_cost %s=%s (date_move(%s) > r.cost_id.date(%s) <= move.date(%s))" % # (move_landed_cost.cost_id.name, move, date_move, move_landed_cost.cost_id.date, # move.date)) # for landed_cost_adjustment in move_landed_cost: # landed_cost_adjustment.former_cost = move.value # move_landed_cost.mapped('cost_id').rebuild_account_move() self._rebuild_moves(product, move, date_move) # date_move = move.date landed_cost.mapped('cost_id').rebuild_account_move()
def _schedule_hours(self, hours, day_dt=None, compute_leaves=False, resource_id=None, default_interval=None): """ Schedule hours of work, using a calendar and an optional resource to compute working and leave days. This method can be used backwards, i.e. scheduling days before a deadline. :param int hours: number of hours to schedule. Use a negative number to compute a backwards scheduling. :param datetime day_dt: reference date to compute working days. If days is > 0 date is the starting date. If days is < 0 date is the ending date. :param boolean compute_leaves: if set, compute the leaves based on calendar and resource. Otherwise no leaves are taken into account. :param int resource_id: the id of the resource to take into account when computing the leaves. If not set, only general leaves are computed. If set, generic and specific leaves are computed. :param tuple default_interval: if no id, try to return a default working day using default_interval[0] as beginning hour, and default_interval[1] as ending hour. Example: default_interval = (8, 16). Otherwise, a void list of working intervals is returned when id is None. :return tuple (datetime, intervals): datetime is the beginning/ending date of the schedulign; intervals are the working intervals of the scheduling. Note: Why not using rrule.rrule ? Because rrule does not seem to allow getting back in time. """ if day_dt is None: day_dt = datetime.datetime.now() backwards = (hours < 0) hours = abs(hours) intervals = [] remaining_hours = hours * 1.0 iterations = 0 current_datetime = day_dt call_args = dict(compute_leaves=compute_leaves, resource_id=resource_id, default_interval=default_interval) while float_compare( remaining_hours, 0.0, precision_digits=2) in (1, 0) and iterations < 1000: if backwards: call_args['end_dt'] = current_datetime else: call_args['start_dt'] = current_datetime working_intervals = self.get_working_intervals_of_day(**call_args) if not self and not working_intervals: # no calendar -> consider working 8 hours remaining_hours -= 8.0 elif working_intervals: if backwards: working_intervals.reverse() new_working_intervals = self.interval_schedule_hours( working_intervals, remaining_hours, not backwards) if backwards: new_working_intervals.reverse() res = timedelta() for interval in working_intervals: res += interval[1] - interval[0] remaining_hours -= (seconds(res) / 3600.0) if backwards: intervals = new_working_intervals + intervals else: intervals = intervals + new_working_intervals # get next day if backwards: current_datetime = datetime.datetime.combine( self.get_previous_day(current_datetime), datetime.time(23, 59, 59)) else: current_datetime = datetime.datetime.combine( self.get_next_day(current_datetime), datetime.time()) # avoid infinite loops iterations += 1 return intervals
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(line.qty_done == 0.0 for line in self.move_line_ids) no_initial_demand = all(move.product_uom_qty == 0.0 for move in self.move_lines) if no_initial_demand and no_quantities_done: raise UserError( _('You cannot validate a transfer if you have not processed any quantity.' )) 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' and ( line.qty_done == 0 or (not line.lot_name and not line.lot_id)): raise UserError( _('You need to supply a lot/serial number for %s.') % product.name) # In draft or with no pack operations edited yet, ask if we can just do everything if self.state == 'draft' or no_quantities_done: view = self.env.ref('stock.view_immediate_transfer') wiz = self.env['stock.immediate.transfer'].create( {'pick_id': 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 _stock_account_prepare_anglo_saxon_in_lines_vals(self): ''' Prepare values used to create the journal items (account.move.line) corresponding to the price difference lines for vendor bills. Example: Buy a product having a cost of 9 and a supplier price of 10 and being a storable product and having a perpetual valuation in FIFO. The vendor bill's journal entries looks like: Account | Debit | Credit --------------------------------------------------------------- 101120 Stock Interim Account (Received) | 10.0 | --------------------------------------------------------------- 101100 Account Payable | | 10.0 --------------------------------------------------------------- This method computes values used to make two additional journal items: --------------------------------------------------------------- 101120 Stock Interim Account (Received) | | 1.0 --------------------------------------------------------------- xxxxxx Price Difference Account | 1.0 | --------------------------------------------------------------- :return: A list of Python dictionary to be passed to env['account.move.line'].create. ''' lines_vals_list = [] price_unit_prec = self.env['decimal.precision'].precision_get( 'Product Price') for move in self: if move.move_type not in ( 'in_invoice', 'in_refund', 'in_receipt' ) or not move.company_id.anglo_saxon_accounting: continue move = move.with_company(move.company_id) for line in move.invoice_line_ids.filtered( lambda line: line.product_id.type == 'product' and line. product_id.valuation == 'real_time'): # Filter out lines being not eligible for price difference. if line.product_id.type != 'product' or line.product_id.valuation != 'real_time': continue # Retrieve accounts needed to generate the price difference. debit_pdiff_account = line.product_id.property_account_creditor_price_difference \ or line.product_id.categ_id.property_account_creditor_price_difference_categ debit_pdiff_account = move.fiscal_position_id.map_account( debit_pdiff_account) if not debit_pdiff_account: continue if line.product_id.cost_method != 'standard' and line.purchase_line_id: po_currency = line.purchase_line_id.currency_id po_company = line.purchase_line_id.company_id # Retrieve stock valuation moves. valuation_stock_moves = self.env['stock.move'].search([ ('purchase_line_id', '=', line.purchase_line_id.id), ('state', '=', 'done'), ('product_qty', '!=', 0.0), ]) if move.move_type == 'in_refund': valuation_stock_moves = valuation_stock_moves.filtered( lambda stock_move: stock_move._is_out()) else: valuation_stock_moves = valuation_stock_moves.filtered( lambda stock_move: stock_move._is_in()) if valuation_stock_moves: valuation_price_unit_total = 0 valuation_total_qty = 0 for val_stock_move in valuation_stock_moves: # In case val_stock_move is a return move, its valuation entries have been made with the # currency rate corresponding to the original stock move valuation_date = val_stock_move.origin_returned_move_id.date or val_stock_move.date svl = val_stock_move.with_context( active_test=False).mapped( 'stock_valuation_layer_ids').filtered( lambda l: l.quantity) layers_qty = sum(svl.mapped('quantity')) layers_values = sum(svl.mapped('value')) valuation_price_unit_total += line.company_currency_id._convert( layers_values, move.currency_id, move.company_id, valuation_date, round=False, ) valuation_total_qty += layers_qty if float_is_zero( valuation_total_qty, precision_rounding=line.product_uom_id.rounding or line.product_id.uom_id.rounding): raise UserError( _('Odoo is not able to generate the anglo saxon entries. The total valuation of %s is zero.' ) % line.product_id.display_name) valuation_price_unit = valuation_price_unit_total / valuation_total_qty valuation_price_unit = line.product_id.uom_id._compute_price( valuation_price_unit, line.product_uom_id) elif line.product_id.cost_method == 'fifo': # In this condition, we have a real price-valuated product which has not yet been received valuation_price_unit = po_currency._convert( line.purchase_line_id.price_unit, move.currency_id, po_company, move.date, round=False, ) else: # For average/fifo/lifo costing method, fetch real cost price from incoming moves. price_unit = line.purchase_line_id.product_uom._compute_price( line.purchase_line_id.price_unit, line.product_uom_id) valuation_price_unit = po_currency._convert( price_unit, move.currency_id, po_company, move.date, round=False) else: # Valuation_price unit is always expressed in invoice currency, so that it can always be computed with the good rate price_unit = line.product_id.uom_id._compute_price( line.product_id.standard_price, line.product_uom_id) valuation_price_unit = line.company_currency_id._convert( price_unit, move.currency_id, move.company_id, fields.Date.today(), round=False) price_unit = line.price_unit * (1 - (line.discount or 0.0) / 100.0) if line.tax_ids: # 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).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
def quants_get_reservation(self, qty, move, pack_operation_id=False, lot_id=False, company_id=False, domain=None, preferred_domain_list=None): ''' This function tries to find quants for the given domain and move/ops, by trying to first limit the choice on the quants that match the first item of preferred_domain_list as well. But if the qty requested is not reached it tries to find the remaining quantity by looping on the preferred_domain_list (tries with the second item and so on). Make sure the quants aren't found twice => all the domains of preferred_domain_list should be orthogonal ''' # TDE FIXME: clean me reservations = [(None, qty)] pack_operation = self.env['stock.pack.operation'].browse( pack_operation_id) location = pack_operation.location_id if pack_operation else move.location_id # don't look for quants in location that are of type production, supplier or inventory. if location.usage in ['inventory', 'production', 'supplier']: return reservations # return self._Reservation(reserved_quants, qty, qty, move, None) restrict_lot_id = lot_id if pack_operation else move.restrict_lot_id.id or lot_id removal_strategy = move.get_removal_strategy() domain = self._quants_get_reservation_domain( move, pack_operation_id=pack_operation_id, lot_id=lot_id, company_id=company_id, initial_domain=domain) if not restrict_lot_id and not preferred_domain_list: meta_domains = [[]] elif restrict_lot_id and not preferred_domain_list: meta_domains = [[('lot_id', '=', restrict_lot_id)], [('lot_id', '=', False)]] elif restrict_lot_id and preferred_domain_list: lot_list = [] no_lot_list = [] for inner_domain in preferred_domain_list: lot_list.append(inner_domain + [('lot_id', '=', restrict_lot_id)]) no_lot_list.append(inner_domain + [('lot_id', '=', False)]) meta_domains = lot_list + no_lot_list else: meta_domains = preferred_domain_list res_qty = qty while (float_compare( res_qty, 0, precision_rounding=move.product_id.uom_id.rounding) and meta_domains): additional_domain = meta_domains.pop(0) reservations.pop() new_reservations = self._quants_get_reservation( res_qty, move, ops=pack_operation, domain=domain + additional_domain, removal_strategy=removal_strategy) for quant in new_reservations: if quant[0]: res_qty -= quant[1] reservations += new_reservations return reservations
def action_assign(self, no_prepare=False): """ Checks the product type and accordingly writes the state. """ # TDE FIXME: remove decorator once everything is migrated # TDE FIXME: clean me, please main_domain = {} Quant = self.env['stock.quant'] Uom = self.env['product.uom'] moves_to_assign = self.env['stock.move'] moves_to_do = self.env['stock.move'] operations = self.env['stock.pack.operation'] ancestors_list = {} # work only on in progress moves moves = self.filtered( lambda move: move.state in ['confirmed', 'waiting', 'assigned']) moves.filtered(lambda move: move.reserved_quant_ids).do_unreserve() for move in moves: if move.location_id.usage in ('supplier', 'inventory', 'production'): moves_to_assign |= move # TDE FIXME: what ? # in case the move is returned, we want to try to find quants before forcing the assignment if not move.origin_returned_move_id: continue # if the move is preceeded, restrict the choice of quants in the ones moved previously in original move ancestors = move.find_move_ancestors() if move.product_id.type in ['consu', 'service'] and not ancestors: moves_to_assign |= move continue else: moves_to_do |= move # we always search for yet unassigned quants main_domain[move.id] = [('reservation_id', '=', False), ('qty', '>', 0)] ancestors_list[move.id] = True if ancestors else False if move.state == 'waiting' and not ancestors: # if the waiting move hasn't yet any ancestor (PO/MO not confirmed yet), don't find any quant available in stock main_domain[move.id] += [('id', '=', False)] elif ancestors: main_domain[move.id] += [('history_ids', 'in', ancestors.ids)] # if the move is returned from another, restrict the choice of quants to the ones that follow the returned move if move.origin_returned_move_id: main_domain[move.id] += [('history_ids', 'in', move.origin_returned_move_id.id)] for link in move.linked_move_operation_ids: operations |= link.operation_id # Check all ops and sort them: we want to process first the packages, then operations with lot then the rest operations = operations.sorted( key=lambda x: ((x.package_id and not x.product_id) and -4 or 0) + (x.package_id and -2 or 0) + (x.pack_lot_ids and -1 or 0)) for ops in operations: # TDE FIXME: this code seems to be in action_done, isn't it ? # first try to find quants based on specific domains given by linked operations for the case where we want to rereserve according to existing pack operations if not (ops.product_id and ops.pack_lot_ids): for record in ops.linked_move_operation_ids: move = record.move_id if move.id in main_domain: qty = record.qty domain = main_domain[move.id] if qty: quants = Quant.quants_get_preferred_domain( qty, move, ops=ops, domain=domain, preferred_domain_list=[]) Quant.quants_reserve(quants, move, record) else: lot_qty = {} rounding = ops.product_id.uom_id.rounding for pack_lot in ops.pack_lot_ids: lot_qty[pack_lot.lot_id. id] = ops.product_uom_id._compute_quantity( pack_lot.qty, ops.product_id.uom_id) for record in ops.linked_move_operation_ids: move_qty = record.qty move = record.move_id domain = main_domain[move.id] for lot in lot_qty: if float_compare( lot_qty[lot], 0, precision_rounding=rounding ) > 0 and float_compare( move_qty, 0, precision_rounding=rounding) > 0: qty = min(lot_qty[lot], move_qty) quants = Quant.quants_get_preferred_domain( qty, move, ops=ops, lot_id=lot, domain=domain, preferred_domain_list=[]) Quant.quants_reserve(quants, move, record) lot_qty[lot] -= qty move_qty -= qty # Sort moves to reserve first the ones with ancestors, in case the same product is listed in # different stock moves. for move in sorted(moves_to_do, key=lambda x: -1 if ancestors_list.get(x.id) else 0): # then if the move isn't totally assigned, try to find quants without any specific domain if move.state != 'assigned' and not self.env.context.get( 'reserve_only_ops'): qty_already_assigned = move.reserved_availability qty = move.product_qty - qty_already_assigned quants = Quant.quants_get_preferred_domain( qty, move, domain=main_domain[move.id], preferred_domain_list=[]) Quant.quants_reserve(quants, move) # force assignation of consumable products and incoming from supplier/inventory/production # Do not take force_assign as it would create pack operations if moves_to_assign: moves_to_assign.write({'state': 'assigned'}) if not no_prepare: self.check_recompute_pack_op()
def _prepare_stock_moves(self, picking): """ Prepare the stock moves data for one order line. This function returns a list of dictionary ready to be used in stock.move's create() """ self.ensure_one() res = [] if self.product_id.type not in ['product', 'consu']: return res qty = 0.0 price_unit = self._get_stock_move_price_unit() for move in self.move_ids.filtered(lambda x: x.state != 'cancel'): qty += move.product_qty template = { 'name': self.name or '', 'product_id': self.product_id.id, 'product_uom': self.product_uom.id, 'date': self.order_id.date_order, 'date_expected': self.date_planned, 'location_id': self.order_id.partner_id.property_stock_supplier.id, 'location_dest_id': self.order_id._get_destination_location(), 'picking_id': picking.id, 'partner_id': self.order_id.dest_address_id.id, 'move_dest_id': False, 'state': 'draft', 'purchase_line_id': self.id, 'company_id': self.order_id.company_id.id, 'price_unit': price_unit, 'picking_type_id': self.order_id.picking_type_id.id, 'group_id': self.order_id.group_id.id, 'procurement_id': False, 'origin': self.order_id.name, 'route_ids': self.order_id.picking_type_id.warehouse_id and [(6, 0, [ x.id for x in self.order_id.picking_type_id.warehouse_id.route_ids ])] or [], 'warehouse_id': self.order_id.picking_type_id.warehouse_id.id, } # Fullfill all related procurements with this po line diff_quantity = self.product_qty - qty for procurement in self.procurement_ids.filtered( lambda p: p.state != 'cancel'): # If the procurement has some moves already, we should deduct their quantity sum_existing_moves = sum(x.product_qty for x in procurement.move_ids if x.state != 'cancel') existing_proc_qty = procurement.product_id.uom_id._compute_quantity( sum_existing_moves, procurement.product_uom) procurement_qty = procurement.product_uom._compute_quantity( procurement.product_qty, self.product_uom) - existing_proc_qty if float_compare( procurement_qty, 0.0, precision_rounding=procurement.product_uom.rounding ) > 0 and float_compare( diff_quantity, 0.0, precision_rounding=self.product_uom.rounding) > 0: tmp = template.copy() tmp.update({ 'product_uom_qty': min(procurement_qty, diff_quantity), 'move_dest_id': procurement.move_dest_id. id, # move destination is same as procurement destination 'procurement_id': procurement.id, 'propagate': procurement.rule_id.propagate, }) res.append(tmp) diff_quantity -= min(procurement_qty, diff_quantity) if float_compare(diff_quantity, 0.0, precision_rounding=self.product_uom.rounding) > 0: template['product_uom_qty'] = diff_quantity res.append(template) return res
def _update_reserved_quantity(self, need, available_quantity, location_id, lot_id=None, package_id=None, owner_id=None, strict=True): """ Create or update move lines. """ self.ensure_one() if not lot_id: lot_id = self.env['stock.production.lot'] if not package_id: package_id = self.env['stock.quant.package'] if not owner_id: owner_id = self.env['res.partner'] taken_quantity = min(available_quantity, need) # `taken_quantity` is in the quants unit of measure. There's a possibility that the move's # unit of measure won't be respected if we blindly reserve this quantity, a common usecase # is if the move's unit of measure's rounding does not allow fractional reservation. We chose # to convert `taken_quantity` to the move's unit of measure with a down rounding method and # then get it back in the quants unit of measure with an half-up rounding_method. This # way, we'll never reserve more than allowed. We do not apply this logic if # `available_quantity` is brought by a chained move line. In this case, `_prepare_move_line_vals` # will take care of changing the UOM to the UOM of the product. if not strict: taken_quantity_move_uom = self.product_id.uom_id._compute_quantity( taken_quantity, self.product_uom, rounding_method='DOWN') taken_quantity = self.product_uom._compute_quantity( taken_quantity_move_uom, self.product_id.uom_id, rounding_method='HALF-UP') quants = [] if self.product_id.tracking == 'serial': rounding = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') if float_compare(taken_quantity, int(taken_quantity), precision_digits=rounding) != 0: taken_quantity = 0 # ADD filters logic to get quants quant_filters = [] for specs in self.move_line_specs_ids: quant_filters.append(specs.create_specs_filter_values()) try: if not float_is_zero( taken_quantity, precision_rounding=self.product_id.uom_id.rounding): quants = self.env['stock.quant']._update_reserved_quantity( self.product_id, location_id, taken_quantity, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict, specs_filter=quant_filters) except UserError: taken_quantity = 0 # Find a candidate move line to update or create a new one. _logger.info("!!!_update_reserved_quantity() UPDATE RESERVED QTY: %r", quants) for reserved_quant, quantity in quants: to_update = self.move_line_ids.filtered( lambda ml: ml._reservation_is_updatable( quantity, reserved_quant)) if to_update: to_update[0].with_context( bypass_reservation_update=True ).product_uom_qty += self.product_id.uom_id._compute_quantity( quantity, to_update[0].product_uom_id, rounding_method='HALF-UP') else: if self.product_id.tracking == 'serial': for i in range(0, int(quantity)): self.env['stock.move.line'].create( self._prepare_move_line_vals( quantity=1, reserved_quant=reserved_quant)) else: self.env['stock.move.line'].create( self._prepare_move_line_vals( quantity=quantity, reserved_quant=reserved_quant)) return taken_quantity
def action_done(self): if self.state == 'done': return if self.label_lines and len(self.label_lines) > 0: raise UserError(u'已生成标签明细请不要重复操作!') if float_compare(self.sundryin_qty, 0.0, precision_rounding=0.000001) <= 0.0: raise UserError(u'杂入总数不能小于0!') if float_compare(self.everyone_qty, 0.0, precision_rounding=0.000001) <= 0.0: raise UserError(u'每标签数量不能小于0!') if float_compare(self.sundryin_qty, self.everyone_qty, precision_rounding=0.000001) < 0.0: raise UserError(u"每标签数量不可以大于杂入总数!") temp_qty, labellines = self.sundryin_qty, [] productlot = self.env['stock.production.lot'].action_checkout_lot( self.product_id.id, self.product_lot) for index in range( 0, int(math.ceil(self.sundryin_qty / self.everyone_qty))): if float_compare(temp_qty, 0.0, precision_rounding=0.000001) <= 0.0: break if float_compare(temp_qty, self.everyone_qty, precision_rounding=0.000001) >= 0.0: labelqty = self.everyone_qty else: labelqty = temp_qty tlabel = self.env['aas.product.label'].create({ 'product_id': self.product_id.id, 'product_lot': productlot.id, 'location_id': self.location_id.id, 'stocked': True, 'product_qty': labelqty }) labellines.append((0, 0, { 'label_id': tlabel.id, 'product_qty': labelqty })) temp_qty -= labelqty self.write({'label_lines': labellines, 'state': 'done'}) company_id, tproduct = self.env.user.company_id.id, self.product_id sundrylocation = self.env.ref('aas_wms.stock_location_sundry') self.env['stock.move'].create({ 'name': u'杂入%s' % tproduct.default_code, 'product_id': tproduct.id, 'product_uom': tproduct.uom_id.id, 'create_date': fields.Datetime.now(), 'restrict_lot_id': productlot.id, 'product_uom_qty': self.sundryin_qty, 'location_id': sundrylocation.id, 'location_dest_id': self.location_id.id, 'company_id': company_id }).action_done()
def _action_done(self): # print ('********kakakak111111') # print('***self 1',self) # moves_todo = super(StockMove, self)._action_done() # return moves_todo self.filtered(lambda move: move.state == 'draft')._action_confirm( ) # MRP allows scrapping draft moves moves = self.exists().filtered(lambda x: x.state not in ('done', 'cancel')) moves_todo = self.env['stock.move'] #moves# # Cancel moves where necessary ; we should do it before creating the extra moves because # this operation could trigger a merge of moves. for move in moves: if move.quantity_done <= 0: if float_compare( move.product_uom_qty, 0.0, precision_rounding=move.product_uom.rounding) == 0: move._action_cancel() # Create extra moves where necessary for move in moves: if move.state == 'cancel' or move.quantity_done <= 0: continue # extra move will not be merged in mrp if not move.picking_id: moves_todo |= move moves_todo |= move._create_extra_move() print('****************moves_todo 1', moves, moves_todo) # Split moves where necessary and move quants for move in moves_todo: print('****************moves_todo 2', moves, moves_todo) # To know whether we need to create a backorder or not, round to the general product's # decimal precision and not the product's UOM. rounding = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') if float_compare(move.quantity_done, move.product_uom_qty, precision_digits=rounding) < 0: # Need to do some kind of conversion here qty_split = move.product_uom._compute_quantity( move.product_uom_qty - move.quantity_done, move.product_id.uom_id, rounding_method='HALF-UP') new_move = move._split(qty_split) for move_line in move.move_line_ids: if move_line.product_qty and move_line.qty_done: # FIXME: there will be an issue if the move was partially available # By decreasing `product_qty`, we free the reservation. # FIXME: if qty_done > product_qty, this could raise if nothing is in stock try: move_line.write( {'product_uom_qty': move_line.qty_done}) except UserError: pass move._unreserve_initial_demand(new_move) move.move_line_ids._action_done() # Check the consistency of the result packages; there should be an unique location across # the contained quants. for result_package in moves_todo\ .mapped('move_line_ids.result_package_id')\ .filtered(lambda p: p.quant_ids and len(p.quant_ids) > 1): if len(result_package.quant_ids.mapped('location_id')) > 1: raise UserError( _('You should not put the contents of a package in different locations.' )) picking = moves_todo and moves_todo[0].picking_id or False moves_todo.write({'state': 'done', 'date': fields.Datetime.now()}) moves_todo.mapped('move_dest_ids')._action_assign() # We don't want to create back order for scrap moves # Replace by a kwarg in master if self.env.context.get('is_scrap'): return moves_todo if picking: picking._create_backorder() return moves_todo
def _action_done(self): """ This method is called during a move's `action_done`. It'll actually move a quant from the source location to the destination location, and unreserve if needed in the source location. This method is intended to be called on all the move lines of a move. This method is not intended to be called when editing a `done` move (that's what the override of `write` here is done. """ Quant = self.env['stock.quant'] # First, we loop over all the move lines to do a preliminary check: `qty_done` should not # be negative and, according to the presence of a picking type or a linked inventory # adjustment, enforce some rules on the `lot_id` field. If `qty_done` is null, we unlink # the line. It is mandatory in order to free the reservation and correctly apply # `action_done` on the next move lines. ml_ids_to_delete = OrderedSet() lot_vals_to_create = [] # lot values for batching the creation associate_line_lot = [] # move_line to associate to the lot for ml in self: # Check here if `ml.qty_done` respects the rounding of `ml.product_uom_id`. uom_qty = float_round( ml.qty_done, precision_rounding=ml.product_uom_id.rounding, rounding_method='HALF-UP') precision_digits = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') qty_done = float_round(ml.qty_done, precision_digits=precision_digits, rounding_method='HALF-UP') if float_compare( uom_qty, qty_done, precision_digits=precision_digits) != 0: raise UserError( _('The quantity done for the product "%s" doesn\'t respect the rounding precision \ defined on the unit of measure "%s". Please change the quantity done or the \ rounding precision of your unit of measure.') % (ml.product_id.display_name, ml.product_uom_id.name)) qty_done_float_compared = float_compare( ml.qty_done, 0, precision_rounding=ml.product_uom_id.rounding) if qty_done_float_compared > 0: if ml.product_id.tracking != 'none': picking_type_id = ml.move_id.picking_type_id if picking_type_id: if picking_type_id.use_create_lots: # If a picking type is linked, we may have to create a production lot on # the fly before assigning it to the move line if the user checked both # `use_create_lots` and `use_existing_lots`. if ml.lot_name and not ml.lot_id: lot_vals_to_create.append({ 'name': ml.lot_name, 'product_id': ml.product_id.id, 'company_id': ml.move_id.company_id.id }) associate_line_lot.append(ml) continue # Avoid the raise after because not lot_id is set elif not picking_type_id.use_create_lots and not picking_type_id.use_existing_lots: # If the user disabled both `use_create_lots` and `use_existing_lots` # checkboxes on the picking type, he's allowed to enter tracked # products without a `lot_id`. continue elif ml.move_id.inventory_id: # If an inventory adjustment is linked, the user is allowed to enter # tracked products without a `lot_id`. continue if not ml.lot_id: raise UserError( _('You need to supply a Lot/Serial number for product %s.' ) % ml.product_id.display_name) elif qty_done_float_compared < 0: raise UserError(_('No negative quantities allowed')) else: ml_ids_to_delete.add(ml.id) mls_to_delete = self.env['stock.move.line'].browse(ml_ids_to_delete) mls_to_delete.unlink() # Batching the creation of lots and associated each to the right ML (order is preserve in the create) lots = self.env['stock.production.lot'].create(lot_vals_to_create) for ml, lot in zip(associate_line_lot, lots): ml.write({'lot_id': lot.id}) mls_todo = (self - mls_to_delete) mls_todo._check_company() # Now, we can actually move the quant. ml_ids_to_ignore = OrderedSet() for ml in mls_todo: if ml.product_id.type == 'product': rounding = ml.product_uom_id.rounding # if this move line is force assigned, unreserve elsewhere if needed if not ml._should_bypass_reservation( ml.location_id) and float_compare( ml.qty_done, ml.product_uom_qty, precision_rounding=rounding) > 0: qty_done_product_uom = ml.product_uom_id._compute_quantity( ml.qty_done, ml.product_id.uom_id, rounding_method='HALF-UP') extra_qty = qty_done_product_uom - ml.product_qty ml_to_ignore = self.env['stock.move.line'].browse( ml_ids_to_ignore) ml._free_reservation(ml.product_id, ml.location_id, extra_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, ml_to_ignore=ml_to_ignore) # unreserve what's been reserved if not ml._should_bypass_reservation( ml.location_id ) and ml.product_id.type == 'product' and ml.product_qty: 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) # move what's been actually done quantity = ml.product_uom_id._compute_quantity( ml.qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP') available_qty, in_date = Quant._update_available_quantity( ml.product_id, ml.location_id, -quantity, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id) if available_qty < 0 and ml.lot_id: # see if we can compensate the negative quants with some untracked quants untracked_qty = Quant._get_available_quantity( ml.product_id, ml.location_id, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) if untracked_qty: taken_from_untracked_qty = min(untracked_qty, abs(quantity)) Quant._update_available_quantity( ml.product_id, ml.location_id, -taken_from_untracked_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id) Quant._update_available_quantity( ml.product_id, ml.location_id, taken_from_untracked_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id) Quant._update_available_quantity( ml.product_id, ml.location_dest_id, quantity, lot_id=ml.lot_id, package_id=ml.result_package_id, owner_id=ml.owner_id, in_date=in_date) ml_ids_to_ignore.add(ml.id) # Reset the reserved quantity as we just moved it to the destination location. mls_todo.with_context(bypass_reservation_update=True).write({ 'product_uom_qty': 0.00, 'date': fields.Datetime.now(), })
def button_validate(self): res = super(Picking, self).button_validate() self.ensure_one() invoice_total = 0 payment_total = 0 exceed_amount = 0 customer_inv = self.env["account.invoice"].search([ ('partner_id', '=', self.partner_id.id), ('state', 'not in', ['draft', 'cancel']), ('type', '=', 'out_invoice') ]) for inv in customer_inv: invoice_total += inv.amount_total customer_payment = self.env["account.payment"].search([ ('partner_id', '=', self.partner_id.id), ('payment_type', '=', 'inbound'), ('state', 'in', ['posted', 'reconciled']) ]) for pay in customer_payment: payment_total += pay.amount sale = self.env['sale.order'].search([('name', '=', self.origin)]) delivered_quantity = all(line.product_id.invoice_policy == 'delivery' for line in self.move_line_ids) if payment_total > invoice_total: print("else") elif invoice_total > payment_total: exceed_amount = (invoice_total + sale.amount_total) - payment_total if delivered_quantity: if exceed_amount > self.partner_id.credit_limit: raise UserError(_('Credit limit exceeded for this customer')) else: 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(line.qty_done == 0.0 for line in self.move_line_ids) no_initial_demand = all(move.product_uom_qty == 0.0 for move in self.move_lines) if no_initial_demand and no_quantities_done: raise UserError( _('You cannot validate a transfer if you have not processed any quantity.' )) 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' and ( line.qty_done == 0 or (not line.lot_name and not line.lot_id)): raise UserError( _('You need to supply a lot/serial number for %s.') % product.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