mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
224 lines
7.3 KiB
Python
224 lines
7.3 KiB
Python
# Copyright 2013 Camptocamp SA - Guewen Baconnier
|
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
|
from odoo import api, fields, models
|
|
from odoo.exceptions import except_orm
|
|
from odoo.tools import float_compare
|
|
from odoo.tools.translate import _
|
|
|
|
|
|
class StockReservation(models.Model):
|
|
"""Allow to reserve products.
|
|
|
|
The fields mandatory for the creation of a reservation are:
|
|
|
|
* product_id
|
|
* product_uom_qty
|
|
* product_uom
|
|
* name
|
|
|
|
The following fields are required but have default values that you may
|
|
want to override:
|
|
|
|
* company_id
|
|
* location_id
|
|
* location_dest_id
|
|
|
|
Optionally, you may be interested to define:
|
|
|
|
* date_validity (once passed, the reservation will be released)
|
|
* note
|
|
"""
|
|
|
|
_name = "stock.reservation"
|
|
_description = "Stock Reservation"
|
|
_inherits = {"stock.move": "move_id"}
|
|
|
|
move_id = fields.Many2one(
|
|
"stock.move",
|
|
"Reservation Move",
|
|
required=True,
|
|
readonly=True,
|
|
ondelete="cascade",
|
|
index=True,
|
|
)
|
|
date_validity = fields.Date("Validity Date")
|
|
|
|
@api.model
|
|
def default_get(self, fields_list):
|
|
"""Fix default values
|
|
|
|
- Ensure default value of computed field `product_qty` is not set
|
|
as it would raise an error
|
|
- Compute default `location_id` based on default `picking_type_id`.
|
|
Note: `default_picking_type_id` may be present in context,
|
|
so code that looks for default `location_id` is implemented here,
|
|
because it relies on already calculated default
|
|
`picking_type_id`.
|
|
"""
|
|
# if there is 'location_id' field requested, ensure that
|
|
# picking_type_id is also requested, because it is required
|
|
# to compute location_id
|
|
if "location_id" in fields_list and "picking_type_id" not in fields_list:
|
|
fields_list = fields_list + ["picking_type_id"]
|
|
|
|
res = super().default_get(fields_list)
|
|
|
|
if "product_qty" in res:
|
|
del res["product_qty"]
|
|
|
|
# At this point picking_type_id and location_id
|
|
# should be computed in default way:
|
|
# 1. look up context
|
|
# 2. look up ir_values
|
|
# 3. look up property fields
|
|
# 4. look up field.default
|
|
# 5. delegate to parent model
|
|
#
|
|
# If picking_type_id is present and location_id is not, try to find
|
|
# default value for location_id
|
|
if not res.get("picking_type_id", None):
|
|
res["picking_type_id"] = (
|
|
self.env["stock.picking.type"]
|
|
.search(
|
|
[
|
|
("code", "=", "outgoing"),
|
|
("company_id", "=", res.get("company_id")),
|
|
],
|
|
limit=1,
|
|
)
|
|
.id
|
|
)
|
|
|
|
picking_type_id = res.get("picking_type_id")
|
|
if picking_type_id and not res.get("location_id", False):
|
|
picking = self.env["stock.picking"].new(
|
|
{"picking_type_id": picking_type_id}
|
|
)
|
|
picking.onchange_picking_type()
|
|
res["location_id"] = picking.location_id.id
|
|
if "location_dest_id" in fields_list:
|
|
res["location_dest_id"] = self._default_location_dest_id()
|
|
if "product_uom_qty" in fields_list:
|
|
res["product_uom_qty"] = 1.0
|
|
return res
|
|
|
|
@api.model
|
|
def get_location_from_ref(self, ref):
|
|
"""Get a location from a xmlid if allowed
|
|
:param ref: tuple (module, xmlid)
|
|
"""
|
|
try:
|
|
location = self.env.ref(ref, raise_if_not_found=True)
|
|
location.check_access_rule("read")
|
|
location_id = location.id
|
|
except (except_orm, ValueError):
|
|
location_id = False
|
|
return location_id
|
|
|
|
@api.model
|
|
def _default_location_dest_id(self):
|
|
ref = "stock_reserve.stock_location_reservation"
|
|
return self.get_location_from_ref(ref)
|
|
|
|
def reserve(self):
|
|
"""Confirm reservations
|
|
|
|
The reservation is done using the default UOM of the product.
|
|
A date until which the product is reserved can be specified.
|
|
"""
|
|
self.write({"date_deadline": fields.Datetime.now()})
|
|
# Don't call _action_confirm() method to prevent assign picking
|
|
self.mapped("move_id").write({"state": "confirmed"})
|
|
self.mapped("move_id")._action_assign()
|
|
return True
|
|
|
|
def release_reserve(self):
|
|
"""
|
|
Release moves from reservation
|
|
"""
|
|
moves = self.mapped("move_id")
|
|
moves._action_cancel()
|
|
return True
|
|
|
|
def _get_state_domain_release_reserve(self, mode):
|
|
if mode == "reserve":
|
|
return
|
|
elif mode == "release":
|
|
return "cancel"
|
|
|
|
@api.model
|
|
def release_validity_exceeded(self, ids=None):
|
|
"""Release all the reservation having an exceeded validity date"""
|
|
domain = [
|
|
("date_validity", "<", fields.date.today()),
|
|
("state", "!=", "cancel"),
|
|
]
|
|
if ids:
|
|
domain.append(("id", "in", ids))
|
|
self.env["stock.reservation"].search(domain).release_reserve()
|
|
return True
|
|
|
|
def unlink(self):
|
|
"""Release the reservation before the unlink"""
|
|
self.release_reserve()
|
|
return super().unlink()
|
|
|
|
@api.onchange("product_id")
|
|
def _onchange_product_id(self):
|
|
"""set product_uom and name from product onchange"""
|
|
# save value before reading of self.move_id as this last one erase
|
|
# product_id value
|
|
self.move_id.product_id = self.product_id
|
|
self.move_id.onchange_product_id()
|
|
self.name = self.move_id.name
|
|
self.product_uom = self.move_id.product_uom
|
|
|
|
@api.onchange("product_uom_qty")
|
|
def _onchange_quantity(self):
|
|
"""On change of product quantity avoid negative quantities"""
|
|
if not self.product_id or self.product_uom_qty <= 0.0:
|
|
self.product_uom_qty = 0.0
|
|
|
|
def open_move(self):
|
|
self.ensure_one()
|
|
action_dict = self.env["ir.actions.act_window"]._for_xml_id(
|
|
"stock.stock_move_action"
|
|
)
|
|
action_dict["name"] = _("Reservation Move")
|
|
# open directly in the form view
|
|
view_id = self.env.ref("stock.view_move_form").id
|
|
action_dict.update(
|
|
views=[(view_id, "form")],
|
|
res_id=self.move_id.id,
|
|
)
|
|
return action_dict
|
|
|
|
def write(self, vals):
|
|
res = super().write(vals)
|
|
rounding = self.product_uom.rounding
|
|
if (
|
|
"product_uom_qty" in vals
|
|
and self.state in ["confirmed", "waiting", "partially_available"]
|
|
and float_compare(
|
|
self.product_id.virtual_available, 0, precision_rounding=rounding
|
|
)
|
|
>= 0
|
|
):
|
|
self.reserve()
|
|
return res
|
|
|
|
def _get_reservations_to_assign_domain(self):
|
|
return [
|
|
("state", "in", ["confirmed", "waiting", "partially_available"]),
|
|
"|",
|
|
("date_validity", ">=", fields.date.today()),
|
|
("date_validity", "=", False),
|
|
]
|
|
|
|
@api.model
|
|
def assign_waiting_confirmed_reserve_moves(self):
|
|
reservations_to_assign = self.search(self._get_reservations_to_assign_domain())
|
|
for reservation in reservations_to_assign:
|
|
reservation.reserve()
|
|
return True
|