Dre4m Shell
Server IP : 127.0.0.2  /  Your IP : 3.144.6.159
Web Server : Apache/2.4.18 (Ubuntu)
System :
User : www-data ( )
PHP Version : 7.0.33-0ubuntu0.16.04.16
Disable Function : disk_free_space,disk_total_space,diskfreespace,dl,exec,fpaththru,getmyuid,getmypid,highlight_file,ignore_user_abord,leak,listen,link,opcache_get_configuration,opcache_get_status,passthru,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,php_uname,phpinfo,posix_ctermid,posix_getcwd,posix_getegid,posix_geteuid,posix_getgid,posix_getgrgid,posix_getgrnam,posix_getgroups,posix_getlogin,posix_getpgid,posix_getpgrp,posix_getpid,posix,_getppid,posix_getpwnam,posix_getpwuid,posix_getrlimit,posix_getsid,posix_getuid,posix_isatty,posix_kill,posix_mkfifo,posix_setegid,posix_seteuid,posix_setgid,posix_setpgid,posix_setsid,posix_setuid,posix_times,posix_ttyname,posix_uname,pclose,popen,proc_open,proc_close,proc_get_status,proc_nice,proc_terminate,shell_exec,source,show_source,system,virtual
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : ON  |  Sudo : ON  |  Pkexec : ON
Directory :  /opt/odoo/addons/stock/models/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ HOME SHELL ]     

Current File : /opt/odoo/addons/stock/models/stock_quant.py
from datetime import datetime

from odoo import api, fields, models
from odoo.tools.float_utils import float_compare, float_round
from odoo.tools.translate import _
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
from odoo.exceptions import UserError

import logging

_logger = logging.getLogger(__name__)


class Quant(models.Model):
    """ Quants are the smallest unit of stock physical instances """
    _name = "stock.quant"
    _description = "Quants"

    name = fields.Char(string='Identifier', compute='_compute_name')
    product_id = fields.Many2one(
        'product.product', 'Product',
        index=True, ondelete="restrict", readonly=True, required=True)
    location_id = fields.Many2one(
        'stock.location', 'Location',
        auto_join=True, index=True, ondelete="restrict", readonly=True, required=True)
    qty = fields.Float(
        'Quantity',
        index=True, readonly=True, required=True,
        help="Quantity of products in this quant, in the default unit of measure of the product")
    product_uom_id = fields.Many2one(
        'product.uom', string='Unit of Measure', related='product_id.uom_id',
        readonly=True)
    package_id = fields.Many2one(
        'stock.quant.package', string='Package',
        index=True, readonly=True,
        help="The package containing this quant")
    packaging_type_id = fields.Many2one(
        'product.packaging', string='Type of packaging', related='package_id.packaging_id',
        readonly=True, store=True)
    reservation_id = fields.Many2one(
        'stock.move', 'Reserved for Move',
        index=True, readonly=True,
        help="The move the quant is reserved for")
    lot_id = fields.Many2one(
        'stock.production.lot', 'Lot/Serial Number',
        index=True, ondelete="restrict", readonly=True)
    cost = fields.Float('Unit Cost', group_operator='avg')
    owner_id = fields.Many2one(
        'res.partner', 'Owner',
        index=True, readonly=True,
        help="This is the owner of the quant")
    create_date = fields.Datetime('Creation Date', readonly=True)
    in_date = fields.Datetime('Incoming Date', index=True, readonly=True)
    history_ids = fields.Many2many(
        'stock.move', 'stock_quant_move_rel', 'quant_id', 'move_id',
        string='Moves', copy=False,
        help='Moves that operate(d) on this quant')
    company_id = fields.Many2one(
        'res.company', 'Company',
        index=True, readonly=True, required=True,
        default=lambda self: self.env['res.company']._company_default_get('stock.quant'),
        help="The company to which the quants belong")
    inventory_value = fields.Float('Inventory Value', compute='_compute_inventory_value', readonly=True)
    # Used for negative quants to reconcile after compensated by a new positive one
    propagated_from_id = fields.Many2one(
        'stock.quant', 'Linked Quant',
        index=True, readonly=True,
        help='The negative quant this is coming from')
    negative_move_id = fields.Many2one(
        'stock.move', 'Move Negative Quant',
        readonly=True,
        help='If this is a negative quant, this will be the move that caused this negative quant.')
    negative_dest_location_id = fields.Many2one(
        'stock.location', "Negative Destination Location", related='negative_move_id.location_dest_id',
        readonly=True,
        help="Technical field used to record the destination location of a move that created a negative quant")

    @api.one
    def _compute_name(self):
        """ Forms complete name of location from parent location to child location. """
        self.name = '%s: %s%s' % (self.lot_id.name or self.product_id.code or '', self.qty, self.product_id.uom_id.name)

    @api.multi
    def _compute_inventory_value(self):
        for quant in self:
            if quant.company_id != self.env.user.company_id:
                # if the company of the quant is different than the current user company, force the company in the context
                # then re-do a browse to read the property fields for the good company.
                quant = quant.with_context(force_company=quant.company_id.id)
            quant.inventory_value = quant.product_id.standard_price * quant.qty

    @api.model_cr
    def init(self):
        self._cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('stock_quant_product_location_index',))
        if not self._cr.fetchone():
            self._cr.execute('CREATE INDEX stock_quant_product_location_index ON stock_quant (product_id, location_id, company_id, qty, in_date, reservation_id)')

    @api.multi
    def unlink(self):
        # TDE FIXME: should probably limitate unlink to admin and sudo calls to unlink, because context is not safe
        if not self.env.context.get('force_unlink'):
            raise UserError(_('Under no circumstances should you delete or change quants yourselves!'))
        return super(Quant, self).unlink()

    @api.model
    def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
        " Overwrite the read_group in order to sum the function field 'inventory_value' in group by "
        # TDE NOTE: WHAAAAT ??? is this because inventory_value is not stored ?
        # TDE FIXME: why not storing the inventory_value field ? company_id is required, stored, and should not create issues
        res = super(Quant, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy)
        if 'inventory_value' in fields:
            for line in res:
                if '__domain' in line:
                    lines = self.search(line['__domain'])
                    inv_value = 0.0
                    for line2 in lines:
                        inv_value += line2.inventory_value
                    line['inventory_value'] = inv_value
        return res

    @api.multi
    def action_view_quant_history(self):
        ''' Returns an action that display the history of the quant, which
        mean all the stock moves that lead to this quant creation with this
        quant quantity. '''
        action = self.env.ref('stock', 'stock_move_action').read()[0]
        action['domain'] = [('id', 'in', self.mapped('history_ids').ids)]
        return action

    @api.model
    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})

    @api.model
    def quants_move(self, quants, move, location_to, location_from=False, lot_id=False, owner_id=False, src_package_id=False, dest_package_id=False, entire_pack=False):
        """Moves all given stock.quant in the given destination location.  Unreserve from current move.
        :param quants: list of tuple(browse record(stock.quant) or None, quantity to move)
        :param move: browse record (stock.move)
        :param location_to: browse record (stock.location) depicting where the quants have to be moved
        :param location_from: optional browse record (stock.location) explaining where the quant has to be taken
                              (may differ from the move source location in case a removal strategy applied).
                              This parameter is only used to pass to _quant_create_from_move if a negative quant must be created
        :param lot_id: ID of the lot that must be set on the quants to move
        :param owner_id: ID of the partner that must own the quants to move
        :param src_package_id: ID of the package that contains the quants to move
        :param dest_package_id: ID of the package that must be set on the moved quant
        """
        # TDE CLEANME: use ids + quantities dict
        if location_to.usage == 'view':
            raise UserError(_('You cannot move to a location of type view %s.') % (location_to.name))

        quants_reconcile_sudo = self.env['stock.quant'].sudo()
        quants_move_sudo = self.env['stock.quant'].sudo()
        check_lot = False
        for quant, qty in quants:
            if not quant:
                #If quant is None, we will create a quant to move (and potentially a negative counterpart too)
                quant = self._quant_create_from_move(
                    qty, move, lot_id=lot_id, owner_id=owner_id, src_package_id=src_package_id, dest_package_id=dest_package_id, force_location_from=location_from, force_location_to=location_to)
                check_lot = True
            else:
                quant._quant_split(qty)
                quants_move_sudo |= quant
            quants_reconcile_sudo |= quant

        if quants_move_sudo:
            moves_recompute = quants_move_sudo.filtered(lambda self: self.reservation_id != move).mapped('reservation_id')
            quants_move_sudo._quant_update_from_move(move, location_to, dest_package_id, lot_id=lot_id, entire_pack=entire_pack)
            moves_recompute.recalculate_move_state()

        if location_to.usage == 'internal':
            # Do manual search for quant to avoid full table scan (order by id)
            self._cr.execute("""
                SELECT 0 FROM stock_quant, stock_location WHERE product_id = %s AND stock_location.id = stock_quant.location_id AND
                ((stock_location.parent_left >= %s AND stock_location.parent_left < %s) OR stock_location.id = %s) AND qty < 0.0 LIMIT 1
            """, (move.product_id.id, location_to.parent_left, location_to.parent_right, location_to.id))
            if self._cr.fetchone():
                quants_reconcile_sudo._quant_reconcile_negative(move)

        # In case of serial tracking, check if the product does not exist somewhere internally already
        # Checking that a positive quant already exists in an internal location is too restrictive.
        # Indeed, if a warehouse is configured with several steps (e.g. "Pick + Pack + Ship") and
        # one step is forced (creates a quant of qty = -1.0), it is not possible afterwards to
        # correct the inventory unless the product leaves the stock.
        picking_type = move.picking_id and move.picking_id.picking_type_id or False
        if check_lot and lot_id and move.product_id.tracking == 'serial' and (not picking_type or (picking_type.use_create_lots or picking_type.use_existing_lots)):
            other_quants = self.search([('product_id', '=', move.product_id.id), ('lot_id', '=', lot_id),
                                        ('qty', '>', 0.0), ('location_id.usage', '=', 'internal')])
            if other_quants:
                # We raise an error if:
                # - the total quantity is strictly larger than 1.0
                # - there are more than one negative quant, to avoid situations where the user would
                #   force the quantity at several steps of the process
                if sum(other_quants.mapped('qty')) > 1.0 or len([q for q in other_quants.mapped('qty') if q < 0]) > 1:
                    lot_name = self.env['stock.production.lot'].browse(lot_id).name
                    raise UserError(_('The serial number %s is already in stock.') % lot_name + _("Otherwise make sure the right stock/owner is set."))

    @api.model
    def _quant_create_from_move(self, qty, move, lot_id=False, owner_id=False,
                                src_package_id=False, dest_package_id=False,
                                force_location_from=False, force_location_to=False):
        '''Create a quant in the destination location and create a negative
        quant in the source location if it's an internal location. '''
        price_unit = move.get_price_unit()
        location = force_location_to or move.location_dest_id
        rounding = move.product_id.uom_id.rounding
        vals = {
            'product_id': move.product_id.id,
            'location_id': location.id,
            'qty': float_round(qty, precision_rounding=rounding),
            'cost': price_unit,
            'history_ids': [(4, move.id)],
            'in_date': datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
            'company_id': move.company_id.id,
            'lot_id': lot_id,
            'owner_id': owner_id,
            'package_id': dest_package_id,
        }
        if move.location_id.usage == 'internal':
            # if we were trying to move something from an internal location and reach here (quant creation),
            # it means that a negative quant has to be created as well.
            negative_vals = vals.copy()
            negative_vals['location_id'] = force_location_from and force_location_from.id or move.location_id.id
            negative_vals['qty'] = float_round(-qty, precision_rounding=rounding)
            negative_vals['cost'] = price_unit
            negative_vals['negative_move_id'] = move.id
            negative_vals['package_id'] = src_package_id
            negative_quant_id = self.sudo().create(negative_vals)
            vals.update({'propagated_from_id': negative_quant_id.id})

        picking_type = move.picking_id and move.picking_id.picking_type_id or False
        if lot_id and move.product_id.tracking == 'serial' and (not picking_type or (picking_type.use_create_lots or picking_type.use_existing_lots)):
            if qty != 1.0:
                raise UserError(_('You should only receive by the piece with the same serial number'))

        # create the quant as superuser, because we want to restrict the creation of quant manually: we should always use this method to create quants
        return self.sudo().create(vals)

    @api.model
    def _quant_create(self, qty, move, lot_id=False, owner_id=False,
                      src_package_id=False, dest_package_id=False,
                      force_location_from=False, force_location_to=False):
        # FIXME - remove me in master/saas-14
        _logger.warning("'_quant_create' has been renamed into '_quant_create_from_move'... Overrides are ignored")
        return self._quant_create_from_move(
            qty, move, lot_id=lot_id, owner_id=owner_id,
            src_package_id=src_package_id, dest_package_id=dest_package_id,
            force_location_from=force_location_from, force_location_to=force_location_to)

    @api.multi
    def _quant_update_from_move(self, move, location_dest_id, dest_package_id, lot_id=False, entire_pack=False):
        vals = {
            'location_id': location_dest_id.id,
            'history_ids': [(4, move.id)],
            'reservation_id': False}
        if lot_id and any(quant for quant in self if not quant.lot_id.id):
            vals['lot_id'] = lot_id
        if not entire_pack:
            vals.update({'package_id': dest_package_id})
        self.write(vals)

    @api.multi
    def move_quants_write(self, move, location_dest_id, dest_package_id, lot_id=False, entire_pack=False):
        # FIXME - remove me in master/saas-14
        _logger.warning("'move_quants_write' has been renamed into '_quant_update_from_move'... Overrides are ignored")
        return self._quant_update_from_move(move, location_dest_id, dest_package_id, lot_id=lot_id, entire_pack=entire_pack)

    @api.one
    def _quant_reconcile_negative(self, move):
        """
            When new quant arrive in a location, try to reconcile it with
            negative quants. If it's possible, apply the cost of the new
            quant to the counterpart of the negative quant.
        """
        solving_quant = self
        quants = self._search_quants_to_reconcile()
        product_uom_rounding = self.product_id.uom_id.rounding
        for quant_neg, qty in quants:
            if not quant_neg or not solving_quant:
                continue
            quants_to_solve = self.search([('propagated_from_id', '=', quant_neg.id)])
            if not quants_to_solve:
                continue
            solving_qty = qty
            solved_quants = self.env['stock.quant'].sudo()
            for to_solve_quant in quants_to_solve:
                if float_compare(solving_qty, 0, precision_rounding=product_uom_rounding) <= 0:
                    continue
                solved_quants |= to_solve_quant
                to_solve_quant._quant_split(min(solving_qty, to_solve_quant.qty))
                solving_qty -= min(solving_qty, to_solve_quant.qty)
            remaining_solving_quant = solving_quant._quant_split(qty)
            remaining_neg_quant = quant_neg._quant_split(-qty)
            # if the reconciliation was not complete, we need to link together the remaining parts
            if remaining_neg_quant:
                remaining_to_solves = self.sudo().search([('propagated_from_id', '=', quant_neg.id), ('id', 'not in', solved_quants.ids)])
                if remaining_to_solves:
                    remaining_to_solves.write({'propagated_from_id': remaining_neg_quant.id})
            if solving_quant.propagated_from_id and solved_quants:
                solved_quants.write({'propagated_from_id': solving_quant.propagated_from_id.id})
            # delete the reconciled quants, as it is replaced by the solved quants
            quant_neg.sudo().with_context(force_unlink=True).unlink()
            if solved_quants:
                # price update + accounting entries adjustments
                solved_quants._price_update(solving_quant.cost)
                # merge history (and cost?)
                solved_quants.write(solving_quant._prepare_history())
            solving_quant.with_context(force_unlink=True).unlink()
            solving_quant = remaining_solving_quant

    def _prepare_history(self):
        return {
            'history_ids': [(4, history_move.id) for history_move in self.history_ids],
        }

    @api.multi
    def _price_update(self, newprice):
        # TDE note: use ACLs instead of sudoing everything
        self.sudo().write({'cost': newprice})

    @api.multi
    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

    @api.model
    def quants_get_preferred_domain(self, qty, move, ops=False, lot_id=False, domain=None, preferred_domain_list=[]):
        ''' 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
        '''
        return self.quants_get_reservation(
            qty, move,
            pack_operation_id=ops and ops.id or False,
            lot_id=lot_id,
            company_id=self.env.context.get('company_id', False),
            domain=domain,
            preferred_domain_list=preferred_domain_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 _quants_get_reservation_domain(self, move, pack_operation_id=False, lot_id=False, company_id=False, initial_domain=None):
        initial_domain = initial_domain if initial_domain is not None else [('qty', '>', 0.0)]
        domain = initial_domain + [('product_id', '=', move.product_id.id)]

        if pack_operation_id:
            pack_operation = self.env['stock.pack.operation'].browse(pack_operation_id)
            domain += [('location_id', '=', pack_operation.location_id.id)]
            if pack_operation.owner_id:
                domain += [('owner_id', '=', pack_operation.owner_id.id)]
            if pack_operation.package_id and not pack_operation.product_id:
                domain += [('package_id', 'child_of', pack_operation.package_id.id)]
            elif pack_operation.package_id and pack_operation.product_id:
                domain += [('package_id', '=', pack_operation.package_id.id)]
            else:
                domain += [('package_id', '=', False)]
        else:
            domain += [('location_id', 'child_of', move.location_id.id)]
            if move.restrict_partner_id:
                domain += [('owner_id', '=', move.restrict_partner_id.id)]

        if company_id:
            domain += [('company_id', '=', company_id)]
        else:
            domain += [('company_id', '=', move.company_id.id)]

        return domain

    @api.model
    def _quants_removal_get_order(self, removal_strategy=None):
        if removal_strategy == 'fifo':
            return 'in_date, id'
        elif removal_strategy == 'lifo':
            return 'in_date desc, id desc'
        raise UserError(_('Removal strategy %s not implemented.') % (removal_strategy,))

    def _quants_get_reservation(self, quantity, move, ops=False, domain=None, orderby=None, removal_strategy=None):
        ''' Implementation of removal strategies.

        :return: a structure containing an ordered list of tuples: quants and
                 the quantity to remove from them. A tuple (None, qty)
                 represents a qty not possible to reserve.
        '''
        # TDE FIXME: try to clean
        if removal_strategy:
            order = self._quants_removal_get_order(removal_strategy)
        elif orderby:
            order = orderby
        else:
            order = 'in_date'
        rounding = move.product_id.uom_id.rounding
        domain = domain if domain is not None else [('qty', '>', 0.0)]
        res = []
        offset = 0

        remaining_quantity = quantity
        quants = self.search(domain, order=order, limit=10, offset=offset)
        while float_compare(remaining_quantity, 0, precision_rounding=rounding) > 0 and quants:
            for quant in quants:
                if float_compare(remaining_quantity, abs(quant.qty), precision_rounding=rounding) >= 0:
                    # reserved_quants.append(self._ReservedQuant(quant, abs(quant.qty)))
                    res += [(quant, abs(quant.qty))]
                    remaining_quantity -= abs(quant.qty)
                elif float_compare(remaining_quantity, 0.0, precision_rounding=rounding) != 0:
                    # reserved_quants.append(self._ReservedQuant(quant, remaining_quantity))
                    res += [(quant, remaining_quantity)]
                    remaining_quantity = 0
            offset += 10
            quants = self.search(domain, order=order, limit=10, offset=offset)

        if float_compare(remaining_quantity, 0, precision_rounding=rounding) > 0:
            res.append((None, remaining_quantity))

        return res

    # Misc tools
    # ----------------------------------------------------------------------

    def _get_top_level_packages(self, product_to_location):
        """ This method searches for as much possible higher level packages that
        can be moved as a single operation, given a list of quants to move and
        their suggested destination, and returns the list of matching packages. """
        top_lvl_packages = self.env['stock.quant.package']
        for package in self.mapped('package_id'):
            all_in = True
            top_package = self.env['stock.quant.package']
            while package:
                if any(quant not in self for quant in package.get_content()):
                    all_in = False
                if all_in:
                    destinations = set([product_to_location[product] for product in package.get_content().mapped('product_id')])
                    if len(destinations) > 1:
                        all_in = False
                if all_in:
                    top_package = package
                    package = package.parent_id
                else:
                    package = False
            top_lvl_packages |= top_package
        return top_lvl_packages

    @api.multi
    def _get_latest_move(self):
        latest_move = self.history_ids[0]
        for move in self.history_ids:
            if move.date > latest_move.date:
                latest_move = move
        return latest_move

    @api.multi
    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


class QuantPackage(models.Model):
    """ Packages containing quants and/or other packages """
    _name = "stock.quant.package"
    _description = "Physical Packages"
    _parent_name = "parent_id"
    _parent_store = True
    _parent_order = 'name'
    _order = 'parent_left'

    name = fields.Char(
        'Package Reference', copy=False, index=True,
        default=lambda self: self.env['ir.sequence'].next_by_code('stock.quant.package') or _('Unknown Pack'))
    quant_ids = fields.One2many('stock.quant', 'package_id', 'Bulk Content', readonly=True)
    parent_id = fields.Many2one(
        'stock.quant.package', 'Parent Package',
        ondelete='restrict', readonly=True,
        help="The package containing this item")
    ancestor_ids = fields.One2many('stock.quant.package', string='Ancestors', compute='_compute_ancestor_ids')
    children_quant_ids = fields.One2many('stock.quant', string='All Bulk Content', compute='_compute_children_quant_ids')
    children_ids = fields.One2many('stock.quant.package', 'parent_id', 'Contained Packages', readonly=True)
    parent_left = fields.Integer('Left Parent', index=True)
    parent_right = fields.Integer('Right Parent', index=True)
    packaging_id = fields.Many2one(
        'product.packaging', 'Package Type', index=True,
        help="This field should be completed only if everything inside the package share the same product, otherwise it doesn't really makes sense.")
    location_id = fields.Many2one(
        'stock.location', 'Location', compute='_compute_package_info', search='_search_location',
        index=True, readonly=True)
    company_id = fields.Many2one(
        'res.company', 'Company', compute='_compute_package_info', search='_search_company',
        index=True, readonly=True)
    owner_id = fields.Many2one(
        'res.partner', 'Owner', compute='_compute_package_info', search='_search_owner',
        index=True, readonly=True)

    @api.one
    @api.depends('parent_id', 'children_ids')
    def _compute_ancestor_ids(self):
        self.ancestor_ids = self.env['stock.quant.package'].search(['id', 'parent_of', self.id]).ids

    @api.multi
    @api.depends('parent_id', 'children_ids', 'quant_ids.package_id')
    def _compute_children_quant_ids(self):
        for package in self:
            if package.id:
                package.children_quant_ids = self.env['stock.quant'].search([('package_id', 'child_of', package.id)]).ids

    @api.depends('quant_ids.package_id', 'quant_ids.location_id', 'quant_ids.company_id', 'quant_ids.owner_id', 'ancestor_ids')
    def _compute_package_info(self):
        for package in self:
            quants = package.children_quant_ids
            if quants:
                values = quants[0]
            else:
                values = {'location_id': False, 'company_id': self.env.user.company_id.id, 'owner_id': False}
            package.location_id = values['location_id']
            package.company_id = values['company_id']
            package.owner_id = values['owner_id']

    @api.multi
    def name_get(self):
        return self._compute_complete_name().items()

    def _compute_complete_name(self):
        """ Forms complete name of location from parent location to child location. """
        res = {}
        for package in self:
            current = package
            name = current.name
            while current.parent_id:
                name = '%s / %s' % (current.parent_id.name, name)
                current = current.parent_id
            res[package.id] = name
        return res

    def _search_location(self, operator, value):
        if value:
            packs = self.search([('quant_ids.location_id', operator, value)])
        else:
            packs = self.search([('quant_ids', operator, value)])
        if packs:
            return [('id', 'parent_of', packs.ids)]
        else:
            return [('id', '=', False)]

    def _search_company(self, operator, value):
        if value:
            packs = self.search([('quant_ids.company_id', operator, value)])
        else:
            packs = self.search([('quant_ids', operator, value)])
        if packs:
            return [('id', 'parent_of', packs.ids)]
        else:
            return [('id', '=', False)]

    def _search_owner(self, operator, value):
        if value:
            packs = self.search([('quant_ids.owner_id', operator, value)])
        else:
            packs = self.search([('quant_ids', operator, value)])
        if packs:
            return [('id', 'parent_of', packs.ids)]
        else:
            return [('id', '=', False)]

    def _check_location_constraint(self):
        '''checks that all quants in a package are stored in the same location. This function cannot be used
           as a constraint because it needs to be checked on pack operations (they may not call write on the
           package)
        '''
        for pack in self:
            parent = pack
            while parent.parent_id:
                parent = parent.parent_id
            locations = parent.get_content().filtered(lambda quant: quant.qty > 0.0).mapped('location_id')
            if len(locations) != 1:
                raise UserError(_('Everything inside a package should be in the same location'))
        return True

    @api.multi
    def action_view_related_picking(self):
        """ Returns an action that display the picking related to this
        package (source or destination).
        """
        self.ensure_one()
        pickings = self.env['stock.picking'].search(['|', ('pack_operation_ids.package_id', '=', self.id), ('pack_operation_ids.result_package_id', '=', self.id)])
        action = self.env.ref('stock.action_picking_tree_all').read()[0]
        action['domain'] = [('id', 'in', pickings.ids)]
        return action

    @api.multi
    def unpack(self):
        for package in self:
            # TDE FIXME: why superuser ?
            package.mapped('quant_ids').sudo().write({'package_id': package.parent_id.id})
            package.mapped('children_ids').write({'parent_id': package.parent_id.id})
        return self.env['ir.actions.act_window'].for_xml_id('stock', 'action_package_view')

    @api.multi
    def view_content_package(self):
        action = self.env['ir.actions.act_window'].for_xml_id('stock', 'quantsact')
        action['domain'] = [('id', 'in', self._get_contained_quants().ids)]
        return action
    get_content_package = view_content_package

    def _get_contained_quants(self):
        return self.env['stock.quant'].search([('package_id', 'child_of', self.ids)])
    get_content = _get_contained_quants

    def _get_all_products_quantities(self):
        '''This function computes the different product quantities for the given package
        '''
        # TDE CLEANME: probably to move somewhere else, like in pack op
        res = {}
        for quant in self._get_contained_quants():
            if quant.product_id not in res:
                res[quant.product_id] = 0
            res[quant.product_id] += quant.qty
        return res

Anon7 - 2022
AnonSec Team