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