From 3751379f1e9a4c215fb6eb898b4ccc67659b9ace Mon Sep 17 00:00:00 2001 From: stephanchrst Date: Tue, 10 May 2022 21:51:50 +0700 Subject: initial commit 2 --- addons/stock/wizard/__init__.py | 16 ++ addons/stock/wizard/product_replenish.py | 89 ++++++++++ addons/stock/wizard/product_replenish_views.xml | 59 +++++++ addons/stock/wizard/stock_assign_serial_numbers.py | 34 ++++ addons/stock/wizard/stock_assign_serial_views.xml | 31 ++++ .../stock/wizard/stock_backorder_confirmation.py | 74 ++++++++ .../wizard/stock_backorder_confirmation_views.xml | 44 +++++ addons/stock/wizard/stock_change_product_qty.py | 44 +++++ .../wizard/stock_change_product_qty_views.xml | 34 ++++ addons/stock/wizard/stock_immediate_transfer.py | 67 +++++++ .../wizard/stock_immediate_transfer_views.xml | 37 ++++ addons/stock/wizard/stock_orderpoint_snooze.py | 34 ++++ .../stock/wizard/stock_orderpoint_snooze_views.xml | 27 +++ addons/stock/wizard/stock_package_destination.py | 29 +++ .../wizard/stock_package_destination_views.xml | 59 +++++++ addons/stock/wizard/stock_picking_return.py | 195 +++++++++++++++++++++ addons/stock/wizard/stock_picking_return_views.xml | 45 +++++ addons/stock/wizard/stock_quantity_history.py | 37 ++++ addons/stock/wizard/stock_quantity_history.xml | 18 ++ addons/stock/wizard/stock_rules_report.py | 50 ++++++ addons/stock/wizard/stock_rules_report_views.xml | 37 ++++ addons/stock/wizard/stock_scheduler_compute.py | 48 +++++ .../stock/wizard/stock_scheduler_compute_views.xml | 29 +++ addons/stock/wizard/stock_track_confirmation.py | 24 +++ .../wizard/stock_track_confirmation_views.xml | 24 +++ addons/stock/wizard/stock_warn_insufficient_qty.py | 51 ++++++ .../wizard/stock_warn_insufficient_qty_views.xml | 49 ++++++ 27 files changed, 1285 insertions(+) create mode 100644 addons/stock/wizard/__init__.py create mode 100644 addons/stock/wizard/product_replenish.py create mode 100644 addons/stock/wizard/product_replenish_views.xml create mode 100644 addons/stock/wizard/stock_assign_serial_numbers.py create mode 100644 addons/stock/wizard/stock_assign_serial_views.xml create mode 100644 addons/stock/wizard/stock_backorder_confirmation.py create mode 100644 addons/stock/wizard/stock_backorder_confirmation_views.xml create mode 100644 addons/stock/wizard/stock_change_product_qty.py create mode 100644 addons/stock/wizard/stock_change_product_qty_views.xml create mode 100644 addons/stock/wizard/stock_immediate_transfer.py create mode 100644 addons/stock/wizard/stock_immediate_transfer_views.xml create mode 100644 addons/stock/wizard/stock_orderpoint_snooze.py create mode 100644 addons/stock/wizard/stock_orderpoint_snooze_views.xml create mode 100644 addons/stock/wizard/stock_package_destination.py create mode 100644 addons/stock/wizard/stock_package_destination_views.xml create mode 100644 addons/stock/wizard/stock_picking_return.py create mode 100644 addons/stock/wizard/stock_picking_return_views.xml create mode 100644 addons/stock/wizard/stock_quantity_history.py create mode 100644 addons/stock/wizard/stock_quantity_history.xml create mode 100644 addons/stock/wizard/stock_rules_report.py create mode 100644 addons/stock/wizard/stock_rules_report_views.xml create mode 100644 addons/stock/wizard/stock_scheduler_compute.py create mode 100644 addons/stock/wizard/stock_scheduler_compute_views.xml create mode 100644 addons/stock/wizard/stock_track_confirmation.py create mode 100644 addons/stock/wizard/stock_track_confirmation_views.xml create mode 100644 addons/stock/wizard/stock_warn_insufficient_qty.py create mode 100644 addons/stock/wizard/stock_warn_insufficient_qty_views.xml (limited to 'addons/stock/wizard') 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 @@ + + + + Replenish + product.replenish + +
+

+ 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. +

+ + + + + + +
+
+
+
+
+ + + Replenish + ir.actions.act_window + product.replenish + + + form + + new + +
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 @@ + + + + stock_assign_serial_numbers + stock.assign.serial + +
+ + + + + + +
+
+
+
+
+ + + Assign Serial Numbers + ir.actions.act_window + stock.assign.serial + form + {} + new + +
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 @@ + + + + stock_backorder_confirmation + stock.backorder.confirmation + +
+ +

+ You have processed less products than the initial demand. +

+ 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. +

+
+ + + + + + > + + + + + + +
+
+ +
+
+
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 @@ + + + + + Change Product Quantity + stock.change.product.qty + +
+ + + + + + +
+
+
+
+
+ + + Change Product Quantity + stock.change.product.qty + form + new + +
+
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 @@ + + + + stock.immediate.transfer.view.form + stock.immediate.transfer + +
+ +

You have not recorded done quantities yet, by clicking on apply Odoo will process all the quantities.

+
+ + + + + + > + + + + + + +
+
+ +
+
+
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 @@ + + + + Stock Orderpoint Snooze + stock.orderpoint.snooze + +
+ + + + + +
+
+
+
+
+ + + Snooze + stock.orderpoint.snooze + form + new + +
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 @@ + + + + stock.package.destination.view + stock.package.destination + +
+
+ You are trying to put products going to different locations into the same package +
+
+ + + + + + + + + + + + + +
+
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+
+ Where do you want to send the products ? +
+
+ + +
+
+
+
+
+
+
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 @@ + + + + Reverse Transfer + stock.return.picking + form + new + + + + Return lines + stock.return.picking + +
+ + + +
+

This picking appears to be chained with another operation. Later, if you receive the goods you are returning now, make sure to reverse the returned picking in order to avoid logistic rules to be applied again (which would create duplicated operations)

+
+
+ + + + + + + + + + + + + + + + +
+
+ +
+
+
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 @@ + + + + Inventory Report at Date + stock.quantity.history + +
+ + + +
+
+
+
+
+
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 @@ + + + + Stock Rules Report + stock.rules.report + +
+ + + + + + +
+
+
+
+
+ + + Stock Rules Report + ir.actions.act_window + stock.rules.report + form + + new + +
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 @@ + + + + Run Schedulers Manually + stock.scheduler.compute + +
+

+ The stock will be reserved for operations waiting for availability and the reordering rules will be triggered. +

+
+
+
+
+
+ + + Run Scheduler + stock.scheduler.compute + form + new + + + + + +
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 @@ + + + + stock.track.confirmation.view.form + stock.track.confirmation + +
+ +

Some products of the inventory adjustment are tracked. Are you sure you don't want to specify a serial or lot number for them?

+ Product(s) tracked: + + + + + + +
+
+ +
+
+
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 @@ + + + + stock.warn.insufficient.qty + stock.warn.insufficient.qty + +
+
+ The product is not available in sufficient quantity + in + . + +
+
+
+ Current Inventory: + + + + + + + +
+
+
+
+
+
+
+
+ + + stock.warn.insufficient.qty.scrap + stock.warn.insufficient.qty.scrap + + primary + + + Do you confirm you want to scrap from location ? This may lead to inconsistencies in your inventory. + + +