diff --git a/base_cron_oneshot/__init__.py b/base_cron_oneshot/__init__.py
new file mode 100644
index 000000000..0650744f6
--- /dev/null
+++ b/base_cron_oneshot/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/base_cron_oneshot/__manifest__.py b/base_cron_oneshot/__manifest__.py
new file mode 100644
index 000000000..c5c0d2942
--- /dev/null
+++ b/base_cron_oneshot/__manifest__.py
@@ -0,0 +1,18 @@
+# Copyright (C) 2018 by Camptocamp
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+{
+ 'name': """Single use crons""",
+ 'summary': """Allows creating of single-use disposable crons""",
+ 'category': "Extra Tools",
+ 'version': "11.0.1.0.0",
+
+ 'author': "Camptocamp SA, "
+ "Odoo Community Association (OCA)",
+ 'website': "https://github.com/OCA/server-tools",
+ 'license': "AGPL-3",
+
+ 'data': [
+ 'views/ir_cron.xml',
+ 'data/ir_sequence.xml',
+ ],
+}
diff --git a/base_cron_oneshot/data/ir_sequence.xml b/base_cron_oneshot/data/ir_sequence.xml
new file mode 100644
index 000000000..aca378f8e
--- /dev/null
+++ b/base_cron_oneshot/data/ir_sequence.xml
@@ -0,0 +1,8 @@
+
+
+
+
+ Oneshot cron names sequence
+
+
+
diff --git a/base_cron_oneshot/models/__init__.py b/base_cron_oneshot/models/__init__.py
new file mode 100644
index 000000000..911559262
--- /dev/null
+++ b/base_cron_oneshot/models/__init__.py
@@ -0,0 +1 @@
+from . import ir_cron
diff --git a/base_cron_oneshot/models/ir_cron.py b/base_cron_oneshot/models/ir_cron.py
new file mode 100644
index 000000000..28707df39
--- /dev/null
+++ b/base_cron_oneshot/models/ir_cron.py
@@ -0,0 +1,52 @@
+# Copyright (C) 2018 by Camptocamp
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+from odoo import api, fields, models
+
+
+class IrCron(models.Model):
+ _inherit = 'ir.cron'
+
+ oneshot = fields.Boolean(
+ string='Single use',
+ default=False,
+ )
+
+ @api.model
+ def create(self, vals):
+ if vals.get('oneshot'):
+ # quite silent - fail loudly if vals['numbercall'] is given?
+ vals['numbercall'] = 1
+ return super(IrCron, self).create(vals)
+
+ @classmethod
+ def _process_job(cls, job_cr, job, cron_cr):
+ res = super(IrCron, cls)._process_job(job_cr, job, cron_cr)
+ try:
+ with api.Environment.manage():
+ cron = api.Environment(job_cr, job['user_id'], {})[cls._name]
+ if job.get('oneshot'):
+ # log this?
+ cron.browse(job.get('id')).unlink()
+ finally:
+ job_cr.commit()
+ cron_cr.commit()
+ return res
+
+ @api.model
+ def schedule_oneshot(
+ self, model=False, method=False, params=False, code=False):
+ # XXX: still under construction.
+ if not model:
+ # generic case, use `base` cause we don't really care
+ model = self.env.ref('base.model_base').id
+ if not isinstance(model, str):
+ model = model.id
+ if method and not code:
+ code = 'model.{method}'.format(**locals())
+ oneshot_name_seq = self.env.ref('base_cron_oneshot.seq_oneshot_name')
+ self.create({
+ 'name': 'Oneshot #{}'.format(oneshot_name_seq.next()),
+ # TODO: retrieve actual ID of a model
+ 'model_id': model.id,
+ 'code': code,
+ })
diff --git a/base_cron_oneshot/views/ir_cron.xml b/base_cron_oneshot/views/ir_cron.xml
new file mode 100644
index 000000000..9ff8fd46d
--- /dev/null
+++ b/base_cron_oneshot/views/ir_cron.xml
@@ -0,0 +1,20 @@
+
+
+
+
+ ir.cron.form
+ ir.cron
+
+
+
+
+
+
+
+ {'invisible': [('oneshot', '=', True)]}
+
+
+
+
+
+