diff --git a/addons/project_timesheet/test/worktask_entry_to_timesheetline_entry.yml b/addons/project_timesheet/test/worktask_entry_to_timesheetline_entry.yml index 99e5fc41401..2d95eb89d71 100644 --- a/addons/project_timesheet/test/worktask_entry_to_timesheetline_entry.yml +++ b/addons/project_timesheet/test/worktask_entry_to_timesheetline_entry.yml @@ -14,8 +14,7 @@ !record {model: product.product, id: product_product_hrmanger0}: categ_id: product.product_category_6 mes_type: fixed - name: HR Manger - procure_method: make_to_stock + name: HR Manager standard_price: 1.0 supply_method: buy type: service diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index 660b70a488c..8efacf7f75e 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -386,6 +386,8 @@ class purchase_order(osv.osv): ''' This function returns an action that display existing picking orders of given purchase order ids. ''' + if context is None: + context = {} mod_obj = self.pool.get('ir.model.data') pick_ids = [] for po in self.browse(cr, uid, ids, context=context): diff --git a/addons/sale/sale.py b/addons/sale/sale.py index b55fccc8984..6535ed7bc6d 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -658,7 +658,7 @@ class sale_order(osv.osv): for order in self.browse(cr, uid, ids, context=context): proc_ids = [] group_id = self.pool.get("procurement.group").create(cr, uid, { - 'name': order.name, 'partner_id': order.partner_shipping_id.id + 'name': order.name, 'partner_id': order.partner_shipping_id.id, 'move_type': order.picking_policy }, context=context) order.write({'procurement_group_id': group_id}, context=context) for line in order.order_line: @@ -1082,4 +1082,4 @@ class procurement_order(osv.osv): _defaults = { 'invoice_state': 'none', } - + \ No newline at end of file diff --git a/addons/sale_stock/sale_stock.py b/addons/sale_stock/sale_stock.py index 3163365ffc0..055b4309d6d 100644 --- a/addons/sale_stock/sale_stock.py +++ b/addons/sale_stock/sale_stock.py @@ -61,7 +61,14 @@ class sale_order(osv.osv): if move.procurement_id and move.procurement_id.sale_line_id: res.add(move.procurement_id.sale_line_id.order_id.id) return list(res) - + + def _get_orders_procurements(self, cr, uid, ids, context=None): + res = set() + for proc in self.pool.get('procurement.order').browse(cr, uid, ids, context=context): + if proc.sale_line_id: + res.add(proc.sale_line_id.order_id.id) + return list(res) + def _get_picking_ids(self, cr, uid, ids, name, args, context=None): res = {} for sale in self.browse(cr, uid, ids, context=context): @@ -107,7 +114,10 @@ class sale_order(osv.osv): ('prepaid', 'Before Delivery'), ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="""On demand: A draft invoice can be created from the sales order when needed. \nOn delivery order: A draft invoice can be created from the delivery order when the products have been delivered. \nBefore delivery: A draft invoice is created from the sales order and must be paid before the products can be delivered."""), - 'shipped': fields.function(_get_shipped, string='Delivered', type='boolean', store={'stock.move': (_get_orders, ['state'], 10)}), + 'shipped': fields.function(_get_shipped, string='Delivered', type='boolean', store={ + 'stock.move': (_get_orders, ['state'], 10), + 'procurement.order': (_get_orders_procurements, ['state'], 10) + }), 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True), 'picking_ids': fields.function(_get_picking_ids, method=True, type='one2many', relation='stock.picking', string='Picking associated to this sale'), } @@ -158,11 +168,12 @@ class sale_order(osv.osv): # if order_policy<>picking: super() # else: call invoice_on_picking_method() def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_invoice = False, context=None): - picking_obj = self.pool.get('stock.picking') + move_obj = self.pool.get("stock.move") res = super(sale_order,self).action_invoice_create(cr, uid, ids, grouped=grouped, states=states, date_invoice = date_invoice, context=context) for order in self.browse(cr, uid, ids, context=context): if order.order_policy == 'picking': - picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'}) + for picking in order.picking_ids: + move_obj.write(cr, uid, [x.id for x in picking.move_lines], {'invoice_state': 'invoiced'}, context=context) return res def action_cancel(self, cr, uid, ids, context=None): @@ -223,7 +234,7 @@ class sale_order(osv.osv): self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'}) if write_cancel_ids: self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'}) - + if mode == 'finished': return finished elif mode == 'canceled': @@ -265,7 +276,7 @@ class sale_order(osv.osv): class sale_order_line(osv.osv): _inherit = 'sale.order.line' - + def _number_packages(self, cr, uid, ids, field_name, arg, context=None): res = {} for line in self.browse(cr, uid, ids, context=context): diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 740e60b9a50..819762b06c2 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -332,7 +332,7 @@ class stock_quant(osv.osv): def quants_unreserve(self, cr, uid, move, context=None): #cr.execute('update stock_quant set reservation_id=NULL where reservation_id=%s', (move.id,)) #need write for related store of remaining qty - related_quants = self.search(cr, uid, [('reservation_id', '=', move.id)], context=context) + related_quants = [x.id for x in move.reserved_quant_ids] self.write(cr, uid, related_quants, {'reservation_id': False}, context=context) return True @@ -1130,7 +1130,10 @@ class stock_move(osv.osv): 'origin_returned_move_id': fields.many2one('stock.move', 'Origin return move', help='move that created the return move'), 'returned_move_ids': fields.one2many('stock.move', 'origin_returned_move_id', 'All returned moves', help='Optional: all returned moves created from this move'), 'availability': fields.function(_get_product_availability, type='float', string='Availability'), - } + } + + + def copy(self, cr, uid, id, default=None, context=None): if default is None: @@ -1442,6 +1445,8 @@ class stock_move(osv.osv): """ context = context or {} for move in self.browse(cr, uid, ids, context=context): + if move.reserved_quant_ids: + self.pool.get("stock.quant").quants_unreserve(cr, uid, move, context=context) if move.move_dest_id: if move.propagate: self.action_cancel(cr, uid, [move.move_dest_id.id], context=context) @@ -1616,10 +1621,9 @@ class stock_move(osv.osv): return res def split(self, cr, uid, move, qty, context=None): - """ Partially (or not) moves a stock.move. - @param partial_datas: Dictionary containing details of partial picking - like partner_id, delivery_date, delivery - moves with product_id, product_qty, uom + """ + Splits qty from move move into a new move + It will check if it can propagate the split """ if move.product_qty==qty: return move.id @@ -1648,6 +1652,9 @@ class stock_move(osv.osv): 'product_uos_qty': move.product_uos_qty - uos_qty, # 'reserved_quant_ids': [(6,0,[])] SHOULD NOT CHANGE as it has been reserved already }, context=context) + + if move.move_dest_id and move.propagate: + self.split(self, cr, uid, move.move_dest_id, qty, context=context) return new_move class stock_inventory(osv.osv): @@ -1925,7 +1932,7 @@ report_sxw.report_sxw('report.stock.quant.package.barcode', 'stock.quant.package class stock_package(osv.osv): """ - These are the packages, containing quants and/or others packages + These are the packages, containing quants and/or other packages """ _name = "stock.quant.package" _description = "Physical Packages" diff --git a/addons/stock_account/stock.py b/addons/stock_account/stock.py index e404ab3dd7b..312b1668f6d 100644 --- a/addons/stock_account/stock.py +++ b/addons/stock_account/stock.py @@ -22,6 +22,55 @@ from openerp.osv import fields, osv from openerp.tools.translate import _ +#---------------------------------------------------------- +# Procurement Rule +#---------------------------------------------------------- +class procurement_rule(osv.osv): + _inherit = 'procurement.rule' + _columns= { + 'invoice_state': fields.selection([ + ("invoiced", "Invoiced"), + ("2binvoiced", "To Be Invoiced"), + ("none", "Not Applicable")], "Invoice Status", + required=False), + } + +#---------------------------------------------------------- +# Procurement Order +#---------------------------------------------------------- + + +class procurement_order(osv.osv): + _inherit = "procurement.order" + + def _run_move_create(self, cr, uid, procurement, context=None): + res = super(procurement_order, self)._run_move_create(cr, uid, procurement, context=context) + res.update({'invoice_state': procurement.rule_id.invoice_state in ('none', False) and procurement.invoice_state or procurement.rule_id.invoice_state }) + return res + + +#---------------------------------------------------------- +# Move +#---------------------------------------------------------- + +class stock_move(osv.osv): + _inherit = "stock.move" + _columns = { + 'invoice_state': fields.selection([("invoiced", "Invoiced"), + ("2binvoiced", "To Be Invoiced"), + ("none", "Not Applicable")], "Invoice Control", + select=True, required=True, track_visibility='onchange', + states={'draft': [('readonly', False)]}), + } + _defaults= { + 'invoice_state': lambda *args, **argv: 'none' + } + + + +#---------------------------------------------------------- +# Picking +#---------------------------------------------------------- class stock_picking(osv.osv): _inherit = 'stock.picking' @@ -30,22 +79,13 @@ class stock_picking(osv.osv): for pick in self.browse(cr, uid, ids, context=context): result[pick.id] = 'none' for move in pick.move_lines: - if move.procurement_id: - if move.procurement_id.invoice_state=='invoiced': - result[pick.id] = 'invoiced' - elif move.procurement_id.invoice_state=='2binvoiced': - result[pick.id] = '2binvoiced' - break + if move.invoice_state=='invoiced': + result[pick.id] = 'invoiced' + elif move.invoice_state=='2binvoiced': + result[pick.id] = '2binvoiced' + break return result - def __get_picking_procurement(self, cr, uid, ids, context={}): - result = {} - for proc in self.pool.get('procurement.order').browse(cr, uid, ids, context=context): - for move in proc.move_ids: - if move.picking_id: - result[move.picking_id.id] = True - return result.keys() - def __get_picking_move(self, cr, uid, ids, context={}): res = [] for move in self.pool.get('stock.move').browse(cr, uid, ids, context=context): @@ -59,8 +99,8 @@ class stock_picking(osv.osv): ("2binvoiced", "To Be Invoiced"), ("none", "Not Applicable") ], string="Invoice Control", required=True, + store={ - 'procurement.order': (__get_picking_procurement, ['invoice_state'], 10), 'stock.picking': (lambda self, cr, uid, ids, c={}: ids, ['state'], 10), 'stock.move': (__get_picking_move, ['picking_id'], 10), }, diff --git a/addons/stock_account/stock_account.py b/addons/stock_account/stock_account.py index f9e62fa80e2..659f53adf91 100644 --- a/addons/stock_account/stock_account.py +++ b/addons/stock_account/stock_account.py @@ -34,11 +34,6 @@ import openerp.addons.decimal_precision as dp import logging _logger = logging.getLogger(__name__) -#---------------------------------------------------------- -# Invoice state -#---------------------------------------------------------- - - #---------------------------------------------------------- diff --git a/addons/stock_account/stock_account_view.xml b/addons/stock_account/stock_account_view.xml index a7e6bc8afe9..c6f9a581858 100644 --- a/addons/stock_account/stock_account_view.xml +++ b/addons/stock_account/stock_account_view.xml @@ -29,8 +29,18 @@ - + + + stock.move.form.inherit + stock.move + + + + + + + diff --git a/addons/stock_complex_routes/__openerp__.py b/addons/stock_complex_routes/__openerp__.py index 872f42d98b5..6120538cc64 100644 --- a/addons/stock_complex_routes/__openerp__.py +++ b/addons/stock_complex_routes/__openerp__.py @@ -38,7 +38,12 @@ This adds a route on the sales order and sales order line (mini module) 'init_xml': [], 'update_xml': ['stock_complex_routes.xml'], 'demo_xml': [], - 'test': ['test/lifo_price.yml'], + 'test': [ + 'test/crossdock.yml', + 'test/dropship.yml', + 'test/procurementexception.yml', + 'test/lifo_price.yml' + ], 'installable': True, 'auto_install': True, } diff --git a/addons/stock_complex_routes/test/crossdock.yml b/addons/stock_complex_routes/test/crossdock.yml new file mode 100644 index 00000000000..e757cbfd554 --- /dev/null +++ b/addons/stock_complex_routes/test/crossdock.yml @@ -0,0 +1,37 @@ +- + Create new product without any routes +- + !record {model: product.product, id: cross_shop_product}: + name: PCE + type: product + categ_id: product.product_category_1 + list_price: 100.0 + standard_price: 70.0 + seller_ids: + - delay: 1 + name: base.res_partner_2 + min_qty: 2.0 + qty: 5.0 + type: product + uom_id: product.product_uom_unit + uom_po_id: product.product_uom_unit +- + Create a sales order with a line of 100 PCE incoming shipment, with route_id crossdock shipping. +- + !record {model: sale.order, id: sale_order_crossdock_shpng}: + partner_id: base.res_partner_4 + note: Create Sales order + order_line: + - product_id: cross_shop_product + product_uom_qty: 100.00 + route_id: stock_location.route_warehouse0_crossdock +- + Confirm sales order +- + !workflow {model: sale.order, action: order_confirm, ref: sale_order_crossdock_shpng} +- + Check a quotation was created to a certain supplier and confirm so it becomes a confirmed purchase order +- + !python {model: purchase.order}: | + po_id = self.search(cr, uid, [('partner_id', '=', ref('base.res_partner_2'))]) + self.wkf_confirm_order(cr, uid, po_id) diff --git a/addons/stock_complex_routes/test/dropship.yml b/addons/stock_complex_routes/test/dropship.yml index 7bcb1b93a71..a8f19969dc4 100644 --- a/addons/stock_complex_routes/test/dropship.yml +++ b/addons/stock_complex_routes/test/dropship.yml @@ -1,53 +1,68 @@ - - Create new product without any routes + Create new product without any routes - - !record {model:product.product, id:} + !record {model: product.product, id: drop_shop_product}: + name: Pen drive + type: product + categ_id: product.product_category_1 + list_price: 100.0 + standard_price: 70.0 + seller_ids: + - delay: 1 + name: base.res_partner_2 + min_qty: 2.0 + qty: 5.0 + uom_id: product.product_uom_unit + uom_po_id: product.product_uom_unit - - Create a sales order with a line of 200 PCE incoming shipment, with route_id drop shipping + Create a sales order with a line of 200 PCE incoming shipment, with route_id drop shipping. - - !record {model: sale.order, id: } : - customer: - - product_id: - product_qty: 200 - product_uom: - supplier_ids: - - + !record {model: sale.order, id: sale_order_drp_shpng}: + partner_id: base.res_partner_2 + note: Create sale order for drop shipping + payment_term: account.account_payment_term + order_line: + - product_id: drop_shop_product + product_uom_qty: 200 + price_unit: 100.00 + route_id: route_drop_shipping - - Confirm sales order + Confirm sales order - - !python{model: sale.order}: | - self.browse(cr, uid, ref()).action_b + !workflow {model: sale.order, action: order_confirm, ref: sale_order_drp_shpng} - - Check the sales order created a procurement group which has a procurement of 200 pieces + Check the sales order created a procurement group which has a procurement of 200 pieces - - !python{model: procurement.group}: | - proc_group = self.search(cr, uid, [('sale_id', '=', ref())]) - assert self.browse(cr, uid, proc_group)[0].procurement_ids[0].product_qty == 200 + !python {model: procurement.group}: | + sale_record = self.pool.get("sale.order").browse(cr, uid, ref('sale_order_drp_shpng')) + assert self.browse(cr, uid, sale_record.procurement_group_id.id).procurement_ids[0].product_qty == 200 - - Check a quotation was created to a certain supplier and confirm so it becomes a confirmed purchase order + Check a quotation was created to a certain supplier and confirm so it becomes a confirmed purchase order +- + !python {model: purchase.order}: | + from openerp import netsvc + sale_record = self.pool.get("sale.order").browse(cr, uid, ref('sale_order_drp_shpng')) + procurement_order = self.pool.get("procurement.group").browse(cr, uid, sale_record.procurement_group_id.id).procurement_ids[0] + purchase_id = procurement_order.purchase_id.id + wf_service = netsvc.LocalService('workflow') + wf_service.trg_validate(uid, 'purchase.order', purchase_id, 'purchase_confirm', cr) - - + Use 'Receive Products' button to immediately view this picking, it should have created a picking with 200 pieces.. +- + !python {model: purchase.order}: | + po_id = self.search(cr, uid, [('partner_id', '=', ref('base.res_partner_2'))]) + self.view_picking(cr, uid, po_id) +- + Send the 200 pieces. - - Check the sales order has one picking with 200 pieces + !python {model: stock.picking}: | + po_id = self.pool.get('purchase.order').search(cr, uid, [('partner_id', '=', ref('base.res_partner_2'))]) + picking_id = self.search(cr, uid, [('purchase_id', '=', po_id[0])]) + self.do_partial(cr, uid, picking_id) - - !python{model: sale.order}: | - assert len(self.browse(cr, uid, ref()).picking_ids) == 1 + Check one quant was created in Customers location with 200 pieces and one move in the history_ids - - Use 'View Delivery Order' button to immediately view this picking and check shipped of purchase order is False -- - -- - Send the 200 pieces -- - !python{model: stock.partial.picking}: | - - -- - Check one quant was created in Customers location with 200 pieces and one move in the move_history_ids -- - -- - Check the sum of the moves related to this product and the sum of the quants is equal -- - + !python {model: stock.quant}: | + quant_id = self.search(cr, uid, [('location_id', '=', ref('stock.stock_location_customers')),('qty', '=', 200)]) + assert len(self.browse(cr, uid, quant_id)[0].history_ids) == 1 + assert len(quant_id) == 1 diff --git a/addons/stock_complex_routes/test/procurementexception.yml b/addons/stock_complex_routes/test/procurementexception.yml index 67afc101089..e5c7dbfd72c 100644 --- a/addons/stock_complex_routes/test/procurementexception.yml +++ b/addons/stock_complex_routes/test/procurementexception.yml @@ -1,37 +1,63 @@ -- - Create product with no seller_ids - - -- - Create a sales order with 1 piece for that product and route crossdock -- - !record{sale.order} -- - Confirm sales order -- - !python { -- - Check there is a procurement in exception that has the procurement group of the sales order -- - !python { - assert -- - Adjust the product that it has at least one seller_id -- - !record{id: model: product.product) or in !python -- - Run the Scheduler -- - !python -- - Check the status changed there is no procurement order in exception any more from that procurement group -- - !python{model: procurement.order} - search procurement from -- - Check a purchase quotation was created -- - - - - + I create a product with no supplier define for it. +- + !record {model: product.product, id: product_with_no_seller}: + name: 'product with no seller' + list_price: 20.00 + standard_price: 15.00 + categ_id: product.product_category_1 +- + I create a sales order with this product with route crossdock. +- + !record {model: sale.order, id: sale_order_route_crossdock}: + partner_id: base.res_partner_2 + partner_invoice_id: base.res_partner_address_3 + partner_shipping_id: base.res_partner_address_3 + note: crossdock route + payment_term: account.account_payment_term + order_line: + - product_id: product_with_no_seller + product_uom_qty: 1 + route_id: stock_location.route_warehouse0_crossdock +- + I confirm the sales order. +- + !workflow {model: sale.order, ref: sale_order_route_crossdock, action: order_confirm} +- + I check there is a procurement in exception that has the procurement group of the sales order created before. +- + !python {model: procurement.order}: | + self.run_scheduler(cr, uid) + sale_id = self.pool.get('sale.order').browse(cr, uid, ref("sale_order_route_crossdock")) + proc_id = self.search(cr, uid, [('group_id.name', '=', sale_id.name), ('state', '=', 'exception')]) + assert proc_id, 'No Procurement!' +- + I set the at least one supplier on the product. +- + !record {model: product.product, id: product_with_no_seller}: + seller_ids: + - delay: 1 + name: base.res_partner_2 + min_qty: 2.0 +- + I run the Procurement. +- + !python {model: procurement.order}: | + sale_id = self.pool.get('sale.order').browse(cr, uid, ref("sale_order_route_crossdock")) + proc_id = self.search(cr, uid, [('group_id.name', '=', sale_id.name), ('state', '=', 'exception')]) + self.run(cr, uid, proc_id) +- + I check the status changed there is no procurement order in exception any more from that procurement group +- + !python {model: procurement.order}: | + sale_id = self.pool.get('sale.order').browse(cr, uid, ref("sale_order_route_crossdock")) + proc_id = self.search(cr, uid, [('group_id.name', '=', sale_id.name), ('state', '=', 'exception')]) + assert not proc_id, 'Procurement should be in running state' +- + I check a purchase quotation was created. +- + !python {model: procurement.order}: | + sale_id = self.pool.get('sale.order').browse(cr, uid, ref("sale_order_route_crossdock")) + proc_ids = self.search(cr, uid, [('group_id.name', '=', sale_id.name)]) + purchase_id = [proc.purchase_id for proc in self.browse(cr, uid, proc_ids) if proc.purchase_id] + assert purchase_id, 'No Purchase Quotation is created' diff --git a/addons/stock_location/stock_location.py b/addons/stock_location/stock_location.py index 77d1a67149a..fc36d48d7f9 100644 --- a/addons/stock_location/stock_location.py +++ b/addons/stock_location/stock_location.py @@ -133,11 +133,7 @@ class procurement_rule(osv.osv): 'delay': fields.integer('Number of Hours'), 'procure_method': fields.selection([('make_to_stock','Make to Stock'),('make_to_order','Make to Order')], 'Procure Method', required=True, help="'Make to Stock': When needed, take from the stock or wait until re-supplying. 'Make to Order': When needed, purchase or produce for the procurement request."), 'partner_address_id': fields.many2one('res.partner', 'Partner Address'), - 'invoice_state': fields.selection([ - ("invoiced", "Invoiced"), - ("2binvoiced", "To Be Invoiced"), - ("none", "Not Applicable")], "Invoice Status", - required=True,), + 'route_sequence': fields.related('route_id', 'sequence', string='Route Sequence', store={'stock.location.route': (_get_rules, ['sequence'], 10)}), 'sequence': fields.integer('Sequence'), 'propagate': fields.boolean('Propagate cancel and split', help='If checked, when the previous move of the move (which was generated by a next procurement) is cancelled or split, the move generated by this move will too'), @@ -155,7 +151,7 @@ class procurement_rule(osv.osv): class procurement_order(osv.osv): _inherit = 'procurement.order' _columns = { - 'route_ids': fields.many2many('stock.location.route', 'stock_location_route_procurement', 'procurement_id', 'route_id', 'Destination route', help="Preferred route to be followed by the procurement order"), + 'route_ids': fields.many2many('stock.location.route', 'stock_location_route_procurement', 'procurement_id', 'route_id', 'Followed Route', help="Preferred route to be followed by the procurement order"), } def _run_move_create(self, cr, uid, procurement, context=None):