[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 <pimodoo@users.noreply.github.com>
This commit is contained in:
Pierre Masereel
2021-01-27 10:46:13 +00:00
parent 3c1f2961ec
commit dd2aaf0b1d
8 changed files with 169 additions and 14 deletions
+12
View File
@@ -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):
+81 -10
View File
@@ -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"
+1 -1
View File
@@ -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:
@@ -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,)
@@ -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;
}
+11 -1
View File
@@ -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({
@@ -77,6 +77,11 @@
t-on-click="openCashbox">
<i class="fa fa-archive" /> Open Cashbox
</div>
<div t-if="env.pos.config.ship_later" class="button"
t-att-class="{ highlight: currentOrder.is_to_ship() }"
t-on-click="toggleIsToShip()">
<i class="fa fa-clock-o" /> Ship Later
</div>
</div>
</div>
</div>
@@ -531,6 +531,31 @@
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="ship_later"/>
</div>
<div class="o_setting_right_pane">
<label for="ship_later" string="Ship Later"/>
<div class="text-muted">
Sell products and deliver them later.
</div>
<div attrs="{'invisible' : [('ship_later', '=', False)]}">
<div>
<label for="route_id" string="Warehouse" class="font-weight-normal"/>
<field name="warehouse_id" attrs="{'required': [('ship_later', '=', True)]}"/>
</div>
<div groups="stock.group_adv_location">
<label for="route_id" string="Specific route" class="font-weight-normal"/>
<field name="route_id"/>
</div>
<div>
<label for="picking_policy" class="font-weight-normal"/>
<field name="picking_policy" attrs="{'required': [('ship_later', '=', True)]}"/>
</div>
</div>
</div>
</div>
</div>
<h2>Accounting</h2>
<div class="row mt16 o_settings_container" id="accounting_section">