[MERGE] trunk-wms

bzr revid: qdp-launchpad@openerp.com-20140130080603-bew8zegu609d7rdi
This commit is contained in:
Quentin (OpenERP) 2014-01-30 09:06:03 +01:00
commit 5729de6a62
20 changed files with 335 additions and 386 deletions

View File

@ -100,7 +100,7 @@ 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, context=None):
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.
@param product_qty: Consumed product quantity
@param location_id: Source location
@ -110,7 +110,8 @@ class StockMove(osv.osv):
production_obj = self.pool.get('mrp.production')
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, 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)
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':
@ -124,7 +125,7 @@ class StockMove(osv.osv):
res.append(new_move)
return res
def action_scrap(self, cr, uid, ids, product_qty, location_id, context=None):
def action_scrap(self, cr, uid, ids, product_qty, location_id, restrict_lot_id=False, restrict_partner_id=False, context=None):
""" Move the scrap/damaged product into scrap location
@param product_qty: Scraped product quantity
@param location_id: Scrap location
@ -133,7 +134,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, 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:

View File

@ -19,13 +19,11 @@
#
##############################################################################
from openerp.osv import fields,osv
from openerp.osv import fields, osv
from datetime import datetime
from dateutil.relativedelta import relativedelta
from openerp.tools.translate import _
import openerp.addons.decimal_precision as dp
# TODO: replace move_id by quant_id everywhere
class mrp_repair(osv.osv):
_name = 'mrp.repair'
@ -77,7 +75,7 @@ class mrp_repair(osv.osv):
val += c['amount']
for line in repair.fees_lines:
if line.to_invoice:
tax_calculate = tax_obj.compute_all(cr, uid, line.tax_id, line.price_unit, line.product_uom_qty, line.product_id, repair.partner_id)
tax_calculate = tax_obj.compute_all(cr, uid, line.tax_id, line.price_unit, line.product_uom_qty, line.product_id, repair.partner_id)
for c in tax_calculate['taxes']:
val += c['amount']
res[repair.id] = cur_obj.round(cr, uid, cur, val)
@ -116,20 +114,23 @@ class mrp_repair(osv.osv):
return result.keys()
_columns = {
'name': fields.char('Repair Reference',size=24, required=True, states={'confirmed':[('readonly',True)]}),
'product_id': fields.many2one('product.product', string='Product to Repair', required=True, readonly=True, states={'draft':[('readonly',False)]}),
'partner_id' : fields.many2one('res.partner', 'Partner', select=True, help='Choose partner for whom the order will be invoiced and delivered.', states={'confirmed':[('readonly',True)]}),
'address_id': fields.many2one('res.partner', 'Delivery Address', domain="[('parent_id','=',partner_id)]", states={'confirmed':[('readonly',True)]}),
'name': fields.char('Repair Reference', size=24, required=True, states={'confirmed': [('readonly', True)]}),
'product_id': fields.many2one('product.product', string='Product to Repair', 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)]}),
'partner_id': fields.many2one('res.partner', 'Partner', select=True, help='Choose partner for whom the order will be invoiced and delivered.', states={'confirmed': [('readonly', True)]}),
'address_id': fields.many2one('res.partner', 'Delivery Address', domain="[('parent_id','=',partner_id)]", states={'confirmed': [('readonly', True)]}),
'default_address_id': fields.function(_get_default_address, type="many2one", relation="res.partner"),
'state': fields.selection([
('draft','Quotation'),
('cancel','Cancelled'),
('confirmed','Confirmed'),
('under_repair','Under Repair'),
('ready','Ready to Repair'),
('2binvoiced','To be Invoiced'),
('invoice_except','Invoice Exception'),
('done','Repaired')
('draft', 'Quotation'),
('cancel', 'Cancelled'),
('confirmed', 'Confirmed'),
('under_repair', 'Under Repair'),
('ready', 'Ready to Repair'),
('2binvoiced', 'To be Invoiced'),
('invoice_except', 'Invoice Exception'),
('done', 'Repaired')
], 'Status', readonly=True, track_visibility='onchange',
help=' * The \'Draft\' status is used when a user is encoding a new and unconfirmed repair order. \
\n* The \'Confirmed\' status is used when a user confirms the repair order. \
@ -137,27 +138,25 @@ class mrp_repair(osv.osv):
\n* The \'To be Invoiced\' status is used to generate the invoice before or after repairing done. \
\n* The \'Done\' status is set when repairing is completed.\
\n* The \'Cancelled\' status is used when user cancel repair order.'),
'location_id': fields.many2one('stock.location', 'Current Location', select=True, readonly=True, states={'draft':[('readonly',False)], 'confirmed':[('readonly',True)]}),
'location_dest_id': fields.many2one('stock.location', 'Delivery Location', readonly=True, states={'draft':[('readonly',False)], 'confirmed':[('readonly',True)]}),
'move_id': fields.many2one('stock.move', 'Move',required=True, domain="[('product_id','=',product_id)]", readonly=True, states={'draft':[('readonly',False)]}),
'guarantee_limit': fields.date('Warranty Expiration', help="The warranty expiration limit is computed as: last move date + warranty defined on selected product. If the current date is below the warranty expiration limit, each operation and fee you will add will be set as 'not to invoiced' by default. Note that you can change manually afterwards.", states={'confirmed':[('readonly',True)]}),
'operations' : fields.one2many('mrp.repair.line', 'repair_id', 'Operation Lines', readonly=True, states={'draft':[('readonly',False)]}),
'location_id': fields.many2one('stock.location', 'Current Location', select=True, required=True, readonly=True, states={'draft': [('readonly', False)], 'confirmed': [('readonly', True)]}),
'location_dest_id': fields.many2one('stock.location', 'Delivery Location', readonly=True, required=True, states={'draft': [('readonly', False)], 'confirmed': [('readonly', True)]}),
'lot_id': fields.many2one('stock.production.lot', 'Repaired Lot', domain="[('product_id','=', product_id)]", help="Products repaired are all belonging to this lot"),
'guarantee_limit': fields.date('Warranty Expiration', help="The warranty expiration limit is computed as: last move date + warranty defined on selected product. If the current date is below the warranty expiration limit, each operation and fee you will add will be set as 'not to invoiced' by default. Note that you can change manually afterwards.", states={'confirmed': [('readonly', True)]}),
'operations': fields.one2many('mrp.repair.line', 'repair_id', 'Operation Lines', readonly=True, states={'draft': [('readonly', False)]}),
'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', help='Pricelist of the selected partner.'),
'partner_invoice_id':fields.many2one('res.partner', 'Invoicing Address'),
'invoice_method':fields.selection([
("none","No Invoice"),
("b4repair","Before Repair"),
("after_repair","After Repair")
'partner_invoice_id': fields.many2one('res.partner', 'Invoicing Address'),
'invoice_method': fields.selection([
("none", "No Invoice"),
("b4repair", "Before Repair"),
("after_repair", "After Repair")
], "Invoice Method",
select=True, required=True, states={'draft':[('readonly',False)]}, readonly=True, help='Selecting \'Before Repair\' or \'After Repair\' will allow you to generate invoice before or after the repair is done respectively. \'No invoice\' means you don\'t want to generate invoice for this repair order.'),
'invoice_id': fields.many2one('account.invoice', 'Invoice', readonly=True),
'picking_id': fields.many2one('stock.picking', 'Picking',readonly=True),
'fees_lines': fields.one2many('mrp.repair.fee', 'repair_id', 'Fees Lines', readonly=True, states={'draft':[('readonly',False)]}),
select=True, required=True, states={'draft': [('readonly', False)]}, readonly=True, help='Selecting \'Before Repair\' or \'After Repair\' will allow you to generate invoice before or after the repair is done respectively. \'No invoice\' means you don\'t want to generate invoice for this repair order.'),
'invoice_id': fields.many2one('account.invoice', 'Invoice', readonly=True, track_visibility="onchange"),
'move_id': fields.many2one('stock.move', 'Move', readonly=True, help="Move created by the repair order", track_visibility="onchange"),
'fees_lines': fields.one2many('mrp.repair.fee', 'repair_id', 'Fees Lines', readonly=True, states={'draft': [('readonly', False)]}),
'internal_notes': fields.text('Internal Notes'),
'quotation_notes': fields.text('Quotation Notes'),
'company_id': fields.many2one('res.company', 'Company'),
'deliver_bool': fields.boolean('Deliver', help="Check this box if you want to manage the delivery once the product is repaired and create a picking with selected product. Note that you can select the locations in the Info tab, if you have the extended view.", states={'confirmed':[('readonly',True)]}),
'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type'),
'invoiced': fields.boolean('Invoiced', readonly=True),
'repaired': fields.boolean('Repaired', readonly=True),
'amount_untaxed': fields.function(_amount_untaxed, string='Untaxed Amount',
@ -177,24 +176,36 @@ class mrp_repair(osv.osv):
}),
}
def _default_stock_location(self, cr, uid, context=None):
try:
warehouse = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'warehouse0')
return warehouse.lot_stock_id.id
except:
return False
_defaults = {
'state': lambda *a: 'draft',
'deliver_bool': lambda *a: True,
'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'mrp.repair'),
'invoice_method': lambda *a: 'none',
'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.repair', context=context),
'pricelist_id': lambda self, cr, uid,context : self.pool.get('product.pricelist').search(cr, uid, [('type','=','sale')])[0]
'pricelist_id': lambda self, cr, uid, context: self.pool.get('product.pricelist').search(cr, uid, [('type', '=', 'sale')])[0],
'product_qty': 1.0,
'location_id': _default_stock_location,
}
_sql_constraints = [
('name', 'unique (name)', 'The name of the Repair Order must be unique!'),
]
def copy(self, cr, uid, id, default=None, context=None):
if not default:
default = {}
default.update({
'state':'draft',
'repaired':False,
'invoiced':False,
'state': 'draft',
'repaired': False,
'invoiced': False,
'invoice_id': False,
'picking_id': False,
'move_id': False,
'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.repair'),
})
return super(mrp_repair, self).copy(cr, uid, id, default, context)
@ -204,39 +215,31 @@ class mrp_repair(osv.osv):
@param product_id: Changed product
@return: Dictionary of values.
"""
product = False
if product_id:
product = self.pool.get("product.product").browse(cr, uid, product_id)
return {'value': {
'move_id': False,
'guarantee_limit' :False,
'location_id': False,
'location_dest_id': False,
'guarantee_limit': False,
'lot_id': False,
'product_uom': product and product.uom_id.id or False,
}
}
def onchange_move_id(self, cr, uid, ids, prod_id=False, move_id=False):
""" On change of move id sets values of guarantee limit, source location,
destination location, partner and partner address.
@param prod_id: Id of product in current record.
@param move_id: Changed move.
@return: Dictionary of values.
def onchange_product_uom(self, cr, uid, ids, product_id, product_uom, context=None):
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)
uom = self.pool.get('product.uom').browse(cr, uid, product_uom, context=context)
if uom.category_id.id != product.uom_id.category_id.id:
res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
res['value'].update({'product_uom': product.uom_id.id})
return res
def onchange_location_id(self, cr, uid, ids, location_id=None):
""" On change of location
"""
data = {}
data['value'] = {'guarantee_limit': False, 'location_id': False, 'partner_id': False}
if not prod_id:
return data
if move_id:
move = self.pool.get('stock.move').browse(cr, uid, move_id)
product = self.pool.get('product.product').browse(cr, uid, prod_id)
limit = datetime.strptime(move.date_expected, '%Y-%m-%d %H:%M:%S') + relativedelta(months=int(product.warranty))
data['value']['guarantee_limit'] = limit.strftime('%Y-%m-%d')
data['value']['location_id'] = move.location_dest_id.id
data['value']['location_dest_id'] = move.location_dest_id.id
if move.partner_id:
data['value']['partner_id'] = move.partner_id.id
else:
data['value']['partner_id'] = False
d = self.onchange_partner_id(cr, uid, ids, data['value']['partner_id'], data['value']['partner_id'])
data['value'].update(d['value'])
return data
return {'value': {'location_dest_id': location_id}}
def button_dummy(self, cr, uid, ids, context=None):
return True
@ -254,7 +257,7 @@ class mrp_repair(osv.osv):
return {'value': {
'address_id': False,
'partner_invoice_id': False,
'pricelist_id': pricelist_obj.search(cr, uid, [('type','=','sale')])[0]
'pricelist_id': pricelist_obj.search(cr, uid, [('type', '=', 'sale')])[0]
}
}
addr = part_obj.address_get(cr, uid, [part], ['delivery', 'invoice', 'default'])
@ -267,40 +270,6 @@ class mrp_repair(osv.osv):
}
}
def onchange_lot_id(self, cr, uid, ids, lot, product_id):
""" On change of Serial Number sets the values of source location,
destination location, move and guarantee limit.
@param lot: Changed id of Serial Number.
@param product_id: Product id from current record.
@return: Dictionary of values.
"""
move_obj = self.pool.get('stock.move')
data = {}
data['value'] = {
'location_id': False,
'location_dest_id': False,
'move_id': False,
'guarantee_limit': False
}
if not lot:
return data
if not len(move_ids):
return data
def get_last_move(lst_move):
while lst_move.move_dest_id and lst_move.move_dest_id.state == 'done':
lst_move = lst_move.move_dest_id
return lst_move
move_id = move_ids[0]
move = get_last_move(move_obj.browse(cr, uid, move_id))
data['value']['move_id'] = move.id
d = self.onchange_move_id(cr, uid, ids, product_id, move.id)
data['value'].update(d['value'])
return data
def action_cancel_draft(self, cr, uid, ids, *args):
""" Cancels repair order when it is in 'Draft' state.
@param *arg: Arguments
@ -311,9 +280,8 @@ class mrp_repair(osv.osv):
mrp_line_obj = self.pool.get('mrp.repair.line')
for repair in self.browse(cr, uid, ids):
mrp_line_obj.write(cr, uid, [l.id for l in repair.operations], {'state': 'draft'})
self.write(cr, uid, ids, {'state':'draft'})
self.create_workflow(cr, uid, ids)
return True
self.write(cr, uid, ids, {'state': 'draft'})
return self.create_workflow(cr, uid, ids)
def action_confirm(self, cr, uid, ids, *args):
""" Repair order state is set to 'To be invoiced' when invoice method
@ -342,13 +310,13 @@ class mrp_repair(osv.osv):
if not repair.invoiced:
mrp_line_obj.write(cr, uid, [l.id for l in repair.operations], {'state': 'cancel'}, context=context)
else:
raise osv.except_osv(_('Warning!'),_('Repair order is already invoiced.'))
return self.write(cr,uid,ids,{'state':'cancel'})
raise osv.except_osv(_('Warning!'), _('Repair order is already invoiced.'))
return self.write(cr, uid, ids, {'state': 'cancel'})
def wkf_invoice_create(self, cr, uid, ids, *args):
self.action_invoice_create(cr, uid, ids)
return True
def action_invoice_create(self, cr, uid, ids, group=False, context=None):
""" Creates invoice(s) for repair order.
@param group: It is set to true when group invoice is to be generated.
@ -362,28 +330,28 @@ class mrp_repair(osv.osv):
repair_fee_obj = self.pool.get('mrp.repair.fee')
for repair in self.browse(cr, uid, ids, context=context):
res[repair.id] = False
if repair.state in ('draft','cancel') or repair.invoice_id:
if repair.state in ('draft', 'cancel') or repair.invoice_id:
continue
if not (repair.partner_id.id and repair.partner_invoice_id.id):
raise osv.except_osv(_('No partner!'),_('You have to select a Partner Invoice Address in the repair form!'))
raise osv.except_osv(_('No partner!'), _('You have to select a Partner Invoice Address in the repair form!'))
comment = repair.quotation_notes
if (repair.invoice_method != 'none'):
if group and repair.partner_invoice_id.id in invoices_group:
inv_id = invoices_group[repair.partner_invoice_id.id]
invoice = inv_obj.browse(cr, uid, inv_id)
invoice_vals = {
'name': invoice.name +', '+repair.name,
'origin': invoice.origin+', '+repair.name,
'comment':(comment and (invoice.comment and invoice.comment+"\n"+comment or comment)) or (invoice.comment and invoice.comment or ''),
'name': invoice.name + ', ' + repair.name,
'origin': invoice.origin + ', ' + repair.name,
'comment': (comment and (invoice.comment and invoice.comment + "\n" + comment or comment)) or (invoice.comment and invoice.comment or ''),
}
inv_obj.write(cr, uid, [inv_id], invoice_vals, context=context)
else:
if not repair.partner_id.property_account_receivable:
raise osv.except_osv(_('Error!'), _('No account defined for partner "%s".') % repair.partner_id.name )
raise osv.except_osv(_('Error!'), _('No account defined for partner "%s".') % repair.partner_id.name)
account_id = repair.partner_id.property_account_receivable.id
inv = {
'name': repair.name,
'origin':repair.name,
'origin': repair.name,
'type': 'out_invoice',
'account_id': account_id,
'partner_id': repair.partner_id.id,
@ -396,7 +364,7 @@ class mrp_repair(osv.osv):
self.write(cr, uid, repair.id, {'invoiced': True, 'invoice_id': inv_id})
for operation in repair.operations:
if operation.to_invoice == True:
if operation.to_invoice:
if group:
name = repair.name + '-' + operation.name
else:
@ -407,7 +375,7 @@ class mrp_repair(osv.osv):
elif operation.product_id.categ_id.property_account_income_categ:
account_id = operation.product_id.categ_id.property_account_income_categ.id
else:
raise osv.except_osv(_('Error!'), _('No account defined for product "%s".') % operation.product_id.name )
raise osv.except_osv(_('Error!'), _('No account defined for product "%s".') % operation.product_id.name)
invoice_line_id = inv_line_obj.create(cr, uid, {
'invoice_id': inv_id,
@ -415,15 +383,15 @@ class mrp_repair(osv.osv):
'origin': repair.name,
'account_id': account_id,
'quantity': operation.product_uom_qty,
'invoice_line_tax_id': [(6,0,[x.id for x in operation.tax_id])],
'invoice_line_tax_id': [(6, 0, [x.id for x in operation.tax_id])],
'uos_id': operation.product_uom.id,
'price_unit': operation.price_unit,
'price_subtotal': operation.product_uom_qty*operation.price_unit,
'price_subtotal': operation.product_uom_qty * operation.price_unit,
'product_id': operation.product_id and operation.product_id.id or False
})
repair_line_obj.write(cr, uid, [operation.id], {'invoiced': True, 'invoice_line_id': invoice_line_id})
for fee in repair.fees_lines:
if fee.to_invoice == True:
if fee.to_invoice:
if group:
name = repair.name + '-' + fee.name
else:
@ -444,11 +412,11 @@ class mrp_repair(osv.osv):
'origin': repair.name,
'account_id': account_id,
'quantity': fee.product_uom_qty,
'invoice_line_tax_id': [(6,0,[x.id for x in fee.tax_id])],
'invoice_line_tax_id': [(6, 0, [x.id for x in fee.tax_id])],
'uos_id': fee.product_uom.id,
'product_id': fee.product_id and fee.product_id.id or False,
'price_unit': fee.price_unit,
'price_subtotal': fee.product_uom_qty*fee.price_unit
'price_subtotal': fee.product_uom_qty * fee.price_unit
})
repair_fee_obj.write(cr, uid, [fee.id], {'invoiced': True, 'invoice_line_id': invoice_fee_id})
res[repair.id] = inv_id
@ -483,9 +451,9 @@ class mrp_repair(osv.osv):
for order in self.browse(cr, uid, ids, context=context):
val = {}
val['repaired'] = True
if (not order.invoiced and order.invoice_method=='after_repair'):
if (not order.invoiced and order.invoice_method == 'after_repair'):
val['state'] = '2binvoiced'
elif (not order.invoiced and order.invoice_method=='b4repair'):
elif (not order.invoiced and order.invoice_method == 'b4repair'):
val['state'] = 'ready'
else:
pass
@ -497,18 +465,19 @@ class mrp_repair(osv.osv):
return True
def action_repair_done(self, cr, uid, ids, context=None):
""" Creates stock move and picking for repair order.
@return: Picking ids.
""" Creates stock move for operation and stock move for final product of repair order.
@return: Move ids of final products
"""
res = {}
move_obj = self.pool.get('stock.move')
repair_line_obj = self.pool.get('mrp.repair.line')
pick_obj = self.pool.get('stock.picking')
for repair in self.browse(cr, uid, ids, context=context):
move_ids = []
for move in repair.operations:
move_id = move_obj.create(cr, uid, {
'name': move.name,
'product_id': move.product_id.id,
'restrict_lot_id': move.lot_id.id,
'product_uom_qty': move.product_uom_qty,
'product_uom': move.product_uom.id,
'partner_id': repair.address_id and repair.address_id.id or False,
@ -516,37 +485,22 @@ class mrp_repair(osv.osv):
'location_dest_id': move.location_dest_id.id,
'state': 'assigned',
})
move_obj.action_done(cr, uid, [move_id], context=context)
move_ids.append(move_id)
repair_line_obj.write(cr, uid, [move.id], {'move_id': move_id, 'state': 'done'}, context=context)
if repair.deliver_bool:
if not repair.picking_type_id:
raise osv.except_osv(_('Warning!'), _('No picking type set.'))
pick_name = self.pool.get('ir.sequence').get_id(cr, uid, repair.picking_type_id.sequence_id.id, 'id', context=context)
picking = pick_obj.create(cr, uid, {
'name': pick_name,
'origin': repair.name,
'state': 'draft',
'move_type': 'one',
'partner_id': repair.address_id and repair.address_id.id or False,
'note': repair.internal_notes,
'invoice_state': 'none',
'picking_type_id': repair.picking_type_id.id,
})
move_id = move_obj.create(cr, uid, {
'name': repair.name,
'picking_id': picking,
'product_id': repair.product_id.id,
'product_uom': repair.product_id.uom_id.id,
'partner_id': repair.address_id and repair.address_id.id or False,
'location_id': repair.location_id.id,
'location_dest_id': repair.location_dest_id.id,
'state': 'assigned',
})
pick_obj.signal_button_confirm(cr, uid, [picking])
self.write(cr, uid, [repair.id], {'state': 'done', 'picking_id': picking})
res[repair.id] = picking
else:
self.write(cr, uid, [repair.id], {'state': 'done'})
move_id = move_obj.create(cr, uid, {
'name': repair.name,
'product_id': repair.product_id.id,
'product_uom': repair.product_uom.id or repair.product_id.uom_id.id,
'product_qty': repair.product_qty,
'partner_id': repair.address_id and repair.address_id.id or False,
'location_id': repair.location_id.id,
'location_dest_id': repair.location_dest_id.id,
'restrict_lot_id': repair.lot_id.id,
})
move_ids.append(move_id)
move_obj.action_done(cr, uid, move_ids, context=context)
self.write(cr, uid, [repair.id], {'state': 'done', 'move_id': move_id}, context=context)
res[repair.id] = move_id
return res
@ -580,24 +534,24 @@ class ProductChangeMixin(object):
result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id or False
if not pricelist:
warning = {
'title':'No Pricelist!',
'title': _('No Pricelist!'),
'message':
'You have to select a pricelist in the Repair form !\n'
'Please set one before choosing a product.'
_('You have to select a pricelist in the Repair form !\n'
'Please set one before choosing a product.')
}
else:
price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
product, product_uom_qty, partner_id, {'uom': uom,})[pricelist]
product, product_uom_qty, partner_id, {'uom': uom})[pricelist]
if price is False:
warning = {
'title':'No valid pricelist line found !',
warning = {
'title': _('No valid pricelist line found !'),
'message':
"Couldn't find a pricelist line matching this product and quantity.\n"
"You have to change either the product, the quantity or the pricelist."
_("Couldn't find a pricelist line matching this product and quantity.\n"
"You have to change either the product, the quantity or the pricelist.")
}
else:
result.update({'price_unit': price, 'price_subtotal': price*product_uom_qty})
result.update({'price_unit': price, 'price_subtotal': price * product_uom_qty})
return {'value': result, 'warning': warning}
@ -607,8 +561,9 @@ class mrp_repair_line(osv.osv, ProductChangeMixin):
_description = 'Repair Line'
def copy_data(self, cr, uid, id, default=None, context=None):
if not default: default = {}
default.update( {'invoice_line_id': False, 'move_id': False, 'invoiced': False, 'state': 'draft'})
if not default:
default = {}
default.update({'invoice_line_id': False, 'move_id': False, 'invoiced': False, 'state': 'draft'})
return super(mrp_repair_line, self).copy_data(cr, uid, id, default, context)
def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
@ -618,7 +573,7 @@ class mrp_repair_line(osv.osv, ProductChangeMixin):
@return: Dictionary of values.
"""
res = {}
cur_obj=self.pool.get('res.currency')
cur_obj = self.pool.get('res.currency')
for line in self.browse(cr, uid, ids, context=context):
res[line.id] = line.to_invoice and line.price_unit * line.product_uom_qty or 0
cur = line.repair_id.pricelist_id.currency_id
@ -626,34 +581,35 @@ class mrp_repair_line(osv.osv, ProductChangeMixin):
return res
_columns = {
'name' : fields.char('Description',size=64,required=True),
'repair_id': fields.many2one('mrp.repair', 'Repair Order Reference',ondelete='cascade', select=True),
'type': fields.selection([('add','Add'),('remove','Remove')],'Type', required=True),
'name': fields.char('Description', size=64, required=True),
'repair_id': fields.many2one('mrp.repair', 'Repair Order Reference', ondelete='cascade', select=True),
'type': fields.selection([('add', 'Add'), ('remove', 'Remove')], 'Type', required=True),
'to_invoice': fields.boolean('To Invoice'),
'product_id': fields.many2one('product.product', 'Product', required=True),
'invoiced': fields.boolean('Invoiced',readonly=True),
'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price')),
'price_subtotal': fields.function(_amount_line, string='Subtotal',digits_compute= dp.get_precision('Account')),
'invoiced': fields.boolean('Invoiced', readonly=True),
'price_unit': fields.float('Unit Price', required=True, digits_compute=dp.get_precision('Product Price')),
'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute=dp.get_precision('Account')),
'tax_id': fields.many2many('account.tax', 'repair_operation_line_tax', 'repair_operation_line_id', 'tax_id', 'Taxes'),
'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product Unit of Measure'), required=True),
'product_uom_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),
'invoice_line_id': fields.many2one('account.invoice.line', 'Invoice Line', readonly=True),
'location_id': fields.many2one('stock.location', 'Source Location', required=True, select=True),
'location_dest_id': fields.many2one('stock.location', 'Dest. Location', required=True, select=True),
'move_id': fields.many2one('stock.move', 'Inventory Move', readonly=True),
'lot_id': fields.many2one('stock.production.lot', 'Lot'),
'state': fields.selection([
('draft','Draft'),
('confirmed','Confirmed'),
('done','Done'),
('cancel','Cancelled')], 'Status', required=True, readonly=True,
('draft', 'Draft'),
('confirmed', 'Confirmed'),
('done', 'Done'),
('cancel', 'Cancelled')], 'Status', required=True, readonly=True,
help=' * The \'Draft\' status is set automatically as draft when repair order in draft status. \
\n* The \'Confirmed\' status is set automatically as confirm when repair order in confirm status. \
\n* The \'Done\' status is set automatically when repair order is completed.\
\n* The \'Cancelled\' status is set automatically when user cancel repair order.'),
}
_defaults = {
'state': lambda *a: 'draft',
'product_uom_qty': lambda *a: 1,
'state': lambda *a: 'draft',
'product_uom_qty': lambda *a: 1,
}
def onchange_operation_type(self, cr, uid, ids, type, guarantee_limit, company_id=False, context=None):
@ -670,7 +626,7 @@ class mrp_repair_line(osv.osv, ProductChangeMixin):
}}
location_obj = self.pool.get('stock.location')
warehouse_obj = self.pool.get('stock.warehouse')
location_id = location_obj.search(cr, uid, [('usage','=','production')], context=context)
location_id = location_obj.search(cr, uid, [('usage', '=', 'production')], context=context)
location_id = location_id and location_id[0] or False
if type == 'add':
@ -702,7 +658,8 @@ class mrp_repair_fee(osv.osv, ProductChangeMixin):
_description = 'Repair Fees Line'
def copy_data(self, cr, uid, id, default=None, context=None):
if not default: default = {}
if not default:
default = {}
default.update({'invoice_line_id': False, 'invoiced': False})
return super(mrp_repair_fee, self).copy_data(cr, uid, id, default, context)
@ -722,17 +679,18 @@ class mrp_repair_fee(osv.osv, ProductChangeMixin):
_columns = {
'repair_id': fields.many2one('mrp.repair', 'Repair Order Reference', required=True, ondelete='cascade', select=True),
'name': fields.char('Description', size=64, select=True,required=True),
'name': fields.char('Description', size=64, select=True, required=True),
'product_id': fields.many2one('product.product', 'Product'),
'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product Unit of Measure'), required=True),
'product_uom_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
'price_unit': fields.float('Unit Price', required=True),
'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
'price_subtotal': fields.function(_amount_line, string='Subtotal',digits_compute= dp.get_precision('Account')),
'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute=dp.get_precision('Account')),
'tax_id': fields.many2many('account.tax', 'repair_fee_line_tax', 'repair_fee_line_id', 'tax_id', 'Taxes'),
'invoice_line_id': fields.many2one('account.invoice.line', 'Invoice Line', readonly=True),
'to_invoice': fields.boolean('To Invoice'),
'invoiced': fields.boolean('Invoiced',readonly=True),
'invoiced': fields.boolean('Invoiced', readonly=True),
}
_defaults = {
'to_invoice': lambda *a: True,
}

View File

@ -1,26 +1,13 @@
-
!record {model: stock.move, id: stock_move_pcbasicpc0}:
company_id: base.main_company
date: !eval datetime.today().strftime("%Y-%m-%d %H:%M:%S")
date_expected: !eval datetime.today().strftime("%Y-%m-%d %H:%M:%S")
location_dest_id: stock.stock_location_14
location_id: stock.stock_location_stock
name: '[PCSC234] PC Assemble SC234'
product_id: product.product_product_3
product_qty: 1.0
product_uom: product.product_uom_unit
product_uos_qty: 1.0
-
!record {model: mrp.repair, id: mrp_repair_rmrp1}:
address_id: base.res_partner_address_1
guarantee_limit: !eval datetime.today().strftime("%Y-%m-%d")
invoice_method: 'none'
product_id: product.product_product_3
product_uom: product.product_uom_unit
partner_invoice_id: base.res_partner_address_1
location_dest_id: stock.stock_location_14
location_id: stock.stock_location_14
picking_type_id: stock.picking_type_out
move_id: 'stock_move_pcbasicpc0'
name: RMA00004
location_id: stock.stock_location_stock
operations:
- location_dest_id: stock.location_production
location_id: stock.stock_location_stock
@ -39,30 +26,16 @@
product_uom: product.product_uom_unit
price_unit: 50.0
partner_id: base.res_partner_9
product_id: product.product_product_3
-
!record {model: stock.move, id: stock.stock_move_stockmvmrp1}:
company_id: base.main_company
date: !eval datetime.today().strftime("%Y-%m-%d %H:%M:%S")
date_expected: !eval datetime.today().strftime("%Y-%m-%d %H:%M:%S")
location_dest_id: stock.stock_location_14
location_id: stock.stock_location_stock
name: '[PC-DEM] PC on Demand'
product_id: product.product_product_5
product_qty: 1.0
product_uom: product.product_uom_unit
product_uos_qty: 1.0
-
!record {model: mrp.repair, id: mrp_repair_rmrp0}:
product_id: product.product_product_5
product_uom: product.product_uom_unit
address_id: base.res_partner_address_1
guarantee_limit: !eval datetime.today().strftime("%Y-%m-%d")
invoice_method: 'after_repair'
partner_invoice_id: base.res_partner_address_1
location_dest_id: stock.stock_location_14
location_id: stock.stock_location_14
picking_type_id: stock.picking_type_out
move_id: 'stock.stock_move_stockmvmrp1'
name: RMA-00007
location_id: stock.stock_location_stock
operations:
- location_dest_id: stock.location_production
location_id: stock.stock_location_stock
@ -81,30 +54,16 @@
product_uom: product.product_uom_unit
price_unit: 50.0
partner_id: base.res_partner_9
product_id: product.product_product_5
-
!record {model: stock.move, id: stock.stock_move_stockmvmrp2}:
company_id: base.main_company
date: !eval datetime.today().strftime("%Y-%m-%d %H:%M:%S")
date_expected: !eval datetime.today().strftime("%Y-%m-%d %H:%M:%S")
location_dest_id: stock.stock_location_14
location_id: stock.stock_location_stock
name: '[LCD15] 15” LCD Monitor'
product_id: product.product_product_6
product_qty: 1.0
product_uom: product.product_uom_unit
product_uos_qty: 1.0
-
!record {model: mrp.repair, id: mrp_repair_rmrp2}:
product_id: product.product_product_6
product_uom: product.product_uom_unit
address_id: base.res_partner_address_1
guarantee_limit: !eval datetime.today().strftime("%Y-%m-%d")
invoice_method: 'b4repair'
partner_invoice_id: base.res_partner_address_1
location_dest_id: stock.stock_location_14
location_dest_id: stock.stock_location_stock
location_id: stock.stock_location_14
picking_type_id: stock.picking_type_out
move_id: 'stock.stock_move_stockmvmrp2'
name: RMA-00011
operations:
- location_dest_id: stock.location_production
location_id: stock.stock_location_stock
@ -122,5 +81,4 @@
product_uom_qty: 1.0
product_uom: product.product_uom_unit
price_unit: 50.0
partner_id: base.res_partner_9
product_id: product.product_product_6
partner_id: base.res_partner_9

View File

@ -9,7 +9,6 @@
<tree string="Repairs order" colors="gray:state in ('done','cancel');black:state not in ('done','cancel');blue:state=='draft'">
<field name="name" />
<field name="product_id" />
<field name="move_id"/>
<field name="partner_id"/>
<field name="address_id"/>
<field name="location_id" groups="stock.group_locations"/>
@ -45,22 +44,25 @@
<group>
<group>
<field name="product_id" on_change="onchange_product_id(product_id)" domain="[('type','!=','service')]"/>
<label for="product_qty"/>
<div>
<field name="product_qty" class="oe_inline"/>
<field name="product_uom" groups="product.group_uom" on_change="onchange_product_uom(product_id, product_uom)" class="oe_inline"/>
</div>
<field name="lot_id" domain="[('product_id', '=', product_id)]" context="{'default_product_id': product_id}" groups="stock.group_tracking_lot"/>
<field name="partner_id" on_change="onchange_partner_id(partner_id,address_id)" attrs="{'required':[('invoice_method','!=','none')]}"/>
<field name="address_id" groups="sale.group_delivery_invoice_address"/>
<field name="move_id" on_change="onchange_move_id(product_id, move_id)" context="{'default_product_id':product_id}"/>
<field name="location_id" attrs="{'required':[('deliver_bool','=', True)]}" groups="stock.group_locations"/>
</group>
<group>
<field name="location_id" on_change="onchange_location_id(location_id)" groups="stock.group_locations" domain="[('usage', 'in', ('internal', 'customer'))]"/>
<field name="location_dest_id" groups="stock.group_locations" domain="[('usage', 'in', ('internal', 'customer'))]"/>
<field name="company_id" groups="base.group_multi_company" widget="selection"/>
<field name="guarantee_limit"/>
<field name="deliver_bool"/>
<field name="picking_type_id" attrs="{'invisible': [('deliver_bool','=', False)], 'required': [('deliver_bool', '=', True)]}"/>
<field name="repaired" groups="base.group_no_one"/>
<field name="invoiced" groups="base.group_no_one"/>
</group>
</group>
<notebook>
<page string="Operations">
<field name="operations">
<field name="operations" context="{'default_product_uom_qty': product_qty}">
<form string="Operations" version="7.0">
<notebook>
<page string="Repair Line">
@ -94,6 +96,7 @@
<field name="type" on_change="onchange_operation_type(type,parent.guarantee_limit,parent.company_id,context)"/>
<field name="product_id" on_change="product_id_change(parent.pricelist_id,product_id,product_uom,product_uom_qty, parent.partner_id)"/>
<field name='name'/>
<field name="lot_id" domain="[('product_id', '=', product_id)]" context="{'default_product_id': product_id}" groups="stock.group_tracking_lot"/>
<field name="location_id" groups="stock.group_locations"/>
<field name="location_dest_id" groups="stock.group_locations"/>
<field name="product_uom_qty" string="Quantity"/>
@ -119,13 +122,15 @@
<page string="Invoicing">
<group col="4">
<field name="invoice_method"/>
<field name="invoice_id" context="{'form_view_ref': 'account.invoice_form'}"/>
<field name="partner_invoice_id" attrs="{'readonly':[('invoice_method','=', 'none')],'required':[('invoice_method','!=','none')]}" groups="sale.group_delivery_invoice_address"/>
<field
name="pricelist_id" groups="product.group_sale_pricelist" context="{'product_id':product_id}"
attrs="{'readonly':[('invoice_method','=', 'none')]}"/>
</group>
<!-- <field name="invoice_id"/> -->
<field name="fees_lines">
<separator string="Fees Lines"/>
<field name="fees_lines" attrs="{'readonly': [('invoice_method','=', 'none')]}">
<form string="Fees" version="7.0">
<label for="name" class="oe_edit_only"/>
<h2>
@ -167,15 +172,14 @@
</tree>
</field>
</page>
<page string="Extra Info">
<page string="Extra Info" groups="base.group_no_one">
<group>
<group>
<field name="picking_id"/>
<field name="invoice_id" context="{'form_view_ref': 'account.invoice_form'}"/>
<field name="company_id" groups="base.group_multi_company" widget="selection"/>
<field name="move_id"/>
</group>
<group>
<field name="location_dest_id" attrs="{'required':[('deliver_bool','=', True)]}" groups="stock.group_locations"/>
<field name="repaired"/>
<field name="invoiced"/>
</group>
</group>
</page>

View File

@ -8,7 +8,7 @@
-
!workflow {model: mrp.repair, action: repair_confirm, ref: mrp_repair_rmrp1}
-
I start the repairing process by clicking on "Start Repair" button for Invoice Method 'No Invoice'.
I start the repairing process by clicking on "Start Repair" button for Invoice Method 'No Invoice'.
-
!workflow {model: mrp.repair, action: repair_ready, ref: mrp_repair_rmrp1}
-
@ -22,9 +22,8 @@
!workflow {model: mrp.repair, action: action_repair_end, ref: mrp_repair_rmrp1}
-
I define Invoice Method 'No Invoice' option in this repair order.
So, I check that Invoice should not be created for this repair order.
So, I check that Invoice has not been created for this repair order.
-
!python {model: mrp.repair}: |
repair_id = self.browse(cr, uid, [ref('mrp_repair_rmrp1')], context=context)[0]
assert not repair_id.invoice_id.id, "Invoice should not exist for this repair order"
assert not repair_id.invoice_id.id, "Invoice should not exist for this repair order"

View File

@ -12,7 +12,7 @@
<field eval="False" name="doall"/>
<field eval="'procurement.order'" name="model"/>
<field eval="'run_scheduler'" name="function"/>
<field eval="'(False,True)'" name="args"/>
<field eval="'(True,)'" name="args"/>
</record>
<record id="sequence_proc_group_type" model="ir.sequence.type">

View File

@ -508,16 +508,6 @@ class product_product(osv.osv):
res.setdefault(id, 0.0)
return res
def _get_product_available_func(states, what):
def _product_available(self, cr, uid, ids, name, arg, context=None):
return {}.fromkeys(ids, 0.0)
return _product_available
_product_qty_available = _get_product_available_func(('done',), ('in', 'out'))
_product_virtual_available = _get_product_available_func(('confirmed','waiting','assigned','done'), ('in', 'out'))
_product_outgoing_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('out',))
_product_incoming_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('in',))
def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
res = {}
product_uom_obj = self.pool.get('product.uom')
@ -622,10 +612,6 @@ class product_product(osv.osv):
_inherit = ['mail.thread']
_order = 'default_code,name_template'
_columns = {
'qty_available': fields.function(_product_qty_available, type='float', string='Quantity On Hand'),
'virtual_available': fields.function(_product_virtual_available, type='float', string='Quantity Available'),
'incoming_qty': fields.function(_product_incoming_qty, type='float', string='Incoming'),
'outgoing_qty': fields.function(_product_outgoing_qty, type='float', string='Outgoing'),
'price': fields.function(_product_price, type='float', string='Price', digits_compute=dp.get_precision('Product Price')),
'lst_price': fields.function(_product_lst_price, type='float', string='Public Price', digits_compute=dp.get_precision('Product Price')),
'code': fields.function(_product_code, type='char', string='Internal Reference'),

View File

@ -33,14 +33,12 @@
<field name="model">product.product</field>
<field eval="7" name="priority"/>
<field name="arch" type="xml">
<tree colors="red:virtual_available&lt;0;blue:virtual_available&gt;=0 and state in ('draft', 'end', 'obsolete');black:virtual_available&gt;=0 and state not in ('draft', 'end', 'obsolete')" string="Products">
<tree string="Products">
<field name="default_code"/>
<field name="name"/>
<field name="categ_id" invisible="1"/>
<field name="type" invisible="1"/>
<field name="uom_id" string="Unit of Measure" groups="product.group_uom"/>
<field name="qty_available"/>
<field name="virtual_available"/>
<field name="lst_price"/>
<field name="price" invisible="not context.get('pricelist',False)"/>
<field name="standard_price" invisible="1"/>

View File

@ -403,7 +403,7 @@ class procurement_order(osv.osv):
def _run(self, cr, uid, procurement, context=None):
requisition_obj = self.pool.get('purchase.requisition')
warehouse_obj = self.pool.get('stock.warehouse')
if procurement.product_id.purchase_requisition:
if procurement.rule_id and procurement.rule_id.action == 'buy' and procurement.product_id.purchase_requisition:
warehouse_id = warehouse_obj.search(cr, uid, [('company_id', '=', procurement.company_id.id)], context=context)
requisition_id = requisition_obj.create(cr, uid, {
'origin': procurement.origin,

View File

@ -275,7 +275,7 @@ class procurement_order(osv.osv):
company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
move_obj = self.pool.get('stock.move')
#Minimum stock rules
self. _procure_orderpoint_confirm(cr, uid, automatic=False,use_new_cursor=False, context=context, user_id=False)
self. _procure_orderpoint_confirm(cr, uid, use_new_cursor=False, context=context, user_id=False)
#Search all confirmed stock_moves and try to assign them
confirmed_ids = move_obj.search(cr, uid, [('state', '=', 'confirmed'), ('company_id','=', company.id)], limit=None, order='picking_priority desc, date_expected asc', context=context)
@ -295,54 +295,6 @@ class procurement_order(osv.osv):
pass
return {}
def _prepare_automatic_op_procurement(self, cr, uid, product, warehouse, location_id, context=None):
return {'name': _('Automatic OP: %s') % (product.name,),
'origin': _('SCHEDULER'),
'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
'product_id': product.id,
'product_qty': -product.virtual_available,
'product_uom': product.uom_id.id,
'location_id': location_id,
'company_id': warehouse.company_id.id,
}
def create_automatic_op(self, cr, uid, context=None):
"""
Create procurement of virtual stock < 0
@param self: The object pointer
@param cr: The current row, from the database cursor,
@param uid: The current user ID for security checks
@param context: A standard dictionary for contextual values
@return: Dictionary of values
"""
if context is None:
context = {}
product_obj = self.pool.get('product.product')
proc_obj = self.pool.get('procurement.order')
warehouse_obj = self.pool.get('stock.warehouse')
warehouse_ids = warehouse_obj.search(cr, uid, [], context=context)
products_ids = product_obj.search(cr, uid, [], order='id', context=context)
for warehouse in warehouse_obj.browse(cr, uid, warehouse_ids, context=context):
context['warehouse'] = warehouse
# Here we check products availability.
# We use the method 'read' for performance reasons, because using the method 'browse' may crash the server.
for product_read in product_obj.read(cr, uid, products_ids, ['virtual_available'], context=context):
if product_read['virtual_available'] >= 0.0:
continue
product = product_obj.browse(cr, uid, [product_read['id']], context=context)[0]
location_id = warehouse.lot_stock_id.id
proc_id = proc_obj.create(cr, uid,
self._prepare_automatic_op_procurement(cr, uid, product, warehouse, location_id, context=context),
context=context)
self.assign(cr, uid, [proc_id])
self.run(cr, uid, [proc_id])
return True
def _get_orderpoint_date_planned(self, cr, uid, orderpoint, start_date, context=None):
date_planned = start_date + \
@ -365,7 +317,7 @@ class procurement_order(osv.osv):
[order_point.product_id.id],
{'location': order_point.location_id.id})[order_point.product_id.id]['virtual_available']
def _procure_orderpoint_confirm(self, cr, uid, automatic=False,\
def _procure_orderpoint_confirm(self, cr, uid, \
use_new_cursor=False, context=None, user_id=False):
'''
Create procurement based on Orderpoint
@ -388,8 +340,6 @@ class procurement_order(osv.osv):
procurement_obj = self.pool.get('procurement.order')
offset = 0
ids = [1]
if automatic:
self.create_automatic_op(cr, uid, context=context)
while ids:
ids = orderpoint_obj.search(cr, uid, [], offset=offset, limit=100)
for op in orderpoint_obj.browse(cr, uid, ids, context=context):

View File

@ -21,6 +21,7 @@
from openerp.osv import fields, osv
from openerp.tools.translate import _
from openerp.tools.safe_eval import safe_eval as eval
import openerp.addons.decimal_precision as dp
class product_product(osv.osv):
@ -99,13 +100,13 @@ class product_product(osv.osv):
)
def _get_domain_dates(self, cr, uid, ids, context):
from_date = context.get('from_date',False)
to_date = context.get('to_date',False)
from_date = context.get('from_date', False)
to_date = context.get('to_date', False)
domain = []
if from_date:
domain.append(('date','>=',from_date))
domain.append(('date', '>=', from_date))
if to_date:
domain.append(('date','<=',to_date))
domain.append(('date', '<=', to_date))
return domain
def _product_available(self, cr, uid, ids, field_names=None, arg=False, context=None):
@ -114,26 +115,25 @@ class product_product(osv.osv):
domain_products = [('product_id', 'in', ids)]
domain_quant, domain_move_in, domain_move_out = self._get_domain_locations(cr, uid, ids, context=context)
domain_move_in += self._get_domain_dates(cr, uid, ids, context=context) + [('state','not in',('done','cancel'))] + domain_products
domain_move_out += self._get_domain_dates(cr, uid, ids, context=context) + [('state','not in',('done','cancel'))] + domain_products
domain_move_in += self._get_domain_dates(cr, uid, ids, context=context) + [('state', 'not in', ('done', 'cancel'))] + domain_products
domain_move_out += self._get_domain_dates(cr, uid, ids, context=context) + [('state', 'not in', ('done', 'cancel'))] + domain_products
domain_quant += domain_products
if context.get('lot_id') or context.get('owner_id') or context.get('package_id'):
if context.get('lot_id'):
domain_quant.append(('lot_id','=',context['lot_id']))
domain_quant.append(('lot_id', '=', context['lot_id']))
if context.get('owner_id'):
domain_quant.append(('owner_id','=',context['owner_id']))
domain_quant.append(('owner_id', '=', context['owner_id']))
if context.get('package_id'):
domain_quant.append(('package_id','=',context['package_id']))
moves_in = []
domain_quant.append(('package_id', '=', context['package_id']))
moves_in = []
moves_out = []
else:
# if field_names in ['incoming_qty', 'outgoing_qty', 'virtual_available']:
moves_in = self.pool.get('stock.move').read_group(cr, uid, domain_move_in, ['product_id', 'product_qty'], ['product_id'], context=context)
moves_in = self.pool.get('stock.move').read_group(cr, uid, domain_move_in, ['product_id', 'product_qty'], ['product_id'], context=context)
moves_out = self.pool.get('stock.move').read_group(cr, uid, domain_move_out, ['product_id', 'product_qty'], ['product_id'], context=context)
quants = self.pool.get('stock.quant').read_group(cr, uid, domain_quant, ['product_id', 'qty'], ['product_id'], context=context)
quants = dict(map(lambda x: (x['product_id'][0], x['qty']), quants))
moves_in = dict(map(lambda x: (x['product_id'][0], x['product_qty']), moves_in))
moves_out = dict(map(lambda x: (x['product_id'][0], x['product_qty']), moves_out))
@ -144,16 +144,39 @@ class product_product(osv.osv):
'incoming_qty': moves_in.get(id, 0.0),
'outgoing_qty': moves_out.get(id, 0.0),
'virtual_available': quants.get(id, 0.0) + moves_in.get(id, 0.0) - moves_out.get(id, 0.0),
}
}
return res
def _search_product_quantity(self, cr, uid, obj, name, domain, context):
res = []
for field, operator, value in domain:
#to prevent sql injections
assert field in ('qty_available', 'virtual_available', 'incoming_qty', 'outgoing_qty'), 'Invalid domain left operand'
assert operator in ('<', '>', '=', '<=', '>='), 'Invalid domain operator'
assert isinstance(value, (float, int)), 'Invalid domain right operand'
if operator == '=':
operator = '=='
product_ids = self.search(cr, uid, [], context=context)
ids = []
if product_ids:
#TODO: use a query instead of this browse record which is probably making the too much requests, but don't forget
#the context that can be set with a location, an owner...
for element in self.browse(cr, uid, product_ids, context=context):
if eval(str(element[field]) + operator + str(value)):
ids.append(element.id)
res.append(('id', 'in', ids))
return res
_columns = {
'reception_count': fields.function(_stock_move_count, string="Reception", type='integer', multi='pickings'),
'delivery_count': fields.function(_stock_move_count, string="Delivery", type='integer', multi='pickings'),
'qty_available': fields.function(_product_available, multi='qty_available',
type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
string='Quantity On Hand',
fnct_search=_search_product_quantity,
help="Current quantity of products.\n"
"In a context with a single Stock Location, this includes "
"goods stored at this Location, or any of its children.\n"
@ -165,8 +188,9 @@ class product_product(osv.osv):
"Otherwise, this includes goods stored in any Stock Location "
"with 'internal' type."),
'virtual_available': fields.function(_product_available, multi='qty_available',
type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
string='Forecasted Quantity',
type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
string='Forecast Quantity',
fnct_search=_search_product_quantity,
help="Forecast quantity (computed as Quantity On Hand "
"- Outgoing + Incoming)\n"
"In a context with a single Stock Location, this includes "
@ -179,8 +203,9 @@ class product_product(osv.osv):
"Otherwise, this includes goods stored in any Stock Location "
"with 'internal' type."),
'incoming_qty': fields.function(_product_available, multi='qty_available',
type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
string='Incoming',
fnct_search=_search_product_quantity,
help="Quantity of products that are planned to arrive.\n"
"In a context with a single Stock Location, this includes "
"goods arriving to this Location, or any of its children.\n"
@ -193,8 +218,9 @@ class product_product(osv.osv):
"Otherwise, this includes goods arriving to any Stock "
"Location with 'internal' type."),
'outgoing_qty': fields.function(_product_available, multi='qty_available',
type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
string='Outgoing',
fnct_search=_search_product_quantity,
help="Quantity of products that are planned to leave.\n"
"In a context with a single Stock Location, this includes "
"goods leaving this Location, or any of its children.\n"

View File

@ -11,6 +11,30 @@
<field name="location_id" widget="selection" context="{'location': self}"/>
<field name="warehouse_id" widget="selection" context="{'warehouse': self}"/>
</field>
<field name="categ_id" position="before">
<separator/>
<filter name="real_stock_available" string="Available Products" domain="[('qty_available','&gt;',0)]"/>
<filter name="virtual_stock_available" string="Forecast Available Products" domain="[('virtual_available','&gt;',0)]"/>
<filter name="real_stock_negative" string="Exhausted Stock" domain="[('qty_available','&lt;=',0)]"/>
<filter name="virtual_stock_negative" string="Forecast Exhausted Stock" domain="[('virtual_available','&lt;=',0)]"/>
</field>
</field>
</record>
<record id="view_stock_product_tree" model="ir.ui.view">
<field name="name">product.stock.tree.inherit</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="product.product_product_tree_view"/>
<field name="arch" type="xml">
<field name="uom_id" position="after">
<field name="qty_available"/>
<field name="virtual_available"/>
</field>
<tree position="attributes">
<attribute name="colors">{'red':virtual_available&lt;0, 'blue':virtual_available&gt;=0 and state in ('draft', 'end', 'obsolete'), 'black':virtual_available&gt;=0 and state not in ('draft', 'end', 'obsolete')}</attribute>
</tree>
</field>
</record>

View File

@ -69,7 +69,7 @@ This installs the module product_expiry."""),
help="""Allows you to create and manage your packaging dimensions and types you want to be maintained in your system."""),
'group_stock_tracking_lot': fields.boolean("Track serial number on products",
implied_group='stock.group_tracking_lot',
help="""This allows you to manage products by using serial numbers. When you select a serial number on product moves, you can get the upstream or downstream traceability of that product."""),
help="""This allows you to manage products by using serial numbers. When you select a serial number on product moves, you can get the traceability of that product."""),
'group_stock_tracking_owner': fields.boolean("Manage owner on stock",
implied_group='stock.group_tracking_owner',
help="""This way you can receive products attributed to a certain owner. """),

View File

@ -171,12 +171,14 @@ class stock_location_route(osv.osv):
'warehouse_selectable': fields.boolean('Applicable on Warehouse'),
'supplied_wh_id': fields.many2one('stock.warehouse', 'Supplied Warehouse'),
'supplier_wh_id': fields.many2one('stock.warehouse', 'Supplier Warehouse'),
'company_id': fields.many2one('res.company', 'Company', select=1, help='Let this field empty if this route is shared between all companies'),
}
_defaults = {
'sequence': lambda self, cr, uid, ctx: 0,
'active': True,
'product_selectable': True,
'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.location.route', context=c),
}
@ -818,11 +820,11 @@ class stock_picking(osv.osv):
return True
def cancel_assign(self, cr, uid, ids, context=None):
""" Cancels picking and moves.
""" 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]
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
@ -1162,6 +1164,34 @@ class stock_production_lot(osv.osv):
('name_ref_uniq', 'unique (name, ref)', 'The combination of Serial Number and internal reference must be unique !'),
]
def action_traceability(self, cr, uid, ids, context=None):
""" It traces the information of lots
@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: 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)))+"])]",
'name': _('Traceability'),
'view_mode': 'tree,form',
'view_type': 'form',
'context': {'tree_view_ref': 'stock.view_move_tree'},
'res_model': 'stock.move',
'type': 'ir.actions.act_window',
}
return False
# ----------------------------------------------------
# Move
@ -1821,7 +1851,7 @@ class stock_move(osv.osv):
if qty > 0:
self.check_tracking(cr, uid, move, move.restrict_lot_id.id, context=context)
quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, move.product_id, qty, domain=main_domain, prefered_domain=prefered_domain, fallback_domain=fallback_domain, restrict_lot_id=move.restrict_lot_id.id, restrict_partner_id=move.restrict_partner_id.id, context=context)
quant_obj.quants_move(cr, uid, quants, move, context=context)
quant_obj.quants_move(cr, uid, quants, move, lot_id=move.restrict_lot_id.id, owner_id=move.restrict_partner_id.id, context=context)
#unreserve the quants and make them available for other operations/moves
quant_obj.quants_unreserve(cr, uid, move, context=context)
@ -1852,7 +1882,7 @@ class stock_move(osv.osv):
raise osv.except_osv(_('User Error!'), _('You can only delete draft moves.'))
return super(stock_move, self).unlink(cr, uid, ids, context=context)
def action_scrap(self, cr, uid, ids, quantity, location_id, restrict_lot_id=False, context=None):
def action_scrap(self, cr, uid, ids, quantity, location_id, restrict_lot_id=False, restrict_partner_id=False, context=None):
""" Move the scrap/damaged product into scrap location
@param cr: the database cursor
@param uid: the user id
@ -1884,6 +1914,7 @@ class stock_move(osv.osv):
'scrapped': True,
'location_dest_id': location_id,
'restrict_lot_id': restrict_lot_id,
'restrict_partner_id': restrict_partner_id,
}
new_move = self.copy(cr, uid, move.id, default_val)
@ -1898,7 +1929,7 @@ 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, context=None):
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)
@ -1921,16 +1952,18 @@ class stock_move(osv.osv):
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, context=ctx))
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}, context=context)
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, context=None):
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
:param move: browse record
:param qty: float. quantity to split (given in product UoM)
@ -1956,6 +1989,7 @@ class stock_move(osv.osv):
'move_dest_id': False,
'reserved_quant_ids': [],
'restrict_lot_id': restrict_lot_id,
'restrict_partner_id': restrict_partner_id
}
if context.get('source_location_id'):
defaults['location_id'] = context['source_location_id']
@ -2211,6 +2245,14 @@ class stock_inventory_line(osv.osv):
_name = "stock.inventory.line"
_description = "Inventory Line"
_rec_name = "inventory_id"
_order = "inventory_id, location_name, product_code, product_name, prod_lot_id"
def _get_product_name_change(self, cr, uid, ids, context=None):
return self.pool.get('stock.inventory.line').search(cr, uid, [('product_id', 'in', ids)], context=context)
def _get_location_change(self, cr, uid, ids, context=None):
return self.pool.get('stock.inventory.line').search(cr, uid, [('location_id', 'in', ids)], context=context)
_columns = {
'inventory_id': fields.many2one('stock.inventory', 'Inventory', ondelete='cascade', select=True),
'location_id': fields.many2one('stock.location', 'Location', required=True, select=True),
@ -2223,6 +2265,15 @@ class stock_inventory_line(osv.osv):
'state': fields.related('inventory_id', 'state', type='char', string='Status', readonly=True),
'th_qty': fields.float('Theoretical Quantity', readonly=True),
'partner_id': fields.many2one('res.partner', 'Owner'),
'product_name': fields.related('product_id', 'name', type='char', string='Product name', store={
'product.product': (_get_product_name_change, ['name', 'default_code'], 20),
'stock.inventory.line': (lambda self, cr, uid, ids, c={}: ids, ['product_id'], 20),}),
'product_code': fields.related('product_id', 'default_code', type='char', string='Product code', store={
'product.product': (_get_product_name_change, ['name', 'default_code'], 20),
'stock.inventory.line': (lambda self, cr, uid, ids, c={}: ids, ['product_id'], 20),}),
'location_name': fields.related('location_id', 'complete_name', type='char', string='Location name', store={
'stock.location': (_get_location_change, ['name', 'location_id', 'active'], 20),
'stock.inventory.line': (lambda self, cr, uid, ids, c={}: ids, ['location_id'], 20),}),
}
_defaults = {
@ -2729,10 +2780,12 @@ class stock_warehouse(osv.osv):
'warehouse_id': new_id,
'code': 'outgoing',
'sequence_id': out_seq_id,
'return_picking_type_id': in_type_id,
'default_location_src_id': output_loc.id,
'default_location_dest_id': customer_loc.id,
'sequence': max_sequence + 4,
'color': color}, context=context)
picking_type_obj.write(cr, uid, [in_type_id], {'return_picking_type_id': out_type_id}, context=context)
int_type_id = picking_type_obj.create(cr, uid, vals={
'name': _('Internal Transfers'),
'warehouse_id': new_id,

View File

@ -78,7 +78,7 @@
<field name="subtype_id" ref="mail.mt_comment"/>
<field name="subject">Warehouse Management application installed!</field>
<field name="body"><![CDATA[<p>Manage your product inventoy and stock locations: you can control stock moves history and planning,
watch your stock valuation, and track production lots upstream and downstream (based on serial numbers).</p>]]></field>
watch your stock valuation, and track production lots (based on serial numbers).</p>]]></field>
</record>

View File

@ -191,8 +191,7 @@
<field name="arch" type="xml">
<form string="Serial Number" version="7.0">
<div class="oe_button_box oe_right">
<button name="action_traceability" string="Upstream Traceability" type="object"/>
<button name="action_traceability" string="Downstream Traceability" type="object"/>
<button name="action_traceability" string="Traceability" type="object" attrs="{'invisible': [('quant_ids','=',[])]}"/>
</div>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
@ -261,9 +260,9 @@
Click to add a serial number.
</p><p>
This is the list of all the production lots you recorded. When
you select a lot, you can get the upstream or downstream
you select a lot, you can get the
traceability of the products contained in lot. By default, the
list is filtred on the serial numbers that are available in
list is filtered on the serial numbers that are available in
your warehouse but you can uncheck the 'Available' button to
get all the lots you produced, received or delivered to
customers.
@ -1186,7 +1185,7 @@
<field eval="3" name="priority"/>
<field name="arch" type="xml">
<search string="Stock Moves">
<field name="origin" filter_domain="['|',('origin','ilike',self),('picking_id','ilike',self)]" string="Reference"/>
<field name="origin" filter_domain="['|', '|', ('origin', 'ilike', self), ('name', 'ilike', self), ('picking_id', 'ilike', self)]" string="Reference"/>
<field name="date" groups="base.group_no_one"/>
<filter icon="terp-camera_test" string="Ready" name="ready" domain="[('state','=','assigned')]" help="Stock moves that are Available (Ready to process)"/>
@ -1848,7 +1847,7 @@
<field name="res_model">stock.warehouse.orderpoint</field>
</record>
<record model="ir.actions.act_window" id="product_open_quants">
<field name="context">{'search_default_product_id': active_id}</field>
<field name="context">{'search_default_product_id': active_id, 'search_default_locationgroup':1}</field>
<field name="name">Quants</field>
<field name="res_model">stock.quant</field>
</record>
@ -1891,10 +1890,9 @@
</xpath>
<xpath expr="//div[@name='buttons']" position="inside">
<button string="Inventory" name="%(action_product_location_tree)d" type="action" attrs="{'invisible':[('type', '=', 'service')]}" groups="stock.group_locations"/>
<button string="Inventory" name="%(product_open_quants)d" type="action" attrs="{'invisible':[('type', '=', 'service')]}" groups="stock.group_locations"/>
<button string="Moves" name= "%(act_product_stock_move_open)d" type="action" attrs="{'invisible':[('type', '=', 'service')]}" groups="stock.group_stock_user"/>
<button string="Orderpoints" name="%(product_open_orderpoint)d" type="action" attrs="{'invisible':[('type', '=', 'service')]}"/>
<button string="Quants" name="%(product_open_quants)d" type="action" attrs="{'invisible':[('type', '=', 'service')]}" groups="base.group_no_one"/>
</xpath>
<xpath expr="//group[@string='Sale Conditions']" position="inside">
@ -2082,6 +2080,7 @@
</div>
<group>
<group>
<field name="company_id" groups="base.group_multi_company" widget="selection"/>
<field name="sequence" string="Sequence" groups="base.group_no_one"/>
</group>
<group>

View File

@ -31,13 +31,6 @@ class procurement_compute(osv.osv_memory):
_name = 'procurement.orderpoint.compute'
_description = 'Automatic Order Point'
_columns = {
'automatic': fields.boolean('Automatic Orderpoint', help='If the stock of a product is under 0, it will act like an orderpoint'),
}
_defaults = {
'automatic': False,
}
def _procure_calculation_orderpoint(self, cr, uid, ids, context=None):
"""
@ -51,7 +44,7 @@ class procurement_compute(osv.osv_memory):
#As this function is in a new thread, I need to open a new cursor, because the old one may be closed
new_cr = self.pool.db.cursor()
for proc in self.browse(new_cr, uid, ids, context=context):
proc_obj._procure_orderpoint_confirm(new_cr, uid, automatic=proc.automatic, use_new_cursor=new_cr.dbname, context=context)
proc_obj._procure_orderpoint_confirm(new_cr, uid, use_new_cursor=new_cr.dbname, context=context)
#close the new cursor
new_cr.close()
return {}

View File

@ -12,9 +12,6 @@
<group>
<label string="Wizard checks all the stock minimum rules and generate procurement order."/>
</group>
<group col="2">
<field name="automatic"/>
</group>
<footer>
<button name="procure_calculation" string="Compute Stock" type="object" class="oe_highlight" />
or

View File

@ -15,7 +15,8 @@
<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"/>
<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>

View File

@ -66,11 +66,11 @@ class stock_return_picking(osv.osv_memory):
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_qty
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_qty
qty -= returned_move.product_uom_qty
if qty > 0:
result1.append({'product_id': line.product_id.id, 'quantity': qty, 'move_id': line.id})
@ -127,7 +127,7 @@ class stock_return_picking(osv.osv_memory):
raise osv.except_osv(_('Warning!'), _("Please specify at least one non-zero quantity."))
pick_obj.action_confirm(cr, uid, [new_picking], context=context)
pick_obj.force_assign(cr, uid, [new_picking], context)
pick_obj.action_assign(cr, uid, [new_picking], context)
return new_picking, pick_type_id
def create_returns(self, cr, uid, ids, context=None):