diff --git a/CHANGELOG b/CHANGELOG
index 4465fe9b9b..eb1e8763db 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,5 @@
+* Add default internal provisioning per location
+
Version 3.6.0 - 2015-04-20
* Bug fixes (see mercurial logs for details)
* Add support for PyPy
diff --git a/__init__.py b/__init__.py
index ca8ad42a71..6e25edfb69 100644
--- a/__init__.py
+++ b/__init__.py
@@ -6,6 +6,7 @@ from .order_point import *
from .product import *
from .purchase_request import *
from .shipment import *
+from .location import *
def register():
@@ -17,6 +18,7 @@ def register():
CreatePurchaseAskParty,
ShipmentInternal,
CreateShipmentInternalStart,
+ Location,
module='stock_supply', type_='model')
Pool.register(
CreatePurchaseRequest,
diff --git a/doc/index.rst b/doc/index.rst
index fb872ef0c9..5b60d08e38 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -66,4 +66,6 @@ The internal shipments and purchase requests are created by schedulers
with respect to stock levels and existing shipments and requests. The
stock levels are computed between the next two supply dates. If the
stock level of a product without order point on the given warehouse is
-below zero, a purchase request is also created.
+below zero, a purchase request is also created. The same happens if
+the stock level of a storage location with a provisioning location is
+below zero.
diff --git a/location.py b/location.py
new file mode 100644
index 0000000000..b5a3830c02
--- /dev/null
+++ b/location.py
@@ -0,0 +1,25 @@
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+from trytond.pool import PoolMeta
+from trytond.model import fields
+from trytond.pyson import Eval
+
+
+__all__ = ['Location']
+
+
+class Location:
+ __metaclass__ = PoolMeta
+ __name__ = 'stock.location'
+
+ provisioning_location = fields.Many2One('stock.location',
+ 'Provisioning Location',
+ states={
+ 'invisible': Eval('type') != 'storage',
+ 'readonly': ~Eval('active'),
+ },
+ domain=[
+ ('type', '=', 'storage'),
+ ],
+ depends=['type', 'active'],
+ help='Leave empty for no default provisioning')
diff --git a/location.xml b/location.xml
new file mode 100644
index 0000000000..82f4599b8c
--- /dev/null
+++ b/location.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+ stock.location
+
+ location_form
+
+
+
diff --git a/shipment.py b/shipment.py
index f8b6c1bdf2..dd3627f88d 100644
--- a/shipment.py
+++ b/shipment.py
@@ -46,6 +46,7 @@ class ShipmentInternal(ModelSQL, ModelView):
"""
pool = Pool()
OrderPoint = pool.get('stock.order_point')
+ Location = pool.get('stock.location')
Product = pool.get('product.product')
Date = pool.get('ir.date')
User = pool.get('res.user')
@@ -57,25 +58,50 @@ class ShipmentInternal(ModelSQL, ModelView):
('type', '=', 'internal'),
])
id2product = {}
- location_ids = []
+ product2op = {}
+ id2location = {}
for op in order_points:
id2product[op.product.id] = op.product
- location_ids.append(op.storage_location.id)
+ product2op[
+ (op.storage_location.id, op.product.id)
+ ] = op
+ id2location[op.storage_location.id] = op.storage_location
+ provisioned = Location.search([
+ ('provisioning_location', '!=', None),
+ ])
+ id2location.update({l.id: l for l in provisioned})
+ # ordered by ids to speedup reduce_ids in products_by_location
+ if provisioned:
+ products = Product.search([
+ ('type', 'in', ['goods', 'assets']),
+ ], order=[('id', 'ASC')])
+ product_ids = [p.id for p in products]
+ else:
+ product_ids = id2product.keys()
+ product_ids.sort()
# TODO Allow to compute for other future date
with Transaction().set_context(forecast=True, stock_date_end=today):
- pbl = Product.products_by_location(location_ids,
- list(id2product.iterkeys()), with_childs=True)
+ pbl = Product.products_by_location(id2location.keys(),
+ product_ids, with_childs=True)
# Create a list of move to create
moves = {}
- for op in order_points:
- qty = pbl.get((op.storage_location.id, op.product.id), 0)
- if qty < op.min_quantity:
- key = (op.provisioning_location.id,
- op.storage_location.id,
- op.product.id)
- moves[key] = op.max_quantity - qty
+ for location in id2location.itervalues():
+ for product_id in product_ids:
+ qty = pbl.get((location.id, product_id), 0)
+ op = product2op.get((location.id, product_id))
+ if op:
+ min_qty, max_qty = op.min_quantity, op.max_quantity
+ provisioning_location = op.provisioning_location
+ elif location and location.provisioning_location:
+ min_qty, max_qty = 0, 0
+ provisioning_location = location.provisioning_location
+ else:
+ continue
+ if qty < min_qty:
+ key = (provisioning_location.id, location.id, product_id)
+ moves[key] = max_qty - qty
# Group moves by {from,to}_location
to_create = {}
@@ -94,13 +120,16 @@ class ShipmentInternal(ModelSQL, ModelView):
)
shipment_moves = []
for move in moves:
- product, qty = move
+ product_id, qty = move
+ product = id2product.setdefault(
+ product_id, Product(product_id))
shipment_moves.append(Move(
from_location=from_location,
to_location=to_location,
+ planned_date=today,
product=product,
quantity=qty,
- uom=id2product[product].default_uom,
+ uom=product.default_uom,
company=user_record.company,
))
shipment.moves = shipment_moves
diff --git a/tests/scenario_stock_internal_supply.rst b/tests/scenario_stock_internal_supply.rst
index ddd3f2fcb0..8a84f070c4 100644
--- a/tests/scenario_stock_internal_supply.rst
+++ b/tests/scenario_stock_internal_supply.rst
@@ -94,8 +94,9 @@ Get stock locations::
>>> customer_loc, = Location.find([('code', '=', 'CUS')])
>>> output_loc, = Location.find([('code', '=', 'OUT')])
>>> storage_loc, = Location.find([('code', '=', 'STO')])
+ >>> lost_loc, = Location.find([('type', '=', 'lost_found')])
-Create new internal location::
+Create provisioning location::
>>> Location = Model.get('stock.location')
>>> provisioning_loc = Location()
@@ -104,12 +105,20 @@ Create new internal location::
>>> provisioning_loc.parent = warehouse_loc
>>> provisioning_loc.save()
+Create a new storage location::
+
+ >>> sec_storage_loc = Location()
+ >>> sec_storage_loc.name = 'Second Storage'
+ >>> sec_storage_loc.type = 'storage'
+ >>> sec_storage_loc.parent = warehouse_loc
+ >>> sec_storage_loc.provisioning_location = provisioning_loc
+ >>> sec_storage_loc.save()
+
Create internal order point::
>>> OrderPoint = Model.get('stock.order_point')
>>> order_point = OrderPoint()
>>> order_point.product = product
- >>> order_point.warehouse_location = warehouse_loc
>>> order_point.storage_location = storage_loc
>>> order_point.provisioning_location = provisioning_loc
>>> order_point.type = 'internal'
@@ -148,3 +157,33 @@ Execute internal supply::
u'Provisioning Location'
>>> move.to_location.code
u'STO'
+
+Create negative quantity in Second Storage::
+
+ >>> Move = Model.get('stock.move')
+ >>> move = Move()
+ >>> move.product = product
+ >>> move.quantity = 10
+ >>> move.from_location = sec_storage_loc
+ >>> move.to_location = lost_loc
+ >>> move.click('do')
+ >>> move.state
+ u'done'
+
+Execute internal supply::
+
+ >>> Wizard('stock.shipment.internal.create').execute('create_')
+ >>> shipment, = ShipmentInternal.find([('id', '!=', shipment.id)])
+ >>> shipment.state
+ u'waiting'
+ >>> len(shipment.moves)
+ 1
+ >>> move, = shipment.moves
+ >>> move.product.template.name
+ u'Product'
+ >>> move.quantity
+ 10.0
+ >>> move.from_location.name
+ u'Provisioning Location'
+ >>> move.to_location.name
+ u'Second Storage'
diff --git a/tryton.cfg b/tryton.cfg
index 1100691c5c..c9e668666f 100644
--- a/tryton.cfg
+++ b/tryton.cfg
@@ -13,3 +13,4 @@ xml:
purchase_request.xml
shipment.xml
product.xml
+ location.xml
diff --git a/view/location_form.xml b/view/location_form.xml
new file mode 100644
index 0000000000..d367016bfe
--- /dev/null
+++ b/view/location_form.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+