mirror of
https://gitlab.com/datalifeit/trytond-stock_distribute
synced 2023-12-14 05:02:53 +01:00
Versión inicial de módulo
This commit is contained in:
commit
c3e56c5c5c
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/.idea
|
||||
*.pyc
|
15
__init__.py
Normal file
15
__init__.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
from trytond.pool import Pool
|
||||
from .stock import *
|
||||
from .production import *
|
||||
|
||||
|
||||
def register():
|
||||
Pool.register(
|
||||
Move,
|
||||
Location,
|
||||
ProductionShipmentData,
|
||||
ProductionShipmentConfirm,
|
||||
module='production_shipment_distribute', type_='model'),
|
||||
Pool.register(
|
||||
ProductionShipment,
|
||||
module='production_shipment_distribute', type_='wizard')
|
55
production.py
Normal file
55
production.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
#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.model import fields
|
||||
from trytond.pool import PoolMeta, Pool
|
||||
|
||||
__all__ = ['ProductionShipment', 'ProductionShipmentData',
|
||||
'ProductionShipmentConfirm']
|
||||
|
||||
__metaclass__ = PoolMeta
|
||||
|
||||
|
||||
class ProductionShipment:
|
||||
__name__ = 'production.shipment'
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super(ProductionShipment, cls).__setup__()
|
||||
|
||||
def default_confirm(self, fields):
|
||||
pool = Pool()
|
||||
Confirm = pool.get('production.shipment.confirm')
|
||||
Move = pool.get('stock.move')
|
||||
|
||||
# res = super(ProductionShipment, self).default_confirm(fields)
|
||||
moves = []
|
||||
for m in self.location.stock:
|
||||
moves.append(Confirm.explode_move(m,
|
||||
self.date.planned_date,
|
||||
self.location.to_location))
|
||||
|
||||
# create new movements
|
||||
new_moves = Move.distribute(moves, self.location.sequence_order)
|
||||
|
||||
# TODO: verify KIT for distribute not occupy_space products
|
||||
result = []
|
||||
for m in new_moves:
|
||||
result.append(Confirm.explode_move_values(m))
|
||||
return {'moves': result}
|
||||
|
||||
|
||||
class ProductionShipmentData:
|
||||
__name__ = 'production.shipment.data'
|
||||
|
||||
sequence_order = fields.Selection([('ascendant', 'Ascendant'),
|
||||
('descendant', 'Descendant')],
|
||||
'Location Sequence order',
|
||||
required=True)
|
||||
|
||||
@staticmethod
|
||||
def default_sequence_order():
|
||||
return 'ascendant'
|
||||
|
||||
|
||||
class ProductionShipmentConfirm:
|
||||
__name__ = 'production.shipment.confirm'
|
11
production.xml
Normal file
11
production.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<tryton>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="shipment_data_view_form">
|
||||
<field name="model">production.shipment.data</field>
|
||||
<field name="type">form</field>
|
||||
<field name="inherit" ref="production_shipment_internal.shipment_data_view_form" />
|
||||
<field name="name">shipment_data_form</field>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
233
stock.py
Normal file
233
stock.py
Normal file
|
@ -0,0 +1,233 @@
|
|||
from trytond.model import fields
|
||||
from trytond.pool import PoolMeta, Pool
|
||||
|
||||
__all__ = ['Move', 'Location']
|
||||
|
||||
__metaclass__ = PoolMeta
|
||||
|
||||
|
||||
class Move:
|
||||
__name__ = 'stock.move'
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super(Move, cls).__setup__()
|
||||
cls._error_messages.update(
|
||||
{'cannot_distribute': 'Cannot distribute movements along Locations. '
|
||||
'Please revise location sequences configuration and space availability.'})
|
||||
@classmethod
|
||||
def distribute(cls, moves, order='ascendant'):
|
||||
"""
|
||||
Distributes given movements along locations
|
||||
in order to not overload storage space
|
||||
|
||||
moves is a list of movements. They can be existing movements or
|
||||
new ones that will be persisted.
|
||||
order determines the filling order based on location sequence field
|
||||
|
||||
Returns new list of movements
|
||||
"""
|
||||
pool = Pool()
|
||||
Location = pool.get('stock.location')
|
||||
Move = pool.get('stock.move')
|
||||
|
||||
if not moves:
|
||||
return []
|
||||
|
||||
new_moves = []
|
||||
to_review = cls._get_locations_to_check_space(moves)
|
||||
for key, extra_space in to_review.iteritems():
|
||||
loc = key[0]
|
||||
date = key[1]
|
||||
# assigned_space = 0
|
||||
for m in moves:
|
||||
if not m.to_location.id == loc:
|
||||
continue
|
||||
if getattr(m, 'effective_date', None) and getattr(m, 'effective_date', None) != date:
|
||||
continue
|
||||
if m.planned_date and not m.planned_date == date:
|
||||
continue
|
||||
if not m.product.occupy_space:
|
||||
new_moves.append(m)
|
||||
continue
|
||||
av_locations = Location.get_next_available_locations(m.to_location,
|
||||
date,
|
||||
m.product.get_space(m.quantity),
|
||||
order)
|
||||
#split movement
|
||||
if not av_locations:
|
||||
cls.raise_user_error('cannot_distribute', {})
|
||||
assigned_qty = 0
|
||||
for av_key, av_space in av_locations.iteritems():
|
||||
if not av_key:
|
||||
cls.raise_user_error('cannot_distribute', {})
|
||||
location = Location(av_key)
|
||||
space = m.product.get_space((m.quantity - assigned_qty))
|
||||
qty = m.product.get_quantity_from_space(av_space if av_space < space else space)
|
||||
move = Move(planned_date=m.planned_date,
|
||||
effective_date=getattr(m, 'effective_date', None)
|
||||
if getattr(m, 'effective_date', None) else None,
|
||||
product=m.product,
|
||||
uom=m.product.default_uom,
|
||||
quantity=qty,
|
||||
from_location=m.from_location,
|
||||
to_location=location,
|
||||
lot=m.lot,
|
||||
company=m.company,
|
||||
currency=m.company.currency if m.company else None,
|
||||
state=m.state)
|
||||
new_moves.append(move)
|
||||
assigned_qty += qty
|
||||
|
||||
# assigned_space += m.product.get_space(av_space if av_space < space else space)
|
||||
|
||||
return new_moves
|
||||
|
||||
|
||||
|
||||
class Location:
|
||||
__name__ = 'stock.location'
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super(Location, cls).__setup__()
|
||||
if cls.sequence.depends:
|
||||
if 'parent' not in cls.sequence.depends:
|
||||
cls.sequence.depends.append('parent')
|
||||
if 'type' not in cls.sequence.depends:
|
||||
cls.sequence.depends.append('type')
|
||||
else:
|
||||
cls.sequence.depends = ['parent', 'type']
|
||||
cls._error_messages.update({
|
||||
'duplicated_sequence': 'Sequence number must be unique among '
|
||||
'childs of type %(type)s of location %(location)s.',
|
||||
'no_next_location': 'Next locations to fill after "%s" cannot be obtained. '
|
||||
'Please check locations sequences configuration.\nIf you decided '
|
||||
'to continue anyway, exceeded quantity will be storage in "%s".'})
|
||||
|
||||
@classmethod
|
||||
def validate(cls, locations):
|
||||
super(Location, cls).validate(locations)
|
||||
for location in locations:
|
||||
location.check_sequence()
|
||||
|
||||
@fields.depends('sequence', 'parent', 'type')
|
||||
def on_change_with_sequence(self):
|
||||
pool = Pool()
|
||||
Location = pool.get('stock.location')
|
||||
|
||||
if self.sequence:
|
||||
return self.sequence
|
||||
if not self.parent:
|
||||
return
|
||||
location = Location(self.parent.id)
|
||||
if not location:
|
||||
return
|
||||
max_seq = 0
|
||||
for c in location.childs:
|
||||
if c.sequence and c.sequence > max_seq:
|
||||
max_seq = c.sequence
|
||||
max_seq += 1
|
||||
return max_seq
|
||||
|
||||
def check_sequence(self):
|
||||
pool = Pool()
|
||||
Location = pool.get('stock.location')
|
||||
|
||||
if not self.parent:
|
||||
return
|
||||
location = Location.search([('parent', '=', self.parent.id),
|
||||
('id', '!=', self.id),
|
||||
('type', '=', self.type),
|
||||
('sequence', '=', self.sequence)])
|
||||
if location:
|
||||
self.raise_user_error('duplicated_sequence',
|
||||
{'location': self.parent.rec_name,
|
||||
'type': self.type})
|
||||
|
||||
@classmethod
|
||||
def get_next_available_locations(cls, start_location,
|
||||
date,
|
||||
needed_space,
|
||||
order='ascendant'):
|
||||
""" Collects locations necessary to complete space needed.
|
||||
|
||||
start_location determines where to start to fill.
|
||||
date is the date to calculate stock forecast.
|
||||
needed_space is the quantity of space needed.
|
||||
order determines the filling order based on location sequence field
|
||||
|
||||
Return a dictionary with location id and quantity that can be stored in it.
|
||||
"""
|
||||
pool = Pool()
|
||||
Location = pool.get('stock.location')
|
||||
|
||||
# when is view or warehouse, starts in a child
|
||||
if start_location.type in ['view', 'warehouse'] and start_location.childs:
|
||||
storage_childs = [c for c in start_location.childs if c.type == 'storage' and c.sequence]
|
||||
if not storage_childs:
|
||||
return {}
|
||||
if order == 'ascendant':
|
||||
seq = min(c.sequence for c in storage_childs)
|
||||
else:
|
||||
seq = max(c.sequence for c in storage_childs)
|
||||
|
||||
storage, = [c for c in storage_childs if c.sequence == seq]
|
||||
return cls.get_next_available_locations(storage, date, needed_space, order)
|
||||
|
||||
if not start_location.control_space:
|
||||
return {start_location.id: needed_space}
|
||||
|
||||
av_space = start_location.get_available_space(date)
|
||||
if av_space >= needed_space:
|
||||
return {start_location.id: needed_space}
|
||||
if not start_location.parent:
|
||||
cls.raise_user_warning('%s.no_next_location' % start_location.id,
|
||||
'no_next_location',
|
||||
(start_location.rec_name, start_location.rec_name))
|
||||
return {start_location.id: needed_space}
|
||||
|
||||
# find next locations to fill
|
||||
result = {start_location.id: av_space}
|
||||
assigned_space = av_space
|
||||
location_domain = [('parent', '=', start_location.parent.id),
|
||||
('sequence', '!=', None),
|
||||
('sequence', '>' if order == 'ascendant' else '<', start_location.sequence)]
|
||||
|
||||
locations = Location.search(location_domain,
|
||||
order=[('sequence', 'ASC' if order == 'ascendant' else 'DESC')])
|
||||
|
||||
for l in locations:
|
||||
if needed_space == assigned_space:
|
||||
return result
|
||||
av_space = l.get_available_space(date)
|
||||
result.setdefault(l.id, 0)
|
||||
result[l.id] = (needed_space - assigned_space) if av_space >= (needed_space - assigned_space) else av_space
|
||||
assigned_space += result[l.id]
|
||||
|
||||
# Goes up in location hierarchy to continue filling
|
||||
if needed_space > assigned_space:
|
||||
if not getattr(start_location.parent, 'parent', None):
|
||||
result.setdefault(None, needed_space - assigned_space)
|
||||
return result
|
||||
location_parent = start_location.parent.parent
|
||||
location_domain = [('parent', '=', location_parent.id),
|
||||
('sequence', '!=', None),
|
||||
('sequence', '>' if order == 'ascendant' else '<', start_location.parent.sequence)]
|
||||
last_location = locations[len(locations)-1] if locations else start_location
|
||||
locations = Location.search(location_domain,
|
||||
order=[('sequence', 'ASC' if order == 'ascendant' else 'DESC')],
|
||||
limit=1)
|
||||
if not locations:
|
||||
cls.raise_user_warning('%s.no_next_location' % start_location.id,
|
||||
'no_next_location',
|
||||
(last_location.rec_name, last_location.rec_name))
|
||||
result[last_location] = result[last_location] + (needed_space - assigned_space)
|
||||
return result
|
||||
|
||||
# get next children locations from the following parent child location
|
||||
result.update(cls.get_next_available_locations(locations[0], date,
|
||||
(needed_space - assigned_space),
|
||||
order))
|
||||
|
||||
return result
|
11
stock.xml
Normal file
11
stock.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<tryton>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="location_view_form">
|
||||
<field name="model">stock.location</field>
|
||||
<field name="type">form</field>
|
||||
<field name="inherit" ref="stock.location_view_form" />
|
||||
<field name="name">location_form</field>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
10
tryton.cfg
Normal file
10
tryton.cfg
Normal file
|
@ -0,0 +1,10 @@
|
|||
[tryton]
|
||||
version=3.2.1
|
||||
depends:
|
||||
stock_location_sequence
|
||||
stock_storage_space
|
||||
production_shipment_internal
|
||||
|
||||
xml:
|
||||
stock.xml
|
||||
production.xml
|
8
view/location_form.xml
Normal file
8
view/location_form.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0"?>
|
||||
<!-- The COPYRIGHT file at the top level of this repository contains the full
|
||||
copyright notices and license terms. -->
|
||||
<data>
|
||||
<xpath expr="/form" position="inside">
|
||||
<field name="childs" colspan="4" />
|
||||
</xpath>
|
||||
</data>
|
9
view/shipment_data_form.xml
Normal file
9
view/shipment_data_form.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0"?>
|
||||
<!-- The COPYRIGHT file at the top level of this repository contains the full
|
||||
copyright notices and license terms. -->
|
||||
<data>
|
||||
<xpath expr="/form/field[@name='to_location']" position="after">
|
||||
<label name="sequence_order" />
|
||||
<field name="sequence_order" />
|
||||
</xpath>
|
||||
</data>
|
Loading…
Reference in a new issue