[MERGE] main trunk-wms branch
bzr revid: qdp-launchpad@openerp.com-20140206145444-tiyk53rvxfqekil8
This commit is contained in:
@@ -63,6 +63,7 @@ Dashboard / Reports for MRP will include:
|
||||
'wizard/change_production_qty_view.xml',
|
||||
'wizard/mrp_price_view.xml',
|
||||
'wizard/mrp_workcenter_load_view.xml',
|
||||
'wizard/stock_move_view.xml',
|
||||
'mrp_view.xml',
|
||||
'mrp_report.xml',
|
||||
'company_view.xml',
|
||||
|
||||
+182
-119
@@ -220,11 +220,11 @@ class mrp_bom(osv.osv):
|
||||
'name': fields.char('Name', size=64),
|
||||
'code': fields.char('Reference', size=16),
|
||||
'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the bills of material without removing it."),
|
||||
'type': fields.selection([('normal','Normal BoM'),('phantom','Sets / Phantom')], 'BoM Type', required=True,
|
||||
'type': fields.selection([('normal', 'Normal BoM'), ('phantom', 'Sets / Phantom')], 'BoM Type', required=True,
|
||||
help= "If a by-product is used in several products, it can be useful to create its own BoM. "\
|
||||
"Though if you don't want separated production orders for this by-product, select Set/Phantom as BoM type. "\
|
||||
"If a Phantom BoM is used for a root product, it will be sold and shipped as a set of components, instead of being produced."),
|
||||
'method': fields.function(_compute_type, string='Method', type='selection', selection=[('',''),('stock','On Stock'),('order','On Order'),('set','Set / Pack')]),
|
||||
'method': fields.function(_compute_type, string='Method', type='selection', selection=[('', ''), ('stock', 'On Stock'), ('order', 'On Order'), ('set', 'Set / Pack')]),
|
||||
'date_start': fields.date('Valid From', help="Validity of this BoM or component. Keep empty if it's always valid."),
|
||||
'date_stop': fields.date('Valid Until', help="Validity of this BoM or component. Keep empty if it's always valid."),
|
||||
'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
|
||||
@@ -239,9 +239,9 @@ class mrp_bom(osv.osv):
|
||||
'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
|
||||
'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
|
||||
'routing_id': fields.many2one('mrp.routing', 'Routing', help="The list of operations (list of work centers) to produce the finished product. The routing is mainly used to compute work center costs during operations and to plan future loads on work centers based on production planning."),
|
||||
'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id','property_id', 'Properties'),
|
||||
'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id', 'property_id', 'Properties'),
|
||||
'child_complete_ids': fields.function(_child_compute, relation='mrp.bom', string="BoM Hierarchy", type='many2many'),
|
||||
'company_id': fields.many2one('res.company','Company',required=True),
|
||||
'company_id': fields.many2one('res.company', 'Company', required=True),
|
||||
}
|
||||
_defaults = {
|
||||
'active': lambda *a: 1,
|
||||
@@ -249,20 +249,20 @@ class mrp_bom(osv.osv):
|
||||
'product_qty': lambda *a: 1.0,
|
||||
'product_rounding': lambda *a: 0.0,
|
||||
'type': lambda *a: 'normal',
|
||||
'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.bom', context=c),
|
||||
'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.bom', context=c),
|
||||
}
|
||||
_order = "sequence"
|
||||
_parent_name = "bom_id"
|
||||
_sql_constraints = [
|
||||
('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
|
||||
('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
|
||||
'You should install the mrp_byproduct module if you want to manage extra products on BoMs !'),
|
||||
]
|
||||
|
||||
def _check_recursion(self, cr, uid, ids, context=None):
|
||||
level = 100
|
||||
while len(ids):
|
||||
cr.execute('select distinct bom_id from mrp_bom where id IN %s',(tuple(ids),))
|
||||
ids = filter(None, map(lambda x:x[0], cr.fetchall()))
|
||||
cr.execute('select distinct bom_id from mrp_bom where id IN %s', (tuple(ids),))
|
||||
ids = filter(None, map(lambda x: x[0], cr.fetchall()))
|
||||
if not level:
|
||||
return False
|
||||
level -= 1
|
||||
@@ -300,7 +300,7 @@ class mrp_bom(osv.osv):
|
||||
return {}
|
||||
|
||||
def onchange_uom(self, cr, uid, ids, product_id, product_uom, context=None):
|
||||
res = {'value':{}}
|
||||
res = {'value': {}}
|
||||
if not product_uom or not product_id:
|
||||
return res
|
||||
product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
|
||||
@@ -320,8 +320,8 @@ class mrp_bom(osv.osv):
|
||||
if properties is None:
|
||||
properties = []
|
||||
domain = [('product_id', '=', product_id), ('bom_id', '=', False),
|
||||
'|', ('date_start', '=', False), ('date_start', '<=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
|
||||
'|', ('date_stop', '=', False), ('date_stop', '>=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT))]
|
||||
'|', ('date_start', '=', False), ('date_start', '<=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
|
||||
'|', ('date_stop', '=', False), ('date_stop', '>=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT))]
|
||||
ids = self.search(cr, uid, domain)
|
||||
max_prop = 0
|
||||
result = False
|
||||
@@ -357,7 +357,7 @@ class mrp_bom(osv.osv):
|
||||
newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
|
||||
|
||||
if newbom:
|
||||
res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
|
||||
res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor * bom.product_qty, properties, addthis=True, level=level + 10)
|
||||
result = result + res[0]
|
||||
result2 = result2 + res[1]
|
||||
phantom = True
|
||||
@@ -365,8 +365,7 @@ class mrp_bom(osv.osv):
|
||||
phantom = False
|
||||
if not phantom:
|
||||
if addthis and not bom.bom_lines:
|
||||
result.append(
|
||||
{
|
||||
result.append({
|
||||
'name': bom.product_id.name,
|
||||
'product_id': bom.product_id.id,
|
||||
'product_qty': bom.product_qty * factor,
|
||||
@@ -382,14 +381,14 @@ class mrp_bom(osv.osv):
|
||||
mult = (d + (m and 1.0 or 0.0))
|
||||
cycle = mult * wc_use.cycle_nbr
|
||||
result2.append({
|
||||
'name': tools.ustr(wc_use.name) + ' - ' + tools.ustr(bom.product_id.name),
|
||||
'name': tools.ustr(wc_use.name) + ' - ' + tools.ustr(bom.product_id.name),
|
||||
'workcenter_id': wc.id,
|
||||
'sequence': level+(wc_use.sequence or 0),
|
||||
'sequence': level + (wc_use.sequence or 0),
|
||||
'cycle': cycle,
|
||||
'hour': float(wc_use.hour_nbr*mult + ((wc.time_start or 0.0)+(wc.time_stop or 0.0)+cycle*(wc.time_cycle or 0.0)) * (wc.time_efficiency or 1.0)),
|
||||
'hour': float(wc_use.hour_nbr * mult + ((wc.time_start or 0.0) + (wc.time_stop or 0.0) + cycle * (wc.time_cycle or 0.0)) * (wc.time_efficiency or 1.0)),
|
||||
})
|
||||
for bom2 in bom.bom_lines:
|
||||
res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
|
||||
res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level + 10)
|
||||
result = result + res[0]
|
||||
result2 = result2 + res[1]
|
||||
return result, result2
|
||||
@@ -414,7 +413,7 @@ class mrp_production(osv.osv):
|
||||
"""
|
||||
_name = 'mrp.production'
|
||||
_description = 'Manufacturing Order'
|
||||
_date_name = 'date_planned'
|
||||
_date_name = 'date_planned'
|
||||
_inherit = ['mail.thread', 'ir.needaction_mixin']
|
||||
|
||||
def _production_calc(self, cr, uid, ids, prop, unknow_none, context=None):
|
||||
@@ -438,7 +437,7 @@ class mrp_production(osv.osv):
|
||||
try:
|
||||
location_model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
|
||||
self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
|
||||
except (orm.except_orm, ValueError):
|
||||
except (orm.except_orm, ValueError):
|
||||
location_id = False
|
||||
return location_id
|
||||
|
||||
@@ -472,25 +471,22 @@ class mrp_production(osv.osv):
|
||||
res[production.id] = False
|
||||
return res
|
||||
|
||||
|
||||
def _mrp_from_move(self, cr, uid, ids, context=None):
|
||||
""" Return mrp"""
|
||||
res = []
|
||||
for move in self.browse(cr, uid, ids, context=context):
|
||||
res += self.pool.get("mrp.production").search(cr, uid, [('move_lines', 'in', move.id)], context=context)
|
||||
return res
|
||||
|
||||
|
||||
|
||||
_columns = {
|
||||
'name': fields.char('Reference', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}),
|
||||
'origin': fields.char('Source Document', size=64, readonly=True, states={'draft': [('readonly', False)]},
|
||||
help="Reference of the document that generated this production order request."),
|
||||
'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority',
|
||||
'priority': fields.selection([('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')], 'Priority',
|
||||
select=True, readonly=True, states=dict.fromkeys(['draft', 'confirmed'], [('readonly', False)])),
|
||||
|
||||
'product_id': fields.many2one('product.product', 'Product', required=True, readonly=True, states={'draft': [('readonly', False)]}),
|
||||
'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, readonly=True, states={'draft':[('readonly',False)]}),
|
||||
'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
|
||||
'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, readonly=True, states={'draft': [('readonly', False)]}),
|
||||
'product_uos_qty': fields.float('Product UoS Quantity', readonly=True, states={'draft': [('readonly', False)]}),
|
||||
'product_uos': fields.many2one('product.uom', 'Product UoS', readonly=True, states={'draft': [('readonly', False)]}),
|
||||
@@ -498,31 +494,31 @@ class mrp_production(osv.osv):
|
||||
string='Production progress'),
|
||||
|
||||
'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
|
||||
readonly=True, states={'draft':[('readonly',False)]},
|
||||
readonly=True, states={'draft': [('readonly', False)]},
|
||||
help="Location where the system will look for components."),
|
||||
'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
|
||||
readonly=True, states={'draft':[('readonly',False)]},
|
||||
readonly=True, states={'draft': [('readonly', False)]},
|
||||
help="Location where the system will stock the finished products."),
|
||||
'date_planned': fields.datetime('Scheduled Date', required=True, select=1, readonly=True, states={'draft':[('readonly',False)]}),
|
||||
'date_planned': fields.datetime('Scheduled Date', required=True, select=1, readonly=True, states={'draft': [('readonly', False)]}),
|
||||
'date_start': fields.datetime('Start Date', select=True, readonly=True),
|
||||
'date_finished': fields.datetime('End Date', select=True, readonly=True),
|
||||
'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)], readonly=True, states={'draft':[('readonly',False)]},
|
||||
'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id', '=', False)], readonly=True, states={'draft': [('readonly', False)]},
|
||||
help="Bill of Materials allow you to define the list of required raw materials to make a finished product."),
|
||||
'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null', readonly=True, states={'draft':[('readonly',False)]},
|
||||
'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null', readonly=True, states={'draft': [('readonly', False)]},
|
||||
help="The list of operations (list of work centers) to produce the finished product. The routing is mainly used to compute work center costs during operations and to plan future loads on work centers based on production plannification."),
|
||||
'move_prod_id': fields.many2one('stock.move', 'Product Move', readonly=True),
|
||||
'move_lines': fields.one2many('stock.move', 'raw_material_production_id', 'Products to Consume',
|
||||
domain=[('state','not in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
|
||||
domain=[('state', 'not in', ('done', 'cancel'))], readonly=True, states={'draft': [('readonly', False)]}),
|
||||
'move_lines2': fields.one2many('stock.move', 'raw_material_production_id', 'Consumed Products',
|
||||
domain=[('state','in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
|
||||
domain=[('state', 'in', ('done', 'cancel'))], readonly=True),
|
||||
'move_created_ids': fields.one2many('stock.move', 'production_id', 'Products to Produce',
|
||||
domain=[('state','not in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
|
||||
domain=[('state', 'not in', ('done', 'cancel'))], readonly=True),
|
||||
'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Produced Products',
|
||||
domain=[('state','in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
|
||||
domain=[('state', 'in', ('done', 'cancel'))], readonly=True),
|
||||
'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods',
|
||||
readonly=True, states={'draft':[('readonly',False)]}),
|
||||
readonly=True),
|
||||
'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation',
|
||||
readonly=True, states={'draft':[('readonly',False)]}),
|
||||
readonly=True, states={'draft': [('readonly', False)]}),
|
||||
'state': fields.selection(
|
||||
[('draft', 'New'), ('cancel', 'Cancelled'), ('picking_except', 'Picking Exception'), ('confirmed', 'Awaiting Raw Materials'),
|
||||
('ready', 'Ready to Produce'), ('in_production', 'Production Started'), ('done', 'Done')],
|
||||
@@ -536,26 +532,28 @@ class mrp_production(osv.osv):
|
||||
When the production is over, the status is set to 'Done'."),
|
||||
'hour_total': fields.function(_production_calc, type='float', string='Total Hours', multi='workorder', store=True),
|
||||
'cycle_total': fields.function(_production_calc, type='float', string='Total Cycles', multi='workorder', store=True),
|
||||
'user_id':fields.many2one('res.users', 'Responsible'),
|
||||
'company_id': fields.many2one('res.company','Company',required=True),
|
||||
'user_id': fields.many2one('res.users', 'Responsible'),
|
||||
'company_id': fields.many2one('res.company', 'Company', required=True),
|
||||
'ready_production': fields.function(_moves_assigned, type='boolean', store={'stock.move': (_mrp_from_move, ['state'], 10)}),
|
||||
|
||||
}
|
||||
|
||||
_defaults = {
|
||||
'priority': lambda *a: '1',
|
||||
'state': lambda *a: 'draft',
|
||||
'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'product_qty': lambda *a: 1.0,
|
||||
'product_qty': lambda *a: 1.0,
|
||||
'user_id': lambda self, cr, uid, c: uid,
|
||||
'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
|
||||
'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
|
||||
'location_src_id': _src_id_default,
|
||||
'location_dest_id': _dest_id_default
|
||||
}
|
||||
|
||||
_sql_constraints = [
|
||||
('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'),
|
||||
]
|
||||
_order = 'priority desc, date_planned asc';
|
||||
|
||||
_order = 'priority desc, date_planned asc'
|
||||
|
||||
def _check_qty(self, cr, uid, ids, context=None):
|
||||
for order in self.browse(cr, uid, ids, context=context):
|
||||
@@ -578,12 +576,12 @@ class mrp_production(osv.osv):
|
||||
default = {}
|
||||
default.update({
|
||||
'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
|
||||
'move_lines' : [],
|
||||
'move_lines2' : [],
|
||||
'move_created_ids' : [],
|
||||
'move_created_ids2' : [],
|
||||
'product_lines' : [],
|
||||
'move_prod_id' : False,
|
||||
'move_lines': [],
|
||||
'move_lines2': [],
|
||||
'move_created_ids': [],
|
||||
'move_created_ids2': [],
|
||||
'product_lines': [],
|
||||
'move_prod_id': False,
|
||||
})
|
||||
return super(mrp_production, self).copy(cr, uid, id, default, context)
|
||||
|
||||
@@ -648,7 +646,7 @@ class mrp_production(osv.osv):
|
||||
"""
|
||||
self.write(cr, uid, ids, {'state': 'picking_except'})
|
||||
return True
|
||||
|
||||
|
||||
def _action_compute_lines(self, cr, uid, ids, properties=None, context=None):
|
||||
""" Compute product_lines and workcenter_lines from BoM structure
|
||||
@return: product_lines
|
||||
@@ -665,10 +663,10 @@ class mrp_production(osv.osv):
|
||||
for production in self.browse(cr, uid, ids, context=context):
|
||||
#unlink product_lines
|
||||
prod_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.product_lines], context=context)
|
||||
|
||||
|
||||
#unlink workcenter_lines
|
||||
workcenter_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.workcenter_lines], context=context)
|
||||
|
||||
|
||||
# search BoM structure and route
|
||||
bom_point = production.bom_id
|
||||
bom_id = production.bom_id.id
|
||||
@@ -678,21 +676,21 @@ class mrp_production(osv.osv):
|
||||
bom_point = bom_obj.browse(cr, uid, bom_id)
|
||||
routing_id = bom_point.routing_id.id or False
|
||||
self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
|
||||
|
||||
|
||||
if not bom_id:
|
||||
raise osv.except_osv(_('Error!'), _("Cannot find a bill of material for this product."))
|
||||
|
||||
|
||||
# get components and workcenter_lines from BoM structure
|
||||
factor = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, bom_point.product_uom.id)
|
||||
res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties, routing_id=production.routing_id.id)
|
||||
results = res[0] # product_lines
|
||||
results2 = res[1] # workcenter_lines
|
||||
|
||||
results = res[0] # product_lines
|
||||
results2 = res[1] # workcenter_lines
|
||||
|
||||
# reset product_lines in production order
|
||||
for line in results:
|
||||
line['production_id'] = production.id
|
||||
prod_line_obj.create(cr, uid, line)
|
||||
|
||||
|
||||
#reset workcenter_lines in production order
|
||||
for line in results2:
|
||||
line['production_id'] = production.id
|
||||
@@ -729,10 +727,10 @@ class mrp_production(osv.osv):
|
||||
|
||||
for production in self.browse(cr, uid, ids, context=context):
|
||||
if not production.move_created_ids:
|
||||
produce_move_id = self._make_production_produce_line(cr, uid, production, context=context)
|
||||
self._make_production_produce_line(cr, uid, production, context=context)
|
||||
for scheduled in production.product_lines:
|
||||
self._make_production_line_procurement(cr, uid, scheduled, False, context=context)
|
||||
|
||||
|
||||
if production.move_prod_id and production.move_prod_id.location_id.id != production.location_dest_id.id:
|
||||
move_obj.write(cr, uid, [production.move_prod_id.id],
|
||||
{'location_id': production.location_dest_id.id})
|
||||
@@ -771,7 +769,92 @@ class mrp_production(osv.osv):
|
||||
"""
|
||||
return 1
|
||||
|
||||
def action_produce(self, cr, uid, production_id, production_qty, production_mode, context=None):
|
||||
def _get_produced_qty(self, cr, uid, production, context=None):
|
||||
''' returns the produced quantity of product 'production.product_id' for the given production, in the product UoM
|
||||
'''
|
||||
produced_qty = 0
|
||||
for produced_product in production.move_created_ids2:
|
||||
if (produced_product.scrapped) or (produced_product.product_id.id != production.product_id.id):
|
||||
continue
|
||||
produced_qty += produced_product.product_qty
|
||||
return produced_qty
|
||||
|
||||
def _get_consumed_data(self, cr, uid, production, context=None):
|
||||
''' returns a dictionary containing for each raw material of the given production, its quantity already consumed (in the raw material UoM)
|
||||
'''
|
||||
consumed_data = {}
|
||||
# Calculate already consumed qtys
|
||||
for consumed in production.move_lines2:
|
||||
if consumed.scrapped:
|
||||
continue
|
||||
if not consumed_data.get(consumed.product_id.id, False):
|
||||
consumed_data[consumed.product_id.id] = 0
|
||||
consumed_data[consumed.product_id.id] += consumed.product_qty
|
||||
return consumed_data
|
||||
|
||||
def _calculate_qty(self, cr, uid, production, product_qty=0.0, context=None):
|
||||
"""
|
||||
Calculates the quantity still needed to produce an extra number of products
|
||||
"""
|
||||
quant_obj = self.pool.get("stock.quant")
|
||||
produced_qty = self._get_produced_qty(cr, uid, production, context=context)
|
||||
consumed_data = self._get_consumed_data(cr, uid, production, context=context)
|
||||
|
||||
#In case no product_qty is given, take the remaining qty to produce for the given production
|
||||
if not product_qty:
|
||||
product_qty = production.product_qty - produced_qty
|
||||
|
||||
dicts = {}
|
||||
# Find product qty to be consumed and consume it
|
||||
for scheduled in production.product_lines:
|
||||
consumed_qty = consumed_data.get(scheduled.product_id.id, 0.0)
|
||||
# qty available for consume and produce
|
||||
qty_avail = scheduled.product_qty - consumed_qty
|
||||
if qty_avail <= 0.0:
|
||||
# there will be nothing to consume for this raw material
|
||||
continue
|
||||
|
||||
if not dicts.get(scheduled.product_id.id):
|
||||
dicts[scheduled.product_id.id] = {}
|
||||
|
||||
# total qty of consumed product we need after this consumption
|
||||
total_consume = ((product_qty + produced_qty) * scheduled.product_qty / production.product_qty)
|
||||
qty = total_consume - consumed_qty
|
||||
|
||||
# Search for quants related to this related move
|
||||
for move in production.move_lines:
|
||||
if qty <= 0.0:
|
||||
break
|
||||
if move.product_id.id != scheduled.product_id.id:
|
||||
continue
|
||||
product_id = scheduled.product_id.id
|
||||
|
||||
q = min(move.product_qty, qty)
|
||||
quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, scheduled.product_id, q, domain=[('qty', '>', 0.0)],
|
||||
prefered_domain=[('reservation_id', '=', move.id)], fallback_domain=[('reservation_id', '=', False)], context=context)
|
||||
for quant, quant_qty in quants:
|
||||
if quant:
|
||||
lot_id = quant.lot_id.id
|
||||
if not product_id in dicts.keys():
|
||||
dicts[product_id] = {lot_id: quant_qty}
|
||||
elif lot_id in dicts[product_id].keys():
|
||||
dicts[product_id][lot_id] += quant_qty
|
||||
else:
|
||||
dicts[product_id][lot_id] = quant_qty
|
||||
qty -= quant_qty
|
||||
if qty > 0:
|
||||
if dicts[product_id].get(False):
|
||||
dicts[product_id][False] += qty
|
||||
else:
|
||||
dicts[product_id][False] = qty
|
||||
|
||||
consume_lines = []
|
||||
for prod in dicts.keys():
|
||||
for lot, qty in dicts[prod].items():
|
||||
consume_lines.append({'product_id': prod, 'product_qty': qty, 'lot_id': lot})
|
||||
return consume_lines
|
||||
|
||||
def action_produce(self, cr, uid, production_id, production_qty, production_mode, wiz=False, context=None):
|
||||
""" To produce final product based on production mode (consume/consume&produce).
|
||||
If Production mode is consume, all stock move lines of raw materials will be done/consumed.
|
||||
If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
|
||||
@@ -779,58 +862,18 @@ class mrp_production(osv.osv):
|
||||
@param production_id: the ID of mrp.production object
|
||||
@param production_qty: specify qty to produce
|
||||
@param production_mode: specify production mode (consume/consume&produce).
|
||||
@param wiz: the mrp produce product wizard, which will tell the amount of consumed products needed
|
||||
@return: True
|
||||
"""
|
||||
stock_mov_obj = self.pool.get('stock.move')
|
||||
production = self.browse(cr, uid, production_id, context=context)
|
||||
|
||||
if not production.move_lines and production.state == 'ready':
|
||||
# trigger workflow if not products to consume (eg: services)
|
||||
self.signal_button_produce(cr, uid, [production_id])
|
||||
|
||||
produced_qty = 0
|
||||
for produced_product in production.move_created_ids2:
|
||||
if (produced_product.scrapped) or (produced_product.product_id.id != production.product_id.id):
|
||||
continue
|
||||
produced_qty += produced_product.product_qty
|
||||
if production_mode in ['consume','consume_produce']:
|
||||
consumed_data = {}
|
||||
|
||||
# Calculate already consumed qtys
|
||||
for consumed in production.move_lines2:
|
||||
if consumed.scrapped:
|
||||
continue
|
||||
if not consumed_data.get(consumed.product_id.id, False):
|
||||
consumed_data[consumed.product_id.id] = 0
|
||||
consumed_data[consumed.product_id.id] += consumed.product_qty
|
||||
|
||||
# Find product qty to be consumed and consume it
|
||||
for scheduled in production.product_lines:
|
||||
|
||||
# total qty of consumed product we need after this consumption
|
||||
total_consume = ((production_qty + produced_qty) * scheduled.product_qty / production.product_qty)
|
||||
|
||||
# qty available for consume and produce
|
||||
qty_avail = scheduled.product_qty - consumed_data.get(scheduled.product_id.id, 0.0)
|
||||
|
||||
if float_compare(qty_avail, 0, precision_rounding=scheduled.product_id.uom_id.rounding) <= 0:
|
||||
# there will be nothing to consume for this raw material
|
||||
continue
|
||||
|
||||
raw_product = [move for move in production.move_lines if move.product_id.id==scheduled.product_id.id]
|
||||
if raw_product:
|
||||
# qtys we have to consume
|
||||
qty = total_consume - consumed_data.get(scheduled.product_id.id, 0.0)
|
||||
if float_compare(qty, qty_avail, precision_rounding=scheduled.product_id.uom_id.rounding) == 1:
|
||||
# if qtys we have to consume is more than qtys available to consume
|
||||
prod_name = scheduled.product_id.name_get()[0][1]
|
||||
raise osv.except_osv(_('Warning!'), _('You are going to consume total %s quantities of "%s".\nBut you can only consume up to total %s quantities.') % (qty, prod_name, qty_avail))
|
||||
if float_compare(qty, 0, precision_rounding=scheduled.product_id.uom_id.rounding) <= 0:
|
||||
# we already have more qtys consumed than we need
|
||||
continue
|
||||
|
||||
raw_product[0].action_consume(qty, raw_product[0].location_id.id, context=context)
|
||||
produced_qty = self._get_produced_qty(cr, uid, production, context=context)
|
||||
|
||||
main_production_move = False
|
||||
if production_mode == 'consume_produce':
|
||||
# To produce remaining qty of final product
|
||||
#vals = {'state':'confirmed'}
|
||||
@@ -849,12 +892,34 @@ class mrp_production(osv.osv):
|
||||
produced_qty = produced_products.get(produce_product.product_id.id, 0)
|
||||
subproduct_factor = self._get_subproduct_factor(cr, uid, production.id, produce_product.id, context=context)
|
||||
rest_qty = (subproduct_factor * production.product_qty) - produced_qty
|
||||
|
||||
if rest_qty < (subproduct_factor * production_qty):
|
||||
if float_compare(rest_qty, (subproduct_factor * production_qty), precision_rounding=produce_product.product_id.uom_id.rounding) < 0:
|
||||
prod_name = produce_product.product_id.name_get()[0][1]
|
||||
raise osv.except_osv(_('Warning!'), _('You are going to produce total %s quantities of "%s".\nBut you can only produce up to total %s quantities.') % ((subproduct_factor * production_qty), prod_name, rest_qty))
|
||||
if rest_qty > 0 :
|
||||
stock_mov_obj.action_consume(cr, uid, [produce_product.id], (subproduct_factor * production_qty), context=context)
|
||||
if float_compare(rest_qty, 0, precision_rounding=produce_product.product_id.uom_id.rounding) > 0:
|
||||
lot_id = False
|
||||
if wiz:
|
||||
lot_id = wiz.lot_id.id
|
||||
new_moves = stock_mov_obj.action_consume(cr, uid, [produce_product.id], (subproduct_factor * production_qty), location_id=produce_product.location_id.id, restrict_lot_id=lot_id, context=context)
|
||||
if produce_product.product_id.id == production.product_id.id and new_moves:
|
||||
main_production_move = new_moves[0]
|
||||
|
||||
if production_mode in ['consume', 'consume_produce']:
|
||||
if wiz:
|
||||
consume_lines = []
|
||||
for cons in wiz.consume_lines:
|
||||
consume_lines.append({'product_id': cons.product_id.id, 'lot_id': cons.lot_id.id, 'product_qty': cons.product_qty})
|
||||
else:
|
||||
consume_lines = self._calculate_qty(cr, uid, production, production_qty, context=context)
|
||||
for consume in consume_lines:
|
||||
remaining_qty = consume['product_qty']
|
||||
for raw_material_line in production.move_lines:
|
||||
if remaining_qty <= 0:
|
||||
break
|
||||
if consume['product_id'] != raw_material_line.product_id.id:
|
||||
continue
|
||||
consumed_qty = min(remaining_qty, raw_material_line.product_qty)
|
||||
stock_mov_obj.action_consume(cr, uid, [raw_material_line.id], consumed_qty, raw_material_line.location_id.id, restrict_lot_id=consume['lot_id'], consumed_for=main_production_move, context=context)
|
||||
remaining_qty -= consumed_qty
|
||||
|
||||
self.message_post(cr, uid, production_id, body=_("%s produced") % self._description, context=context)
|
||||
self.signal_button_produce_done(cr, uid, [production_id])
|
||||
@@ -888,14 +953,14 @@ class mrp_production(osv.osv):
|
||||
'product_id': wc.product_id.id,
|
||||
'unit_amount': wc_line.hour,
|
||||
'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
|
||||
} )
|
||||
})
|
||||
# Cost per cycle
|
||||
value = wc_line.cycle * wc.costs_cycle
|
||||
account = wc.costs_cycle_account_id.id
|
||||
if value and account:
|
||||
amount += value
|
||||
analytic_line_obj.create(cr, SUPERUSER_ID, {
|
||||
'name': wc_line.name+' (C)',
|
||||
'name': wc_line.name + ' (C)',
|
||||
'amount': value,
|
||||
'account_id': account,
|
||||
'general_account_id': wc.costs_general_account_id.id,
|
||||
@@ -904,7 +969,7 @@ class mrp_production(osv.osv):
|
||||
'product_id': wc.product_id.id,
|
||||
'unit_amount': wc_line.cycle,
|
||||
'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
|
||||
} )
|
||||
})
|
||||
return amount
|
||||
|
||||
def action_in_production(self, cr, uid, ids, context=None):
|
||||
@@ -918,7 +983,7 @@ class mrp_production(osv.osv):
|
||||
for order in self.browse(cr, uid, ids, context={}):
|
||||
res += [x.id for x in order.move_lines]
|
||||
return res
|
||||
|
||||
|
||||
def test_ready(self, cr, uid, ids):
|
||||
res = False
|
||||
for production in self.browse(cr, uid, ids):
|
||||
@@ -959,7 +1024,7 @@ class mrp_production(osv.osv):
|
||||
'product_id': production.product_id.id,
|
||||
'product_qty': production.product_qty,
|
||||
'product_uom': production.product_uom.id,
|
||||
'product_uom_qty': production.product_qty,
|
||||
'product_uom_qty': production.product_qty,
|
||||
'product_uos_qty': production.product_uos and production.product_uos_qty or False,
|
||||
'product_uos': production.product_uos and production.product_uos.id or False,
|
||||
'location_id': source_location_id,
|
||||
@@ -986,12 +1051,11 @@ class mrp_production(osv.osv):
|
||||
'date': production.date_planned,
|
||||
'product_id': production_line.product_id.id,
|
||||
'product_uom_qty': production_line.product_qty,
|
||||
'product_uom': production_line.product_uom.id,
|
||||
'product_uom': production_line.product_uom.id,
|
||||
'product_uos_qty': production_line.product_uos and production_line.product_uos_qty or False,
|
||||
'product_uos': production_line.product_uos and production_line.product_uos.id or False,
|
||||
'location_id': source_location_id,
|
||||
'location_dest_id': destination_location_id,
|
||||
'move_dest_id': parent_move_id,
|
||||
'company_id': production.company_id.id,
|
||||
'procure_method': 'make_to_order',
|
||||
})
|
||||
@@ -1003,7 +1067,7 @@ class mrp_production(osv.osv):
|
||||
""" Confirms production order.
|
||||
@return: Newly generated Shipment Id.
|
||||
"""
|
||||
uncompute_ids = filter(lambda x:x, [not x.product_lines and x.id or False for x in self.browse(cr, uid, ids, context=context)])
|
||||
uncompute_ids = filter(lambda x: x, [not x.product_lines and x.id or False for x in self.browse(cr, uid, ids, context=context)])
|
||||
self.action_compute(cr, uid, uncompute_ids, context=context)
|
||||
for production in self.browse(cr, uid, ids, context=context):
|
||||
produce_move_id = self._make_production_produce_line(cr, uid, production, context=context)
|
||||
@@ -1012,10 +1076,10 @@ class mrp_production(osv.osv):
|
||||
source_location_id = production.location_src_id.id
|
||||
if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
|
||||
source_location_id = production.bom_id.routing_id.location_id.id
|
||||
|
||||
|
||||
for line in production.product_lines:
|
||||
self._make_production_consume_line(cr, uid, line, produce_move_id, source_location_id=source_location_id, context=context)
|
||||
production.write({'state':'confirmed'}, context=context)
|
||||
production.write({'state': 'confirmed'}, context=context)
|
||||
return 0
|
||||
|
||||
def force_production(self, cr, uid, ids, *args):
|
||||
@@ -1023,7 +1087,6 @@ class mrp_production(osv.osv):
|
||||
@param *args: Arguments
|
||||
@return: True
|
||||
"""
|
||||
|
||||
move_obj = self.pool.get('stock.move')
|
||||
for order in self.browse(cr, uid, ids):
|
||||
move_obj.force_assign(cr, uid, [x.id for x in order.move_lines])
|
||||
@@ -1039,8 +1102,8 @@ class mrp_production_workcenter_line(osv.osv):
|
||||
_columns = {
|
||||
'name': fields.char('Work Order', size=64, required=True),
|
||||
'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
|
||||
'cycle': fields.float('Number of Cycles', digits=(16,2)),
|
||||
'hour': fields.float('Number of Hours', digits=(16,2)),
|
||||
'cycle': fields.float('Number of Cycles', digits=(16, 2)),
|
||||
'hour': fields.float('Number of Hours', digits=(16, 2)),
|
||||
'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
|
||||
'production_id': fields.many2one('mrp.production', 'Manufacturing Order',
|
||||
track_visibility='onchange', select=True, ondelete='cascade', required=True),
|
||||
|
||||
+5
-14
@@ -725,19 +725,12 @@
|
||||
<field name="product_id"/>
|
||||
<field name="product_qty" string="Quantity"/>
|
||||
<field name="product_uom" string="Unit of Measure" groups="product.group_uom"/>
|
||||
<field name="restrict_lot_id"/>
|
||||
<field name="restrict_partner_id"/> <!-- TODO group on lot and ownerhsip feature -->
|
||||
<field name="state" invisible="1"/>
|
||||
<button name="%(stock.move_consume)d"
|
||||
<button name="%(mrp.move_consume)d"
|
||||
string="Consume Products" type="action"
|
||||
icon="gtk-go-forward" context="{'consume': True}"
|
||||
states="draft,waiting,confirmed,assigned"/>
|
||||
<button
|
||||
name="%(stock.track_line)d"
|
||||
string="Split in Serial Numbers"
|
||||
type="action" icon="gtk-justify-fill"
|
||||
states="draft,waiting,confirmed,assigned"
|
||||
groups="stock.group_tracking_lot"/>
|
||||
states="assigned"
|
||||
/>
|
||||
<button name="%(stock.move_scrap)d"
|
||||
string="Scrap Products" type="action"
|
||||
icon="terp-gtk-jump-to-ltr" context="{'scrap': True}"
|
||||
@@ -749,9 +742,9 @@
|
||||
<field name="move_lines2" nolabel="1" options="{'reload_on_button': true}">
|
||||
<tree colors="red:scrapped==True;blue:state == 'draft';black:state in('picking_except','confirmed','ready','in_production');gray:state == 'cancel' " string="Consumed Products" editable="bottom">
|
||||
<field name="product_id" readonly="1"/>
|
||||
<field name="restrict_lot_id" context="{'product_id': product_id}" groups="stock.group_tracking_lot"/>
|
||||
<field name="product_qty" readonly="1"/>
|
||||
<field name="product_uom" readonly="1" string="Unit of Measure" groups="product.group_uom"/>
|
||||
<!--<field name="prodlot_id" context="{'product_id': product_id}" groups="stock.group_tracking_lot"/>-->
|
||||
<field name="state" invisible="1"/>
|
||||
<field name="scrapped" invisible="1"/>
|
||||
</tree>
|
||||
@@ -777,13 +770,11 @@
|
||||
<tree colors="red:scrapped==True;blue:state == 'draft';black:state in('picking_except','confirmed','ready','in_production');gray:state in('cancel','done') " string="Finished Products">
|
||||
<field name="product_id" readonly="1"/>
|
||||
<field name="product_qty" readonly="1"/>
|
||||
<field name="restrict_lot_id" groups="stock.group_tracking_lot"/>
|
||||
<field name="product_uom" readonly="1" string="Unit of Measure" groups="product.group_uom"/>
|
||||
<field name="location_dest_id" readonly="1" string="Destination Loc." widget="selection" groups="stock.group_locations"/>
|
||||
<!--<field name="prodlot_id" context="{'product_id': product_id}" groups="stock.group_tracking_lot"/>-->
|
||||
<field name="scrapped" invisible="1"/>
|
||||
<field name="state" invisible="1"/>
|
||||
<button name="%(stock.track_line)d"
|
||||
string="Split in Serial Numbers" type="action" icon="gtk-justify-fill" states="done,cancel"/>
|
||||
<button name="%(stock.move_scrap)d"
|
||||
string="Scrap Products" type="action" icon="terp-gtk-jump-to-ltr"
|
||||
states="done,cancel"/>
|
||||
|
||||
+52
-40
@@ -26,17 +26,20 @@ from openerp.tools.translate import _
|
||||
|
||||
class StockMove(osv.osv):
|
||||
_inherit = 'stock.move'
|
||||
|
||||
|
||||
_columns = {
|
||||
'production_id': fields.many2one('mrp.production', 'Production Order for Produced Products', select=True),
|
||||
'raw_material_production_id': fields.many2one('mrp.production', 'Production Order for Raw Materials', select=True),
|
||||
'consumed_for': fields.many2one('stock.move', 'Consumed for', help='Technical field used to make the traceability of produced products'),
|
||||
}
|
||||
|
||||
def check_tracking(self, cr, uid, move, lot_id, context=None):
|
||||
super(StockMove, self).check_tracking(cr, uid, move, lot_id, context=context)
|
||||
if move.product_id.track_production and (move.location_id.usage == 'production' or move.location_dest_id.usage == 'production') and not lot_id:
|
||||
raise osv.except_osv(_('Warning!'), _('You must assign a serial number for the product %s') % (move.product_id.name))
|
||||
|
||||
if move.raw_material_production_id and move.location_dest_id.usage == 'production' and move.raw_material_production_id.product_id.track_production and not move.consumed_for:
|
||||
raise osv.except_osv(_('Warning!'), _("Because the product %s requires it, you must assign a serial number to your raw material %s to proceed further in your production. Please use the 'Produce' button to do so.") % (move.raw_material_production_id.product_id.name, move.product_id.name))
|
||||
|
||||
def _action_explode(self, cr, uid, move, context=None):
|
||||
""" Explodes pickings.
|
||||
@param move: Stock moves
|
||||
@@ -47,11 +50,11 @@ class StockMove(osv.osv):
|
||||
procurement_obj = self.pool.get('procurement.order')
|
||||
product_obj = self.pool.get('product.product')
|
||||
processed_ids = [move.id]
|
||||
|
||||
|
||||
bis = bom_obj.search(cr, uid, [
|
||||
('product_id','=',move.product_id.id),
|
||||
('bom_id','=',False),
|
||||
('type','=','phantom')])
|
||||
('product_id', '=', move.product_id.id),
|
||||
('bom_id', '=', False),
|
||||
('type', '=', 'phantom')])
|
||||
if bis:
|
||||
factor = move.product_qty
|
||||
bom_point = bom_obj.browse(cr, uid, bis[0], context=context)
|
||||
@@ -59,7 +62,7 @@ class StockMove(osv.osv):
|
||||
state = 'confirmed'
|
||||
if move.state == 'assigned':
|
||||
state = 'assigned'
|
||||
for line in res[0]:
|
||||
for line in res[0]:
|
||||
valdef = {
|
||||
'picking_id': move.picking_id.id,
|
||||
'product_id': line['product_id'],
|
||||
@@ -83,7 +86,7 @@ class StockMove(osv.osv):
|
||||
'product_qty': line['product_qty'],
|
||||
'product_uom': line['product_uom'],
|
||||
'product_uos_qty': line['product_uos'] and line['product_uos_qty'] or False,
|
||||
'product_uos': line['product_uos'],
|
||||
'product_uos': line['product_uos'],
|
||||
'location_id': move.location_id.id,
|
||||
'procure_method': prodobj.procure_method,
|
||||
'move_id': mid,
|
||||
@@ -100,29 +103,52 @@ class StockMove(osv.osv):
|
||||
procurement_obj.signal_button_wait_done(cr, uid, procurement_ids)
|
||||
return processed_ids
|
||||
|
||||
def action_consume(self, cr, uid, ids, product_qty, location_id=False, restrict_lot_id=False, restrict_partner_id=False, context=None):
|
||||
""" Consumed product with specific quatity from specific source location.
|
||||
def action_consume(self, cr, uid, ids, product_qty, location_id=False, restrict_lot_id=False, restrict_partner_id=False,
|
||||
consumed_for=False, context=None):
|
||||
""" Consumed product with specific quantity from specific source location.
|
||||
@param product_qty: Consumed product quantity
|
||||
@param location_id: Source location
|
||||
@param restrict_lot_id: optionnal parameter that allows to restrict the choice of quants on this specific lot
|
||||
@param restrict_partner_id: optionnal parameter that allows to restrict the choice of quants to this specific partner
|
||||
@param consumed_for: optionnal parameter given to this function to make the link between raw material consumed and produced product, for a better traceability
|
||||
@return: Consumed lines
|
||||
"""
|
||||
if context is None:
|
||||
context = {}
|
||||
res = []
|
||||
production_obj = self.pool.get('mrp.production')
|
||||
uom_obj = self.pool.get('product.uom')
|
||||
|
||||
if product_qty <= 0:
|
||||
raise osv.except_osv(_('Warning!'), _('Please provide proper quantity.'))
|
||||
for move in self.browse(cr, uid, ids, context=context):
|
||||
self.action_confirm(cr, uid, [move.id], context=context)
|
||||
new_moves = super(StockMove, self).action_consume(cr, uid, [move.id], product_qty, location_id, restrict_lot_id=restrict_lot_id,
|
||||
restrict_partner_id=restrict_partner_id, context=context)
|
||||
if move.state == 'draft':
|
||||
self.action_confirm(cr, uid, [move.id], context=context)
|
||||
move_qty = move.product_qty
|
||||
uom_qty = uom_obj._compute_qty(cr, uid, move.product_id.uom_id.id, product_qty, move.product_uom.id)
|
||||
if move_qty <= 0:
|
||||
raise osv.except_osv(_('Error!'), _('Cannot consume a move with negative or zero quantity.'))
|
||||
quantity_rest = move.product_qty - uom_qty
|
||||
if quantity_rest > 0:
|
||||
ctx = context.copy()
|
||||
if location_id:
|
||||
ctx['source_location_id'] = location_id
|
||||
new_mov = self.split(cr, uid, move, move_qty - quantity_rest, restrict_lot_id=restrict_lot_id, restrict_partner_id=restrict_partner_id, context=ctx)
|
||||
self.write(cr, uid, new_mov, {'consumed_for': consumed_for}, context=context)
|
||||
res.append(new_mov)
|
||||
else:
|
||||
res.append(move.id)
|
||||
if location_id:
|
||||
self.write(cr, uid, [move.id], {'location_id': location_id, 'restrict_lot_id': restrict_lot_id,
|
||||
'restrict_partner_id': restrict_partner_id,
|
||||
'consumed_for': consumed_for}, context=context)
|
||||
self.action_done(cr, uid, res, context=context)
|
||||
production_ids = production_obj.search(cr, uid, [('move_lines', 'in', [move.id])])
|
||||
for prod in production_obj.browse(cr, uid, production_ids, context=context):
|
||||
if prod.state == 'confirmed':
|
||||
production_obj.force_production(cr, uid, [prod.id])
|
||||
production_obj.signal_button_produce(cr, uid, production_ids)
|
||||
for new_move in new_moves:
|
||||
if new_move == move.id:
|
||||
#This move is already there in move lines of production order
|
||||
continue
|
||||
production_obj.write(cr, uid, production_ids, {'move_lines': [(4, new_move)]})
|
||||
res.append(new_move)
|
||||
for new_move in res:
|
||||
if new_move != move.id:
|
||||
#This move is not already there in move lines of production order
|
||||
production_obj.write(cr, uid, production_ids, {'move_lines': [(4, new_move)]})
|
||||
return res
|
||||
|
||||
def action_scrap(self, cr, uid, ids, product_qty, location_id, restrict_lot_id=False, restrict_partner_id=False, context=None):
|
||||
@@ -134,9 +160,9 @@ class StockMove(osv.osv):
|
||||
res = []
|
||||
production_obj = self.pool.get('mrp.production')
|
||||
for move in self.browse(cr, uid, ids, context=context):
|
||||
new_moves = super(StockMove, self).action_scrap(cr, uid, [move.id], product_qty, location_id,
|
||||
restrict_lot_id = restrict_lot_id,
|
||||
restrict_partner_id = restrict_partner_id, context=context)
|
||||
new_moves = super(StockMove, self).action_scrap(cr, uid, [move.id], product_qty, location_id,
|
||||
restrict_lot_id=restrict_lot_id,
|
||||
restrict_partner_id=restrict_partner_id, context=context)
|
||||
#If we are not scrapping our whole move, tracking and lot references must not be removed
|
||||
production_ids = production_obj.search(cr, uid, [('move_lines', 'in', [move.id])])
|
||||
for prod_id in production_ids:
|
||||
@@ -156,6 +182,7 @@ class StockMove(osv.osv):
|
||||
workflow.trg_trigger(uid, 'stock.move', move.id, cr)
|
||||
return res
|
||||
|
||||
|
||||
class StockPicking(osv.osv):
|
||||
_inherit = 'stock.picking'
|
||||
|
||||
@@ -171,21 +198,6 @@ class StockPicking(osv.osv):
|
||||
return list(set(todo))
|
||||
|
||||
|
||||
class split_in_production_lot(osv.osv_memory):
|
||||
_inherit = "stock.move.split"
|
||||
|
||||
def split(self, cr, uid, ids, move_ids, context=None):
|
||||
""" Splits move lines into given quantities.
|
||||
@param move_ids: Stock moves.
|
||||
@return: List of new moves.
|
||||
"""
|
||||
new_moves = super(split_in_production_lot, self).split(cr, uid, ids, move_ids, context=context)
|
||||
production_obj = self.pool.get('mrp.production')
|
||||
production_ids = production_obj.search(cr, uid, [('move_lines', 'in', move_ids)])
|
||||
production_obj.write(cr, uid, production_ids, {'move_lines': [(4, m) for m in new_moves]})
|
||||
return new_moves
|
||||
|
||||
|
||||
class stock_warehouse(osv.osv):
|
||||
_inherit = 'stock.warehouse'
|
||||
_columns = {
|
||||
|
||||
@@ -49,10 +49,12 @@
|
||||
!python {model: mrp.product.produce}: |
|
||||
context.update({'active_id': ref('mrp_production_servicetype_mo1')})
|
||||
-
|
||||
!record {model: mrp.product.produce, id: mrp_product_produce_1}:
|
||||
!record {model: mrp.product.produce, id: mrp_product_produce_1, view: mrp.view_mrp_product_produce_wizard}:
|
||||
mode: 'consume_produce'
|
||||
-
|
||||
!python {model: mrp.product.produce}: |
|
||||
lines = self.on_change_qty(cr, uid, [ref('mrp_product_produce_1')], 1.0, [], context=context)
|
||||
self.write(cr, uid, [ref('mrp_product_produce_1')], lines['value'], context=context)
|
||||
self.do_produce(cr, uid, [ref('mrp_product_produce_1')], context=context)
|
||||
-
|
||||
I check production order after produced.
|
||||
@@ -60,67 +62,3 @@
|
||||
!python {model: mrp.production}: |
|
||||
order = self.browse(cr, uid, ref("mrp_production_servicetype_mo1"))
|
||||
assert order.state == 'done', "Production order should be closed."
|
||||
-
|
||||
I create Bill of Materials with two service type products.
|
||||
-
|
||||
!record {model: mrp.bom, id: mrp_bom_test_2}:
|
||||
company_id: base.main_company
|
||||
name: PC Assemble SC234
|
||||
product_id: product.product_product_3
|
||||
product_qty: 1.0
|
||||
type: normal
|
||||
bom_lines:
|
||||
- company_id: base.main_company
|
||||
name: On Site Monitoring
|
||||
product_id: product.product_product_1
|
||||
product_qty: 1.0
|
||||
- company_id: base.main_company
|
||||
name: On Site Assistance
|
||||
product_id: product.product_product_2
|
||||
product_qty: 1.0
|
||||
-
|
||||
I make the production order using BoM having two service type products.
|
||||
-
|
||||
!record {model: mrp.production, id: mrp_production_servicetype_2}:
|
||||
product_id: product.product_product_3
|
||||
product_qty: 1.0
|
||||
bom_id: mrp_bom_test_2
|
||||
date_planned: !eval time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
-
|
||||
I compute the data of production order.
|
||||
-
|
||||
!python {model: mrp.production}: |
|
||||
self.action_compute(cr, uid, [ref("mrp_production_servicetype_2")], {"lang": "en_US", "tz": False, "search_default_Current": 1,
|
||||
"active_model": "ir.ui.menu", "active_ids": [ref("mrp.menu_mrp_production_action")],
|
||||
"active_id": ref("mrp.menu_mrp_production_action"), })
|
||||
-
|
||||
I confirm the production order.
|
||||
-
|
||||
!workflow {model: mrp.production, action: button_confirm, ref: mrp_production_servicetype_2}
|
||||
-
|
||||
Now I start production.
|
||||
-
|
||||
!workflow {model: mrp.production, action: button_produce, ref: mrp_production_servicetype_2}
|
||||
-
|
||||
I check that production order in production state after start production.
|
||||
-
|
||||
!python {model: mrp.production}: |
|
||||
order = self.browse(cr, uid, ref("mrp_production_servicetype_2"))
|
||||
#assert order.state == 'in_production', 'Production order should be in production State.'
|
||||
-
|
||||
I produce product.
|
||||
-
|
||||
!python {model: mrp.product.produce}: |
|
||||
context.update({'active_id': ref('mrp_production_servicetype_2')})
|
||||
-
|
||||
!record {model: mrp.product.produce, id: mrp_product_produce_2}:
|
||||
mode: 'consume_produce'
|
||||
-
|
||||
!python {model: mrp.product.produce}: |
|
||||
self.do_produce(cr, uid, [ref('mrp_product_produce_2')], context=context)
|
||||
-
|
||||
I check production order after produced.
|
||||
-
|
||||
!python {model: mrp.production}: |
|
||||
order = self.browse(cr, uid, ref("mrp_production_servicetype_2"))
|
||||
#assert order.state == 'done', "Production order should be closed."
|
||||
|
||||
@@ -154,6 +154,9 @@
|
||||
mode: 'consume_produce'
|
||||
-
|
||||
!python {model: mrp.product.produce}: |
|
||||
qty = self.browse(cr, uid, ref('mrp_product_produce1')).product_qty
|
||||
lines = self.on_change_qty(cr, uid, [ref('mrp_product_produce1')], qty, [], context=context)
|
||||
self.write(cr, uid, [ref('mrp_product_produce1')], lines['value'], context=context)
|
||||
self.do_produce(cr, uid, [ref('mrp_product_produce1')], context=context)
|
||||
-
|
||||
I check production order after produced.
|
||||
|
||||
@@ -23,6 +23,7 @@ import mrp_product_produce
|
||||
import mrp_price
|
||||
import mrp_workcenter_load
|
||||
import change_production_qty
|
||||
import stock_move
|
||||
#import mrp_change_standard_price
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
@@ -22,19 +22,56 @@
|
||||
from openerp.osv import fields, osv
|
||||
import openerp.addons.decimal_precision as dp
|
||||
|
||||
|
||||
class mrp_product_produce_line(osv.osv_memory):
|
||||
_name="mrp.product.produce.line"
|
||||
_description = "Product Produce Consume lines"
|
||||
|
||||
_columns = {
|
||||
'product_id': fields.many2one('product.product', 'Product'),
|
||||
'product_qty': fields.float('Quantity'),
|
||||
'lot_id': fields.many2one('stock.production.lot', 'Lot'),
|
||||
'produce_id': fields.many2one('mrp.product.produce'),
|
||||
'track_production': fields.related('product_id', 'track_production', type='boolean')
|
||||
}
|
||||
|
||||
class mrp_product_produce(osv.osv_memory):
|
||||
_name = "mrp.product.produce"
|
||||
_description = "Product Produce"
|
||||
|
||||
_columns = {
|
||||
'product_id': fields.many2one('product.product', type='many2one'),
|
||||
'product_qty': fields.float('Select Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
|
||||
'mode': fields.selection([('consume_produce', 'Consume & Produce'),
|
||||
('consume', 'Consume Only')], 'Mode', required=True,
|
||||
help="'Consume only' mode will only consume the products with the quantity selected.\n"
|
||||
"'Consume & Produce' mode will consume as well as produce the products with the quantity selected "
|
||||
"and it will finish the production order when total ordered quantities are produced."),
|
||||
'lot_id': fields.many2one('stock.production.lot', 'Lot'), #Should only be visible when it is consume and produce mode
|
||||
'consume_lines': fields.one2many('mrp.product.produce.line', 'produce_id', 'Products Consumed'),
|
||||
'track_production': fields.boolean('Track production'),
|
||||
}
|
||||
|
||||
def on_change_qty(self, cr, uid, ids, product_qty, consume_lines, context=None):
|
||||
"""
|
||||
When changing the quantity of products to be produced it will
|
||||
recalculate the number of raw materials needed according
|
||||
to the scheduled products and the already consumed/produced products
|
||||
It will return the consume lines needed for the products to be produced
|
||||
which the user can still adapt
|
||||
"""
|
||||
prod_obj = self.pool.get("mrp.production")
|
||||
production = prod_obj.browse(cr, uid, context['active_id'], context=context)
|
||||
consume_lines = []
|
||||
new_consume_lines = []
|
||||
if product_qty > 0.0:
|
||||
consume_lines = prod_obj._calculate_qty(cr, uid, production, product_qty=product_qty, context=context)
|
||||
|
||||
for consume in consume_lines:
|
||||
new_consume_lines.append([0, False, consume])
|
||||
return {'value': {'consume_lines': new_consume_lines}}
|
||||
|
||||
|
||||
def _get_product_qty(self, cr, uid, context=None):
|
||||
""" To obtain product quantity
|
||||
@param self: The object pointer.
|
||||
@@ -54,9 +91,26 @@ class mrp_product_produce(osv.osv_memory):
|
||||
done += move.product_qty
|
||||
return (prod.product_qty - done) or prod.product_qty
|
||||
|
||||
def _get_product_id(self, cr, uid, context=None):
|
||||
""" To obtain product id
|
||||
@return: id
|
||||
"""
|
||||
prod=False
|
||||
if context and context.get("active_id"):
|
||||
prod = self.pool.get('mrp.production').browse(cr, uid,
|
||||
context['active_id'], context=context)
|
||||
return prod and prod.product_id.id or False
|
||||
|
||||
def _get_track(self, cr, uid, context=None):
|
||||
prod = self._get_product_id(cr, uid, context=context)
|
||||
prod_obj = self.pool.get("product.product")
|
||||
return prod and prod_obj.browse(cr, uid, prod, context=context).track_production or False
|
||||
|
||||
_defaults = {
|
||||
'product_qty': _get_product_qty,
|
||||
'mode': lambda *x: 'consume_produce'
|
||||
'mode': lambda *x: 'consume_produce',
|
||||
'product_id': _get_product_id,
|
||||
'track_production': _get_track,
|
||||
}
|
||||
|
||||
def do_produce(self, cr, uid, ids, context=None):
|
||||
@@ -64,7 +118,7 @@ class mrp_product_produce(osv.osv_memory):
|
||||
assert production_id, "Production Id should be specified in context as a Active ID."
|
||||
data = self.browse(cr, uid, ids[0], context=context)
|
||||
self.pool.get('mrp.production').action_produce(cr, uid, production_id,
|
||||
data.product_qty, data.mode, context=context)
|
||||
data.product_qty, data.mode, data, context=context)
|
||||
return {}
|
||||
|
||||
|
||||
|
||||
@@ -12,8 +12,25 @@
|
||||
<form string="Produce" version="7.0">
|
||||
<group string="Produce">
|
||||
<field name="mode"/>
|
||||
<field name="product_qty" colspan="2"/>
|
||||
</group>
|
||||
<field name="product_qty" colspan="2" on_change="on_change_qty(product_qty, consume_lines, context)"/>
|
||||
<field name="product_id" invisible="1"/>
|
||||
<field name="track_production" invisible="1"/>
|
||||
<field name="lot_id" domain="[('product_id', '=', product_id)]"
|
||||
context="{'default_product_id':product_id}"
|
||||
attrs="{'required': [('track_production', '=', True), ('mode', '=', 'consume_produce')]}"
|
||||
groups="stock.group_tracking_lot"/>
|
||||
</group>
|
||||
<group string="To Consume">
|
||||
<field name="consume_lines">
|
||||
<tree string="Consume Lines" editable="top">
|
||||
<field name="product_id"/>
|
||||
<field name="product_qty"/>
|
||||
<field name="lot_id" domain="[('product_id', '=', product_id)]"
|
||||
context="{'default_product_id':product_id}"
|
||||
groups="stock.group_tracking_lot"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="do_produce" type="object" string="Confirm" class="oe_highlight"/>
|
||||
or
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from openerp.osv import fields, osv
|
||||
from openerp.tools.translate import _
|
||||
import openerp.addons.decimal_precision as dp
|
||||
|
||||
class stock_move_consume(osv.osv_memory):
|
||||
_name = "stock.move.consume"
|
||||
_description = "Consume Products"
|
||||
|
||||
_columns = {
|
||||
'product_id': fields.many2one('product.product', 'Product', required=True, select=True),
|
||||
'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
|
||||
'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
|
||||
'location_id': fields.many2one('stock.location', 'Location', required=True),
|
||||
'restrict_lot_id': fields.many2one('stock.production.lot', 'Lot'),
|
||||
}
|
||||
|
||||
#TOFIX: product_uom should not have different category of default UOM of product. Qty should be convert into UOM of original move line before going in consume and scrap
|
||||
def default_get(self, cr, uid, fields, context=None):
|
||||
""" Get default values
|
||||
@param self: The object pointer.
|
||||
@param cr: A database cursor
|
||||
@param uid: ID of the user currently logged in
|
||||
@param fields: List of fields for default value
|
||||
@param context: A standard dictionary
|
||||
@return: default values of fields
|
||||
"""
|
||||
if context is None:
|
||||
context = {}
|
||||
res = super(stock_move_consume, self).default_get(cr, uid, fields, context=context)
|
||||
move = self.pool.get('stock.move').browse(cr, uid, context['active_id'], context=context)
|
||||
if 'product_id' in fields:
|
||||
res.update({'product_id': move.product_id.id})
|
||||
if 'product_uom' in fields:
|
||||
res.update({'product_uom': move.product_uom.id})
|
||||
if 'product_qty' in fields:
|
||||
res.update({'product_qty': move.product_qty})
|
||||
if 'location_id' in fields:
|
||||
res.update({'location_id': move.location_id.id})
|
||||
return res
|
||||
|
||||
|
||||
|
||||
def do_move_consume(self, cr, uid, ids, context=None):
|
||||
""" To move consumed products
|
||||
@param self: The object pointer.
|
||||
@param cr: A database cursor
|
||||
@param uid: ID of the user currently logged in
|
||||
@param ids: the ID or list of IDs if we want more than one
|
||||
@param context: A standard dictionary
|
||||
@return:
|
||||
"""
|
||||
if context is None:
|
||||
context = {}
|
||||
move_obj = self.pool.get('stock.move')
|
||||
move_ids = context['active_ids']
|
||||
for data in self.browse(cr, uid, ids, context=context):
|
||||
move_obj.action_consume(cr, uid, move_ids,
|
||||
data.product_qty, data.location_id.id, restrict_lot_id=data.restrict_lot_id.id,
|
||||
context=context)
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<!-- Consume, scrap move -->
|
||||
|
||||
<record id="view_stock_move_consume_wizard" model="ir.ui.view">
|
||||
<field name="name">Consume Move</field>
|
||||
<field name="model">stock.move.consume</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Consume Move" version="7.0">
|
||||
<group string="Consume Products">
|
||||
<field name="product_id" readonly="1"/>
|
||||
<label for="product_qty"/>
|
||||
<div>
|
||||
<field name="product_qty" class="oe_inline"/>
|
||||
<field name="product_uom" class="oe_inline" readonly="1" groups="product.group_uom"/>
|
||||
</div>
|
||||
<field name="restrict_lot_id" domain="[('product_id','=',product_id)]" groups="stock.group_tracking_lot"
|
||||
context="{'default_product_id': product_id}"/>
|
||||
<field name="location_id" groups="stock.group_locations"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="do_move_consume" string="Ok" type="object" class="oe_highlight"/>
|
||||
or
|
||||
<button string="Cancel" class="oe_link" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="move_consume" model="ir.actions.act_window">
|
||||
<field name="name">Consume Move</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">stock.move.consume</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -64,6 +64,11 @@
|
||||
I finish the production order.
|
||||
-
|
||||
!python {model: mrp.product.produce}: |
|
||||
qty = self.browse(cr, uid, ref('mrp_product_produce0')).product_qty
|
||||
ctx = context.copy()
|
||||
ctx['active_id'] = ref("mrp_production_mo0")
|
||||
lines = self.on_change_qty(cr, uid, [ref('mrp_product_produce0')], qty, [], context=ctx)
|
||||
self.write(cr, uid, [ref('mrp_product_produce0')], lines['value'], context=context)
|
||||
self.do_produce(cr, uid, [ref("mrp_product_produce0")], {"active_model": "mrp.production", "active_ids":[ref("mrp_production_mo0")], "active_id": ref("mrp_production_mo0")})
|
||||
-
|
||||
I see that stock moves of External Hard Disk including Headset USB are done now.
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
return_picking_id, dummy = self._create_returns(cr, uid, [return_id], context={'active_id': pick_ids[0].id})
|
||||
self.pool.get('stock.picking').do_transfer(cr, uid, [return_picking_id])
|
||||
-
|
||||
Check the standard price of the product changed to 70.0 (as returns to supplier are processed as normal outgoing shipment we have sent 10kg at 50€ and 20kg at 80€, because of FIFO, so the average to store on the product is 70€)
|
||||
Check the standard price of the product changed to 80.0 as we returned the quants of purchase order 2
|
||||
-
|
||||
!python {model: product.product}: |
|
||||
assert self.browse(cr, uid, ref("product_fiforet_icecream")).standard_price == 70.0, 'Standard price should have changed to 70.0! %s found instead' % (self.browse(cr, uid, ref("product_fiforet_icecream")).standard_price,)
|
||||
assert self.browse(cr, uid, ref("product_fiforet_icecream")).standard_price == 80.0, 'Standard price should have changed to 80.0! %s found instead' % (self.browse(cr, uid, ref("product_fiforet_icecream")).standard_price,)
|
||||
|
||||
@@ -74,9 +74,7 @@ Dashboard / Reports for Warehouse Management will include:
|
||||
'stock_data.yml',
|
||||
'wizard/stock_move_view.xml',
|
||||
'wizard/stock_change_product_qty_view.xml',
|
||||
'wizard/stock_inventory_merge_view.xml',
|
||||
'wizard/stock_location_product_view.xml',
|
||||
'wizard/stock_inventory_line_split_view.xml',
|
||||
'wizard/stock_return_picking_view.xml',
|
||||
'wizard/make_procurement_view.xml',
|
||||
'wizard/mrp_procurement_view.xml',
|
||||
|
||||
@@ -167,7 +167,7 @@
|
||||
</tr>
|
||||
</blockTable>
|
||||
<section>
|
||||
<para style="terp_default_2">[[ repeatIn(o.inventory_line_id, 'p') ]]</para>
|
||||
<para style="terp_default_2">[[ repeatIn(o.line_ids, 'p') ]]</para>
|
||||
<blockTable colWidths="58.0,87.0,212.0,91.0,91.0" style="Table8">
|
||||
<tr>
|
||||
<td>
|
||||
@@ -201,7 +201,7 @@
|
||||
<para style="terp_default_Bold_9">Total:</para>
|
||||
</td>
|
||||
<td>
|
||||
<para style="terp_default_Bold_Right_9">[[ formatLang(qty_total(o.inventory_line_id)['quantity']) ]] [[ qty_total(o.inventory_line_id)['uom'] ]]</para>
|
||||
<para style="terp_default_Bold_Right_9">[[ formatLang(qty_total(o.line_ids)['quantity']) ]] [[ qty_total(o.line_ids)['uom'] ]]</para>
|
||||
</td>
|
||||
<td>
|
||||
<para style="terp_default_Bold_Right_9">
|
||||
|
||||
@@ -32,7 +32,7 @@ class stock_inventory_move(report_sxw.rml_parse):
|
||||
|
||||
def _qty_total(self, objects):
|
||||
total = 0.0
|
||||
uom = objects[0].product_uom.name
|
||||
uom = objects[0].product_uom_id.name
|
||||
for obj in objects:
|
||||
total += obj.product_qty
|
||||
return {'quantity':total,'uom':uom}
|
||||
|
||||
+35
-69
@@ -26,7 +26,6 @@ import time
|
||||
|
||||
from openerp.osv import fields, osv
|
||||
from openerp.tools.translate import _
|
||||
from openerp import tools
|
||||
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_DATE_FORMAT
|
||||
from openerp import SUPERUSER_ID
|
||||
import openerp.addons.decimal_precision as dp
|
||||
@@ -293,13 +292,15 @@ class stock_quant(osv.osv):
|
||||
'''This function reserves quants for the given move (and optionally given link). If the total of quantity reserved is enough, the move's state
|
||||
is also set to 'assigned'
|
||||
|
||||
:param quants: list of tuple(quant browse record or None, qty to reserve). If None is given as first tuple element, the item will be ignored
|
||||
:param quants: list of tuple(quant browse record or None, qty to reserve). If None is given as first tuple element, the item will be ignored. Negative quants should not be received as argument
|
||||
:param move: browse record
|
||||
:param link: browse record (stock.move.operation.link)
|
||||
'''
|
||||
toreserve = []
|
||||
#split quants if needed
|
||||
for quant, qty in quants:
|
||||
if qty <= 0.0 or (quant and quant.qty <= 0.0):
|
||||
raise osv.except_osv(_('Error!'), _('You can not reserve a negative quantity or a negative quant.'))
|
||||
if not quant:
|
||||
continue
|
||||
self._quant_split(cr, uid, quant, qty, context=context)
|
||||
@@ -547,6 +548,9 @@ class stock_quant(osv.osv):
|
||||
'''
|
||||
domain += location and [('location_id', 'child_of', location.id)] or []
|
||||
domain += [('product_id', '=', product.id)] + domain
|
||||
#don't take into account location that are production, supplier or inventory
|
||||
ignore_location_ids = self.pool.get('stock.location').search(cr, uid, [('usage', 'in', ('production', 'supplier', 'inventory'))], context=context)
|
||||
domain.append(('location_id','not in',ignore_location_ids))
|
||||
res = []
|
||||
offset = 0
|
||||
while quantity > 0:
|
||||
@@ -735,9 +739,9 @@ class stock_picking(osv.osv):
|
||||
* Transferred: has been processed, can't be modified or cancelled anymore\n
|
||||
* Cancelled: has been cancelled, can't be confirmed anymore"""
|
||||
),
|
||||
'priority': fields.selection([('0', 'Low'), ('1', 'Normal'), ('2', 'High')], string='Priority', required=True),
|
||||
'priority': fields.selection([('0', 'Low'), ('1', 'Normal'), ('2', 'High')], states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, string='Priority', required=True),
|
||||
'min_date': fields.function(get_min_max_date, multi="min_max_date", fnct_inv=_set_min_date,
|
||||
store={'stock.move': (_get_pickings, ['state', 'date_expected'], 20)}, type='datetime', string='Scheduled Date', select=1, help="Scheduled time for the first part of the shipment to be processed. Setting manually a value here would set it as expected date for all the stock moves.", track_visibility='onchange'),
|
||||
store={'stock.move': (_get_pickings, ['state', 'date_expected'], 20)}, type='datetime', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, string='Scheduled Date', select=1, help="Scheduled time for the first part of the shipment to be processed. Setting manually a value here would set it as expected date for all the stock moves.", track_visibility='onchange'),
|
||||
'max_date': fields.function(get_min_max_date, multi="min_max_date",
|
||||
store={'stock.move': (_get_pickings, ['state', 'date_expected'], 20)}, type='datetime', string='Max. Expected Date', select=2, help="Scheduled time for the last part of the shipment to be processed"),
|
||||
'date': fields.datetime('Commitment Date', help="Date promised for the completion of the transfer order, usually set the time of the order and revised later on.", select=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, track_visibility='onchange'),
|
||||
@@ -746,12 +750,12 @@ class stock_picking(osv.osv):
|
||||
'quant_reserved_exist': fields.function(_get_quant_reserved_exist, type='boolean', string='Quant already reserved ?', help='technical field used to know if there is already at least one quant reserved on moves of a given picking'),
|
||||
'partner_id': fields.many2one('res.partner', 'Partner', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}),
|
||||
'company_id': fields.many2one('res.company', 'Company', required=True, select=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}),
|
||||
'pack_operation_ids': fields.one2many('stock.pack.operation', 'picking_id', string='Related Packing Operations'),
|
||||
'pack_operation_ids': fields.one2many('stock.pack.operation', 'picking_id', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, string='Related Packing Operations'),
|
||||
'pack_operation_exist': fields.function(_get_pack_operation_exist, type='boolean', string='Pack Operation Exists?', help='technical field for attrs in view'),
|
||||
'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type', required=True),
|
||||
'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, required=True),
|
||||
'picking_type_code': fields.related('picking_type_id', 'code', type='char', string='Picking Type Code', help="Technical field used to display the correct label on print button in the picking view"),
|
||||
|
||||
'owner_id': fields.many2one('res.partner', 'Owner', help="Default Owner"),
|
||||
'owner_id': fields.many2one('res.partner', 'Owner', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="Default Owner"),
|
||||
# Used to search on pickings
|
||||
'product_id': fields.related('move_lines', 'product_id', type='many2one', relation='product.product', string='Product'),
|
||||
'location_id': fields.related('move_lines', 'location_id', type='many2one', relation='stock.location', string='Location', readonly=True),
|
||||
@@ -792,8 +796,8 @@ class stock_picking(osv.osv):
|
||||
'''This function prints the delivery order'''
|
||||
assert len(ids) == 1, 'This option should only be used for a single id at a time'
|
||||
datas = {
|
||||
'model': 'stock.picking',
|
||||
'ids': ids,
|
||||
'model': 'stock.picking',
|
||||
'ids': ids,
|
||||
}
|
||||
return {'type': 'ir.actions.report.xml', 'report_name': 'stock.picking.list', 'datas': datas, 'nodestroy': True}
|
||||
|
||||
@@ -801,8 +805,8 @@ class stock_picking(osv.osv):
|
||||
'''This function prints the picking list'''
|
||||
assert len(ids) == 1, 'This option should only be used for a single id at a time'
|
||||
datas = {
|
||||
'model': 'stock.picking',
|
||||
'ids': ids,
|
||||
'model': 'stock.picking',
|
||||
'ids': ids,
|
||||
}
|
||||
return {'type': 'ir.actions.report.xml', 'report_name': 'stock.picking.list.internal', 'datas': datas, 'nodestroy': True}
|
||||
|
||||
@@ -849,15 +853,6 @@ class stock_picking(osv.osv):
|
||||
self.do_prepare_partial(cr, uid, [pick.id], context=None)
|
||||
return True
|
||||
|
||||
def cancel_assign(self, cr, uid, ids, context=None):
|
||||
""" Cancels picking for the moves that are in the assigned state
|
||||
@return: True
|
||||
"""
|
||||
for pick in self.browse(cr, uid, ids, context=context):
|
||||
move_ids = [x.id for x in pick.move_lines if x not in ['done', 'cancel']]
|
||||
self.pool.get('stock.move').cancel_assign(cr, uid, move_ids, context=context)
|
||||
return True
|
||||
|
||||
def action_cancel(self, cr, uid, ids, context=None):
|
||||
for pick in self.browse(cr, uid, ids, context=context):
|
||||
ids2 = [move.id for move in pick.move_lines]
|
||||
@@ -1019,11 +1014,11 @@ class stock_picking(osv.osv):
|
||||
moves_to_unreserve = []
|
||||
pack_line_to_unreserve = []
|
||||
for picking in self.browse(cr, uid, picking_ids, context=context):
|
||||
moves_to_unreserve += [m.id for m in picking.move_lines]
|
||||
moves_to_unreserve += [m.id for m in picking.move_lines if m.state not in ('done', 'cancel')]
|
||||
pack_line_to_unreserve += [p.id for p in picking.pack_operation_ids]
|
||||
if pack_line_to_unreserve:
|
||||
self.pool.get('stock.pack.operation').unlink(cr, uid, pack_line_to_unreserve, context=context)
|
||||
if moves_to_unreserve:
|
||||
if pack_line_to_unreserve:
|
||||
self.pool.get('stock.pack.operation').unlink(cr, uid, pack_line_to_unreserve, context=context)
|
||||
self.pool.get('stock.move').do_unreserve(cr, uid, moves_to_unreserve, context=context)
|
||||
|
||||
def do_recompute_remaining_quantities(self, cr, uid, picking_ids, context=None):
|
||||
@@ -1228,14 +1223,13 @@ class stock_production_lot(osv.osv):
|
||||
@return: A dictionary of values
|
||||
"""
|
||||
quant_obj = self.pool.get("stock.quant")
|
||||
move_obj = self.pool.get("stock.move")
|
||||
quants = quant_obj.search(cr, uid, [('lot_id', 'in', ids)], context=context)
|
||||
moves = set()
|
||||
for quant in quant_obj.browse(cr, uid, quants, context=context):
|
||||
moves |= {move.id for move in quant.history_ids}
|
||||
if moves:
|
||||
return {
|
||||
'domain': "[('id','in',["+','.join(map(str, list(moves)))+"])]",
|
||||
return {
|
||||
'domain': "[('id','in',[" + ','.join(map(str, list(moves))) + "])]",
|
||||
'name': _('Traceability'),
|
||||
'view_mode': 'tree,form',
|
||||
'view_type': 'form',
|
||||
@@ -1244,7 +1238,6 @@ class stock_production_lot(osv.osv):
|
||||
'type': 'ir.actions.act_window',
|
||||
}
|
||||
return False
|
||||
|
||||
|
||||
|
||||
# ----------------------------------------------------
|
||||
@@ -1420,6 +1413,7 @@ class stock_move(osv.osv):
|
||||
'price_unit': fields.float('Unit Price', help="Technical field used to record the product cost set by the user during a picking confirmation (when costing method used is 'average price' or 'real'). Value given in company currency and in product uom."), # as it's a technical field, we intentionally don't provide the digits attribute
|
||||
|
||||
'company_id': fields.many2one('res.company', 'Company', required=True, select=True),
|
||||
'split_from': fields.many2one('stock.move', string="Move Split From", help="Technical field used to track the origin of a split move, which can be useful in case of debug"),
|
||||
'backorder_id': fields.related('picking_id', 'backorder_id', type='many2one', relation="stock.picking", string="Back Order of", select=True),
|
||||
'origin': fields.char("Source"),
|
||||
'procure_method': fields.selection([('make_to_stock', 'Make to Stock'), ('make_to_order', 'Make to Order')], 'Procurement Method', required=True, help="Make to Stock: When needed, the product is taken from the stock or we wait for replenishment. \nMake to Order: When needed, the product is purchased or produced."),
|
||||
@@ -1506,13 +1500,16 @@ class stock_move(osv.osv):
|
||||
default['reserved_quant_ids'] = []
|
||||
default['returned_move_ids'] = []
|
||||
default['linked_move_operation_ids'] = []
|
||||
default['origin_returned_move_id'] = False
|
||||
if not default.get('origin_returned_move_id'):
|
||||
default['origin_returned_move_id'] = False
|
||||
default['state'] = 'draft'
|
||||
return super(stock_move, self).copy_data(cr, uid, id, default, context)
|
||||
|
||||
def do_unreserve(self, cr, uid, move_ids, context=None):
|
||||
quant_obj = self.pool.get("stock.quant")
|
||||
for move in self.browse(cr, uid, move_ids, context=context):
|
||||
if move.state in ('done', 'cancel'):
|
||||
raise osv.except_osv(_('Operation Forbidden!'), _('Cannot unreserve a done move'))
|
||||
quant_obj.quants_unreserve(cr, uid, move, context=context)
|
||||
putaway_values = []
|
||||
for putaway_rec in move.putaway_ids:
|
||||
@@ -1547,7 +1544,10 @@ class stock_move(osv.osv):
|
||||
def _push_apply(self, cr, uid, moves, context=None):
|
||||
push_obj = self.pool.get("stock.location.path")
|
||||
for move in moves:
|
||||
if not move.move_dest_id:
|
||||
#1) if the move is already chained, there is no need to check push rules
|
||||
#2) if the move is a returned move, we don't want to check push rules, as returning a returned move is the only decent way
|
||||
# to receive goods without triggering the push rules again (which would duplicate chained operations)
|
||||
if not move.move_dest_id and not move.origin_returned_move_id:
|
||||
domain = [('location_from_id', '=', move.location_dest_id.id)]
|
||||
if move.warehouse_id:
|
||||
domain += ['|', ('warehouse_id', '=', move.warehouse_id.id), ('warehouse_id', '=', False)]
|
||||
@@ -1825,12 +1825,6 @@ class stock_move(osv.osv):
|
||||
self._putaway_check(cr, uid, ids, context=context)
|
||||
return self.write(cr, uid, ids, {'state': 'assigned'})
|
||||
|
||||
def cancel_assign(self, cr, uid, ids, context=None):
|
||||
""" Changes the state to confirmed.
|
||||
@return: True
|
||||
"""
|
||||
return self.write(cr, uid, ids, {'state': 'confirmed'})
|
||||
|
||||
def check_tracking(self, cr, uid, move, lot_id, context=None):
|
||||
""" Checks if serial number is assigned to stock move or not and raise an error if it had to.
|
||||
"""
|
||||
@@ -1866,6 +1860,9 @@ class stock_move(osv.osv):
|
||||
fallback_domain = prev_quant_ids and [('id', 'not in', prev_quant_ids)] or []
|
||||
#we always keep the quants already assigned and try to find the remaining quantity on quants not assigned only
|
||||
main_domain = [('reservation_id', '=', False), ('qty', '>', 0)]
|
||||
#if the move is returned from another, restrict the choice of quants to the ones that follow the returned move
|
||||
if move.origin_returned_move_id:
|
||||
main_domain += [('history_ids', 'in', move.origin_returned_move_id.id)]
|
||||
#first try to find quants based on specific domains given by linked operations
|
||||
for record in move.linked_move_operation_ids:
|
||||
domain = main_domain + self.pool.get('stock.move.operation.link').get_specific_domain(cr, uid, record, context=context)
|
||||
@@ -2070,39 +2067,6 @@ class stock_move(osv.osv):
|
||||
self.action_done(cr, uid, res, context=context)
|
||||
return res
|
||||
|
||||
def action_consume(self, cr, uid, ids, quantity, location_id=False, restrict_lot_id=False, restrict_partner_id=False, context=None):
|
||||
""" Consumed product with specific quantity from specific source location. This correspond to a split of the move (or write if the quantity to consume is >= than the quantity of the move) followed by an action_done
|
||||
@param ids: ids of stock move object to be consumed
|
||||
@param quantity : specify consume quantity (given in move UoM)
|
||||
@param location_id : specify source location
|
||||
@return: Consumed lines
|
||||
"""
|
||||
uom_obj = self.pool.get('product.uom')
|
||||
if context is None:
|
||||
context = {}
|
||||
if quantity <= 0:
|
||||
raise osv.except_osv(_('Warning!'), _('Please provide proper quantity.'))
|
||||
res = []
|
||||
for move in self.browse(cr, uid, ids, context=context):
|
||||
move_qty = move.product_qty
|
||||
uom_qty = uom_obj._compute_qty(cr, uid, move.product_id.uom_id.id, quantity, move.product_uom.id)
|
||||
if move_qty <= 0:
|
||||
raise osv.except_osv(_('Error!'), _('Cannot consume a move with negative or zero quantity.'))
|
||||
quantity_rest = move.product_qty - uom_qty
|
||||
if quantity_rest > 0:
|
||||
ctx = context.copy()
|
||||
if location_id:
|
||||
ctx['source_location_id'] = location_id
|
||||
res.append(self.split(cr, uid, move, move_qty - quantity_rest, restrict_lot_id=restrict_lot_id,
|
||||
restrict_partner_id=restrict_partner_id, context=ctx))
|
||||
else:
|
||||
res.append(move.id)
|
||||
if location_id:
|
||||
self.write(cr, uid, [move.id], {'location_id': location_id, 'restrict_lot_id': restrict_lot_id,
|
||||
'restrict_partner_id': restrict_partner_id}, context=context)
|
||||
|
||||
self.action_done(cr, uid, res, context=context)
|
||||
return res
|
||||
|
||||
def split(self, cr, uid, move, qty, restrict_lot_id=False, restrict_partner_id=False, context=None):
|
||||
""" Splits qty from move move into a new move
|
||||
@@ -2130,7 +2094,8 @@ class stock_move(osv.osv):
|
||||
'move_dest_id': False,
|
||||
'reserved_quant_ids': [],
|
||||
'restrict_lot_id': restrict_lot_id,
|
||||
'restrict_partner_id': restrict_partner_id
|
||||
'restrict_partner_id': restrict_partner_id,
|
||||
'split_from': move.id,
|
||||
}
|
||||
if context.get('source_location_id'):
|
||||
defaults['location_id'] = context['source_location_id']
|
||||
@@ -2151,6 +2116,7 @@ class stock_move(osv.osv):
|
||||
self.action_confirm(cr, uid, [new_move], context=context)
|
||||
return new_move
|
||||
|
||||
|
||||
class stock_inventory(osv.osv):
|
||||
_name = "stock.inventory"
|
||||
_description = "Inventory"
|
||||
|
||||
@@ -859,6 +859,7 @@
|
||||
<field name="partner_id" filter_domain="[('partner_id','child_of',self)]"/>
|
||||
<field name="product_id"/>
|
||||
<field name="picking_type_id"/>
|
||||
<field name="group_id"/>
|
||||
<group expand="0" string="Group By...">
|
||||
<filter string="Status" icon="terp-stock_effects-object-colorize" domain="[]" context="{'group_by':'state'}"/>
|
||||
<filter string="Order Date" icon="terp-go-month" domain="[]" context="{'group_by':'date'}"/>
|
||||
@@ -1797,6 +1798,7 @@
|
||||
<field name="reservation_id"/>
|
||||
<field name="propagated_from_id"/>
|
||||
<field name="owner_id" groups="stock.group_tracking_owner"/>
|
||||
<field name="inventory_value"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
##############################################################################
|
||||
|
||||
import stock_move
|
||||
import stock_inventory_merge
|
||||
import stock_inventory_line_split
|
||||
import stock_location_product
|
||||
import stock_return_picking
|
||||
import stock_change_product_qty
|
||||
|
||||
@@ -74,15 +74,15 @@ class stock_change_product_qty(osv.osv_memory):
|
||||
rec_id = context and context.get('active_id', False)
|
||||
assert rec_id, _('Active ID is not set in Context')
|
||||
|
||||
inventry_obj = self.pool.get('stock.inventory')
|
||||
inventry_line_obj = self.pool.get('stock.inventory.line')
|
||||
inventory_obj = self.pool.get('stock.inventory')
|
||||
inventory_line_obj = self.pool.get('stock.inventory.line')
|
||||
prod_obj_pool = self.pool.get('product.product')
|
||||
|
||||
res_original = prod_obj_pool.browse(cr, uid, rec_id, context=context)
|
||||
for data in self.browse(cr, uid, ids, context=context):
|
||||
if data.new_quantity < 0:
|
||||
raise osv.except_osv(_('Warning!'), _('Quantity cannot be negative.'))
|
||||
inventory_id = inventry_obj.create(cr , uid, {'name': _('INV: %s') % tools.ustr(res_original.name)}, context=context)
|
||||
inventory_id = inventory_obj.create(cr , uid, {'name': _('INV: %s') % tools.ustr(res_original.name), 'product_id': rec_id, 'location_id': data.location_id.id, 'lot_id': data.lot_id.id}, context=context)
|
||||
line_data ={
|
||||
'inventory_id' : inventory_id,
|
||||
'product_qty' : data.new_quantity,
|
||||
@@ -91,8 +91,8 @@ class stock_change_product_qty(osv.osv_memory):
|
||||
'product_uom_id' : res_original.uom_id.id,
|
||||
'prod_lot_id' : data.lot_id.id
|
||||
}
|
||||
inventry_line_obj.create(cr , uid, line_data, context=context)
|
||||
inventry_obj.action_done(cr, uid, [inventory_id], context=context)
|
||||
inventory_line_obj.create(cr , uid, line_data, context=context)
|
||||
inventory_obj.action_done(cr, uid, [inventory_id], context=context)
|
||||
return {}
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from openerp.osv import fields, osv
|
||||
|
||||
class stock_inventory_line_split(osv.osv_memory):
|
||||
_inherit = "stock.move.split"
|
||||
_name = "stock.inventory.line.split"
|
||||
_description = "Split inventory lines"
|
||||
|
||||
_columns = {
|
||||
'line_ids': fields.one2many('stock.inventory.line.split.lines', 'wizard_id', 'Serial Numbers'),
|
||||
'line_exist_ids': fields.one2many('stock.inventory.line.split.lines', 'wizard_exist_id', 'Serial Numbers'),
|
||||
}
|
||||
|
||||
def default_get(self, cr, uid, fields, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
record_id = context and context.get('active_id',False)
|
||||
res = {}
|
||||
line = self.pool.get('stock.inventory.line').browse(cr, uid, record_id, context=context)
|
||||
if 'product_id' in fields:
|
||||
res.update({'product_id':line.product_id.id})
|
||||
if 'product_uom' in fields:
|
||||
res.update({'product_uom': line.product_uom_id.id})
|
||||
if 'qty' in fields:
|
||||
res.update({'qty': line.product_qty})
|
||||
return res
|
||||
|
||||
def split(self, cr, uid, ids, line_ids, context=None):
|
||||
""" To split stock inventory lines according to serial numbers.
|
||||
|
||||
:param line_ids: the ID or list of IDs of inventory lines we want to split
|
||||
"""
|
||||
if context is None:
|
||||
context = {}
|
||||
assert context.get('active_model') == 'stock.inventory.line',\
|
||||
'Incorrect use of the inventory line split wizard.'
|
||||
prodlot_obj = self.pool.get('stock.production.lot')
|
||||
ir_sequence_obj = self.pool.get('ir.sequence')
|
||||
line_obj = self.pool.get('stock.inventory.line')
|
||||
new_line = []
|
||||
for data in self.browse(cr, uid, ids, context=context):
|
||||
for inv_line in line_obj.browse(cr, uid, line_ids, context=context):
|
||||
line_qty = inv_line.product_qty
|
||||
quantity_rest = inv_line.product_qty
|
||||
new_line = []
|
||||
if data.use_exist:
|
||||
lines = [l for l in data.line_exist_ids if l]
|
||||
else:
|
||||
lines = [l for l in data.line_ids if l]
|
||||
for line in lines:
|
||||
quantity = line.quantity
|
||||
if quantity <= 0 or line_qty == 0:
|
||||
continue
|
||||
quantity_rest -= quantity
|
||||
if quantity_rest < 0:
|
||||
quantity_rest = quantity
|
||||
break
|
||||
default_val = {
|
||||
'product_qty': quantity,
|
||||
}
|
||||
if quantity_rest > 0:
|
||||
current_line = line_obj.copy(cr, uid, inv_line.id, default_val)
|
||||
new_line.append(current_line)
|
||||
if quantity_rest == 0:
|
||||
current_line = inv_line.id
|
||||
lot_id = False
|
||||
if data.use_exist:
|
||||
lot_id = line.lot_id.id
|
||||
if not lot_id:
|
||||
lot_id = prodlot_obj.create(cr, uid, {
|
||||
'name': line.name,
|
||||
'product_id': inv_line.product_id.id},
|
||||
context=context)
|
||||
line_obj.write(cr, uid, [current_line], {'prod_lot_id': lot_id})
|
||||
prodlot = prodlot_obj.browse(cr, uid, lot_id)
|
||||
|
||||
update_val = {}
|
||||
if quantity_rest > 0:
|
||||
update_val['product_qty'] = quantity_rest
|
||||
line_obj.write(cr, uid, [inv_line.id], update_val)
|
||||
|
||||
return new_line
|
||||
|
||||
class stock_inventory_split_lines(osv.osv_memory):
|
||||
_inherit = "stock.move.split.lines"
|
||||
_name = "stock.inventory.line.split.lines"
|
||||
_description = "Inventory Split lines"
|
||||
_columns = {
|
||||
'wizard_id': fields.many2one('stock.inventory.line.split', 'Parent Wizard'),
|
||||
'wizard_exist_id': fields.many2one('stock.inventory.line.split', 'Parent Wizard'),
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<record id="view_split_in_lots_inherit" model="ir.ui.view">
|
||||
<field name="name">Split Inventory Line</field>
|
||||
<field name="model">stock.inventory.line.split</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Split in Serial Numbers" version="7.0">
|
||||
<group>
|
||||
<field name="product_id" colspan="4" readonly="1"/>
|
||||
<label for="qty"/>
|
||||
<div>
|
||||
<field name="qty" readonly="1"/>
|
||||
<field name="product_uom" readonly="1"/>
|
||||
</div>
|
||||
<field name="use_exist"/>
|
||||
</group>
|
||||
<field name="line_ids" attrs="{'invisible':[('use_exist','=',True)]}">
|
||||
<tree string="Lots Number" editable="bottom">
|
||||
<field name="name" string="Lots"/>
|
||||
<field name="quantity" />
|
||||
</tree>
|
||||
<form string="Lots Number" version="7.0">
|
||||
<group>
|
||||
<field name="name" string="Lots"/>
|
||||
<field name="quantity" />
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
<field name="line_exist_ids" attrs="{'invisible':[('use_exist','!=',True)]}">
|
||||
<tree string="Lots Number" editable="bottom">
|
||||
<field name="lot_id" string="Lots" domain="[('product_id','=',parent.product_id)]"/>
|
||||
<field name="quantity" />
|
||||
</tree>
|
||||
<form string="Lots Number" version="7.0">
|
||||
<group>
|
||||
<field name="lot_id" string="Lots" domain="[('product_id','=',parent.product_id)]"/>
|
||||
<field name="quantity" />
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
<footer>
|
||||
<button name="split_lot" string="Ok" type="object" class="oe_highlight"/>
|
||||
or
|
||||
<button string="Cancel" class="oe_link" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_view_stock_inventory_line_split" model="ir.actions.act_window">
|
||||
<field name="name">Split inventory lines</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">stock.inventory.line.split</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -1,91 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from openerp.osv import fields, osv
|
||||
from openerp.tools.translate import _
|
||||
|
||||
class stock_inventory_merge(osv.osv_memory):
|
||||
_name = "stock.inventory.merge"
|
||||
_description = "Merge Inventory"
|
||||
|
||||
def fields_view_get(self, cr, uid, view_id=None, view_type='form',
|
||||
context=None, toolbar=False, submenu=False):
|
||||
"""
|
||||
Changes the view dynamically
|
||||
@param self: The object pointer.
|
||||
@param cr: A database cursor
|
||||
@param uid: ID of the user currently logged in
|
||||
@param context: A standard dictionary
|
||||
@return: New arch of view.
|
||||
"""
|
||||
if context is None:
|
||||
context={}
|
||||
res = super(stock_inventory_merge, self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar,submenu=False)
|
||||
if context.get('active_model','') == 'stock.inventory' and len(context['active_ids']) < 2:
|
||||
raise osv.except_osv(_('Warning!'),
|
||||
_('Please select multiple physical inventories to merge in the list view.'))
|
||||
return res
|
||||
|
||||
def do_merge(self, cr, uid, ids, context=None):
|
||||
""" To merge selected Inventories.
|
||||
@param self: The object pointer.
|
||||
@param cr: A database cursor
|
||||
@param uid: ID of the user currently logged in
|
||||
@param ids: List of IDs selected
|
||||
@param context: A standard dictionary
|
||||
@return:
|
||||
"""
|
||||
invent_obj = self.pool.get('stock.inventory')
|
||||
invent_line_obj = self.pool.get('stock.inventory.line')
|
||||
invent_lines = {}
|
||||
if context is None:
|
||||
context = {}
|
||||
for inventory in invent_obj.browse(cr, uid, context['active_ids'], context=context):
|
||||
if inventory.state == "done":
|
||||
raise osv.except_osv(_('Warning!'),
|
||||
_('Merging is only allowed on draft inventories.'))
|
||||
|
||||
for line in inventory.inventory_line_id:
|
||||
key = (line.location_id.id, line.product_id.id, line.product_uom_id.id)
|
||||
if key in invent_lines:
|
||||
invent_lines[key] += line.product_qty
|
||||
else:
|
||||
invent_lines[key] = line.product_qty
|
||||
|
||||
|
||||
new_invent = invent_obj.create(cr, uid, {
|
||||
'name': 'Merged inventory'
|
||||
}, context=context)
|
||||
|
||||
for key, quantity in invent_lines.items():
|
||||
invent_line_obj.create(cr, uid, {
|
||||
'inventory_id': new_invent,
|
||||
'location_id': key[0],
|
||||
'product_id': key[1],
|
||||
'product_uom_id': key[2],
|
||||
'product_qty': quantity,
|
||||
})
|
||||
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record id="name_form" model="ir.ui.view">
|
||||
<field name="name">stock.inventory.merge.form</field>
|
||||
<field name="model">stock.inventory.merge</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Merge Inventories" version="7.0">
|
||||
<separator string="Merge Inventories" />
|
||||
<label string="Do you want to merge theses inventories?"/>
|
||||
<footer>
|
||||
<button name="do_merge" string="Yes" type="object" icon="gtk-apply" class="oe_highlight"/>
|
||||
or
|
||||
<button string="Cancel" class="oe_link" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<act_window name="Merge inventories"
|
||||
res_model="stock.inventory.merge"
|
||||
src_model="stock.inventory"
|
||||
view_mode="form"
|
||||
multi="True"
|
||||
target="new"
|
||||
key2="client_action_multi"
|
||||
id="action_view_stock_merge_inventories"/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -23,9 +23,11 @@ from openerp.osv import fields, osv
|
||||
from openerp.tools.translate import _
|
||||
import openerp.addons.decimal_precision as dp
|
||||
|
||||
class stock_move_consume(osv.osv_memory):
|
||||
_name = "stock.move.consume"
|
||||
_description = "Consume Products"
|
||||
|
||||
|
||||
class stock_move_scrap(osv.osv_memory):
|
||||
_name = "stock.move.scrap"
|
||||
_description = "Scrap Products"
|
||||
|
||||
_columns = {
|
||||
'product_id': fields.many2one('product.product', 'Product', required=True, select=True),
|
||||
@@ -35,57 +37,6 @@ class stock_move_consume(osv.osv_memory):
|
||||
'restrict_lot_id': fields.many2one('stock.production.lot', 'Lot'),
|
||||
}
|
||||
|
||||
#TOFIX: product_uom should not have differemt category of default UOM of product. Qty should be convert into UOM of original move line before going in consume and scrap
|
||||
def default_get(self, cr, uid, fields, context=None):
|
||||
""" Get default values
|
||||
@param self: The object pointer.
|
||||
@param cr: A database cursor
|
||||
@param uid: ID of the user currently logged in
|
||||
@param fields: List of fields for default value
|
||||
@param context: A standard dictionary
|
||||
@return: default values of fields
|
||||
"""
|
||||
if context is None:
|
||||
context = {}
|
||||
res = super(stock_move_consume, self).default_get(cr, uid, fields, context=context)
|
||||
move = self.pool.get('stock.move').browse(cr, uid, context['active_id'], context=context)
|
||||
if 'product_id' in fields:
|
||||
res.update({'product_id': move.product_id.id})
|
||||
if 'product_uom' in fields:
|
||||
res.update({'product_uom': move.product_uom.id})
|
||||
if 'product_qty' in fields:
|
||||
res.update({'product_qty': move.product_qty})
|
||||
if 'location_id' in fields:
|
||||
res.update({'location_id': move.location_id.id})
|
||||
|
||||
return res
|
||||
|
||||
def do_move_consume(self, cr, uid, ids, context=None):
|
||||
""" To move consumed products
|
||||
@param self: The object pointer.
|
||||
@param cr: A database cursor
|
||||
@param uid: ID of the user currently logged in
|
||||
@param ids: the ID or list of IDs if we want more than one
|
||||
@param context: A standard dictionary
|
||||
@return:
|
||||
"""
|
||||
if context is None:
|
||||
context = {}
|
||||
move_obj = self.pool.get('stock.move')
|
||||
move_ids = context['active_ids']
|
||||
for data in self.browse(cr, uid, ids, context=context):
|
||||
move_obj.action_consume(cr, uid, move_ids,
|
||||
data.product_qty, data.location_id.id, restrict_lot_id=data.restrict_lot_id and data.restrict_lot_id.id or False,
|
||||
context=context)
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
|
||||
|
||||
class stock_move_scrap(osv.osv_memory):
|
||||
_name = "stock.move.scrap"
|
||||
_description = "Scrap Products"
|
||||
_inherit = "stock.move.consume"
|
||||
|
||||
_defaults = {
|
||||
'location_id': lambda *x: False
|
||||
}
|
||||
@@ -101,8 +52,9 @@ class stock_move_scrap(osv.osv_memory):
|
||||
"""
|
||||
if context is None:
|
||||
context = {}
|
||||
res = super(stock_move_consume, self).default_get(cr, uid, fields, context=context)
|
||||
res = super(stock_move_scrap, self).default_get(cr, uid, fields, context=context)
|
||||
move = self.pool.get('stock.move').browse(cr, uid, context['active_id'], context=context)
|
||||
|
||||
location_obj = self.pool.get('stock.location')
|
||||
scrap_location_id = location_obj.search(cr, uid, [('scrap_location','=',True)])
|
||||
|
||||
@@ -115,7 +67,6 @@ class stock_move_scrap(osv.osv_memory):
|
||||
res.update({'location_id': scrap_location_id[0]})
|
||||
else:
|
||||
res.update({'location_id': False})
|
||||
|
||||
return res
|
||||
|
||||
def move_scrap(self, cr, uid, ids, context=None):
|
||||
@@ -139,137 +90,4 @@ class stock_move_scrap(osv.osv_memory):
|
||||
|
||||
|
||||
|
||||
class split_in_production_lot(osv.osv_memory):
|
||||
_name = "stock.move.split"
|
||||
_description = "Split in Serial Numbers"
|
||||
|
||||
def default_get(self, cr, uid, fields, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
res = super(split_in_production_lot, self).default_get(cr, uid, fields, context=context)
|
||||
if context.get('active_id'):
|
||||
move = self.pool.get('stock.move').browse(cr, uid, context['active_id'], context=context)
|
||||
if 'product_id' in fields:
|
||||
res.update({'product_id': move.product_id.id})
|
||||
if 'product_uom' in fields:
|
||||
res.update({'product_uom': move.product_uom.id})
|
||||
if 'qty' in fields:
|
||||
res.update({'qty': move.product_qty})
|
||||
if 'use_exist' in fields:
|
||||
res.update({'use_exist': (move.picking_id and move.picking_id.type=='out' and True) or False})
|
||||
if 'location_id' in fields:
|
||||
res.update({'location_id': move.location_id.id})
|
||||
return res
|
||||
|
||||
_columns = {
|
||||
'qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure')),
|
||||
'product_id': fields.many2one('product.product', 'Product', required=True, select=True),
|
||||
'product_uom': fields.many2one('product.uom', 'Unit of Measure'),
|
||||
'line_ids': fields.one2many('stock.move.split.lines', 'wizard_id', 'Serial Numbers'),
|
||||
'line_exist_ids': fields.one2many('stock.move.split.lines', 'wizard_exist_id', 'Serial Numbers'),
|
||||
'use_exist' : fields.boolean('Existing Serial Numbers',
|
||||
help="Check this option to select existing serial numbers in the list below, otherwise you should enter new ones line by line."),
|
||||
'location_id': fields.many2one('stock.location', 'Source Location')
|
||||
}
|
||||
|
||||
def split_lot(self, cr, uid, ids, context=None):
|
||||
""" To split a lot"""
|
||||
if context is None:
|
||||
context = {}
|
||||
res = self.split(cr, uid, ids, context.get('active_ids'), context=context)
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
def split(self, cr, uid, ids, move_ids, context=None):
|
||||
""" To split stock moves into serial numbers
|
||||
|
||||
:param move_ids: the ID or list of IDs of stock move we want to split
|
||||
"""
|
||||
if context is None:
|
||||
context = {}
|
||||
assert context.get('active_model') == 'stock.move',\
|
||||
'Incorrect use of the stock move split wizard'
|
||||
inventory_id = context.get('inventory_id', False)
|
||||
prodlot_obj = self.pool.get('stock.production.lot')
|
||||
inventory_obj = self.pool.get('stock.inventory')
|
||||
move_obj = self.pool.get('stock.move')
|
||||
new_move = []
|
||||
for data in self.browse(cr, uid, ids, context=context):
|
||||
for move in move_obj.browse(cr, uid, move_ids, context=context):
|
||||
move_qty = move.product_qty
|
||||
quantity_rest = move.product_qty
|
||||
uos_qty_rest = move.product_uos_qty
|
||||
new_move = []
|
||||
if data.use_exist:
|
||||
lines = [l for l in data.line_exist_ids if l]
|
||||
else:
|
||||
lines = [l for l in data.line_ids if l]
|
||||
total_move_qty = 0.0
|
||||
for line in lines:
|
||||
quantity = line.quantity
|
||||
total_move_qty += quantity
|
||||
if total_move_qty > move_qty:
|
||||
raise osv.except_osv(_('Processing Error!'), _('Serial number quantity %d of %s is larger than available quantity (%d)!') \
|
||||
% (total_move_qty, move.product_id.name, move_qty))
|
||||
if quantity <= 0 or move_qty == 0:
|
||||
continue
|
||||
quantity_rest -= quantity
|
||||
uos_qty = quantity / move_qty * move.product_uos_qty
|
||||
uos_qty_rest = quantity_rest / move_qty * move.product_uos_qty
|
||||
if quantity_rest < 0:
|
||||
quantity_rest = quantity
|
||||
self.pool.get('stock.move').log(cr, uid, move.id, _('Unable to assign all lots to this move!'))
|
||||
return False
|
||||
default_val = {
|
||||
'product_qty': quantity,
|
||||
'product_uos_qty': uos_qty,
|
||||
'state': move.state
|
||||
}
|
||||
if quantity_rest > 0:
|
||||
current_move = move_obj.copy(cr, uid, move.id, default_val, context=context)
|
||||
if inventory_id and current_move:
|
||||
inventory_obj.write(cr, uid, inventory_id, {'move_ids': [(4, current_move)]}, context=context)
|
||||
new_move.append(current_move)
|
||||
|
||||
if quantity_rest == 0:
|
||||
current_move = move.id
|
||||
lot_id = False
|
||||
if data.use_exist:
|
||||
lot_id = line.lot_id.id
|
||||
if not lot_id:
|
||||
lot_id = prodlot_obj.create(cr, uid, {
|
||||
'name': line.name,
|
||||
'product_id': move.product_id.id},
|
||||
context=context)
|
||||
|
||||
move_obj.write(cr, uid, [current_move], {'lot_id': lot_id, 'state':move.state})
|
||||
|
||||
update_val = {}
|
||||
if quantity_rest > 0:
|
||||
update_val['product_qty'] = quantity_rest
|
||||
update_val['product_uos_qty'] = uos_qty_rest
|
||||
update_val['state'] = move.state
|
||||
move_obj.write(cr, uid, [move.id], update_val)
|
||||
|
||||
return new_move
|
||||
|
||||
|
||||
class stock_move_split_lines_exist(osv.osv_memory):
|
||||
_name = "stock.move.split.lines"
|
||||
_description = "Stock move Split lines"
|
||||
_columns = {
|
||||
'name': fields.char('Serial Number', size=64),
|
||||
'quantity': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure')),
|
||||
'wizard_id': fields.many2one('stock.move.split', 'Parent Wizard'),
|
||||
'wizard_exist_id': fields.many2one('stock.move.split', 'Parent Wizard (for existing lines)'),
|
||||
'lot_id': fields.many2one('stock.production.lot', 'Serial Number'),
|
||||
}
|
||||
_defaults = {
|
||||
'quantity': 1.0,
|
||||
}
|
||||
|
||||
def onchange_lot_id(self, cr, uid, ids, lot_id=False, product_qty=False,
|
||||
loc_id=False, product_id=False, uom_id=False,context=None):
|
||||
return self.pool.get('stock.move').onchange_lot_id(cr, uid, [], lot_id, product_qty,
|
||||
loc_id, product_id, uom_id, context)
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
@@ -1,41 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<!-- Consume, scrap move -->
|
||||
|
||||
<record id="view_stock_move_consume_wizard" model="ir.ui.view">
|
||||
<field name="name">Consume Move</field>
|
||||
<field name="model">stock.move.consume</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Consume Move" version="7.0">
|
||||
<group string="Consume Products">
|
||||
<field name="product_id" readonly="1"/>
|
||||
<label for="product_qty"/>
|
||||
<div>
|
||||
<field name="product_qty" class="oe_inline"/>
|
||||
<field name="product_uom" class="oe_inline" readonly="1" groups="product.group_uom"/>
|
||||
</div>
|
||||
<field name="restrict_lot_id" domain="[('product_id','=',product_id)]" groups="stock.group_tracking_lot"
|
||||
context="{'default_product_id': product_id}"/>
|
||||
<field name="location_id" groups="stock.group_locations"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="do_move_consume" string="Ok" type="object" class="oe_highlight"/>
|
||||
or
|
||||
<button string="Cancel" class="oe_link" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="move_consume" model="ir.actions.act_window">
|
||||
<field name="name">Consume Move</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">stock.move.consume</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
<!-- Scrap move -->
|
||||
|
||||
<record id="view_stock_move_scrap_wizard" model="ir.ui.view">
|
||||
<field name="name">Scrap Move</field>
|
||||
@@ -74,62 +40,5 @@
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
<record id="view_split_in_lots" model="ir.ui.view">
|
||||
<field name="name">Split in Serial Numbers</field>
|
||||
<field name="model">stock.move.split</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Split in Serial Numbers" version="7.0">
|
||||
<group>
|
||||
<field name="product_id" readonly="1"/>
|
||||
<label for="qty"/>
|
||||
<div>
|
||||
<field name="qty" readonly="1" class="oe_inline"/>
|
||||
<field name="product_uom" readonly="1" class="oe_inline"/>
|
||||
</div>
|
||||
<field name="location_id" invisible="1"/>
|
||||
<field name="use_exist"/>
|
||||
</group>
|
||||
<field name="line_ids" attrs="{'invisible':[('use_exist','=',True)]}">
|
||||
<tree string="Serial Numbers" editable="bottom">
|
||||
<field name="name"/>
|
||||
<field name="quantity" />
|
||||
</tree>
|
||||
<form string="Serial Number" version="7.0">
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="quantity" />
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
<field name="line_exist_ids" attrs="{'invisible':[('use_exist','!=',True)]}">
|
||||
<tree string="Serial Numbers" editable="bottom">
|
||||
<field name="lot_id" string="Serial Number" quick_create="false" domain="[('product_id','=',parent.product_id)]" on_change="onchange_lot_id(lot_id, quantity, parent.location_id, parent.product_id, parent.product_uom, context)" context="{'product_id': parent.product_id}"/>
|
||||
<field name="quantity" on_change="onchange_lot_id(lot_id, quantity, parent.location_id, parent.product_id, parent.product_uom,context)" />
|
||||
</tree>
|
||||
<form string="Serial Number" version="7.0">
|
||||
<group>
|
||||
<field name="lot_id" domain="[('product_id','=',parent.product_id)]" on_change="onchange_lot_id(lot_id, quantity, parent.location_id, parent.product_id, parent.product_uom, context)"/>
|
||||
<field name="quantity" on_change="onchange_lot_id(lot_id, quantity, parent.location_id, parent.product_id, parent.product_uom, context)" />
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
<footer>
|
||||
<button name="split_lot" string="Split" type="object" class="oe_highlight"/>
|
||||
or
|
||||
<button string="Cancel" class="oe_link" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="track_line" model="ir.actions.act_window">
|
||||
<field name="name">Split in Serial Numbers</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">stock.move.split</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import time
|
||||
|
||||
from openerp.osv import osv, fields
|
||||
from openerp.tools.translate import _
|
||||
import openerp.addons.decimal_precision as dp
|
||||
@@ -34,7 +32,7 @@ class stock_return_picking_line(osv.osv_memory):
|
||||
'quantity': fields.float("Quantity", digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
|
||||
'wizard_id': fields.many2one('stock.return.picking', string="Wizard"),
|
||||
'move_id': fields.many2one('stock.move', "Move"),
|
||||
'lot_id': fields.related('move_id', 'quant_ids', 'lot_id', type="many2one", relation='stock.production.lot', string='Serial Number', readonly=True),
|
||||
'lot_id': fields.many2one('stock.production.lot', 'Serial Number', help="Used to choose the lot/serial number of the product returned"),
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +41,7 @@ class stock_return_picking(osv.osv_memory):
|
||||
_description = 'Return Picking'
|
||||
_columns = {
|
||||
'product_return_moves': fields.one2many('stock.return.picking.line', 'wizard_id', 'Moves'),
|
||||
'move_dest_exists': fields.boolean('Chained Move Exists', readonly=True, help="Technical field used to hide help tooltip if not needed"),
|
||||
}
|
||||
|
||||
def default_get(self, cr, uid, fields, context=None):
|
||||
@@ -60,24 +59,33 @@ class stock_return_picking(osv.osv_memory):
|
||||
context = {}
|
||||
res = super(stock_return_picking, self).default_get(cr, uid, fields, context=context)
|
||||
record_id = context and context.get('active_id', False) or False
|
||||
uom_obj = self.pool.get('product.uom')
|
||||
pick_obj = self.pool.get('stock.picking')
|
||||
pick = pick_obj.browse(cr, uid, record_id, context=context)
|
||||
quant_obj = self.pool.get("stock.quant")
|
||||
chained_move_exist = False
|
||||
if pick:
|
||||
if pick.state != 'done':
|
||||
raise osv.except_osv(_('Warning!'), _("You may only return pickings that are Done!"))
|
||||
for line in pick.move_lines:
|
||||
qty = line.product_uom_qty
|
||||
if line.returned_move_ids:
|
||||
for returned_move in line.returned_move_ids:
|
||||
if returned_move.product_id.id == line.product_id.id:
|
||||
qty -= returned_move.product_uom_qty
|
||||
|
||||
if qty > 0:
|
||||
result1.append({'product_id': line.product_id.id, 'quantity': qty, 'move_id': line.id})
|
||||
for move in pick.move_lines:
|
||||
if move.move_dest_id:
|
||||
chained_move_exist = True
|
||||
#Sum the quants in that location that can be returned (they should have been moved by the moves that were included in the returned picking)
|
||||
qty = 0
|
||||
quant_search = quant_obj.search(cr, uid, [('history_ids', 'in', move.id), ('qty', '>', 0.0), ('location_id', 'child_of', move.location_dest_id.id)], context=context)
|
||||
for quant in quant_obj.browse(cr, uid, quant_search, context=context):
|
||||
if not quant.reservation_id or quant.reservation_id.origin_returned_move_id.id != move.id:
|
||||
qty += quant.qty
|
||||
qty = uom_obj._compute_qty(cr, uid, move.product_id.uom_id.id, qty, move.product_uom.id)
|
||||
result1.append({'product_id': move.product_id.id, 'quantity': qty, 'move_id': move.id})
|
||||
|
||||
if len(result1) == 0:
|
||||
raise osv.except_osv(_('Warning!'), _("No products to return (only lines in Done state and not fully returned yet can be returned)!"))
|
||||
if 'product_return_moves' in fields:
|
||||
res.update({'product_return_moves': result1})
|
||||
if 'move_dest_exists' in fields:
|
||||
res.update({'move_dest_exists': chained_move_exist})
|
||||
return res
|
||||
|
||||
def _create_returns(self, cr, uid, ids, context=None):
|
||||
@@ -92,16 +100,32 @@ class stock_return_picking(osv.osv_memory):
|
||||
data = self.read(cr, uid, ids[0], context=context)
|
||||
returned_lines = 0
|
||||
|
||||
# Cancel assignment of existing chained assigned moves
|
||||
moves_to_unreserve = []
|
||||
for move in pick.move_lines:
|
||||
to_check_moves = [move.move_dest_id]
|
||||
while to_check_moves:
|
||||
current_move = to_check_moves.pop()
|
||||
if current_move.state not in ('done', 'cancel') and current_move.reserved_quant_ids:
|
||||
moves_to_unreserve.append(current_move.id)
|
||||
split_move_ids = move_obj.search(cr, uid, [('split_from', '=', current_move.id)], context=context)
|
||||
if split_move_ids:
|
||||
to_check_moves += move_obj.browse(cr, uid, split_move_ids, context=context)
|
||||
|
||||
if moves_to_unreserve:
|
||||
move_obj.do_unreserve(cr, uid, moves_to_unreserve, context=context)
|
||||
#break the link between moves in order to be able to fix them later if needed
|
||||
move_obj.write(cr, uid, moves_to_unreserve, {'move_orig_ids': False}, context=context)
|
||||
|
||||
#Create new picking for returned products
|
||||
pick_type_id = pick.picking_type_id.return_picking_type_id and pick.picking_type_id.return_picking_type_id.id or pick.picking_type_id.id
|
||||
|
||||
new_picking = pick_obj.copy(cr, uid, pick.id, {
|
||||
'move_lines': [],
|
||||
'picking_type_id': pick_type_id,
|
||||
'state': 'draft',
|
||||
'origin': pick.name,
|
||||
},context=context)
|
||||
|
||||
}, context=context)
|
||||
|
||||
for data_get in data_obj.browse(cr, uid, data['product_return_moves'], context=context):
|
||||
move = data_get.move_id
|
||||
if not move:
|
||||
@@ -109,9 +133,6 @@ class stock_return_picking(osv.osv_memory):
|
||||
new_qty = data_get.quantity
|
||||
if new_qty:
|
||||
returned_lines += 1
|
||||
quant_ids = []
|
||||
for quant in move.quant_ids:
|
||||
quant_ids.append((4, quant.id))
|
||||
move_obj.copy(cr, uid, move.id, {
|
||||
'product_id': data_get.product_id.id,
|
||||
'product_uom_qty': new_qty,
|
||||
@@ -120,9 +141,11 @@ class stock_return_picking(osv.osv_memory):
|
||||
'state': 'draft',
|
||||
'location_id': move.location_dest_id.id,
|
||||
'location_dest_id': move.location_id.id,
|
||||
'reserved_quant_ids': quant_ids,
|
||||
'origin_returned_move_id': move.id,
|
||||
'procure_method': 'make_to_stock',
|
||||
'restrict_lot_id': data_get.lot_id.id,
|
||||
})
|
||||
|
||||
if not returned_lines:
|
||||
raise osv.except_osv(_('Warning!'), _("Please specify at least one non-zero quantity."))
|
||||
|
||||
@@ -143,13 +166,13 @@ class stock_return_picking(osv.osv_memory):
|
||||
new_picking_id, pick_type_id = self._create_returns(cr, uid, ids, context=context)
|
||||
# Override the context to disable all the potential filters that could have been set previously
|
||||
ctx = {
|
||||
'search_default_picking_type_id': pick_type_id,
|
||||
'search_default_draft': False,
|
||||
'search_default_assigned': False,
|
||||
'search_default_confirmed': False,
|
||||
'search_default_ready': False,
|
||||
'search_default_late': False,
|
||||
'search_default_available': False,
|
||||
'search_default_picking_type_id': pick_type_id,
|
||||
'search_default_draft': False,
|
||||
'search_default_assigned': False,
|
||||
'search_default_confirmed': False,
|
||||
'search_default_ready': False,
|
||||
'search_default_late': False,
|
||||
'search_default_available': False,
|
||||
}
|
||||
return {
|
||||
'domain': "[('id', 'in', [" + str(new_picking_id) + "])]",
|
||||
|
||||
@@ -41,9 +41,18 @@
|
||||
<field name="model">stock.return.picking</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Return lines" version="7.0">
|
||||
<label string="Provide the quantities of the returned products."/>
|
||||
<group>
|
||||
<field name="product_return_moves"/>
|
||||
<group string="Provide the quantities of the returned products.">
|
||||
<field name="move_dest_exists" invisible="1"/>
|
||||
<div attrs="{'invisible': [('move_dest_exists', '=', False)]}" class="oe_grey" colspan="4">
|
||||
<p>This picking appears to be chained with another operation. Later, if you receive the goods you are returning now, make sure to <b>reverse</b> the returned picking in order to avoid logistic rules to be applied again (which would create duplicated operations)</p>
|
||||
</div>
|
||||
<field name="product_return_moves" colspan="4">
|
||||
<tree editable="top">
|
||||
<field name="product_id"/>
|
||||
<field name="lot_id" domain="[('product_id', '=', product_id)]" context="{'default_product_id': product_id}"/>
|
||||
<field name="quantity"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="create_returns" string="Return" type="object" class="oe_highlight"/>
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
<field name="name">Dropship</field>
|
||||
<field name="sequence_id" ref="seq_picking_type_dropship"/>
|
||||
<field name="code">incoming</field>
|
||||
<field name="default_location_dest_id" ref="stock.stock_location_suppliers"/>
|
||||
<field name="default_location_src_id" ref="stock.stock_location_suppliers"/>
|
||||
<field name="default_location_dest_id" ref="stock.stock_location_customers"/>
|
||||
</record>
|
||||
|
||||
|
||||
|
||||
@@ -82,5 +82,5 @@ class stock_picking_wave(osv.osv):
|
||||
class stock_picking(osv.osv):
|
||||
_inherit = "stock.picking"
|
||||
_columns = {
|
||||
'wave_id': fields.many2one('stock.picking.wave', 'Picking Wave', help='Picking wave associated to this picking'),
|
||||
'wave_id': fields.many2one('stock.picking.wave', 'Picking Wave', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help='Picking wave associated to this picking'),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user