diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/stock/wizard | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/stock/wizard')
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> |
