1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
|
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
from odoo.osv import expression
from odoo.exceptions import UserError
from odoo.tools.float_utils import float_compare
class RemovalStrategy(models.Model):
_name = 'product.removal'
_description = 'Removal Strategy'
name = fields.Char('Name', required=True, translate=True)
method = fields.Char("Method", required=True, translate=True, help="FIFO, LIFO...")
class StockPutawayRule(models.Model):
_name = 'stock.putaway.rule'
_order = 'sequence,product_id'
_description = 'Putaway Rule'
_check_company_auto = True
def _default_category_id(self):
if self.env.context.get('active_model') == 'product.category':
return self.env.context.get('active_id')
def _default_location_id(self):
if self.env.context.get('active_model') == 'stock.location':
return self.env.context.get('active_id')
if not self.env.user.has_group('stock.group_stock_multi_warehouses'):
wh = self.env['stock.warehouse'].search(self.env['stock.warehouse']._check_company_domain(self.env.company), limit=1)
input_loc, _ = wh._get_input_output_locations(wh.reception_steps, wh.delivery_steps)
return input_loc
def _default_product_id(self):
if self.env.context.get('active_model') == 'product.template' and self.env.context.get('active_id'):
product_template = self.env['product.template'].browse(self.env.context.get('active_id'))
product_template = product_template.exists()
if product_template.product_variant_count == 1:
return product_template.product_variant_id
elif self.env.context.get('active_model') == 'product.product':
return self.env.context.get('active_id')
product_id = fields.Many2one(
'product.product', 'Product', check_company=True,
default=_default_product_id,
domain="[('product_tmpl_id', '=', context.get('active_id', False))] if context.get('active_model') == 'product.template' else [('type', '!=', 'service')]",
ondelete='cascade')
category_id = fields.Many2one('product.category', 'Product Category',
default=_default_category_id, domain=[('filter_for_stock_putaway_rule', '=', True)], ondelete='cascade')
location_in_id = fields.Many2one(
'stock.location', 'When product arrives in', check_company=True,
domain="[('child_ids', '!=', False)]",
default=_default_location_id, required=True, ondelete='cascade', index=True)
location_out_id = fields.Many2one(
'stock.location', 'Store to sublocation', check_company=True,
domain="[('id', 'child_of', location_in_id)]",
required=True, ondelete='cascade')
sequence = fields.Integer('Priority', help="Give to the more specialized category, a higher priority to have them in top of the list.")
company_id = fields.Many2one(
'res.company', 'Company', required=True,
default=lambda s: s.env.company.id, index=True)
package_type_ids = fields.Many2many('stock.package.type', string='Package Type', check_company=True)
storage_category_id = fields.Many2one(
'stock.storage.category', 'Storage Category',
compute='_compute_storage_category', store=True, readonly=False,
ondelete='cascade', check_company=True)
active = fields.Boolean('Active', default=True)
sublocation = fields.Selection([
('no', 'No'),
('last_used', 'Last Used'),
('closest_location', 'Closest Location')
], default='no')
@api.depends('sublocation')
def _compute_storage_category(self):
for rule in self:
if rule.sublocation != 'closest_location':
rule.storage_category_id = False
@api.onchange('sublocation', 'location_out_id', 'storage_category_id')
def _onchange_sublocation(self):
if self.sublocation == 'closest_location':
child_location_ids = self.env['stock.location'].search([
('id', 'child_of', self.location_out_id.id),
('storage_category_id', '=', self.storage_category_id.id)
])
if not child_location_ids:
return {
'warning': {
'title': _("Warning"),
'message': _("Selected storage category does not exist in the 'store to' location or any of its sublocations"),
},
}
@api.onchange('location_in_id')
def _onchange_location_in(self):
child_location_count = 0
if self.location_out_id:
child_location_count = self.env['stock.location'].search_count([
('id', '=', self.location_out_id.id),
('id', 'child_of', self.location_in_id.id),
('id', '!=', self.location_in_id.id),
])
if not child_location_count or not self.location_out_id:
self.location_out_id = self.location_in_id
@api.model_create_multi
def create(self, vals_list):
rules = super().create(vals_list)
return rules
def write(self, vals):
if 'company_id' in vals:
for rule in self:
if rule.company_id.id != vals['company_id']:
raise UserError(_("Changing the company of this record is forbidden at this point, you should rather archive it and create a new one."))
return super(StockPutawayRule, self).write(vals)
def _get_last_used_search_domain(self, product):
self.ensure_one()
domain = expression.AND([
[('state', '=', 'done')],
[('location_dest_id', 'child_of', self.location_out_id.id)],
[('product_id', '=', product.id)],
])
if self.package_type_ids:
domain = expression.AND([
domain,
[('result_package_id.package_type_id', 'in', self.package_type_ids.ids)],
])
return domain
def _get_last_used_location(self, product):
self.ensure_one()
return self.env['stock.move.line'].search(
domain=self._get_last_used_search_domain(product),
limit=1,
order='date desc'
).location_dest_id
def _get_putaway_location(self, product, quantity=0, package=None, packaging=None, qty_by_location=None):
# find package type on package or packaging
package_type = self.env['stock.package.type']
if package:
package_type = package.package_type_id
elif packaging:
package_type = packaging.package_type_id
checked_locations = set()
for putaway_rule in self:
location_out = putaway_rule.location_out_id
if putaway_rule.sublocation == 'last_used':
location_dest_id = putaway_rule._get_last_used_location(product)
location_out = location_dest_id or location_out
child_locations = location_out.child_internal_location_ids
if not putaway_rule.storage_category_id:
if location_out in checked_locations:
continue
if location_out._check_can_be_used(product, quantity, package, qty_by_location[location_out.id]):
return location_out
continue
else:
child_locations = child_locations.filtered(lambda loc: loc.storage_category_id == putaway_rule.storage_category_id)
# check if already have the product/package type stored
for location in child_locations:
if location in checked_locations:
continue
if package_type:
if location.quant_ids.filtered(lambda q: q.package_id and q.package_id.package_type_id == package_type):
if location._check_can_be_used(product, quantity, package=package, location_qty=qty_by_location[location.id]):
return location
else:
checked_locations.add(location)
elif float_compare(qty_by_location[location.id], 0, precision_rounding=product.uom_id.rounding) > 0:
if location._check_can_be_used(product, quantity, location_qty=qty_by_location[location.id]):
return location
else:
checked_locations.add(location)
# check locations with matched storage category
for location in child_locations.filtered(lambda l: l.storage_category_id == putaway_rule.storage_category_id):
if location in checked_locations:
continue
if location._check_can_be_used(product, quantity, package, qty_by_location[location.id]):
return location
checked_locations.add(location)
return None
|