Files
tryton/move.py
T
2010-01-20 11:04:28 +01:00

713 lines
31 KiB
Python

#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
"Move"
from trytond.model import ModelView, ModelSQL, fields
from trytond.backend import TableHandler
from trytond.pyson import In, Eval, Not, In, Equal, If, Get, Bool
from decimal import Decimal
STATES = {
'readonly': In(Eval('state'), ['cancel', 'done']),
}
class Move(ModelSQL, ModelView):
"Stock Move"
_name = 'stock.move'
_description = __doc__
product = fields.Many2One("product.product", "Product", required=True,
select=1, states=STATES,
on_change=['product', 'currency', 'uom', 'company',
'from_location', 'to_location'],
domain=[('type', '!=', 'service')])
uom = fields.Many2One("product.uom", "Uom", required=True, states=STATES,
domain=[
('category', '=',
(Eval('product'), 'product.default_uom.category')),
],
context={
'category': (Eval('product'), 'product.default_uom.category'),
},
on_change=['product', 'currency', 'uom', 'company',
'from_location', 'to_location'])
unit_digits = fields.Function('get_unit_digits', type='integer',
string='Unit Digits', on_change_with=['uom'])
quantity = fields.Float("Quantity", required=True,
digits=(16, Eval('unit_digits', 2)), states=STATES)
from_location = fields.Many2One("stock.location", "From Location", select=1,
required=True, states=STATES,
domain=[('type', 'not in', ('warehouse', 'view'))])
to_location = fields.Many2One("stock.location", "To Location", select=1,
required=True, states=STATES,
domain=[('type', 'not in', ('warehouse', 'view'))])
shipment_in = fields.Many2One('stock.shipment.in', 'Supplier Shipment',
readonly=True, select=1, ondelete='CASCADE')
shipment_out = fields.Many2One('stock.shipment.out', 'Customer Shipment',
readonly=True, select=1, ondelete='CASCADE')
shipment_out_return = fields.Many2One('stock.shipment.out.return',
'Customer Return Shipment', readonly=True, select=1,
ondelete='CASCADE')
shipment_in_return = fields.Many2One('stock.shipment.in.return',
'Supplier Return Shipment', readonly=True, select=1,
ondelete='CASCADE')
shipment_internal = fields.Many2One('stock.shipment.internal',
'Internal Shipment', readonly=True, select=1, ondelete='CASCADE')
planned_date = fields.Date("Planned Date", states=STATES, select=2)
effective_date = fields.Date("Effective Date", readonly=True, select=2)
state = fields.Selection([
('draft', 'Draft'),
('assigned', 'Assigned'),
('done', 'Done'),
('cancel', 'Canceled'),
], 'State', select=1, readonly=True)
company = fields.Many2One('company.company', 'Company', required=True,
states={
'readonly': Not(Equal(Eval('state'), 'draft')),
}, domain=[
('id', If(In('company', Eval('context', {})), '=', '!='),
Get(Eval('context', {}), 'company', 0)),
])
unit_price = fields.Numeric('Unit Price', digits=(16, 4),
states={
'invisible': Not(Bool(Eval('unit_price_required'))),
'required': Bool(Eval('unit_price_required')),
'readonly': Not(Equal(Eval('state'), 'draft')),
})
cost_price = fields.Numeric('Cost Price', digits=(16, 4), readonly=True)
currency = fields.Many2One('currency.currency', 'Currency',
states={
'invisible': Not(Bool(Eval('unit_price_required'))),
'required': Not(Bool(Eval('unit_price_required'))),
'readonly': Not(Equal(Eval('state'), 'draft')),
})
unit_price_required = fields.Function('get_unit_price_required',
type='boolean', string='Unit Price Required',
on_change_with=['from_location', 'to_location'])
def __init__(self):
super(Move, self).__init__()
self._sql_constraints += [
('check_move_qty_pos',
'CHECK(quantity >= 0.0)', 'Move quantity must be positive'),
('check_from_to_locations',
'CHECK(from_location != to_location)',
'Source and destination location must be different'),
('check_shipment',
'CHECK((COALESCE(shipment_in, 0) / COALESCE(shipment_in, 1) ' \
'+ COALESCE(shipment_out, 0) / ' \
'COALESCE(shipment_out, 1) ' \
'+ COALESCE(shipment_internal, 0) / ' \
'COALESCE(shipment_internal, 1) ' \
'+ COALESCE(shipment_in_return, 0) / ' \
'COALESCE(shipment_in_return, 1) ' \
'+ COALESCE(shipment_out_return, 0) / ' \
'COALESCE(shipment_out_return, 1)) ' \
'<= 1)',
'Move can be on only one Shipment'),
]
self._constraints += [
('check_product_type', 'service_product'),
]
self._order[0] = ('id', 'DESC')
self._error_messages.update({
'set_state_draft': 'You can not set state to draft!',
'set_state_assigned': 'You can not set state to assigned!',
'set_state_done': 'You can not set state to done!',
'del_draft_cancel': 'You can only delete draft or cancelled moves!',
'service_product': 'You can not use service products for a move!',
})
def init(self, cursor, module_name):
# Migration from 1.2: packing renamed into shipment
table = TableHandler(cursor, self, module_name)
table.drop_constraint('check_packing')
for suffix in ('in', 'out', 'in_return', 'out_return', 'internal'):
old_column = 'packing_%s' % suffix
new_column = 'shipment_%s' % suffix
if table.column_exist(old_column):
table.index_action(old_column, action='remove')
table.drop_fk(old_column)
table.column_rename(old_column, new_column)
super(Move, self).init(cursor, module_name)
# Migration from 1.0 check_packing_in_out has been removed
table = TableHandler(cursor, self, module_name)
table.drop_constraint('check_packing_in_out')
def default_planned_date(self, cursor, user, context=None):
if context and context.get('planned_date'):
return context.get('planned_date')
def default_to_location(self, cursor, user, context=None):
location_obj = self.pool.get('stock.location')
party_obj = self.pool.get('party.party')
res = False
if context is None:
context = {}
warehouse = None
if context.get('warehouse'):
warehouse = location_obj.browse(cursor, user, context['warehouse'],
context=context)
if context.get('type', '') == 'inventory_in':
if warehouse:
res = warehouse.storage_location.id
elif context.get('type', '') == 'inventory_out':
if warehouse:
res = warehouse.output_location.id
elif context.get('type', '') == 'incoming':
if warehouse:
res = warehouse.input_location.id
elif context.get('type', '') == 'outgoing':
if context.get('customer'):
customer = party_obj.browse(cursor, user, context['customer'],
context=context)
res = customer.customer_location.id
if context.get('to_location'):
res = context.get('to_location')
return res
def default_from_location(self, cursor, user, context=None):
location_obj = self.pool.get('stock.location')
party_obj = self.pool.get('party.party')
res = False
if context is None:
context = {}
warehouse = None
if context.get('warehouse'):
warehouse = location_obj.browse(cursor, user, context['warehouse'],
context=context)
if context.get('type', '') == 'inventory_in':
if warehouse:
res = warehouse.input_location.id
elif context.get('type', '') == 'inventory_out':
if warehouse:
res = warehouse.storage_location.id
elif context.get('type', '') == 'outgoing':
if warehouse:
res = warehouse.output_location.id
elif context.get('type', '') == 'incoming':
if context.get('supplier'):
supplier = party_obj.browse(cursor, user, context['supplier'],
context=context)
res = supplier.supplier_location.id
elif context.get('customer'):
customer = party_obj.browse(cursor, user, context['customer'],
context=context)
res = customer.customer_location.id
if context.get('from_location'):
res = context.get('from_location')
return res
def default_state(self, cursor, user, context=None):
return 'draft'
def default_company(self, cursor, user, context=None):
if context is None:
context = {}
if context.get('company'):
return context['company']
return False
def default_currency(self, cursor, user, context=None):
company_obj = self.pool.get('company.company')
currency_obj = self.pool.get('currency.currency')
if context is None:
context = {}
company = None
if context.get('company'):
company = company_obj.browse(cursor, user, context['company'],
context=context)
return company.currency.id
return False
def on_change_with_unit_digits(self, cursor, user, ids, vals,
context=None):
uom_obj = self.pool.get('product.uom')
if vals.get('uom'):
uom = uom_obj.browse(cursor, user, vals['uom'],
context=context)
return uom.digits
return 2
def get_unit_digits(self, cursor, user, ids, name, arg, context=None):
res = {}
for move in self.browse(cursor, user, ids, context=context):
res[move.id] = move.uom.digits
return res
def on_change_product(self, cursor, user, ids, vals, context=None):
product_obj = self.pool.get('product.product')
uom_obj = self.pool.get('product.uom')
currency_obj = self.pool.get('currency.currency')
company_obj = self.pool.get('company.company')
location_obj = self.pool.get('stock.location')
if context is None:
context = {}
res = {
'unit_price': Decimal('0.0'),
}
if vals.get('product'):
product = product_obj.browse(cursor, user, vals['product'],
context=context)
res['uom'] = product.default_uom.id
res['uom.rec_name'] = product.default_uom.rec_name
res['unit_digits'] = product.default_uom.digits
to_location = None
if vals.get('to_location'):
to_location = location_obj.browse(cursor, user,
vals['to_location'], context=context)
if to_location and to_location.type == 'storage':
unit_price = product.cost_price
if vals.get('uom') and vals['uom'] != product.default_uom.id:
uom = uom_obj.browse(cursor, user, vals['uom'],
context=context)
unit_price = uom_obj.compute_price(cursor, user,
product.default_uom, unit_price, uom,
context=context)
if vals.get('currency') and vals.get('company'):
currency = currency_obj.browse(cursor, user,
vals['currency'], context=context)
company = company_obj.browse(cursor, user,
vals['company'], context=context)
unit_price = currency_obj.compute(cursor, user,
company.currency, unit_price, currency,
round=False, context=context)
res['unit_price'] = unit_price
return res
def on_change_uom(self, cursor, user, ids, vals, context=None):
product_obj = self.pool.get('product.product')
uom_obj = self.pool.get('product.uom')
currency_obj = self.pool.get('currency.currency')
company_obj = self.pool.get('company.company')
location_obj = self.pool.get('stock.location')
if context is None:
context = {}
res = {
'unit_price': Decimal('0.0'),
}
if vals.get('product'):
product = product_obj.browse(cursor, user, vals['product'],
context=context)
to_location = None
if vals.get('to_location'):
to_location = location_obj.browse(cursor, user,
vals['to_location'], context=context)
if to_location and to_location.type == 'storage':
unit_price = product.cost_price
if vals.get('uom') and vals['uom'] != product.default_uom.id:
uom = uom_obj.browse(cursor, user, vals['uom'],
context=context)
unit_price = uom_obj.compute_price(cursor, user,
product.default_uom, unit_price, uom,
context=context)
if vals.get('currency') and vals.get('company'):
currency = currency_obj.browse(cursor, user,
vals['currency'], context=context)
company = company_obj.browse(cursor, user,
vals['company'], context=context)
unit_price = currency_obj.compute(cursor, user,
company.currency, unit_price, currency,
round=False, context=context)
res['unit_price'] = unit_price
return res
def default_unit_price_required(self, cursor, user, context=None):
from_location = self.default_from_location(cursor, user,
context=context)
to_location = self.default_to_location(cursor,user,
context=context)
vals = {
'from_location': from_location,
'to_location': to_location,
}
return self.on_change_with_unit_price_required(cursor, user, [],
vals, context=context)
def on_change_with_unit_price_required(self, cursor, user, ids, vals,
context=None):
location_obj = self.pool.get('stock.location')
if vals.get('from_location'):
from_location = location_obj.browse(cursor, user,
vals['from_location'], context=context)
if from_location.type == 'supplier':
return True
if vals.get('to_location'):
to_location = location_obj.browse(cursor, user,
vals['to_location'], context=context)
if to_location.type == 'customer':
return True
return False
def get_unit_price_required(self, cursor, user, ids, name, arg,
context=None):
res = {}
for move in self.browse(cursor, user, ids, context=context):
res[move.id] = False
if move.from_location.type == 'supplier':
res[move.id] = True
if move.to_location.type == 'customer':
res[move.id] = True
return res
def check_product_type(self, cursor, user, ids):
for move in self.browse(cursor, user, ids):
if move.product.type == 'service':
return False
return True
def get_rec_name(self, cursor, user, ids, name, arg, context=None):
if not ids:
return {}
res = {}
moves = self.browse(cursor, user, ids, context=context)
for m in moves:
res[m.id] = "%s%s %s" % (m.quantity, m.uom.symbol, m.product.rec_name)
return res
def search_rec_name(self, cursor, user, name, args, context=None):
args2 = []
i = 0
while i < len(args):
args2.append(('product', args[i][1], args[i][2]))
i += 1
return args2
def search(self, cursor, user, args, offset=0, limit=None, order=None,
context=None, count=False, query_string=False):
location_obj = self.pool.get('stock.location')
args = args[:]
def process_args(args):
i = 0
while i < len(args):
if isinstance(args[i], list):
process_args(args[i])
if isinstance(args[i], tuple) \
and args[i][0] == 'to_location_warehouse':
location_id = False
if args[i][2]:
location = location_obj.browse(cursor, user,
args[i][2], context=context)
if location.type == 'warehouse':
location_id = location.input_location.id
args[i] = ('to_location', args[i][1], location_id)
i += 1
process_args(args)
return super(Move, self).search(cursor, user, args, offset=offset,
limit=limit, order=order, context=context, count=count,
query_string=query_string)
def _update_product_cost_price(self, cursor, user, product_id, quantity, uom,
unit_price, currency, company, context=None):
"""
Update the cost price on the given product
:param cursor: the database cursor
:param user: the user id
:param product_id: the id of the product
:param quantity: the quantity of the product, positive if incoming and
negative if outgoing
:param uom: the uom id or a BrowseRecord of the uom
:param unit_price: the unit price of the product
:param currency: the currency of the unit price
:param company: the company id ot a BrowseRecord of the company
:param context: the context
"""
uom_obj = self.pool.get('product.uom')
product_obj = self.pool.get('product.product')
location_obj = self.pool.get('stock.location')
currency_obj = self.pool.get('currency.currency')
company_obj = self.pool.get('company.company')
date_obj = self.pool.get('ir.date')
if context is None:
context = {}
if isinstance(uom, (int, long)):
uom = uom_obj.browse(cursor, user, uom, context=context)
if isinstance(company, (int, long)):
company = company_obj.browse(cursor, user, company, context=context)
ctx = context and context.copy() or {}
ctx['locations'] = location_obj.search(
cursor, user, [('type', '=', 'storage')], context=context)
ctx['stock_date_end'] = date_obj.today(cursor, user, context=context)
product = product_obj.browse(cursor, user, product_id, context=ctx)
qty = uom_obj.compute_qty(
cursor, user, uom, quantity, product.default_uom, context=context)
qty = Decimal(str(qty))
product_qty = Decimal(str(product.template.quantity))
# convert wrt currency
unit_price = currency_obj.compute(
cursor, user, currency, unit_price, company.currency,
round=False, context=context)
# convert wrt to the uom
unit_price = uom_obj.compute_price(
cursor, user, uom, unit_price, product.default_uom, context=context)
if product_qty + qty != Decimal('0.0'):
new_cost_price = (
(product.cost_price * product_qty) + (unit_price * qty)
) / (product_qty + qty)
else:
new_cost_price = product.cost_price
ctx = context.copy()
ctx['user'] = user
product_obj.write(
cursor, 0, product.id, {'cost_price': new_cost_price},
context=ctx)
def create(self, cursor, user, vals, context=None):
location_obj = self.pool.get('stock.location')
product_obj = self.pool.get('product.product')
date_obj = self.pool.get('ir.date')
vals = vals.copy()
if vals.get('state') == 'done':
if not vals.get('effective_date'):
vals['effective_date'] = date_obj.today(cursor, user,
context=context)
from_location = location_obj.browse(cursor, user,
vals['from_location'], context=context)
to_location = location_obj.browse(cursor, user,
vals['to_location'], context=context)
product = product_obj.browse(cursor, user, vals['product'],
context=context)
if from_location.type == 'supplier' \
and product.cost_price_method == 'average':
self._update_product_cost_price(
cursor, user, vals['product'], vals['quantity'],
vals['uom'], vals['unit_price'], vals['currency'],
vals['company'], context=context)
if to_location.type == 'supplier' \
and product.cost_price_method == 'average':
self._update_product_cost_price(
cursor, user, vals['product'], -vals['quantity'],
vals['uom'], vals['unit_price'], vals['currency'],
vals['company'], context=context)
if not vals.get('cost_price'):
# Re-read product to get the updated cost_price
product = product_obj.browse(cursor, user, vals['product'],
context=context)
vals['cost_price'] = product.cost_price
elif vals.get('state') == 'assigned':
if not vals.get('effective_date'):
vals['effective_date'] = date_obj.today(cursor, user,
context=context)
return super(Move, self).create(cursor, user, vals, context=context)
def write(self, cursor, user, ids, vals, context=None):
date_obj = self.pool.get('ir.date')
if context is None:
context = {}
if isinstance(ids, (int, long)):
ids = [ids]
if 'state' in vals:
for move in self.browse(cursor, user, ids, context=context):
if vals['state'] == 'cancel':
vals['effective_date'] = False
if move.from_location.type == 'supplier' \
and move.state != 'cancel' \
and move.product.cost_price_method == 'average':
self._update_product_cost_price(
cursor, user, move.product.id, -move.quantity,
move.uom, move.unit_price, move.currency,
move.company, context=context)
if move.to_location.type == 'supplier' \
and move.state != 'cancel' \
and move.product.cost_price_method == 'average':
self._update_product_cost_price(
cursor, user, move.product.id, move.quantity,
move.uom, move.unit_price, move.currency,
move.company, context=context)
elif vals['state'] == 'draft':
if move.state == 'done':
self.raise_user_error(cursor, 'set_state_draft',
context=context)
elif vals['state'] == 'assigned':
if move.state in ('cancel', 'done'):
self.raise_user_error(cursor, 'set_state_assigned',
context=context)
vals['effective_date'] = date_obj.today(cursor, user,
context=context)
elif vals['state'] == 'done':
if move.state in ('cancel'):
self.raise_user_error(cursor, 'set_state_done',
context=context)
vals['effective_date'] = date_obj.today(cursor, user,
context=context)
if move.from_location.type == 'supplier' \
and move.state != 'done' \
and move.product.cost_price_method == 'average':
self._update_product_cost_price(
cursor, user, move.product.id, move.quantity,
move.uom, move.unit_price, move.currency,
move.company, context=context)
if move.to_location.type == 'supplier' \
and move.state != 'done' \
and move.product.cost_price_method == 'average':
self._update_product_cost_price(
cursor, user, move.product.id, -move.quantity,
move.uom, move.unit_price, move.currency,
move.company, context=context)
res = super(Move, self).write(cursor, user, ids, vals, context=context)
if vals.get('state', '') == 'done':
#Re-read the move because cost_price has been updated
for move in self.browse(cursor, user, ids, context=context):
if not move.cost_price:
self.write(cursor, user, move.id, {
'cost_price': move.product.cost_price,
}, context=context)
return res
def delete(self, cursor, user, ids, context=None):
for move in self.browse(cursor, user, ids, context=context):
if move.state not in ('draft', 'cancel'):
self.raise_user_error(cursor, 'del_draft_cancel',
context=context)
return super(Move, self).delete(cursor, user, ids, context=context)
def pick_product(self, cursor, user, move, location_quantities, context=None):
"""
Pick the product across the location. Naive (fast) implementation.
:param cursor: the database cursor
:param user: the user id
:param move: a BrowseRecord of stock.move
:param location_quantities: a list of tuple (location, available_qty)
where location is a BrowseRecord of stock.location.
:param context: the context
:return: a list of tuple (location, quantity) for quantities
that can be picked
"""
to_pick = []
needed_qty = move.quantity
for location, available_qty in location_quantities.iteritems():
# Ignore available_qty when too small
if available_qty < move.uom.rounding:
continue
if needed_qty <= available_qty:
to_pick.append((location, needed_qty))
return to_pick
else:
to_pick.append((location, available_qty))
needed_qty -= available_qty
# Force assignation for consumables:
if move.product.type == "consumable":
to_pick.append((move.from_location, needed_qty))
return to_pick
return to_pick
def assign_try(self, cursor, user, moves, context=None):
'''
Try to assign moves.
It will split the moves to assign as much possible.
:param cursor: the database cursor
:param user: the user id
:param moves: a BrowseRecordList of stock.move to assign
:param context: the context
:return: True if succeed or False if not
'''
product_obj = self.pool.get('product.product')
uom_obj = self.pool.get('product.uom')
date_obj = self.pool.get('ir.date')
location_obj = self.pool.get('stock.location')
if context is None:
context = {}
if cursor.has_lock():
cursor.execute('LOCK TABLE stock_move')
local_ctx = context and context.copy() or {}
local_ctx['stock_date_end'] = date_obj.today(cursor, user,
context=context)
local_ctx['stock_assign'] = True
location_ids = location_obj.search(cursor, user, [
('parent', 'child_of', [x.from_location.id for x in moves]),
], context=context)
pbl = product_obj.products_by_location(cursor, user,
location_ids=location_ids,
product_ids=[m.product.id for m in moves],
context=local_ctx)
success = True
for move in moves:
if move.state != 'draft':
continue
to_location = move.to_location
location_qties = {}
child_ids = location_obj.search(cursor, user, [
('parent', 'child_of', [move.from_location.id]),
], context=context)
for location in location_obj.browse(cursor, user, child_ids,
context=context):
if (location.id, move.product.id) in pbl:
location_qties[location] = uom_obj.compute_qty(
cursor, user, move.product.default_uom,
pbl[(location.id, move.product.id)], move.uom,
round=False, context=context)
to_pick = self.pick_product(
cursor, user, move, location_qties, context=context)
picked_qties = 0.0
for _, qty in to_pick:
picked_qties += qty
if picked_qties < move.quantity:
success = False
first = False
self.write(cursor, user, move.id, {
'quantity': move.quantity - picked_qties,
}, context=context)
else:
first = True
for from_location, qty in to_pick:
values = {
'from_location': from_location.id,
'quantity': qty,
'state': 'assigned',
}
if first:
self.write(cursor, user, move.id, values, context=context)
first = False
else:
move_id = self.copy(cursor, user, move.id, default=values,
context=context)
qty_default_uom = uom_obj.compute_qty(
cursor, user, move.uom, qty, move.product.default_uom,
round=False, context=context)
pbl[(from_location.id, move.product.id)] = \
pbl.get((from_location.id, move.product.id), 0.0) - qty_default_uom
pbl[(to_location.id, move.product.id)]= \
pbl.get((to_location.id, move.product.id), 0.0) + qty_default_uom
return success
Move()