From dd2aaf0b1dc4ff3997ca753ee1feb67920f5cbfa Mon Sep 17 00:00:00 2001 From: Pierre Masereel Date: Thu, 24 Sep 2020 13:51:46 +0000 Subject: [PATCH] [IMP] point_of_sale: Ship products later For now, when you are selling products in the point of sales, the stock is affected once the order is validated or the session closed. But in some case, you don't have the product in your store, and you have to plan a delivery for the order. To be able to plan a delivery, you can now choose the warehouse in POS Config and the routes of your warehouse will be applied. You can also set a specific rule on your POS Config if you've enabled the multi-step-routes in the inventory module closes odoo/odoo#58506 Task-id: 2082104 Signed-off-by: pimodoo --- addons/point_of_sale/models/pos_config.py | 12 +++ addons/point_of_sale/models/pos_order.py | 91 +++++++++++++++++-- addons/point_of_sale/models/pos_session.py | 2 +- addons/point_of_sale/models/stock_picking.py | 18 ++++ .../js/Screens/PaymentScreen/PaymentScreen.js | 18 +++- addons/point_of_sale/static/src/js/models.js | 12 ++- .../Screens/PaymentScreen/PaymentScreen.xml | 5 + .../point_of_sale/views/pos_config_view.xml | 25 +++++ 8 files changed, 169 insertions(+), 14 deletions(-) diff --git a/addons/point_of_sale/models/pos_config.py b/addons/point_of_sale/models/pos_config.py index f361601efa1..f925942d50b 100644 --- a/addons/point_of_sale/models/pos_config.py +++ b/addons/point_of_sale/models/pos_config.py @@ -76,6 +76,9 @@ class PosConfig(models.Model): _name = 'pos.config' _description = 'Point of Sale Configuration' + def _default_warehouse_id(self): + return self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1).id + def _default_picking_type_id(self): return self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1).pos_type_id.id @@ -217,6 +220,15 @@ class PosConfig(models.Model): has_active_session = fields.Boolean(compute='_compute_current_session') show_allow_invoicing_alert = fields.Boolean(compute="_compute_show_allow_invoicing_alert") manual_discount = fields.Boolean(string="Manual Discounts", default=True) + ship_later = fields.Boolean(string="Ship Later") + warehouse_id = fields.Many2one('stock.warehouse', default=_default_warehouse_id, ondelete='restrict') + route_id = fields.Many2one('stock.location.route', string="Spefic route for products delivered later.") + picking_policy = fields.Selection([ + ('direct', 'As soon as possible'), + ('one', 'When all products are ready')], + string='Shipping Policy', required=True, default='direct', + help="If you deliver all products at once, the delivery order will be scheduled based on the greatest " + "product lead time. Otherwise, it will be based on the shortest.") @api.depends('use_pricelist', 'available_pricelist_ids') def _compute_allowed_pricelist_ids(self): diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index ee34c7733a7..c6dac57d67a 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -49,6 +49,7 @@ class PosOrder(models.Model): 'amount_return': ui_order['amount_return'], 'company_id': self.env['pos.session'].browse(ui_order['pos_session_id']).company_id.id, 'to_invoice': ui_order['to_invoice'] if "to_invoice" in ui_order else False, + 'to_ship': ui_order['to_ship'] if "to_ship" in ui_order else False, 'is_tipped': ui_order.get('is_tipped', False), 'tip_amount': ui_order.get('tip_amount', 0), } @@ -242,6 +243,7 @@ class PosOrder(models.Model): picking_count = fields.Integer(compute='_compute_picking_count') failed_pickings = fields.Boolean(compute='_compute_picking_count') picking_type_id = fields.Many2one('stock.picking.type', related='session_id.config_id.picking_type_id', string="Operation Type", readonly=False) + procurement_group_id = fields.Many2one('procurement.group', 'Procurement Group', copy=False) note = fields.Text(string='Internal Notes') nb_print = fields.Integer(string='Number of Print', readonly=True, copy=False, default=0) @@ -255,6 +257,7 @@ class PosOrder(models.Model): payment_ids = fields.One2many('pos.payment', 'pos_order_id', string='Payments', readonly=True) session_move_id = fields.Many2one('account.move', string='Session Journal Entry', related='session_id.move_id', readonly=True, copy=False) to_invoice = fields.Boolean('To invoice') + to_ship = fields.Boolean('To ship') is_invoiced = fields.Boolean('Is Invoiced', compute='_compute_is_invoiced') is_tipped = fields.Boolean('Is this already tipped?', readonly=True) tip_amount = fields.Float(string='Tip Amount', digits=0, readonly=True) @@ -474,17 +477,20 @@ class PosOrder(models.Model): def _create_order_picking(self): self.ensure_one() - if not self.session_id.update_stock_at_closing or (self.company_id.anglo_saxon_accounting and self.to_invoice): - picking_type = self.config_id.picking_type_id - if self.partner_id.property_stock_customer: - destination_id = self.partner_id.property_stock_customer.id - elif not picking_type or not picking_type.default_location_dest_id: - destination_id = self.env['stock.warehouse']._get_partner_locations()[0].id - else: - destination_id = picking_type.default_location_dest_id.id + if self.to_ship: + self.lines._launch_stock_rule_from_pos_order_lines() + else: + if not self.session_id.update_stock_at_closing or (self.company_id.anglo_saxon_accounting and self.to_invoice): + picking_type = self.config_id.picking_type_id + if self.partner_id.property_stock_customer: + destination_id = self.partner_id.property_stock_customer.id + elif not picking_type or not picking_type.default_location_dest_id: + destination_id = self.env['stock.warehouse']._get_partner_locations()[0].id + else: + destination_id = picking_type.default_location_dest_id.id - pickings = self.env['stock.picking']._create_picking_from_pos_order_lines(destination_id, self.lines, picking_type, self.partner_id) - pickings.write({'pos_session_id': self.session_id.id, 'pos_order_id': self.id, 'origin': self.name}) + pickings = self.env['stock.picking']._create_picking_from_pos_order_lines(destination_id, self.lines, picking_type, self.partner_id) + pickings.write({'pos_session_id': self.session_id.id, 'pos_order_id': self.id, 'origin': self.name}) def add_payment(self, data): """Create a new payment for the order""" @@ -623,6 +629,7 @@ class PosOrder(models.Model): 'creation_date': order.date_order.astimezone(timezone), 'fiscal_position_id': order.fiscal_position_id.id, 'to_invoice': order.to_invoice, + 'to_ship': order.to_ship, 'state': order.state, 'account_move': order.account_move.id, 'id': order.id, @@ -788,6 +795,70 @@ class PosOrderLine(models.Model): def export_for_ui(self): return self.mapped(self._export_for_ui) if self else [] + def _get_procurement_group(self): + return self.order_id.procurement_group_id + + def _prepare_procurement_group_vals(self): + return { + 'name': self.order_id.name, + 'move_type': self.order_id.config_id.picking_policy, + 'pos_order_id': self.order_id.id, + 'partner_id': self.order_id.partner_id.id, + } + + def _prepare_procurement_values(self, group_id=False): + """ Prepare specific key for moves or other components that will be created from a stock rule + comming from a sale order line. This method could be override in order to add other custom key that could + be used in move/po creation. + """ + self.ensure_one() + # Use the delivery date if there is else use date_order and lead time + date_deadline = self.order_id.date_order + values = { + 'group_id': group_id, + 'date_planned': date_deadline, + 'date_deadline': date_deadline, + 'route_ids': self.order_id.config_id.route_id, + 'warehouse_id': self.order_id.config_id.warehouse_id or False, + 'partner_id': self.order_id.partner_id.id, + 'product_description_variants': self.full_product_name, + 'company_id': self.order_id.company_id, + } + return values + + def _launch_stock_rule_from_pos_order_lines(self): + + procurements = [] + for line in self: + line = line.with_company(line.company_id) + if not line.product_id.type in ('consu','product'): + continue + + group_id = line._get_procurement_group() + if not group_id: + group_id = self.env['procurement.group'].create(line._prepare_procurement_group_vals()) + line.order_id.procurement_group_id = group_id + + values = line._prepare_procurement_values(group_id=group_id) + product_qty = line.qty + + procurement_uom = line.product_id.uom_id + procurements.append(self.env['procurement.group'].Procurement( + line.product_id, product_qty, procurement_uom, + line.order_id.partner_id.property_stock_customer, + line.name, line.order_id.name, line.order_id.company_id, values)) + if procurements: + self.env['procurement.group'].run(procurements) + + # This next block is currently needed only because the scheduler trigger is done by picking confirmation rather than stock.move confirmation + orders = self.mapped('order_id') + for order in orders: + pickings_to_confirm = order.picking_ids + if pickings_to_confirm: + # Trigger the Scheduler for Pickings + pickings_to_confirm.action_confirm() + return True + class PosOrderLineLot(models.Model): _name = "pos.pack.operation.lot" diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index 799677a207d..6b902eae18b 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -340,7 +340,7 @@ class PosSession(models.Model): session_destination_id = picking_type.default_location_dest_id.id for order in self.order_ids: - if order.company_id.anglo_saxon_accounting and order.to_invoice: + if (order.company_id.anglo_saxon_accounting and order.to_invoice) or order.to_ship: continue destination_id = order.partner_id.property_stock_customer.id or session_destination_id if destination_id in lines_grouped_by_dest_location: diff --git a/addons/point_of_sale/models/stock_picking.py b/addons/point_of_sale/models/stock_picking.py index 1a0d6fce777..c4865a818d4 100644 --- a/addons/point_of_sale/models/stock_picking.py +++ b/addons/point_of_sale/models/stock_picking.py @@ -155,3 +155,21 @@ class StockPicking(models.Model): # Avoid sending Mail/SMS for POS deliveries pickings = self.filtered(lambda p: p.picking_type_id != p.picking_type_id.warehouse_id.pos_type_id) return super(StockPicking, pickings)._send_confirmation_email() + +class ProcurementGroup(models.Model): + _inherit = 'procurement.group' + + pos_order_id = fields.Many2one('pos.order', 'POS Order') + +class StockMove(models.Model): + _inherit = 'stock.move' + + def _get_new_picking_values(self): + vals = super(StockMove, self)._get_new_picking_values() + vals['pos_session_id'] = self.mapped('group_id.pos_order_id.session_id').id + vals['pos_order_id'] = self.mapped('group_id.pos_order_id').id + return vals + + def _key_assign_picking(self): + keys = super(StockMove, self)._key_assign_picking() + return keys + (self.group_id.pos_order_id,) diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js index 89a7a1de622..702f2894cf3 100644 --- a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js @@ -117,6 +117,11 @@ odoo.define('point_of_sale.PaymentScreen', function (require) { this.currentOrder.set_tip(parse.float(payload)); } } + toggleIsToShip() { + // click_ship + this.currentOrder.set_to_ship(!this.currentOrder.is_to_ship()); + this.render(); + } deletePaymentLine(event) { const { cid } = event.detail; const line = this.paymentLines.find((line) => line.cid === cid); @@ -220,11 +225,11 @@ odoo.define('point_of_sale.PaymentScreen', function (require) { return false; } - if (this.currentOrder.is_to_invoice() && !this.currentOrder.get_client()) { + if ((this.currentOrder.is_to_invoice() || this.currentOrder.is_to_ship()) && !this.currentOrder.get_client()) { const { confirmed } = await this.showPopup('ConfirmPopup', { title: this.env._t('Please select the Customer'), body: this.env._t( - 'You need to select the customer before you can invoice an order.' + 'You need to select the customer before you can invoice or ship an order.' ), }); if (confirmed) { @@ -233,6 +238,15 @@ odoo.define('point_of_sale.PaymentScreen', function (require) { return false; } + var customer = this.currentOrder.get_client() + if (this.currentOrder.is_to_ship() && !(customer.name && customer.street && customer.city && customer.country_id)) { + this.showPopup('ErrorPopup', { + title: this.env._t('Incorrect address for shipping'), + body: this.env._t('The selected customer needs an address.'), + }); + return false; + } + if (!this.currentOrder.is_paid() || this.invoicing) { return false; } diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 89eca97f97f..2c0af650a65 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -2664,6 +2664,7 @@ exports.Order = Backbone.Model.extend({ this.temporary = false; // FIXME this.to_invoice = false; // FIXME + this.to_ship = false; var orderlines = json.lines; for (var i = 0; i < orderlines.length; i++) { @@ -2721,6 +2722,7 @@ exports.Order = Backbone.Model.extend({ fiscal_position_id: this.fiscal_position ? this.fiscal_position.id : false, server_id: this.server_id ? this.server_id : false, to_invoice: this.to_invoice ? this.to_invoice : false, + to_ship: this.to_ship ? this.to_ship : false, is_tipped: this.is_tipped || false, tip_amount: this.tip_amount || 0, }; @@ -3431,7 +3433,15 @@ exports.Order = Backbone.Model.extend({ } this.fiscal_position = newClientFiscalPosition; this.set_pricelist(newClientPricelist); - } + }, + /* ---- Ship later --- */ + set_to_ship: function(to_ship) { + this.assert_editable(); + this.to_ship = to_ship; + }, + is_to_ship: function(){ + return this.to_ship; + }, }); var OrderCollection = Backbone.Collection.extend({ diff --git a/addons/point_of_sale/static/src/xml/Screens/PaymentScreen/PaymentScreen.xml b/addons/point_of_sale/static/src/xml/Screens/PaymentScreen/PaymentScreen.xml index a06e09db484..d3f818d249b 100644 --- a/addons/point_of_sale/static/src/xml/Screens/PaymentScreen/PaymentScreen.xml +++ b/addons/point_of_sale/static/src/xml/Screens/PaymentScreen/PaymentScreen.xml @@ -77,6 +77,11 @@ t-on-click="openCashbox"> Open Cashbox +
+ Ship Later +
diff --git a/addons/point_of_sale/views/pos_config_view.xml b/addons/point_of_sale/views/pos_config_view.xml index 50735adaa57..622b8c404be 100644 --- a/addons/point_of_sale/views/pos_config_view.xml +++ b/addons/point_of_sale/views/pos_config_view.xml @@ -531,6 +531,31 @@ +
+
+ +
+
+
+

Accounting