diff --git a/addons/sale_mrp/__openerp__.py b/addons/sale_mrp/__openerp__.py index f36a565a3c7d464101d48726c291777d50c3ab25..b9c7b03356c34b55b9869b5efc75ec209a678899 100644 --- a/addons/sale_mrp/__openerp__.py +++ b/addons/sale_mrp/__openerp__.py @@ -21,9 +21,9 @@ from sales order. It adds sales name and sales Reference on production order. ], 'demo': [], 'test':[ - 'test/cancellation_propagated.yml', - 'test/sale_mrp.yml', - ], + 'test/cancellation_propagated.yml', + 'test/sale_mrp.yml', + ], 'installable': True, 'auto_install': True, } diff --git a/addons/sale_mrp/sale_mrp.py b/addons/sale_mrp/sale_mrp.py index a113adaae0924ba040b64f154900108619262acd..9fb457abfb84fdbf499457ea4899e05cd8f81e86 100644 --- a/addons/sale_mrp/sale_mrp.py +++ b/addons/sale_mrp/sale_mrp.py @@ -1,93 +1,86 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.osv import fields, osv +from openerp import api, fields, models +from openerp.tools import float_compare -class mrp_production(osv.osv): - _inherit = 'mrp.production' - def _ref_calc(self, cr, uid, ids, field_names=None, arg=False, context=None): - """ Finds reference of sales order for production order. - @param field_names: Names of fields. - @param arg: User defined arguments - @return: Dictionary of values. - """ - res = {} - if not field_names: - field_names = [] - for id in ids: - res[id] = {}.fromkeys(field_names, False) - for f in field_names: - field_name = False - if f == 'sale_name': - field_name = 'name' - if f == 'sale_ref': - field_name = 'client_order_ref' - for key, value in self._get_sale_ref(cr, uid, ids, field_name).items(): - res[key][f] = value - return res +class MrpProduction(models.Model): + _inherit = 'mrp.production' - def _get_sale_ref(self, cr, uid, ids, field_name=False): - move_obj = self.pool.get('stock.move') + sale_name = fields.Char(compute='_compute_sale_name_sale_ref', string='Sale Name', help='Indicate the name of sales order.') + sale_ref = fields.Char(compute='_compute_sale_name_sale_ref', string='Sale Reference', help='Indicate the Customer Reference from sales order.') - def get_parent_move(move_id): - move = move_obj.browse(cr, uid, move_id) + @api.multi + def _compute_sale_name_sale_ref(self): + def get_parent_move(move): if move.move_dest_id: return get_parent_move(move.move_dest_id.id) - return move_id - - res = {} - productions = self.browse(cr, uid, ids) - for production in productions: - res[production.id] = False + return move + for production in self: if production.move_prod_id: - parent_move_line = get_parent_move(production.move_prod_id.id) - if parent_move_line: - move = move_obj.browse(cr, uid, parent_move_line) - if field_name == 'name': - res[production.id] = move.procurement_id and move.procurement_id.sale_line_id and move.procurement_id.sale_line_id.order_id.name or False - if field_name == 'client_order_ref': - res[production.id] = move.procurement_id and move.procurement_id.sale_line_id and move.procurement_id.sale_line_id.order_id.client_order_ref or False - return res + move = get_parent_move(production.move_prod_id) + production.sale_name = move.procurement_id and move.procurement_id.sale_line_id and move.procurement_id.sale_line_id.order_id.name or False + production.sale_ref = move.procurement_id and move.procurement_id.sale_line_id and move.procurement_id.sale_line_id.order_id.client_order_ref or False - _columns = { - 'sale_name': fields.function(_ref_calc, multi='sale_name', type='char', string='Sale Name', help='Indicate the name of sales order.'), - 'sale_ref': fields.function(_ref_calc, multi='sale_name', type='char', string='Sale Reference', help='Indicate the Customer Reference from sales order.'), - } +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' -class sale_order(osv.Model): - _inherit = 'sale.order' + property_ids = fields.Many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}) - def _prepare_order_line_procurement(self, cr, uid, order, line, group_id=False, context=None): - result = super(sale_order, self)._prepare_order_line_procurement(cr, uid, order, line, group_id=group_id, context=context) - result['property_ids'] = [(6, 0, [x.id for x in line.property_ids])] - return result + @api.multi + def _get_delivered_qty(self): + self.ensure_one() + precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') + # In the case of a kit, we need to check if all components are shipped. We use a all or + # nothing policy. A product can have several BoMs, we don't know which one was used when the + # delivery was created. + bom_delivered = {} + for bom in self.product_id.product_tmpl_id.bom_ids: + if bom.type != 'phantom': + continue + bom_delivered[bom.id] = False + bom_exploded = self.env['mrp.bom']._bom_explode(bom, self.product_id.product_tmpl_id, self.product_uom_qty)[0] + for bom_line in bom_exploded: + qty = 0.0 + for move in self.procurement_ids.mapped('move_ids'): + if move.state == 'done' and move.product_id.id == bom_line.get('product_id', False): + qty += self.env['product.uom']._compute_qty_obj(move.product_uom, move.product_uom_qty, self.product_uom) + if float_compare(qty, bom_line['product_qty'], precision_digits=precision) < 0: + bom_delivered[bom.id] = False + break + else: + bom_delivered[bom.id] = True + if bom_delivered and any(bom_delivered.values()): + return self.product_uom_qty + elif bom_delivered: + return 0.0 + return super(SaleOrderLine, self)._get_delivered_qty() -class sale_order_line(osv.osv): + @api.multi + def _prepare_order_line_procurement(self, group_id=False): + vals = super(SaleOrderLine, self)._prepare_order_line_procurement(group_id=group_id) + vals['property_ids'] = [(6, 0, self.property_ids.ids)] + return vals - _inherit = 'sale.order.line' - _columns = { - 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}), - } - -class stock_move(osv.osv): +class StockMove(models.Model): _inherit = 'stock.move' - - def _prepare_procurement_from_move(self, cr, uid, move, context=None): - res = super(stock_move, self)._prepare_procurement_from_move(cr, uid, move, context=context) + + @api.model + def _prepare_procurement_from_move(self, move): + res = super(StockMove, self)._prepare_procurement_from_move(move) if res and move.procurement_id and move.procurement_id.property_ids: - res['property_ids'] = [(6, 0, [x.id for x in move.procurement_id.property_ids])] + res['property_ids'] = [(6, 0, self.property_ids.ids)] return res - def _action_explode(self, cr, uid, move, context=None): + @api.model + def _action_explode(self, move): """ Explodes pickings. @param move: Stock moves @return: True """ - if context is None: - context = {} - property_ids = map(int, move.procurement_id.sale_line_id.property_ids or []) - return super(stock_move, self)._action_explode(cr, uid, move, context=dict(context, property_ids=property_ids)) + property_ids = move.procurement_id.sale_line_id.property_ids.ids + return super(StockMove, self.with_context(property_ids=property_ids))._action_explode(move) diff --git a/addons/sale_mrp/sale_mrp_view.xml b/addons/sale_mrp/sale_mrp_view.xml index 002a5641fc5a800bd474482cd06194d2c6363116..a30fbcf54f0538439c235cc07bd1989277413e6e 100644 --- a/addons/sale_mrp/sale_mrp_view.xml +++ b/addons/sale_mrp/sale_mrp_view.xml @@ -1,8 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <openerp> <data> - - <record id="view_mrp_production_form" model="ir.ui.view"> + <record id="mrp_production_form_view_inherit_sale_mrp" model="ir.ui.view"> <field name="name">mrp.production.form</field> <field name="model">mrp.production</field> <field name="inherit_id" ref="mrp.mrp_production_form_view"/> @@ -16,18 +15,16 @@ </field> </record> - <record id="view_order_form_inherit_mrp" model="ir.ui.view"> + <record id="view_order_form_inherit_sale_mrp" model="ir.ui.view"> <field name="name">sale.order.form.sale.stock.mrp</field> <field name="model">sale.order</field> <field name="inherit_id" ref="sale.view_order_form"/> <field name="arch" type="xml"> - <xpath expr="//page/field[@name='order_line']/form/group/group/field[@name='th_weight']" position="after"> + <xpath expr="//page/field[@name='order_line']/form/group/group/field[@name='tax_id']" position="after"> <field name="property_ids" widget="many2many_tags" groups="sale.group_mrp_properties"/> </xpath> </field> </record> - - </data> </openerp> diff --git a/addons/sale_mrp/test/cancellation_propagated.yml b/addons/sale_mrp/test/cancellation_propagated.yml index 0d72e3d9f3e5231c885dbfdc1bdf4871e3bbc84d..84b795b1d32d71dca871744ebef7797a484f2a19 100644 --- a/addons/sale_mrp/test/cancellation_propagated.yml +++ b/addons/sale_mrp/test/cancellation_propagated.yml @@ -43,14 +43,18 @@ partner_id: base.res_partner_3 note: Create Sales order warehouse_id: wh_pps + pricelist_id: product.list0 order_line: - product_id: product_manu + name: "product_manu" product_uom_qty: 5.00 + product_uom: product.product_uom_unit - Confirm sales order - - !workflow {model: sale.order, action: order_confirm, ref: sale_order_product_manu} + !python {model: sale.order}: | + self.action_confirm(cr, uid, ref("sale_order_product_manu"), context=context) - I run scheduler. - diff --git a/addons/sale_mrp/test/sale_mrp.yml b/addons/sale_mrp/test/sale_mrp.yml index 023bdae08f4c715e10cb3e0240641f9002df1945..eddf48a20bb98b8e73fc59e62debface3ccb3054 100644 --- a/addons/sale_mrp/test/sale_mrp.yml +++ b/addons/sale_mrp/test/sale_mrp.yml @@ -16,7 +16,6 @@ !record {model: product.template, id: product_template_slidermobile0}: categ_id: product_category_allproductssellable0 list_price: 200.0 - mes_type: fixed name: Slider Mobile standard_price: 189.0 type: product @@ -28,7 +27,6 @@ !record {model: product.product, id: product_product_slidermobile0}: categ_id: product_category_allproductssellable0 list_price: 200.0 - mes_type: fixed name: Slider Mobile seller_delay: '1' seller_ids: @@ -72,10 +70,8 @@ product_uom: product.product_uom_unit product_uom_qty: 500.0 state: draft - delay: 7.0 + customer_lead: 7.0 product_id: product_product_slidermobile0 - product_uos_qty: 500.0 - order_policy: manual partner_id: base.res_partner_4 partner_invoice_id: base.res_partner_address_7 partner_shipping_id: base.res_partner_address_7 @@ -84,7 +80,8 @@ - I confirm the sale order - - !workflow {model: sale.order, action: order_confirm, ref: sale_order_so0} + !python {model: sale.order}: | + self.action_confirm(cr, uid, ref("sale_order_so0"), context=context) - I verify that a procurement has been generated for sale order - @@ -120,5 +117,5 @@ mnf_id = mnf_obj.search(cr, uid, [('origin','like',so.name)]) assert mnf_id, 'Manufacturing order has not been generated' mo = mnf_obj.browse(cr, uid, mnf_id)[0] - assert mo.sale_name == so.name, 'Wrong Name for the Manufacturing Order. Expected %s, Got %s' % (so.name, mo.name) + assert mo.sale_name == so.name, 'Wrong Name for the Manufacturing Order. Expected %s, Got %s' % (so.name, mo.sale_name) assert mo.sale_ref == so.client_order_ref, 'Wrong Sale Reference for the Manufacturing Order' diff --git a/addons/sale_mrp/tests/test_move_explode.py b/addons/sale_mrp/tests/test_move_explode.py index c5f803408e081b15c4ecd0370ca02bacdc25e185..a7e3bdea946b80f769afd26fde73edd82b430f05 100644 --- a/addons/sale_mrp/tests/test_move_explode.py +++ b/addons/sale_mrp/tests/test_move_explode.py @@ -8,66 +8,71 @@ class TestMoveExplode(common.TransactionCase): def setUp(self): super(TestMoveExplode, self).setUp() - cr, uid = self.cr, self.uid - # Usefull models - self.ir_model_data = self.registry('ir.model.data') - self.sale_order_line = self.registry('sale.order.line') - self.sale_order = self.registry('sale.order') - self.mrp_bom = self.registry('mrp.bom') - self.product = self.registry('product.product') + self.SaleOrderLine = self.env['sale.order.line'] + self.SaleOrder = self.env['sale.order'] + self.MrpBom = self.env['mrp.bom'] + self.Product = self.env['product.product'] #product that has a phantom bom - self.product_bom_id = self.ir_model_data.get_object_reference(cr, uid, 'product', 'product_product_3')[1] + self.product_bom = self.env.ref('product.product_product_3') #bom with that product - self.bom_id = self.ir_model_data.get_object_reference(cr, uid, 'mrp', 'mrp_bom_9')[1] + self.bom = self.env.ref('mrp.mrp_bom_9') #partner agrolait - self.partner_id = self.ir_model_data.get_object_reference(cr, uid, 'base', 'res_partner_1')[1] + self.partner = self.env.ref('base.res_partner_1') #bom: PC Assemble (with property: DDR 512MB) - self.bom_prop_id = self.ir_model_data.get_object_reference(cr, uid, 'mrp', 'mrp_bom_property_0')[1] + self.bom_prop = self.env.ref('mrp.mrp_bom_property_0') - self.template_id = self.ir_model_data.get_object_reference(cr, uid, 'product', 'product_product_3_product_template')[1] + self.template = self.env.ref('product.product_product_3_product_template') #property: DDR 512MB - self.mrp_property_id = self.ir_model_data.get_object_reference(cr, uid, 'mrp', 'mrp_property_0')[1] + self.mrp_property = self.env.ref('mrp.mrp_property_0') #product: RAM SR2 - self.product_bom_prop_id = self.ir_model_data.get_object_reference(cr, uid, 'product', 'product_product_14')[1] + self.product_bom_prop = self.env.ref('product.product_product_14') #phantom bom for RAM SR2 with three lines containing properties - self.bom_prop_line_id = self.ir_model_data.get_object_reference(cr, uid, 'mrp', 'mrp_bom_property_line')[1] + self.bom_prop_line = self.env.ref('mrp.mrp_bom_property_line') #product: iPod included in the phantom bom - self.product_A_id = self.ir_model_data.get_object_reference(cr, uid, 'product', 'product_product_11')[1] + self.product_A = self.env.ref('product.product_product_11') #product: Mouse, Wireless included in the phantom bom - self.product_B_id = self.ir_model_data.get_object_reference(cr, uid, 'product', 'product_product_12')[1] + self.product_B = self.env.ref('product.product_product_12') + #pricelist + self.pricelist = self.env.ref('product.list0') def test_00_sale_move_explode(self): """check that when creating a sale order with a product that has a phantom BoM, move explode into content of the BoM""" - cr, uid, context = self.cr, self.uid, {} #create sale order with one sale order line containing product with a phantom bom - so_id = self.sale_order.create(cr, uid, vals={'partner_id': self.partner_id}, context=context) - self.sale_order_line.create(cr, uid, values={'order_id': so_id, 'product_id': self.product_bom_id, 'product_uom_qty': 1}, context=context) + so_vals = { + 'partner_id': self.partner.id, + 'partner_invoice_id': self.partner.id, + 'partner_shipping_id': self.partner.id, + 'pricelist_id': self.pricelist.id, + } + self.so = self.SaleOrder.create(vals=so_vals) + sol_vals = { + 'order_id': self.so.id, + 'name': self.product_bom.name, + 'product_id': self.product_bom.id, + 'product_uom': self.product_bom.uom_id.id, + 'product_uom_qty': 1.0, + } + self.SaleOrderLine.create(values=sol_vals) #confirm sale order - self.sale_order.action_button_confirm(cr, uid, [so_id], context=context) + self.so.action_confirm() #get all move associated to that sale_order - browse_move_ids = self.sale_order.browse(cr, uid, so_id, context=context).picking_ids[0].move_lines - move_ids = [x.id for x in browse_move_ids] + move_ids = self.so.picking_ids.mapped('move_lines').ids #we should have same amount of move as the component in the phatom bom - bom = self.mrp_bom.browse(cr, uid, self.bom_id, context=context) - bom_component_length = self.mrp_bom._bom_explode(cr, uid, bom, self.product_bom_id, 1, []) + bom_component_length = self.MrpBom._bom_explode(self.bom, self.product_bom, 1.0, []) self.assertEqual(len(move_ids), len(bom_component_length[0])) def test_00_bom_find(self): """Check that _bom_find searches the bom corresponding to the properties passed or takes the bom with the smallest sequence.""" - cr, uid, context = self.cr, self.uid, {} - res_id = self.mrp_bom._bom_find(cr, uid, product_tmpl_id=self.template_id, product_id=None, properties=[self.mrp_property_id], context=context) - self.assertEqual(res_id, self.bom_prop_id) + res_id = self.MrpBom._bom_find(product_tmpl_id=self.template.id, product_id=None, properties=[self.mrp_property.id]) + self.assertEqual(res_id, self.bom_prop.id) def test_00_bom_explode(self): """Check that _bom_explode only takes the lines with the right properties.""" - cr, uid, context = self.cr, self.uid, {} - bom = self.mrp_bom.browse(cr, uid, self.bom_prop_line_id) - product = self.product.browse(cr, uid, self.product_bom_prop_id) - res = self.mrp_bom._bom_explode(cr, uid, bom, product, 1, properties=[self.mrp_property_id], context=context) + res = self.MrpBom._bom_explode(self.bom_prop_line, self.product_bom_prop, 1, properties=[self.mrp_property.id]) res = set([p['product_id'] for p in res[0]]) - self.assertEqual(res, set([self.product_A_id, self.product_B_id])) + self.assertEqual(res, set([self.product_A.id, self.product_B.id])) diff --git a/addons/sale_mrp/tests/test_sale_mrp_flow.py b/addons/sale_mrp/tests/test_sale_mrp_flow.py index 91696286168c5551d53fd38ff8ccc37756a32dda..d8c703a6ba3af8168c33f287af292ead50024f3f 100644 --- a/addons/sale_mrp/tests/test_sale_mrp_flow.py +++ b/addons/sale_mrp/tests/test_sale_mrp_flow.py @@ -8,7 +8,7 @@ class TestSaleMrpFlow(common.TransactionCase): def setUp(self): super(TestSaleMrpFlow, self).setUp() - # Usefull models + # Useful models self.SaleOrderLine = self.env['sale.order.line'] self.SaleOrder = self.env['sale.order'] self.MrpBom = self.env['mrp.bom'] @@ -116,16 +116,20 @@ class TestSaleMrpFlow(common.TransactionCase): order = self.SaleOrder.create({ 'partner_id': self.partner_agrolite.id, + 'partner_invoice_id': self.partner_agrolite.id, + 'partner_shipping_id': self.partner_agrolite.id, 'date_order': datetime.today(), + 'pricelist_id': self.env.ref('product.list0').id, }) self.SaleOrderLine.create({ + 'name': product_a.name, 'order_id': order.id, 'product_id': product_a.id, 'product_uom_qty': 10, 'product_uom': self.uom_dozen.id }) self.assertTrue(order, "Sale order not created.") - order.action_button_confirm() + order.action_confirm() # =============================================================================== # Sale order of 10 Dozen product A should create production order @@ -346,3 +350,48 @@ class TestSaleMrpFlow(common.TransactionCase): self.assertEqual(mnf_product_a.state, 'done', 'Manufacturing order should be done.') # Check product A avaialble quantity should be 120. self.assertEqual(product_a.qty_available, 120, 'Wrong quantity available of product A.') + + def test_01_sale_mrp_delivery_kit(self): + """ Test delivered quantity on SO based on delivered quantity in pickings.""" + # intial so + self.partner = self.env.ref('base.res_partner_1') + self.product = self.env.ref('product.product_product_3') + so_vals = { + 'partner_id': self.partner.id, + 'partner_invoice_id': self.partner.id, + 'partner_shipping_id': self.partner.id, + 'order_line': [(0, 0, {'name': self.product.name, 'product_id': self.product.id, 'product_uom_qty': 5, 'product_uom': self.product.uom_id.id, 'price_unit': self.product.list_price})], + 'pricelist_id': self.env.ref('product.list0').id, + } + self.so = self.SaleOrder.create(so_vals) + + # confirm our standard so, check the picking + self.so.action_confirm() + self.assertTrue(self.so.picking_ids, 'Sale MRP: no picking created for "invoice on delivery" stockable products') + + # invoice in on delivery, nothing should be invoiced + self.so.action_invoice_create() + self.assertEqual(self.so.invoice_status, 'no', 'Sale MRP: so invoice_status should be "nothing to invoice" after invoicing') + + # deliver partially (1 of each instead of 5), check the so's invoice_status and delivered quantities + pick = self.so.picking_ids + pick.force_assign() + pick.pack_operation_product_ids.write({'qty_done': 1}) + wiz_act = pick.do_new_transfer() + wiz = self.env[wiz_act['res_model']].browse(wiz_act['res_id']) + wiz.process() + + self.assertEqual(self.so.invoice_status, 'no', 'Sale MRP: so invoice_status should be "no" after partial delivery of a kit') + del_qty = sum(sol.qty_delivered for sol in self.so.order_line) + self.assertEqual(del_qty, 0.0, 'Sale MRP: delivered quantity should be zero after partial delivery of a kit') + + # deliver remaining products, check the so's invoice_status and delivered quantities + self.assertEqual(len(self.so.picking_ids), 2, 'Sale MRP: number of pickings should be 2') + pick_2 = self.so.picking_ids[0] + pick_2.force_assign() + pick_2.pack_operation_product_ids.write({'qty_done': 4}) + pick_2.do_new_transfer() + + del_qty = sum(sol.qty_delivered for sol in self.so.order_line) + self.assertEqual(del_qty, 5.0, 'Sale MRP: delivered quantity should be 5.0 after complete delivery of a kit') + self.assertEqual(self.so.invoice_status, 'to invoice', 'Sale MRP: so invoice_status should be "to invoice" after complete delivery of a kit')