summaryrefslogtreecommitdiff
path: root/addons/stock/wizard
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/stock/wizard
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/stock/wizard')
-rw-r--r--addons/stock/wizard/__init__.py16
-rw-r--r--addons/stock/wizard/product_replenish.py89
-rw-r--r--addons/stock/wizard/product_replenish_views.xml59
-rw-r--r--addons/stock/wizard/stock_assign_serial_numbers.py34
-rw-r--r--addons/stock/wizard/stock_assign_serial_views.xml31
-rw-r--r--addons/stock/wizard/stock_backorder_confirmation.py74
-rw-r--r--addons/stock/wizard/stock_backorder_confirmation_views.xml44
-rw-r--r--addons/stock/wizard/stock_change_product_qty.py44
-rw-r--r--addons/stock/wizard/stock_change_product_qty_views.xml34
-rw-r--r--addons/stock/wizard/stock_immediate_transfer.py67
-rw-r--r--addons/stock/wizard/stock_immediate_transfer_views.xml37
-rw-r--r--addons/stock/wizard/stock_orderpoint_snooze.py34
-rw-r--r--addons/stock/wizard/stock_orderpoint_snooze_views.xml27
-rw-r--r--addons/stock/wizard/stock_package_destination.py29
-rw-r--r--addons/stock/wizard/stock_package_destination_views.xml59
-rw-r--r--addons/stock/wizard/stock_picking_return.py195
-rw-r--r--addons/stock/wizard/stock_picking_return_views.xml45
-rw-r--r--addons/stock/wizard/stock_quantity_history.py37
-rw-r--r--addons/stock/wizard/stock_quantity_history.xml18
-rw-r--r--addons/stock/wizard/stock_rules_report.py50
-rw-r--r--addons/stock/wizard/stock_rules_report_views.xml37
-rw-r--r--addons/stock/wizard/stock_scheduler_compute.py48
-rw-r--r--addons/stock/wizard/stock_scheduler_compute_views.xml29
-rw-r--r--addons/stock/wizard/stock_track_confirmation.py24
-rw-r--r--addons/stock/wizard/stock_track_confirmation_views.xml24
-rw-r--r--addons/stock/wizard/stock_warn_insufficient_qty.py51
-rw-r--r--addons/stock/wizard/stock_warn_insufficient_qty_views.xml49
27 files changed, 1285 insertions, 0 deletions
diff --git a/addons/stock/wizard/__init__.py b/addons/stock/wizard/__init__.py
new file mode 100644
index 00000000..fac40354
--- /dev/null
+++ b/addons/stock/wizard/__init__.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import stock_assign_serial_numbers
+from . import stock_picking_return
+from . import stock_change_product_qty
+from . import stock_scheduler_compute
+from . import stock_immediate_transfer
+from . import stock_backorder_confirmation
+from . import stock_quantity_history
+from . import stock_rules_report
+from . import stock_warn_insufficient_qty
+from . import product_replenish
+from . import stock_track_confirmation
+from . import stock_package_destination
+from . import stock_orderpoint_snooze
diff --git a/addons/stock/wizard/product_replenish.py b/addons/stock/wizard/product_replenish.py
new file mode 100644
index 00000000..ff697299
--- /dev/null
+++ b/addons/stock/wizard/product_replenish.py
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import datetime
+
+from odoo import _, api, fields, models
+from odoo.exceptions import UserError
+from odoo.tools.misc import clean_context
+
+
+class ProductReplenish(models.TransientModel):
+ _name = 'product.replenish'
+ _description = 'Product Replenish'
+
+ product_id = fields.Many2one('product.product', string='Product', required=True)
+ product_tmpl_id = fields.Many2one('product.template', string='Product Template', required=True)
+ product_has_variants = fields.Boolean('Has variants', default=False, required=True)
+ product_uom_category_id = fields.Many2one('uom.category', related='product_id.uom_id.category_id', readonly=True, required=True)
+ product_uom_id = fields.Many2one('uom.uom', string='Unity of measure', required=True)
+ quantity = fields.Float('Quantity', default=1, required=True)
+ date_planned = fields.Datetime('Scheduled Date', required=True, help="Date at which the replenishment should take place.")
+ warehouse_id = fields.Many2one(
+ 'stock.warehouse', string='Warehouse', required=True,
+ domain="[('company_id', '=', company_id)]")
+ route_ids = fields.Many2many(
+ 'stock.location.route', string='Preferred Routes',
+ help="Apply specific route(s) for the replenishment instead of product's default routes.",
+ domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
+ company_id = fields.Many2one('res.company')
+
+ @api.model
+ def default_get(self, fields):
+ res = super(ProductReplenish, self).default_get(fields)
+ product_tmpl_id = self.env['product.template']
+ if 'product_id' in fields:
+ if self.env.context.get('default_product_id'):
+ product_id = self.env['product.product'].browse(self.env.context['default_product_id'])
+ product_tmpl_id = product_id.product_tmpl_id
+ res['product_tmpl_id'] = product_id.product_tmpl_id.id
+ res['product_id'] = product_id.id
+ elif self.env.context.get('default_product_tmpl_id'):
+ product_tmpl_id = self.env['product.template'].browse(self.env.context['default_product_tmpl_id'])
+ res['product_tmpl_id'] = product_tmpl_id.id
+ res['product_id'] = product_tmpl_id.product_variant_id.id
+ if len(product_tmpl_id.product_variant_ids) > 1:
+ res['product_has_variants'] = True
+ company = product_tmpl_id.company_id or self.env.company
+ if 'product_uom_id' in fields:
+ res['product_uom_id'] = product_tmpl_id.uom_id.id
+ if 'company_id' in fields:
+ res['company_id'] = company.id
+ if 'warehouse_id' in fields and 'warehouse_id' not in res:
+ warehouse = self.env['stock.warehouse'].search([('company_id', '=', company.id)], limit=1)
+ res['warehouse_id'] = warehouse.id
+ if 'date_planned' in fields:
+ res['date_planned'] = datetime.datetime.now()
+ return res
+
+ def launch_replenishment(self):
+ uom_reference = self.product_id.uom_id
+ self.quantity = self.product_uom_id._compute_quantity(self.quantity, uom_reference)
+ try:
+ self.env['procurement.group'].with_context(clean_context(self.env.context)).run([
+ self.env['procurement.group'].Procurement(
+ self.product_id,
+ self.quantity,
+ uom_reference,
+ self.warehouse_id.lot_stock_id, # Location
+ _("Manual Replenishment"), # Name
+ _("Manual Replenishment"), # Origin
+ self.warehouse_id.company_id,
+ self._prepare_run_values() # Values
+ )
+ ])
+ except UserError as error:
+ raise UserError(error)
+
+ def _prepare_run_values(self):
+ replenishment = self.env['procurement.group'].create({
+ 'partner_id': self.product_id.with_company(self.company_id).responsible_id.partner_id.id,
+ })
+
+ values = {
+ 'warehouse_id': self.warehouse_id,
+ 'route_ids': self.route_ids,
+ 'date_planned': self.date_planned,
+ 'group_id': replenishment,
+ }
+ return values
diff --git a/addons/stock/wizard/product_replenish_views.xml b/addons/stock/wizard/product_replenish_views.xml
new file mode 100644
index 00000000..42c7b881
--- /dev/null
+++ b/addons/stock/wizard/product_replenish_views.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="view_product_replenish" model="ir.ui.view">
+ <field name="name">Replenish</field>
+ <field name="model">product.replenish</field>
+ <field name="arch" type="xml">
+ <form string="Replenish wizard">
+ <p>
+ Use this assistant to replenish your stock.
+ Depending on your product configuration, launching a replenishment may trigger a request for quotation,
+ a manufacturing order or a transfer.
+ </p>
+ <group>
+ <field name="product_tmpl_id" invisible="1"/>
+ <field name="product_has_variants" invisible="1"/>
+ <field name="product_id"
+ domain="[('product_tmpl_id', '=', product_tmpl_id)]"
+ attrs="{'readonly': [('product_has_variants', '=', False)]}"
+ options="{'no_create_edit':1}"/>
+ <field name="product_uom_category_id" invisible="1"/>
+ <label for="quantity"/>
+ <div class="o_row">
+ <field name="quantity" />
+ <field name="product_uom_id"
+ domain="[('category_id', '=', product_uom_category_id)]"
+ groups="uom.group_uom"/>
+ </div>
+ <field name="date_planned"/>
+ <field name="warehouse_id"
+ groups="stock.group_stock_multi_warehouses"/>
+ <field name="route_ids"
+ widget="many2many_tags"/>
+ <field name="company_id" invisible="1"/>
+ </group>
+ <footer>
+ <button name="launch_replenishment"
+ string="Confirm"
+ type="object"
+ class="btn-primary"/>
+ <button string="Discard"
+ class="btn-secondary"
+ special="cancel" />
+ </footer>
+ </form>
+ </field>
+ </record>
+
+ <record id="action_product_replenish" model="ir.actions.act_window">
+ <field name="name">Replenish</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="res_model">product.replenish</field>
+ <!-- binding_model_id evaluated to False
+ to remove it in existing db's as it was bug-prone -->
+ <field name="binding_model_id" eval="False"/>
+ <field name="view_mode">form</field>
+ <field name="view_id" ref="view_product_replenish"/>
+ <field name="target">new</field>
+ </record>
+</odoo>
diff --git a/addons/stock/wizard/stock_assign_serial_numbers.py b/addons/stock/wizard/stock_assign_serial_numbers.py
new file mode 100644
index 00000000..1429f445
--- /dev/null
+++ b/addons/stock/wizard/stock_assign_serial_numbers.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import _, api, fields, models
+from odoo.exceptions import ValidationError
+
+
+class StockAssignSerialNumbers(models.TransientModel):
+ _name = 'stock.assign.serial'
+ _description = 'Stock Assign Serial Numbers'
+
+ def _default_next_serial_count(self):
+ move = self.env['stock.move'].browse(self.env.context.get('default_move_id'))
+ if move.exists():
+ filtered_move_lines = move.move_line_ids.filtered(lambda l: not l.lot_name and not l.lot_id)
+ return len(filtered_move_lines)
+
+ product_id = fields.Many2one('product.product', 'Product',
+ related='move_id.product_id', required=True)
+ move_id = fields.Many2one('stock.move', required=True)
+ next_serial_number = fields.Char('First SN', required=True)
+ next_serial_count = fields.Integer('Number of SN',
+ default=_default_next_serial_count, required=True)
+
+ @api.constrains('next_serial_count')
+ def _check_next_serial_count(self):
+ for wizard in self:
+ if wizard.next_serial_count < 1:
+ raise ValidationError(_("The number of Serial Numbers to generate must greater than zero."))
+
+ def generate_serial_numbers(self):
+ self.ensure_one()
+ self.move_id.next_serial = self.next_serial_number or ""
+ return self.move_id._generate_serial_numbers(next_serial_count=self.next_serial_count)
diff --git a/addons/stock/wizard/stock_assign_serial_views.xml b/addons/stock/wizard/stock_assign_serial_views.xml
new file mode 100644
index 00000000..785e372f
--- /dev/null
+++ b/addons/stock/wizard/stock_assign_serial_views.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="view_assign_serial_numbers" model="ir.ui.view">
+ <field name="name">stock_assign_serial_numbers</field>
+ <field name="model">stock.assign.serial</field>
+ <field name="arch" type="xml">
+ <form string="Assign Serial Numbers">
+ <group>
+ <field name="move_id" invisible="1"/>
+ <field name="product_id" readonly="1"/>
+ <field name="next_serial_number"/>
+ <field name="next_serial_count"/>
+ </group>
+ <footer>
+ <button name="generate_serial_numbers" type="object"
+ string="Assign Serial Numbers" class="oe_highlight"/>
+ <button special="cancel" string="Cancel"/>
+ </footer>
+ </form>
+ </field>
+ </record>
+
+ <record id="act_assign_serial_numbers" model="ir.actions.act_window">
+ <field name="name">Assign Serial Numbers</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="res_model">stock.assign.serial</field>
+ <field name="view_mode">form</field>
+ <field name="context">{}</field>
+ <field name="target">new</field>
+ </record>
+</odoo>
diff --git a/addons/stock/wizard/stock_backorder_confirmation.py b/addons/stock/wizard/stock_backorder_confirmation.py
new file mode 100644
index 00000000..3705efec
--- /dev/null
+++ b/addons/stock/wizard/stock_backorder_confirmation.py
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+from odoo.tools.float_utils import float_compare
+
+
+class StockBackorderConfirmationLine(models.TransientModel):
+ _name = 'stock.backorder.confirmation.line'
+ _description = 'Backorder Confirmation Line'
+
+ backorder_confirmation_id = fields.Many2one('stock.backorder.confirmation', 'Immediate Transfer')
+ picking_id = fields.Many2one('stock.picking', 'Transfer')
+ to_backorder = fields.Boolean('To Backorder')
+
+
+class StockBackorderConfirmation(models.TransientModel):
+ _name = 'stock.backorder.confirmation'
+ _description = 'Backorder Confirmation'
+
+ pick_ids = fields.Many2many('stock.picking', 'stock_picking_backorder_rel')
+ show_transfers = fields.Boolean()
+ backorder_confirmation_line_ids = fields.One2many(
+ 'stock.backorder.confirmation.line',
+ 'backorder_confirmation_id',
+ string="Backorder Confirmation Lines")
+
+ @api.model
+ def default_get(self, fields):
+ res = super().default_get(fields)
+ if 'backorder_confirmation_line_ids' in fields and res.get('pick_ids'):
+ res['backorder_confirmation_line_ids'] = [
+ (0, 0, {'to_backorder': True, 'picking_id': pick_id})
+ for pick_id in res['pick_ids'][0][2]
+ ]
+ # default_get returns x2m values as [(6, 0, ids)]
+ # because of webclient limitations
+ return res
+
+ def process(self):
+ pickings_to_do = self.env['stock.picking']
+ pickings_not_to_do = self.env['stock.picking']
+ for line in self.backorder_confirmation_line_ids:
+ if line.to_backorder is True:
+ pickings_to_do |= line.picking_id
+ else:
+ pickings_not_to_do |= line.picking_id
+
+ for pick_id in pickings_not_to_do:
+ moves_to_log = {}
+ for move in pick_id.move_lines:
+ if float_compare(move.product_uom_qty,
+ move.quantity_done,
+ precision_rounding=move.product_uom.rounding) > 0:
+ moves_to_log[move] = (move.quantity_done, move.product_uom_qty)
+ pick_id._log_less_quantities_than_expected(moves_to_log)
+
+ pickings_to_validate = self.env.context.get('button_validate_picking_ids')
+ if pickings_to_validate:
+ pickings_to_validate = self.env['stock.picking'].browse(pickings_to_validate).with_context(skip_backorder=True)
+ if pickings_not_to_do:
+ pickings_to_validate = pickings_to_validate.with_context(picking_ids_not_to_backorder=pickings_not_to_do.ids)
+ return pickings_to_validate.button_validate()
+ return True
+
+ def process_cancel_backorder(self):
+ pickings_to_validate = self.env.context.get('button_validate_picking_ids')
+ if pickings_to_validate:
+ return self.env['stock.picking']\
+ .browse(pickings_to_validate)\
+ .with_context(skip_backorder=True, picking_ids_not_to_backorder=self.pick_ids.ids)\
+ .button_validate()
+ return True
+
diff --git a/addons/stock/wizard/stock_backorder_confirmation_views.xml b/addons/stock/wizard/stock_backorder_confirmation_views.xml
new file mode 100644
index 00000000..1258b70f
--- /dev/null
+++ b/addons/stock/wizard/stock_backorder_confirmation_views.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<odoo>
+ <record id="view_backorder_confirmation" model="ir.ui.view">
+ <field name="name">stock_backorder_confirmation</field>
+ <field name="model">stock.backorder.confirmation</field>
+ <field name="arch" type="xml">
+ <form string="Backorder creation">
+ <group>
+ <p>
+ You have processed less products than the initial demand.
+ </p><p class="text-muted">
+ Create a backorder if you expect to process the remaining
+ products later. Do not create a backorder if you will not
+ process the remaining products.
+ </p>
+ </group>
+
+ <!-- Added to ensure a correct default_get behavior
+
+ The wizard is always opened with default_pick_ids values in context,
+ which are used to generate the backorder_confirmation_line_ids.
+
+ To ensure default_pick_ids is correctly converted from the context
+ by default_get, the field has to be present in the view.
+ -->
+ <field name="pick_ids" invisible="1"/>
+
+ <field name="show_transfers" invisible="1"/>
+ <field name="backorder_confirmation_line_ids" nolabel="1" attrs="{'invisible': [('show_transfers', '=', False)]}">>
+ <tree create="0" delete="0" editable="top">
+ <field name="picking_id"/>
+ <field name="to_backorder" widget="boolean_toggle"/>
+ </tree>
+ </field>
+
+ <footer>
+ <button name="process" string="Create Backorder" type="object" class="oe_highlight"/>
+ <button name="process_cancel_backorder" string="No Backorder" type="object" class="btn-primary" attrs="{'invisible': [('show_transfers', '=', True)]}"/>
+ <button string="_Cancel" class="btn-secondary" special="cancel" />
+ </footer>
+ </form>
+ </field>
+ </record>
+</odoo>
diff --git a/addons/stock/wizard/stock_change_product_qty.py b/addons/stock/wizard/stock_change_product_qty.py
new file mode 100644
index 00000000..ee4d1563
--- /dev/null
+++ b/addons/stock/wizard/stock_change_product_qty.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import _, api, fields, models
+from odoo.exceptions import UserError
+
+
+class ProductChangeQuantity(models.TransientModel):
+ _name = "stock.change.product.qty"
+ _description = "Change Product Quantity"
+
+ product_id = fields.Many2one('product.product', 'Product', required=True)
+ product_tmpl_id = fields.Many2one('product.template', 'Template', required=True)
+ product_variant_count = fields.Integer('Variant Count',
+ related='product_tmpl_id.product_variant_count', readonly=False)
+ new_quantity = fields.Float(
+ 'New Quantity on Hand', default=1,
+ digits='Product Unit of Measure', required=True,
+ help='This quantity is expressed in the Default Unit of Measure of the product.')
+
+ @api.onchange('product_id')
+ def _onchange_product_id(self):
+ self.new_quantity = self.product_id.qty_available
+
+ @api.constrains('new_quantity')
+ def check_new_quantity(self):
+ if any(wizard.new_quantity < 0 for wizard in self):
+ raise UserError(_('Quantity cannot be negative.'))
+
+ def change_product_qty(self):
+ """ Changes the Product Quantity by creating/editing corresponding quant.
+ """
+ warehouse = self.env['stock.warehouse'].search(
+ [('company_id', '=', self.env.company.id)], limit=1
+ )
+ # Before creating a new quant, the quand `create` method will check if
+ # it exists already. If it does, it'll edit its `inventory_quantity`
+ # instead of create a new one.
+ self.env['stock.quant'].with_context(inventory_mode=True).create({
+ 'product_id': self.product_id.id,
+ 'location_id': warehouse.lot_stock_id.id,
+ 'inventory_quantity': self.new_quantity,
+ })
+ return {'type': 'ir.actions.act_window_close'}
diff --git a/addons/stock/wizard/stock_change_product_qty_views.xml b/addons/stock/wizard/stock_change_product_qty_views.xml
new file mode 100644
index 00000000..84bd3aef
--- /dev/null
+++ b/addons/stock/wizard/stock_change_product_qty_views.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+ <record id="view_change_product_quantity" model="ir.ui.view">
+ <field name="name">Change Product Quantity</field>
+ <field name="model">stock.change.product.qty</field>
+ <field name="arch" type="xml">
+ <form string="Update Product Quantity">
+ <group>
+ <field name="product_tmpl_id" invisible="1"/>
+ <field name="product_variant_count" invisible="1"/>
+ <field name="product_id" widget="selection"
+ domain="[('product_tmpl_id', '=', product_tmpl_id)]"
+ attrs="{'invisible': [('product_variant_count', '=', 1)]}"
+ invisible="context.get('default_product_id')"
+ readonly="context.get('default_product_id')"/>
+ <field name="new_quantity"/>
+ </group>
+ <footer>
+ <button name="change_product_qty" string="Apply" type="object" class="btn-primary"/>
+ <button string="Cancel" class="btn-secondary" special="cancel"/>
+ </footer>
+ </form>
+ </field>
+ </record>
+
+ <record id="action_change_product_quantity" model="ir.actions.act_window">
+ <field name="name">Change Product Quantity</field>
+ <field name="res_model">stock.change.product.qty</field>
+ <field name="view_mode">form</field>
+ <field name="target">new</field>
+ </record>
+ </data>
+</odoo>
diff --git a/addons/stock/wizard/stock_immediate_transfer.py b/addons/stock/wizard/stock_immediate_transfer.py
new file mode 100644
index 00000000..2afbc633
--- /dev/null
+++ b/addons/stock/wizard/stock_immediate_transfer.py
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import _, api, fields, models
+from odoo.exceptions import UserError
+
+
+class StockImmediateTransferLine(models.TransientModel):
+ _name = 'stock.immediate.transfer.line'
+ _description = 'Immediate Transfer Line'
+
+ immediate_transfer_id = fields.Many2one('stock.immediate.transfer', 'Immediate Transfer', required=True)
+ picking_id = fields.Many2one('stock.picking', 'Transfer', required=True)
+ to_immediate = fields.Boolean('To Process')
+
+
+class StockImmediateTransfer(models.TransientModel):
+ _name = 'stock.immediate.transfer'
+ _description = 'Immediate Transfer'
+
+ pick_ids = fields.Many2many('stock.picking', 'stock_picking_transfer_rel')
+ show_transfers = fields.Boolean()
+ immediate_transfer_line_ids = fields.One2many(
+ 'stock.immediate.transfer.line',
+ 'immediate_transfer_id',
+ string="Immediate Transfer Lines")
+
+ @api.model
+ def default_get(self, fields):
+ res = super().default_get(fields)
+ if 'immediate_transfer_line_ids' in fields and res.get('pick_ids'):
+ res['immediate_transfer_line_ids'] = [
+ (0, 0, {'to_immediate': True, 'picking_id': pick_id})
+ for pick_id in res['pick_ids'][0][2]
+ ]
+ # default_get returns x2m values as [(6, 0, ids)]
+ # because of webclient limitations
+ return res
+
+ def process(self):
+ pickings_to_do = self.env['stock.picking']
+ pickings_not_to_do = self.env['stock.picking']
+ for line in self.immediate_transfer_line_ids:
+ if line.to_immediate is True:
+ pickings_to_do |= line.picking_id
+ else:
+ pickings_not_to_do |= line.picking_id
+
+ for picking in pickings_to_do:
+ # If still in draft => confirm and assign
+ if picking.state == 'draft':
+ picking.action_confirm()
+ if picking.state != 'assigned':
+ picking.action_assign()
+ if picking.state != 'assigned':
+ raise UserError(_("Could not reserve all requested products. Please use the \'Mark as Todo\' button to handle the reservation manually."))
+ for move in picking.move_lines.filtered(lambda m: m.state not in ['done', 'cancel']):
+ for move_line in move.move_line_ids:
+ move_line.qty_done = move_line.product_uom_qty
+
+ pickings_to_validate = self.env.context.get('button_validate_picking_ids')
+ if pickings_to_validate:
+ pickings_to_validate = self.env['stock.picking'].browse(pickings_to_validate)
+ pickings_to_validate = pickings_to_validate - pickings_not_to_do
+ return pickings_to_validate.with_context(skip_immediate=True).button_validate()
+ return True
+
diff --git a/addons/stock/wizard/stock_immediate_transfer_views.xml b/addons/stock/wizard/stock_immediate_transfer_views.xml
new file mode 100644
index 00000000..ef562bfd
--- /dev/null
+++ b/addons/stock/wizard/stock_immediate_transfer_views.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<odoo>
+ <record id="view_immediate_transfer" model="ir.ui.view">
+ <field name="name">stock.immediate.transfer.view.form</field>
+ <field name="model">stock.immediate.transfer</field>
+ <field name="arch" type="xml">
+ <form string="Immediate transfer?">
+ <group>
+ <p>You have not recorded <i>done</i> quantities yet, by clicking on <i>apply</i> Odoo will process all the quantities.</p>
+ </group>
+
+ <!-- Added to ensure a correct default_get behavior
+
+ The wizard is always opened with default_pick_ids values in context,
+ which are used to generate the backorder_confirmation_line_ids.
+
+ To ensure default_pick_ids is correctly converted from the context
+ by default_get, the field has to be present in the view.
+ -->
+ <field name="pick_ids" invisible="1"/>
+
+ <field name="show_transfers" invisible="1"/>
+ <field name="immediate_transfer_line_ids" nolabel="1" attrs="{'invisible': [('show_transfers', '=', False)]}">>
+ <tree create="0" delete="0" editable="top">
+ <field name="picking_id"/>
+ <field name="to_immediate" widget="boolean_toggle"/>
+ </tree>
+ </field>
+
+ <footer>
+ <button name="process" string="Apply" type="object" class="btn-primary"/>
+ <button string="Cancel" class="btn-secondary" special="cancel" />
+ </footer>
+ </form>
+ </field>
+ </record>
+</odoo>
diff --git a/addons/stock/wizard/stock_orderpoint_snooze.py b/addons/stock/wizard/stock_orderpoint_snooze.py
new file mode 100644
index 00000000..00808dbb
--- /dev/null
+++ b/addons/stock/wizard/stock_orderpoint_snooze.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+from odoo.tools.date_utils import add
+
+
+class StockOrderpointSnooze(models.TransientModel):
+ _name = 'stock.orderpoint.snooze'
+ _description = 'Snooze Orderpoint'
+
+ orderpoint_ids = fields.Many2many('stock.warehouse.orderpoint')
+ predefined_date = fields.Selection([
+ ('day', '1 Day'),
+ ('week', '1 Week'),
+ ('month', '1 Month'),
+ ('custom', 'Custom')
+ ], string='Snooze for', default='day')
+ snoozed_until = fields.Date('Snooze Date')
+
+ @api.onchange('predefined_date')
+ def _onchange_predefined_date(self):
+ today = fields.Date.today()
+ if self.predefined_date == 'day':
+ self.snoozed_until = add(today, days=1)
+ elif self.predefined_date == 'week':
+ self.snoozed_until = add(today, weeks=1)
+ elif self.predefined_date == 'month':
+ self.snoozed_until = add(today, months=1)
+
+ def action_snooze(self):
+ self.orderpoint_ids.write({
+ 'snoozed_until': self.snoozed_until
+ })
diff --git a/addons/stock/wizard/stock_orderpoint_snooze_views.xml b/addons/stock/wizard/stock_orderpoint_snooze_views.xml
new file mode 100644
index 00000000..d3b2d357
--- /dev/null
+++ b/addons/stock/wizard/stock_orderpoint_snooze_views.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<odoo>
+ <record id="view_stock_orderpoint_snooze" model="ir.ui.view">
+ <field name="name">Stock Orderpoint Snooze</field>
+ <field name="model">stock.orderpoint.snooze</field>
+ <field name="arch" type="xml">
+ <form string="Snooze">
+ <group>
+ <field name="orderpoint_ids" invisible="1"/>
+ <field name="predefined_date" widget="radio"/>
+ <field name="snoozed_until" attrs="{'readonly': [('predefined_date', '!=', 'custom')]}" force_save="1"/>
+ </group>
+ <footer>
+ <button string="Snooze" name="action_snooze" type="object" class="btn-primary"/>
+ <button string="Discard" name="cancel_button" class="btn-secondary" special="cancel"/>
+ </footer>
+ </form>
+ </field>
+ </record>
+
+ <record id="action_orderpoint_snooze" model="ir.actions.act_window">
+ <field name="name">Snooze</field>
+ <field name="res_model">stock.orderpoint.snooze</field>
+ <field name="view_mode">form</field>
+ <field name="target">new</field>
+ </record>
+</odoo>
diff --git a/addons/stock/wizard/stock_package_destination.py b/addons/stock/wizard/stock_package_destination.py
new file mode 100644
index 00000000..e4abde19
--- /dev/null
+++ b/addons/stock/wizard/stock_package_destination.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+
+
+class ChooseDestinationLocation(models.TransientModel):
+ _name = 'stock.package.destination'
+ _description = 'Stock Package Destination'
+
+ picking_id = fields.Many2one('stock.picking', required=True)
+ move_line_ids = fields.Many2many('stock.move.line', 'Products', compute='_compute_move_line_ids', required=True)
+ location_dest_id = fields.Many2one('stock.location', 'Destination location', required=True)
+ filtered_location = fields.One2many(comodel_name='stock.location', compute='_filter_location')
+
+ @api.depends('picking_id')
+ def _compute_move_line_ids(self):
+ for destination in self:
+ destination.move_line_ids = destination.picking_id.move_line_ids.filtered(lambda l: l.qty_done > 0 and not l.result_package_id)
+
+ @api.depends('move_line_ids')
+ def _filter_location(self):
+ for destination in self:
+ destination.filtered_location = destination.move_line_ids.mapped('location_dest_id')
+
+ def action_done(self):
+ # set the same location on each move line and pass again in action_put_in_pack
+ self.move_line_ids.location_dest_id = self.location_dest_id
+ return self.picking_id.action_put_in_pack()
diff --git a/addons/stock/wizard/stock_package_destination_views.xml b/addons/stock/wizard/stock_package_destination_views.xml
new file mode 100644
index 00000000..c9a2765d
--- /dev/null
+++ b/addons/stock/wizard/stock_package_destination_views.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="stock_package_destination_form_view" model="ir.ui.view">
+ <field name="name">stock.package.destination.view</field>
+ <field name="model">stock.package.destination</field>
+ <field name="arch" type="xml">
+ <form>
+ <div>
+ You are trying to put products going to different locations into the same package
+ </div>
+ <div>
+ <field name="move_line_ids" style="margin-top:10px;">
+ <tree>
+ <field name="product_id"/>
+ <field name="location_dest_id"/>
+ <field name="qty_done" String="quantity"/>
+ <field name="lot_id" groups="stock.group_production_lot"/>
+ </tree>
+ <kanban>
+ <field name="product_id"/>
+ <field name="qty_done"/>
+ <field name="location_dest_id"/>
+ <templates>
+ <t t-name="kanban-box">
+ <div class="container o_kanban_card_content">
+ <div class="row">
+ <div class="col-6 o_kanban_primary_left">
+ <field name="product_id"/>
+ </div>
+ <div class="col-6 o_kanban_primary_right">
+ <field name="qty_done" String="quantity"/>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-12">
+ <field name="location_dest_id"/>
+ </div>
+ </div>
+ </div>
+ </t>
+ </templates>
+ </kanban>
+ </field>
+ </div>
+ <div>
+ <strong>Where do you want to send the products ?</strong>
+ </div>
+ <div>
+ <field name="filtered_location" invisible="1"/>
+ <field name="location_dest_id" domain="[('id', 'in', filtered_location)]" options="{'no_create': True, 'no_open': True}"/>
+ </div>
+ <footer>
+ <button string="Confirm" name="action_done" type="object" class="btn-primary"/>
+ <button string="Discard" name="cancel_button" class="btn-secondary" special="cancel"/>
+ </footer>
+ </form>
+ </field>
+ </record>
+</odoo>
diff --git a/addons/stock/wizard/stock_picking_return.py b/addons/stock/wizard/stock_picking_return.py
new file mode 100644
index 00000000..b5c3fcf3
--- /dev/null
+++ b/addons/stock/wizard/stock_picking_return.py
@@ -0,0 +1,195 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import _, api, fields, models
+from odoo.exceptions import UserError
+from odoo.tools.float_utils import float_round
+
+
+class ReturnPickingLine(models.TransientModel):
+ _name = "stock.return.picking.line"
+ _rec_name = 'product_id'
+ _description = 'Return Picking Line'
+
+ product_id = fields.Many2one('product.product', string="Product", required=True, domain="[('id', '=', product_id)]")
+ quantity = fields.Float("Quantity", digits='Product Unit of Measure', required=True)
+ uom_id = fields.Many2one('uom.uom', string='Unit of Measure', related='move_id.product_uom', readonly=False)
+ wizard_id = fields.Many2one('stock.return.picking', string="Wizard")
+ move_id = fields.Many2one('stock.move', "Move")
+
+
+class ReturnPicking(models.TransientModel):
+ _name = 'stock.return.picking'
+ _description = 'Return Picking'
+
+ @api.model
+ def default_get(self, fields):
+ if len(self.env.context.get('active_ids', list())) > 1:
+ raise UserError(_("You may only return one picking at a time."))
+ res = super(ReturnPicking, self).default_get(fields)
+ if self.env.context.get('active_id') and self.env.context.get('active_model') == 'stock.picking':
+ picking = self.env['stock.picking'].browse(self.env.context.get('active_id'))
+ if picking.exists():
+ res.update({'picking_id': picking.id})
+ return res
+
+ picking_id = fields.Many2one('stock.picking')
+ product_return_moves = fields.One2many('stock.return.picking.line', 'wizard_id', 'Moves')
+ move_dest_exists = fields.Boolean('Chained Move Exists', readonly=True)
+ original_location_id = fields.Many2one('stock.location')
+ parent_location_id = fields.Many2one('stock.location')
+ company_id = fields.Many2one(related='picking_id.company_id')
+ location_id = fields.Many2one(
+ 'stock.location', 'Return Location',
+ domain="['|', ('id', '=', original_location_id), '|', '&', ('return_location', '=', True), ('company_id', '=', False), '&', ('return_location', '=', True), ('company_id', '=', company_id)]")
+
+ @api.onchange('picking_id')
+ def _onchange_picking_id(self):
+ move_dest_exists = False
+ product_return_moves = [(5,)]
+ if self.picking_id and self.picking_id.state != 'done':
+ raise UserError(_("You may only return Done pickings."))
+ # In case we want to set specific default values (e.g. 'to_refund'), we must fetch the
+ # default values for creation.
+ line_fields = [f for f in self.env['stock.return.picking.line']._fields.keys()]
+ product_return_moves_data_tmpl = self.env['stock.return.picking.line'].default_get(line_fields)
+ for move in self.picking_id.move_lines:
+ if move.state == 'cancel':
+ continue
+ if move.scrapped:
+ continue
+ if move.move_dest_ids:
+ move_dest_exists = True
+ product_return_moves_data = dict(product_return_moves_data_tmpl)
+ product_return_moves_data.update(self._prepare_stock_return_picking_line_vals_from_move(move))
+ product_return_moves.append((0, 0, product_return_moves_data))
+ if self.picking_id and not product_return_moves:
+ raise UserError(_("No products to return (only lines in Done state and not fully returned yet can be returned)."))
+ if self.picking_id:
+ self.product_return_moves = product_return_moves
+ self.move_dest_exists = move_dest_exists
+ self.parent_location_id = self.picking_id.picking_type_id.warehouse_id and self.picking_id.picking_type_id.warehouse_id.view_location_id.id or self.picking_id.location_id.location_id.id
+ self.original_location_id = self.picking_id.location_id.id
+ location_id = self.picking_id.location_id.id
+ if self.picking_id.picking_type_id.return_picking_type_id.default_location_dest_id.return_location:
+ location_id = self.picking_id.picking_type_id.return_picking_type_id.default_location_dest_id.id
+ self.location_id = location_id
+
+ @api.model
+ def _prepare_stock_return_picking_line_vals_from_move(self, stock_move):
+ quantity = stock_move.product_qty
+ for move in stock_move.move_dest_ids:
+ if move.origin_returned_move_id and move.origin_returned_move_id != stock_move:
+ continue
+ if move.state in ('partially_available', 'assigned'):
+ quantity -= sum(move.move_line_ids.mapped('product_qty'))
+ elif move.state in ('done'):
+ quantity -= move.product_qty
+ quantity = float_round(quantity, precision_rounding=stock_move.product_id.uom_id.rounding)
+ return {
+ 'product_id': stock_move.product_id.id,
+ 'quantity': quantity,
+ 'move_id': stock_move.id,
+ 'uom_id': stock_move.product_id.uom_id.id,
+ }
+
+ def _prepare_move_default_values(self, return_line, new_picking):
+ vals = {
+ 'product_id': return_line.product_id.id,
+ 'product_uom_qty': return_line.quantity,
+ 'product_uom': return_line.product_id.uom_id.id,
+ 'picking_id': new_picking.id,
+ 'state': 'draft',
+ 'date': fields.Datetime.now(),
+ 'location_id': return_line.move_id.location_dest_id.id,
+ 'location_dest_id': self.location_id.id or return_line.move_id.location_id.id,
+ 'picking_type_id': new_picking.picking_type_id.id,
+ 'warehouse_id': self.picking_id.picking_type_id.warehouse_id.id,
+ 'origin_returned_move_id': return_line.move_id.id,
+ 'procure_method': 'make_to_stock',
+ }
+ return vals
+
+ def _create_returns(self):
+ # TODO sle: the unreserve of the next moves could be less brutal
+ for return_move in self.product_return_moves.mapped('move_id'):
+ return_move.move_dest_ids.filtered(lambda m: m.state not in ('done', 'cancel'))._do_unreserve()
+
+ # create new picking for returned products
+ picking_type_id = self.picking_id.picking_type_id.return_picking_type_id.id or self.picking_id.picking_type_id.id
+ new_picking = self.picking_id.copy({
+ 'move_lines': [],
+ 'picking_type_id': picking_type_id,
+ 'state': 'draft',
+ 'origin': _("Return of %s", self.picking_id.name),
+ 'location_id': self.picking_id.location_dest_id.id,
+ 'location_dest_id': self.location_id.id})
+ new_picking.message_post_with_view('mail.message_origin_link',
+ values={'self': new_picking, 'origin': self.picking_id},
+ subtype_id=self.env.ref('mail.mt_note').id)
+ returned_lines = 0
+ for return_line in self.product_return_moves:
+ if not return_line.move_id:
+ raise UserError(_("You have manually created product lines, please delete them to proceed."))
+ # TODO sle: float_is_zero?
+ if return_line.quantity:
+ returned_lines += 1
+ vals = self._prepare_move_default_values(return_line, new_picking)
+ r = return_line.move_id.copy(vals)
+ vals = {}
+
+ # +--------------------------------------------------------------------------------------------------------+
+ # | picking_pick <--Move Orig-- picking_pack --Move Dest--> picking_ship
+ # | | returned_move_ids ↑ | returned_move_ids
+ # | ↓ | return_line.move_id ↓
+ # | return pick(Add as dest) return toLink return ship(Add as orig)
+ # +--------------------------------------------------------------------------------------------------------+
+ move_orig_to_link = return_line.move_id.move_dest_ids.mapped('returned_move_ids')
+ # link to original move
+ move_orig_to_link |= return_line.move_id
+ # link to siblings of original move, if any
+ move_orig_to_link |= return_line.move_id\
+ .mapped('move_dest_ids').filtered(lambda m: m.state not in ('cancel'))\
+ .mapped('move_orig_ids').filtered(lambda m: m.state not in ('cancel'))
+ move_dest_to_link = return_line.move_id.move_orig_ids.mapped('returned_move_ids')
+ # link to children of originally returned moves, if any. Note that the use of
+ # 'return_line.move_id.move_orig_ids.returned_move_ids.move_orig_ids.move_dest_ids'
+ # instead of 'return_line.move_id.move_orig_ids.move_dest_ids' prevents linking a
+ # return directly to the destination moves of its parents. However, the return of
+ # the return will be linked to the destination moves.
+ move_dest_to_link |= return_line.move_id.move_orig_ids.mapped('returned_move_ids')\
+ .mapped('move_orig_ids').filtered(lambda m: m.state not in ('cancel'))\
+ .mapped('move_dest_ids').filtered(lambda m: m.state not in ('cancel'))
+ vals['move_orig_ids'] = [(4, m.id) for m in move_orig_to_link]
+ vals['move_dest_ids'] = [(4, m.id) for m in move_dest_to_link]
+ r.write(vals)
+ if not returned_lines:
+ raise UserError(_("Please specify at least one non-zero quantity."))
+
+ new_picking.action_confirm()
+ new_picking.action_assign()
+ return new_picking.id, picking_type_id
+
+ def create_returns(self):
+ for wizard in self:
+ new_picking_id, pick_type_id = wizard._create_returns()
+ # Override the context to disable all the potential filters that could have been set previously
+ ctx = dict(self.env.context)
+ ctx.update({
+ 'default_partner_id': self.picking_id.partner_id.id,
+ 'search_default_picking_type_id': pick_type_id,
+ 'search_default_draft': False,
+ 'search_default_assigned': False,
+ 'search_default_confirmed': False,
+ 'search_default_ready': False,
+ 'search_default_planning_issues': False,
+ 'search_default_available': False,
+ })
+ return {
+ 'name': _('Returned Picking'),
+ 'view_mode': 'form,tree,calendar',
+ 'res_model': 'stock.picking',
+ 'res_id': new_picking_id,
+ 'type': 'ir.actions.act_window',
+ 'context': ctx,
+ }
diff --git a/addons/stock/wizard/stock_picking_return_views.xml b/addons/stock/wizard/stock_picking_return_views.xml
new file mode 100644
index 00000000..1334625e
--- /dev/null
+++ b/addons/stock/wizard/stock_picking_return_views.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="act_stock_return_picking" model="ir.actions.act_window">
+ <field name="name">Reverse Transfer</field>
+ <field name="res_model">stock.return.picking</field>
+ <field name="view_mode">form</field>
+ <field name="target">new</field>
+ </record>
+
+ <record id="view_stock_return_picking_form" model="ir.ui.view">
+ <field name="name">Return lines</field>
+ <field name="model">stock.return.picking</field>
+ <field name="arch" type="xml">
+ <form>
+ <field name="move_dest_exists" invisible="1"/>
+ <field name="picking_id" invisible="1" force_save="1"/>
+ <group attrs="{'invisible': [('move_dest_exists', '=', False)]}">
+ <div class="oe_grey">
+ <p>This picking appears to be chained with another operation. Later, if you receive the goods you are returning now, make sure to <b>reverse</b> the returned picking in order to avoid logistic rules to be applied again (which would create duplicated operations)</p>
+ </div>
+ </group>
+ <group>
+ <field name="product_return_moves" nolabel="1">
+ <tree editable="top" create="0">
+ <field name="product_id" options="{'no_create': True, 'no_open': True}" force_save="1"/>
+ <field name="quantity"/>
+ <field name="uom_id" readonly="1" groups="uom.group_uom"/>
+ <field name="move_id" invisible="1"/>
+ </tree>
+ </field>
+ </group>
+ <group>
+ <field name="parent_location_id" invisible="1"/>
+ <field name="original_location_id" invisible="1"/>
+ <field name="location_id" options="{'no_create': True, 'no_open': True}" groups="stock.group_stock_multi_locations" required="1"/>
+ <field name="company_id" invisible="1"/>
+ </group>
+ <footer>
+ <button name="create_returns" string="Return" type="object" class="btn-primary"/>
+ <button string="Cancel" class="btn-secondary" special="cancel" />
+ </footer>
+ </form>
+ </field>
+ </record>
+</odoo>
diff --git a/addons/stock/wizard/stock_quantity_history.py b/addons/stock/wizard/stock_quantity_history.py
new file mode 100644
index 00000000..1fd8a811
--- /dev/null
+++ b/addons/stock/wizard/stock_quantity_history.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import _, fields, models
+from odoo.osv import expression
+
+
+class StockQuantityHistory(models.TransientModel):
+ _name = 'stock.quantity.history'
+ _description = 'Stock Quantity History'
+
+ inventory_datetime = fields.Datetime('Inventory at Date',
+ help="Choose a date to get the inventory at that date",
+ default=fields.Datetime.now)
+
+ def open_at_date(self):
+ tree_view_id = self.env.ref('stock.view_stock_product_tree').id
+ form_view_id = self.env.ref('stock.product_form_view_procurement_button').id
+ domain = [('type', '=', 'product')]
+ product_id = self.env.context.get('product_id', False)
+ product_tmpl_id = self.env.context.get('product_tmpl_id', False)
+ if product_id:
+ domain = expression.AND([domain, [('id', '=', product_id)]])
+ elif product_tmpl_id:
+ domain = expression.AND([domain, [('product_tmpl_id', '=', product_tmpl_id)]])
+ # We pass `to_date` in the context so that `qty_available` will be computed across
+ # moves until date.
+ action = {
+ 'type': 'ir.actions.act_window',
+ 'views': [(tree_view_id, 'tree'), (form_view_id, 'form')],
+ 'view_mode': 'tree,form',
+ 'name': _('Products'),
+ 'res_model': 'product.product',
+ 'domain': domain,
+ 'context': dict(self.env.context, to_date=self.inventory_datetime),
+ }
+ return action
diff --git a/addons/stock/wizard/stock_quantity_history.xml b/addons/stock/wizard/stock_quantity_history.xml
new file mode 100644
index 00000000..2b7fe150
--- /dev/null
+++ b/addons/stock/wizard/stock_quantity_history.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="view_stock_quantity_history" model="ir.ui.view">
+ <field name="name">Inventory Report at Date</field>
+ <field name="model">stock.quantity.history</field>
+ <field name="arch" type="xml">
+ <form string="Choose your date">
+ <group>
+ <field name="inventory_datetime"/>
+ </group>
+ <footer>
+ <button name="open_at_date" string="Confirm" type="object" class="btn-primary"/>
+ <button string="Cancel" class="btn-secondary" special="cancel" />
+ </footer>
+ </form>
+ </field>
+ </record>
+</odoo>
diff --git a/addons/stock/wizard/stock_rules_report.py b/addons/stock/wizard/stock_rules_report.py
new file mode 100644
index 00000000..4df5a4f2
--- /dev/null
+++ b/addons/stock/wizard/stock_rules_report.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+
+
+class StockRulesReport(models.TransientModel):
+ _name = 'stock.rules.report'
+ _description = 'Stock Rules report'
+
+ product_id = fields.Many2one('product.product', string='Product', required=True)
+ product_tmpl_id = fields.Many2one('product.template', string='Product Template', required=True)
+ warehouse_ids = fields.Many2many('stock.warehouse', string='Warehouses', required=True,
+ help="Show the routes that apply on selected warehouses.")
+ product_has_variants = fields.Boolean('Has variants', default=False, required=True)
+
+ @api.model
+ def default_get(self, fields):
+ res = super(StockRulesReport, self).default_get(fields)
+ product_tmpl_id = self.env['product.template']
+ if 'product_id' in fields:
+ if self.env.context.get('default_product_id'):
+ product_id = self.env['product.product'].browse(self.env.context['default_product_id'])
+ product_tmpl_id = product_id.product_tmpl_id
+ res['product_tmpl_id'] = product_id.product_tmpl_id.id
+ res['product_id'] = product_id.id
+ elif self.env.context.get('default_product_tmpl_id'):
+ product_tmpl_id = self.env['product.template'].browse(self.env.context['default_product_tmpl_id'])
+ res['product_tmpl_id'] = product_tmpl_id.id
+ res['product_id'] = product_tmpl_id.product_variant_id.id
+ if len(product_tmpl_id.product_variant_ids) > 1:
+ res['product_has_variants'] = True
+ if 'warehouse_ids' in fields:
+ company = product_tmpl_id.company_id or self.env.company
+ warehouse_id = self.env['stock.warehouse'].search([('company_id', '=', company.id)], limit=1).id
+ res['warehouse_ids'] = [(6, 0, [warehouse_id])]
+ return res
+
+ def _prepare_report_data(self):
+ data = {
+ 'product_id': self.product_id.id,
+ 'warehouse_ids': self.warehouse_ids.ids,
+ }
+ return data
+
+ def print_report(self):
+ self.ensure_one()
+ data = self._prepare_report_data()
+ return self.env.ref('stock.action_report_stock_rule').report_action(None, data=data)
+
diff --git a/addons/stock/wizard/stock_rules_report_views.xml b/addons/stock/wizard/stock_rules_report_views.xml
new file mode 100644
index 00000000..a2c6c7eb
--- /dev/null
+++ b/addons/stock/wizard/stock_rules_report_views.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="view_stock_rules_report" model="ir.ui.view">
+ <field name="name">Stock Rules Report</field>
+ <field name="model">stock.rules.report</field>
+ <field name="arch" type="xml">
+ <form string="Product Routes Report">
+ <group>
+ <field name="product_tmpl_id" invisible="1" />
+ <field name="product_has_variants" invisible="1" />
+ <field name="product_id"
+ domain="[('product_tmpl_id', '=', product_tmpl_id)]"
+ attrs="{'readonly': [('product_has_variants', '=', False)]}" options="{'no_create': True}"/>
+ <field name="warehouse_ids"
+ groups="stock.group_stock_multi_warehouses"
+ widget="many2many_tags" />
+ </group>
+ <footer>
+ <button name="print_report"
+ string="Overview"
+ type="object"
+ class="btn-primary"/>
+ <button string="Cancel" class="btn-default" special="cancel"/>
+ </footer>
+ </form>
+ </field>
+ </record>
+
+ <record id="action_stock_rules_report" model="ir.actions.act_window">
+ <field name="name">Stock Rules Report</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="res_model">stock.rules.report</field>
+ <field name="view_mode">form</field>
+ <field name="view_id" ref="view_stock_rules_report"/>
+ <field name="target">new</field>
+ </record>
+</odoo>
diff --git a/addons/stock/wizard/stock_scheduler_compute.py b/addons/stock/wizard/stock_scheduler_compute.py
new file mode 100644
index 00000000..ac1793ed
--- /dev/null
+++ b/addons/stock/wizard/stock_scheduler_compute.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+#
+# Order Point Method:
+# - Order if the virtual stock of today is below the min of the defined order point
+#
+
+from odoo import api, models, tools
+
+import logging
+import threading
+
+_logger = logging.getLogger(__name__)
+
+
+class StockSchedulerCompute(models.TransientModel):
+ _name = 'stock.scheduler.compute'
+ _description = 'Run Scheduler Manually'
+
+ def _procure_calculation_orderpoint(self):
+ with api.Environment.manage():
+ # 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.cursor()
+ self = self.with_env(self.env(cr=new_cr))
+ scheduler_cron = self.sudo().env.ref('stock.ir_cron_scheduler_action')
+ # Avoid to run the scheduler multiple times in the same time
+ try:
+ with tools.mute_logger('odoo.sql_db'):
+ self._cr.execute("SELECT id FROM ir_cron WHERE id = %s FOR UPDATE NOWAIT", (scheduler_cron.id,))
+ except Exception:
+ _logger.info('Attempt to run procurement scheduler aborted, as already running')
+ self._cr.rollback()
+ self._cr.close()
+ return {}
+
+ for company in self.env.user.company_ids:
+ cids = (self.env.user.company_id | self.env.user.company_ids).ids
+ self.env['procurement.group'].with_context(allowed_company_ids=cids).run_scheduler(
+ use_new_cursor=self._cr.dbname,
+ company_id=company.id)
+ new_cr.close()
+ return {}
+
+ def procure_calculation(self):
+ threaded_calculation = threading.Thread(target=self._procure_calculation_orderpoint, args=())
+ threaded_calculation.start()
+ return {'type': 'ir.actions.client', 'tag': 'reload'}
diff --git a/addons/stock/wizard/stock_scheduler_compute_views.xml b/addons/stock/wizard/stock_scheduler_compute_views.xml
new file mode 100644
index 00000000..066d7977
--- /dev/null
+++ b/addons/stock/wizard/stock_scheduler_compute_views.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="view_procurement_compute_wizard" model="ir.ui.view">
+ <field name="name">Run Schedulers Manually</field>
+ <field name="model">stock.scheduler.compute</field>
+ <field name="arch" type="xml">
+ <form string="Parameters">
+ <p>
+ The stock will be reserved for operations waiting for availability and the reordering rules will be triggered.
+ </p>
+ <footer>
+ <button name="procure_calculation" string="Run Scheduler" type="object" class="btn-primary"/>
+ <button string="Cancel" class="btn-secondary" special="cancel" />
+ </footer>
+ </form>
+ </field>
+ </record>
+
+ <record id="action_procurement_compute" model="ir.actions.act_window">
+ <field name="name">Run Scheduler</field>
+ <field name="res_model">stock.scheduler.compute</field>
+ <field name="view_mode">form</field>
+ <field name="target">new</field>
+ </record>
+
+ <menuitem action="action_procurement_compute" id="menu_procurement_compute" parent="menu_stock_warehouse_mgmt" sequence="135"/>
+
+
+</odoo>
diff --git a/addons/stock/wizard/stock_track_confirmation.py b/addons/stock/wizard/stock_track_confirmation.py
new file mode 100644
index 00000000..786b14cd
--- /dev/null
+++ b/addons/stock/wizard/stock_track_confirmation.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class StockTrackConfirmation(models.TransientModel):
+ _name = 'stock.track.confirmation'
+ _description = 'Stock Track Confirmation'
+
+ tracking_line_ids = fields.One2many('stock.track.line', 'wizard_id')
+ inventory_id = fields.Many2one('stock.inventory', 'Inventory')
+
+ def action_confirm(self):
+ for confirmation in self:
+ confirmation.inventory_id._action_done()
+
+class StockTrackingLines(models.TransientModel):
+ _name = 'stock.track.line'
+ _description = 'Stock Track Line'
+
+ product_id = fields.Many2one('product.product', 'Product', readonly=True)
+ tracking = fields.Selection([('lot', 'Tracked by lot'), ('serial', 'Tracked by serial number')], readonly=True)
+ wizard_id = fields.Many2one('stock.track.confirmation', readonly=True)
diff --git a/addons/stock/wizard/stock_track_confirmation_views.xml b/addons/stock/wizard/stock_track_confirmation_views.xml
new file mode 100644
index 00000000..cc257a84
--- /dev/null
+++ b/addons/stock/wizard/stock_track_confirmation_views.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<odoo>
+ <record id="view_stock_track_confirmation" model="ir.ui.view">
+ <field name="name">stock.track.confirmation.view.form</field>
+ <field name="model">stock.track.confirmation</field>
+ <field name="arch" type="xml">
+ <form string="Lots or serial numbers were not provided to tracked products">
+ <field name="inventory_id" invisible="1"/>
+ <p>Some products of the inventory adjustment are tracked. Are you sure you don't want to specify a serial or lot number for them?</p>
+ <strong>Product(s) tracked: </strong>
+ <field name="tracking_line_ids" readonly="1">
+ <tree>
+ <field name="product_id"/>
+ <field name="tracking"/>
+ </tree>
+ </field>
+ <footer>
+ <button name="action_confirm" string="Confirm" type="object" class="btn-primary"/>
+ <button string="Discard" special="cancel" class="btn-secondary"/>
+ </footer>
+ </form>
+ </field>
+ </record>
+</odoo>
diff --git a/addons/stock/wizard/stock_warn_insufficient_qty.py b/addons/stock/wizard/stock_warn_insufficient_qty.py
new file mode 100644
index 00000000..19dccd23
--- /dev/null
+++ b/addons/stock/wizard/stock_warn_insufficient_qty.py
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+
+
+class StockWarnInsufficientQty(models.AbstractModel):
+ _name = 'stock.warn.insufficient.qty'
+ _description = 'Warn Insufficient Quantity'
+
+ product_id = fields.Many2one('product.product', 'Product', required=True)
+ location_id = fields.Many2one('stock.location', 'Location', domain="[('usage', '=', 'internal')]", required=True)
+ quant_ids = fields.Many2many('stock.quant', compute='_compute_quant_ids')
+ quantity = fields.Float(string="Quantity", required=True)
+ product_uom_name = fields.Char("Unit of Measure", required=True)
+
+ def _get_reference_document_company_id(self):
+ raise NotImplementedError()
+
+ @api.depends('product_id')
+ def _compute_quant_ids(self):
+ for quantity in self:
+ quantity.quant_ids = self.env['stock.quant'].search([
+ ('product_id', '=', quantity.product_id.id),
+ ('location_id.usage', '=', 'internal'),
+ ('company_id', '=', quantity._get_reference_document_company_id().id)
+ ])
+
+ def action_done(self):
+ raise NotImplementedError()
+
+
+class StockWarnInsufficientQtyScrap(models.TransientModel):
+ _name = 'stock.warn.insufficient.qty.scrap'
+ _inherit = 'stock.warn.insufficient.qty'
+ _description = 'Warn Insufficient Scrap Quantity'
+
+ scrap_id = fields.Many2one('stock.scrap', 'Scrap')
+
+ def _get_reference_document_company_id(self):
+ return self.scrap_id.company_id
+
+ def action_done(self):
+ return self.scrap_id.do_scrap()
+
+ def action_cancel(self):
+ # FIXME in master: we should not have created the scrap in a first place
+ if self.env.context.get('not_unlink_on_discard'):
+ return True
+ else:
+ return self.scrap_id.sudo().unlink()
diff --git a/addons/stock/wizard/stock_warn_insufficient_qty_views.xml b/addons/stock/wizard/stock_warn_insufficient_qty_views.xml
new file mode 100644
index 00000000..84db1979
--- /dev/null
+++ b/addons/stock/wizard/stock_warn_insufficient_qty_views.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="stock_warn_insufficient_qty_form_view" model="ir.ui.view">
+ <field name="name">stock.warn.insufficient.qty</field>
+ <field name="model">stock.warn.insufficient.qty</field>
+ <field name="arch" type="xml">
+ <form>
+ <div>
+ The product is not available in sufficient quantity
+ <span class="oe_inline" groups="stock.group_stock_multi_locations"> in
+ <strong><field name="location_id" readonly="True"/></strong>.
+ </span>
+ </div>
+ <div attrs="{'invisible': [('quant_ids', '=', [])]}">
+ <br/>
+ <strong>Current Inventory: </strong>
+ <field name="quant_ids" style="margin-top:10px;">
+ <tree>
+ <field name="location_id" options="{'no_create': True}"/>
+ <field name="lot_id" groups="stock.group_production_lot"/>
+ <field name="quantity"/>
+ </tree>
+ </field>
+ </div>
+ <div name="description">
+ </div>
+ <footer>
+ <button name="cancel_button" string="Discard" class="btn-primary" special="cancel"/>
+ <button string="Confirm" name="action_done" type="object" class="btn-secondary"/>
+ </footer>
+ </form>
+ </field>
+ </record>
+
+ <record id="stock_warn_insufficient_qty_scrap_form_view" model="ir.ui.view">
+ <field name="name">stock.warn.insufficient.qty.scrap</field>
+ <field name="model">stock.warn.insufficient.qty.scrap</field>
+ <field name="inherit_id" ref="stock.stock_warn_insufficient_qty_form_view"/>
+ <field name="mode">primary</field>
+ <field name="arch" type="xml">
+ <xpath expr="//div[@name='description']" position="inside">
+ Do you confirm you want to scrap <strong><field name="quantity" readonly="True"/></strong><field name="product_uom_name" readonly="True" class="mx-1"/>from location <strong><field name="location_id" readonly="True"/></strong>? This may lead to inconsistencies in your inventory.
+ </xpath>
+ <xpath expr="//button[@name='cancel_button']" position="replace">
+ <button string="Discard" name="action_cancel" type="object" class="btn-primary"/>
+ </xpath>
+ </field>
+ </record>
+</odoo>