diff --git a/runbot/runbot.py b/runbot/runbot.py index 488d6f59..7ffc87d3 100644 --- a/runbot/runbot.py +++ b/runbot/runbot.py @@ -438,6 +438,16 @@ def cron(self, cr, uid, ids=None, context=None): self.scheduler(cr, uid, ids, context=context) self.reload_nginx(cr, uid, context=context) + def unlink(self, cr, uid, ids, context=None): + # ondelete=cascade works on database level, but we want + # runbot.build#unlink to execute + branch_obj = self.pool['runbot.branch'] + branch_obj.unlink( + cr, uid, branch_obj.search(cr, uid, [('repo_id', 'in', ids)], + context=context), + context=context) + return super(runbot_repo, self).unlink(cr, uid, ids, context=context) + class runbot_branch(osv.osv): _name = "runbot.branch" _order = 'name' @@ -498,6 +508,15 @@ def _is_on_remote(self, cr, uid, ids, context=None): return False return True + def unlink(self, cr, uid, ids, context=None): + # ondelete=cascade works on database level, but we want + # runbot.build#unlink to execute + build_obj = self.pool['runbot.build'] + build_obj.unlink( + cr, uid, build_obj.search(cr, uid, [('branch_id', 'in', ids)], + context=context), + context=context) + return super(runbot_branch, self).unlink(cr, uid, ids, context=context) class runbot_build(osv.osv): _name = "runbot.build" @@ -1126,7 +1145,7 @@ def schedule(self, cr, uid, ids, context=None): build._local_cleanup() def skip(self, cr, uid, ids, context=None): - self.write(cr, uid, ids, {'state': 'done', 'result': 'skipped'}, context=context) + self.kill(cr, uid, ids, result='skipped', context=context) to_unduplicate = self.search(cr, uid, [('id', 'in', ids), ('duplicate_id', '!=', False)]) if len(to_unduplicate): self.force(cr, uid, to_unduplicate, context=context) @@ -1148,7 +1167,11 @@ def _local_cleanup(self, cr, uid, ids, context=None): # cleanup: find any build older than 7 days. root = self.pool['runbot.repo'].root(cr, uid) build_dir = os.path.join(root, 'build') + if not os.path.exists(build_dir): + return builds = os.listdir(build_dir) + if not builds: + return cr.execute(""" SELECT dest FROM runbot_build @@ -1164,6 +1187,8 @@ def _local_cleanup(self, cr, uid, ids, context=None): def kill(self, cr, uid, ids, result=None, context=None): for build in self.browse(cr, uid, ids, context=context): + if not build.pid: + continue build._log('kill', 'Kill build %s' % build.dest) build.logger('killing %s', build.pid) try: @@ -1202,6 +1227,10 @@ def _log(self, cr, uid, ids, func, message, context=None): 'line': '0', }, context=context) + def unlink(self, cr, uid, ids, context=None): + self.kill(cr, uid, ids, result='killed', context=context) + return super(runbot_build, self).unlink(cr, uid, ids, context=context) + class runbot_event(osv.osv): _inherit = 'ir.logging' _order = 'id' diff --git a/stock_picking_refund/__init__.py b/stock_picking_refund/__init__.py new file mode 100644 index 00000000..0d10f2e6 --- /dev/null +++ b/stock_picking_refund/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import models \ No newline at end of file diff --git a/stock_picking_refund/__openerp__.py b/stock_picking_refund/__openerp__.py new file mode 100644 index 00000000..150d8b8c --- /dev/null +++ b/stock_picking_refund/__openerp__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Decrease delivred quantity', + 'version': '1.0', + 'category': 'Stock', + 'description': """ +This allows to decrease the quantity delivered in the +associated SO, and therefore to generate refunds more easily. +============================================================== +""", + 'depends': ['sale_stock'], + 'data': [ + 'security/ir.model.access.csv', + 'views/sale_stock_view.xml', + + ], + 'demo': [], + 'test': [], + 'installable': True, + 'auto_install': False, +} diff --git a/stock_picking_refund/models/__init__.py b/stock_picking_refund/models/__init__.py new file mode 100644 index 00000000..e1c6cf31 --- /dev/null +++ b/stock_picking_refund/models/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +import sale_stock \ No newline at end of file diff --git a/stock_picking_refund/models/sale_stock.py b/stock_picking_refund/models/sale_stock.py new file mode 100644 index 00000000..6e62053a --- /dev/null +++ b/stock_picking_refund/models/sale_stock.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from openerp import api, fields, models + + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + @api.multi + def _get_delivered_qty(self): + """Computes the delivered quantity on sale order lines, based on done stock moves related to its procurements + """ + self.ensure_one() + qty = super(SaleOrderLine, self)._get_delivered_qty() + for move in self.procurement_ids.mapped('move_ids').filtered(lambda r: r.state == 'done' and not r.scrapped): + if move.location_dest_id.usage == "internal" and move.to_refund_so: + qty -= self.env['product.uom']._compute_qty_obj(move.product_uom, move.product_uom_qty, self.product_uom) + return qty + +class StockMove(models.Model): + _inherit = "stock.move" + + to_refund_so = fields.Boolean(string="To Refund in SO", default=False, + help='Trigger a decrease of the delivered quantity in the associated Sale Order') + +class StockReturnPicking(models.TransientModel): + _inherit = "stock.return.picking" + + @api.multi + def _create_returns(self): + new_picking_id, pick_type_id = super(StockReturnPicking, self)._create_returns() + new_picking = self.env['stock.picking'].browse([new_picking_id]) + for move in new_picking.move_lines: + return_picking_line = self.product_return_moves.filtered(lambda r: r.move_id == move.origin_returned_move_id) + if return_picking_line and return_picking_line.to_refund_so: + move.to_refund_so = True + + return new_picking_id, pick_type_id + + +class StockReturnPickingLine(models.TransientModel): + _inherit = "stock.return.picking.line" + + to_refund_so = fields.Boolean(string="To Refund in SO", help='Trigger a decrease of the delivered quantity in the associated Sale Order') diff --git a/stock_picking_refund/security/ir.model.access.csv b/stock_picking_refund/security/ir.model.access.csv new file mode 100644 index 00000000..58262d44 --- /dev/null +++ b/stock_picking_refund/security/ir.model.access.csv @@ -0,0 +1 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink \ No newline at end of file diff --git a/stock_picking_refund/tests/test_sale_stock.py b/stock_picking_refund/tests/test_sale_stock.py new file mode 100644 index 00000000..0e0abfd4 --- /dev/null +++ b/stock_picking_refund/tests/test_sale_stock.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from openerp.addons.sale.tests.test_sale_common import TestSale + + +class TestSaleStock(TestSale): + def test_00_sale_stock_return(self): + """ + Test a SO with a product invoiced on delivery. Deliver and invoice the SO, then do a return + of the picking. Check that a refund invoice is well generated. + """ + # intial so + self.partner = self.env.ref('base.res_partner_1') + self.product = self.env.ref('product.product_delivery_01') + 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.0, + 'product_uom': self.product.uom_id.id, + 'price_unit': self.product.list_price})], + 'pricelist_id': self.env.ref('product.list0').id, + } + self.so = self.env['sale.order'].create(so_vals) + + # confirm our standard so + self.so.action_confirm() + + # deliver completely + pick = self.so.picking_ids + pick.force_assign() + pick.pack_operation_product_ids.write({'qty_done': 5}) + pick.do_new_transfer() + + # Create invoice + inv_1_id = self.so.action_invoice_create() + self.inv_1 = self.env['account.invoice'].browse(inv_1_id) + self.inv_1.signal_workflow('invoice_open') + + # Create return picking + stockreturnpicking = self.env['stock.return.picking'] + default_data = stockreturnpicking.with_context(active_ids=pick.ids, active_id=pick.ids[0]).default_get(['move_dest_exists', 'original_location_id', 'product_return_moves', 'parent_location_id', 'location_id']) + return_wiz = stockreturnpicking.with_context(active_ids=pick.ids, active_id=pick.ids[0]).create(default_data) + return_wiz.product_return_moves.quantity = 2.0 # Return only 2 + return_wiz.product_return_moves.to_refund_so = True # Refund these 2 + res = return_wiz.create_returns() + return_pick = self.env['stock.picking'].browse(res['res_id']) + + # Validate picking + return_pick.force_assign() + return_pick.pack_operation_product_ids.write({'qty_done': 2}) + return_pick.do_new_transfer() + + # Check invoice + self.assertEqual(self.so.invoice_status, 'to invoice', 'Sale Stock: so invoice_status should be "to invoice" instead of "%s" after picking return' % self.so.invoice_status) + self.assertEqual(self.so.order_line[0].qty_delivered, 3.0, 'Sale Stock: delivered quantity should be 3.0 instead of "%s" after picking return' % self.so.order_line[0].qty_delivered) + # let's do an invoice with refunds + adv_wiz = self.env['sale.advance.payment.inv'].with_context(active_ids=[self.so.id]).create({ + 'advance_payment_method': 'all', + }) + adv_wiz.with_context(open_invoices=True).create_invoices() + self.inv_2 = self.so.invoice_ids.filtered(lambda r: r.state == 'draft') + self.assertEqual(self.inv_2.invoice_line_ids[0].quantity, 2.0, 'Sale Stock: refund quantity on the invoice should be 2.0 instead of "%s".' % self.inv_2.invoice_line_ids[0].quantity) + self.assertEqual(self.so.invoice_status, 'no', 'Sale Stock: so invoice_status should be "no" instead of "%s" after invoicing the return' % self.so.invoice_status) diff --git a/stock_picking_refund/views/sale_stock_view.xml b/stock_picking_refund/views/sale_stock_view.xml new file mode 100644 index 00000000..2732e251 --- /dev/null +++ b/stock_picking_refund/views/sale_stock_view.xml @@ -0,0 +1,17 @@ + + + + + + stock.return.picking.sale.stock.form + + stock.return.picking + + + + + + + + + \ No newline at end of file