+
Point Of Sale - Picking Load
+
+
+
+
This module extends the functionality of point of sale to allow you to
+load your pickings in the Point of Sale, in order to add / remove products
+and so create a PoS Order and mark it as paid.
+
Detailled Use Case
+
This module is usefull for the following use case
+
+- You have many Sale Orders that have generated pickings. Typically if you have
+connected your Odoo instance to an online store like Shop Invader,
+Prestashop, Magento, or if you use light Odoo shop (website_sale
+module).
+- Once the order validated, you prepare your pickings
+- The customer come in your shop to recover his order
+- the customer add (or remove) some products
+- the customer pay his order, based on the real delivered products list
+
+
Table of contents
+
+
+
+
To configure this module, you need to:
+
+- Go to Warehouse / Configuration / Types of Operation
+- Select the picking type(s) you want to see in the point of sale
+- Check the box ‘Available in Point of Sale’
+
+
+
Note: This box is NOT enabled by default except in demo data for the type
+‘Delivery Orders’ of the demo company ‘YourCompany’.
+
+- Go to Point of Sale / Configuration / Point of Sales
+- Select the Point(s) of Sales witch those you want to enable the feature
+- Check the box ‘Load Pickings’
+- Set the max quantity of pickings you want to load
+
+
+
Note: This box is enabled by default
+
Technical Notes
+
+- By default, the Point of Sale will display only the pickings if the state is
+in ‘Waiting Availability’, ‘Partially Available’ or ‘Ready to Transfer’.
+
+
You can change this filter by overloading the _prepare_filter_for_pos
+function of the model stock.picking.
+
+- By default, the search of pickings will be done on the fields name,
+origin and partner_id of the picking.
+
+
You can change this feature by overloading the
+_prepare_filter_query_for_pos function of the model stock.picking.
+
+- By default, when the PoS order is confirmed, the original picking is
+cancelled and the sale order is set to the state ‘Done’.
+
+
You can change this behaviour by overloading
+_handle_orders_with_original_picking function of the model pos.order.
+
+
+
+
To use this module, you need to:
+
+- Launch the point of sale
+- On a new order (without lines), click on the ‘Load Picking’ button.
+
+
+
+- Point of sale will load available pickings. (About displayed pickings, see
+‘Technical Notes’ section).
+
+
+
+- Click on a picking will check if the picking is loadable and if yes, will
+display a ‘Select’ button. (See ‘Possible Warnings’ Section)
+
+
+
+- Confirm the selection, by clicking on ‘Select’ button. It will display
+the content of the moves (as PoS Order Lines)
+
+
+
The price and the discount will be the sale price and the discount set in
+the according Sale Order Line, if it was found. Otherwise, discount will be
+set to 0, and unit price will be the unit price of the product when it has been
+loaded in the Point of Sale.
+
Related Sale Order:
+
+
Related Picking:
+
+
+- Finally, you can add / remove products or change quantity and collect the
+payment.
+
+
When, the order is marked as paid, the original picking will be cancelled,
+because Point Of Sale generates a new picking related to the real delivered
+products and the original Sale Order will pass to the state ‘Done’. (Delivery
+exception is ignored).
+(See ‘Technical Notes’ section).
+
Possible Warnings
+
Some warning messages can appear:
+
+- if some products are not available in the Point of Sale
+
+
+
+- if the partner is not available in the Point of Sale
+
+
+
+- if the picking has been still loaded in another PoS order
+
+
+
+
+
+
+- This module will try to get original unit price from the sale order and not
+use the Current unit price of the product.
+(The price at which you pledged to sell the product).
+Some VAT troubles will occure if a product is set with VAT marked as
+‘VAT included’ and if in the sale order line, there are some VAT marked as
+‘VAT excluded’ for exemple.
+
+
The VAT settings should be consistent.
+
+
+
+
Bugs are tracked on GitHub Issues.
+In case of trouble, please check there if your issue has already been reported.
+If you spotted it first, help us smashing it by providing a detailed and welcomed
+feedback.
+
Do not contact contributors directly about support or help with technical issues.
+
+
+
+
+
+
+
+
Current maintainer:
+
+
This module is part of the legalsylvain/pos project on GitHub.
+
You are welcome to contribute.
+
+
+
+
+
diff --git a/pos_picking_load/static/description/load_picking_01_load_button.png b/pos_picking_load/static/description/load_picking_01_load_button.png
new file mode 100644
index 00000000..3f52aae1
Binary files /dev/null and b/pos_picking_load/static/description/load_picking_01_load_button.png differ
diff --git a/pos_picking_load/static/description/load_picking_02_picking_list.png b/pos_picking_load/static/description/load_picking_02_picking_list.png
new file mode 100644
index 00000000..d23c63ad
Binary files /dev/null and b/pos_picking_load/static/description/load_picking_02_picking_list.png differ
diff --git a/pos_picking_load/static/description/load_picking_03_confirm.png b/pos_picking_load/static/description/load_picking_03_confirm.png
new file mode 100644
index 00000000..becc45ee
Binary files /dev/null and b/pos_picking_load/static/description/load_picking_03_confirm.png differ
diff --git a/pos_picking_load/static/description/load_picking_04_pos_order.png b/pos_picking_load/static/description/load_picking_04_pos_order.png
new file mode 100644
index 00000000..91e07fca
Binary files /dev/null and b/pos_picking_load/static/description/load_picking_04_pos_order.png differ
diff --git a/pos_picking_load/static/description/load_picking_sale_order.png b/pos_picking_load/static/description/load_picking_sale_order.png
new file mode 100644
index 00000000..e019ede4
Binary files /dev/null and b/pos_picking_load/static/description/load_picking_sale_order.png differ
diff --git a/pos_picking_load/static/description/load_picking_stock_picking.png b/pos_picking_load/static/description/load_picking_stock_picking.png
new file mode 100644
index 00000000..808a126b
Binary files /dev/null and b/pos_picking_load/static/description/load_picking_stock_picking.png differ
diff --git a/pos_picking_load/static/description/load_picking_warning_partner.png b/pos_picking_load/static/description/load_picking_warning_partner.png
new file mode 100644
index 00000000..61438a38
Binary files /dev/null and b/pos_picking_load/static/description/load_picking_warning_partner.png differ
diff --git a/pos_picking_load/static/description/load_picking_warning_picking_still_loaded.png b/pos_picking_load/static/description/load_picking_warning_picking_still_loaded.png
new file mode 100644
index 00000000..1a0118c0
Binary files /dev/null and b/pos_picking_load/static/description/load_picking_warning_picking_still_loaded.png differ
diff --git a/pos_picking_load/static/description/load_picking_warning_product.png b/pos_picking_load/static/description/load_picking_warning_product.png
new file mode 100644
index 00000000..3084d105
Binary files /dev/null and b/pos_picking_load/static/description/load_picking_warning_product.png differ
diff --git a/pos_picking_load/static/description/pos_config_form.png b/pos_picking_load/static/description/pos_config_form.png
new file mode 100644
index 00000000..42a01b59
Binary files /dev/null and b/pos_picking_load/static/description/pos_config_form.png differ
diff --git a/pos_picking_load/static/description/stock_picking_type_form.png b/pos_picking_load/static/description/stock_picking_type_form.png
new file mode 100644
index 00000000..f93fbfca
Binary files /dev/null and b/pos_picking_load/static/description/stock_picking_type_form.png differ
diff --git a/pos_picking_load/static/src/css/pos_picking_load.css b/pos_picking_load/static/src/css/pos_picking_load.css
new file mode 100644
index 00000000..afa6e7e5
--- /dev/null
+++ b/pos_picking_load/static/src/css/pos_picking_load.css
@@ -0,0 +1,50 @@
+
+/* LoadPickingScreenWidget - Header Part*/
+.pos .pickinglist-screen .button.picking-button {
+ width: 150px;
+}
+
+.pos .pickinglist-screen .searchbox{
+ top: 4px;
+ position: relative;
+}
+
+.pos .pickinglist-screen .searchbox input{
+ width: 300px;
+}
+
+.pos .pickinglist-screen .button.validate.picking-button{
+ right: 0px;
+}
+
+/* LoadPickingScreenWidget - List Part*/
+
+.pos .pickinglist-screen .picking-list{
+ font-size: 16px;
+ width: 100%;
+ line-height: 40px;
+}
+
+.pos .pickinglist-screen .picking-list th,
+.pos .pickinglist-screen .picking-list td {
+ padding: 0px 8px;
+}
+
+.pos .pickinglist-screen .picking-list tbody > tr{
+ background: rgb(230,230,230);
+ cursor: pointer;
+}
+
+.pos .pickinglist-screen .picking-list thead > tr,
+.pos .pickinglist-screen .picking-list tr:nth-child(even) {
+ background: rgb(247,247,247);
+}
+
+.pos .pickinglist-screen .picking-list .highlight {
+ background-color: rgb(110, 200, 155) !important;
+ color: rgb(255, 255, 255);
+}
+
+.pos .pickinglist-screen .picking-list tbody > tr:hover {
+ background: rgb(220,220,220);
+}
diff --git a/pos_picking_load/static/src/js/model.js b/pos_picking_load/static/src/js/model.js
new file mode 100644
index 00000000..228b98df
--- /dev/null
+++ b/pos_picking_load/static/src/js/model.js
@@ -0,0 +1,77 @@
+/** ***************************************************************************
+ Copyright (C) 2017 - Today: GRAP (http://www.grap.coop)
+ @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
+ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+******************************************************************************/
+
+odoo.define('pos_picking_load.model', function (require) {
+ "use strict";
+
+ var models = require('point_of_sale.models');
+
+ /** **********************************************************************
+ Extend Model Order:
+ * Add getter and setter function for field 'origin_picking_id';
+ */
+
+ var moduleOrderParent = models.Order;
+ models.Order = models.Order.extend({
+
+ load_from_picking_data: function (picking_data) {
+ var self = this;
+
+ var partner = this.pos.db.get_partner_by_id(
+ picking_data.partner_id);
+
+ this.set({
+ 'origin_picking_id': picking_data.id,
+ 'origin_picking_name': picking_data.name,
+ });
+ this.set_client(partner);
+
+ picking_data.line_ids.forEach(function (picking_line_data) {
+ // Create new line and add it to the current order
+ var product = self.pos.db.get_product_by_id(
+ picking_line_data.product_id);
+ var order_line_data =
+ self.prepare_order_line_from_picking_line_data(
+ product, picking_line_data);
+ self.add_product(product, order_line_data);
+ });
+ },
+
+ prepare_order_line_from_picking_line_data: function (
+ product, picking_line_data) {
+ return {
+ quantity: picking_line_data.quantity,
+ price: picking_line_data.price_unit || product.price,
+ discount: picking_line_data.discount || 0.0,
+ };
+ },
+
+ export_for_printing: function () {
+ var order = moduleOrderParent.prototype.export_for_printing.apply(
+ this, arguments);
+ order.origin_picking_name = this.get('origin_picking_name');
+ return order;
+ },
+
+ export_as_JSON: function () {
+ var order = moduleOrderParent.prototype.export_as_JSON.apply(
+ this, arguments);
+ order.origin_picking_id = this.get('origin_picking_id');
+ order.origin_picking_name = this.get('origin_picking_name');
+ return order;
+ },
+
+ init_from_JSON: function (json) {
+ moduleOrderParent.prototype.init_from_JSON.apply(this, arguments);
+ this.set({
+ 'origin_picking_id': json.origin_picking_id,
+ 'origin_picking_name': json.origin_picking_name,
+ });
+ },
+
+ });
+
+});
diff --git a/pos_picking_load/static/src/js/widget.js b/pos_picking_load/static/src/js/widget.js
new file mode 100644
index 00000000..4a4ad813
--- /dev/null
+++ b/pos_picking_load/static/src/js/widget.js
@@ -0,0 +1,320 @@
+/** ***************************************************************************
+ Copyright (C) 2017 - Today: GRAP (http://www.grap.coop)
+ @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
+ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+******************************************************************************/
+
+odoo.define('pos_picking_load.widget', function (require) {
+ "use strict";
+
+ var core = require('web.core');
+ var framework = require('web.framework');
+ var rpc = require('web.rpc');
+
+ var gui = require('point_of_sale.gui');
+ var screens = require('point_of_sale.screens');
+
+ var QWeb = core.qweb;
+ var _t = core._t;
+
+
+ /** **********************************************************************
+ New ScreenWidget LoadPickingScreenWidget:
+ * On show, display all pickings;
+ * on click on a picking, display the content;
+ * on click on 'validate', allow to use this picking;
+ * on click on 'cancel', display the preview screen;
+ */
+ var LoadPickingScreenWidget = screens.ScreenWidget.extend({
+ template: 'LoadPickingScreenWidget',
+ auto_back: true,
+
+ current_picking_id: false,
+ current_picking_name: false,
+
+ show: function () {
+ var self = this;
+ this._super();
+
+ this.renderElement();
+
+ // Bind functions
+ this.$('.back').click(_.bind(this.clickBackButton, this));
+ this.$('.validate').click(_.bind(this.clickValidateButton, this));
+
+ // Initialize display
+ this.$('.validate').hide();
+
+ this.search_pickings();
+
+ this.$('.picking-list-contents').delegate(
+ '.picking-line', 'click', function (event) {
+ self.select_picking(event);
+ });
+
+ // Handle search
+ var search_timeout = null;
+ this.$('.searchbox input').on('keyup', function () {
+ clearTimeout(search_timeout);
+ var query = this.value;
+ search_timeout = setTimeout(function () {
+ self.perform_search(query);
+ }, 70);
+ });
+
+ this.$('.searchbox .search-clear').click(function () {
+ self.clear_search();
+ });
+
+ },
+
+ select_picking: function (event) {
+ var origin_picking_id = parseInt(
+ event.target.parentNode.dataset.pickingId, 10);
+ var self = this;
+ this.current_picking_data = false;
+ this.$('span.button.validate').hide();
+ this.$('.picking-list .highlight').removeClass('highlight');
+
+ framework.blockUI();
+ var params = {
+ model: 'stock.picking',
+ method: 'load_picking_for_pos',
+ args: [origin_picking_id],
+ };
+ rpc.query(params)
+ .then(function (picking_data) {
+ framework.unblockUI();
+ if (self.check_picking(picking_data)) {
+ self.current_picking_data = picking_data;
+ $(event.target.parentNode).addClass('highlight');
+ self.$('span.button.validate').show();
+ }
+ }).fail(function (error, fail_event) {
+ framework.unblockUI();
+ self.handle_errors(error, fail_event);
+ });
+ },
+
+ check_picking: function (picking_data) {
+ var self = this;
+
+ var picking_selectable = true;
+
+ // Forbid POS Order loading if some products are unknown
+ var unknown_products = [];
+
+ picking_data.line_ids.forEach(function (picking_line_data) {
+ var line_name = picking_line_data.name.replace('\n', ' ');
+ var product = self.pos.db.get_product_by_id(
+ picking_line_data.product_id);
+ if (_.isUndefined(product)) {
+ unknown_products.push(line_name);
+ }
+ });
+ if (unknown_products.length > 0) {
+ self.gui.show_popup(
+ 'error-traceback', {
+ 'title': _t('Unknown Products'),
+ 'body': _t(
+ "Unable to load some picking lines because the" +
+ " products are not available in the POS" +
+ " cache.\n\n" +
+ "Please check that lines :\n\n * ") +
+ unknown_products.join("; \n *"),
+ });
+ picking_selectable = false;
+ }
+
+ // Check if the partner is unknown
+ var partner = self.pos.db.get_partner_by_id(
+ picking_data.partner_id);
+
+ if (_.isUndefined(partner)) {
+ self.gui.show_popup(
+ 'error-traceback', {
+ 'title': _t('Unknown Partner'),
+ 'body': _t(
+ "Unable to load this picking because the partner" +
+ " is not known in the Point Of Sale" +
+ " as a customer"),
+ });
+ picking_selectable = false;
+ }
+
+ // Check if the picking is still loaded in another PoS tab
+ self.pos.db.get_unpaid_orders().forEach(function (order) {
+ if (order.origin_picking_id === picking_data.id) {
+ self.gui.show_popup(
+ 'error-traceback', {
+ 'title': _t('Picking Still Loaded'),
+ 'body': _t(
+ "Unable to load this picking because it has" +
+ " been loaded in another draft" +
+ " PoS Order :\n\n") +
+ order.name,
+ });
+ picking_selectable = false;
+ }
+ });
+
+ // Check if the picking has still been handled in another PoS Order
+ self.pos.db.get_orders().forEach(function (order) {
+ if (order.origin_picking_id === picking_data.id) {
+ self.gui.show_popup(
+ 'error-traceback', {
+ 'title': _t('Picking Still Loaded'),
+ 'body': _t(
+ "Unable to load this picking because it has" +
+ " been loaded in another confirmed" +
+ " PoS Order :\n\n") +
+ order.name,
+ });
+ picking_selectable = false;
+ }
+ });
+ return picking_selectable;
+ },
+
+ perform_search: function (query) {
+ this.search_pickings(query);
+ },
+
+ clear_search: function () {
+ this.search_pickings();
+ this.$('.searchbox input')[0].value = '';
+ this.$('.searchbox input').focus();
+ },
+
+ search_pickings: function (query) {
+ var self = this;
+ var params = {
+ model: 'stock.picking',
+ method: 'search_pickings_for_pos',
+ args: [query || '', this.pos.pos_session.id],
+ };
+ rpc.query(params)
+ .then(function (result) {
+ self.render_list(result);
+ }).fail(function (error, event) {
+ self.handle_errors(error, event);
+ });
+ },
+
+ render_list: function (pickings) {
+ var contents = this.$el[0].querySelector('.picking-list-contents');
+ contents.innerHTML = "";
+ var line_list = document.createDocumentFragment();
+ _.each(pickings, function (picking) {
+ var picking_line_html = QWeb.render(
+ 'LoadPickingLine', {widget: this, picking:picking});
+ var picking_line = document.createElement('tbody');
+ picking_line.innerHTML = picking_line_html;
+ picking_line = picking_line.childNodes[1];
+ line_list.appendChild(picking_line);
+ });
+ contents.appendChild(line_list);
+ },
+
+ // User Event
+ clickBackButton: function () {
+ this.gui.back();
+ },
+
+ clickValidateButton: function () {
+ var order = this.pos.get_order();
+ order.load_from_picking_data(this.current_picking_data);
+ this.gui.show_screen('products');
+ },
+
+ handle_errors: function (error, event) {
+ var self = this;
+ if (parseInt(error.code, 10) === 200) {
+ // Business Logic Error, not a connection problem
+ self.gui.show_popup('error-traceback', {
+ 'title': error.data.message || _t("Server Error"),
+ 'body': error.data.debug || _t(
+ "The server encountered an error while" +
+ " receiving your order."),
+ });
+ } else {
+ self.gui.show_popup('error', {
+ 'title': _t('Connection error'),
+ 'body': _t(
+ "Can not execute this action because the POS" +
+ " is currently offline"),
+ });
+ }
+ event.preventDefault();
+ },
+ });
+
+ gui.define_screen({
+ 'name': 'load_picking',
+ 'widget': LoadPickingScreenWidget,
+ 'condition': function () {
+ return this.pos.config.iface_load_picking;
+ },
+ });
+
+
+ /** **********************************************************************
+ New Widget LoadPickingButtonWidget:
+ * On click, display a new screen to select a picking;
+ */
+ var LoadPickingButtonWidget = screens.ActionButtonWidget.extend({
+ template: 'LoadPickingButtonWidget',
+
+ button_click: function () {
+ if (_.isUndefined(this.pos.get_order().get('origin_picking_id'))) {
+ this.gui.show_screen('load_picking');
+ } else {
+ this.gui.show_popup('error', {
+ 'title': _t('Pending Picking'),
+ 'body': _t(
+ "A picking is still loaded. You can not load" +
+ " another picking. Please create a new order."),
+ });
+ }
+ },
+
+ button_text: function () {
+ if (! this.pos.get_order() ||
+ _.isUndefined(
+ this.pos.get_order().get('origin_picking_id'))) {
+ return _t("Load Picking");
+ }
+ return this.pos.get_order().get('origin_picking_name');
+ },
+
+ is_visible: function () {
+ if (this.pos.get_order()) {
+ return (
+ this.pos.get_order().get_orderlines().length === 0 ||
+ ! _.isUndefined(
+ this.pos.get_order().get('origin_picking_id')));
+ }
+ return false;
+ },
+
+ });
+
+ screens.define_action_button({
+ 'name': 'load_picking',
+ 'widget': LoadPickingButtonWidget,
+ 'condition': function () {
+ return this.pos.config.iface_load_picking;
+ },
+ });
+
+ screens.OrderWidget.include({
+ update_summary: function () {
+ this._super();
+ if (this.getParent().action_buttons &&
+ this.getParent().action_buttons.load_picking) {
+ this.getParent().action_buttons.load_picking.renderElement();
+ }
+ },
+ });
+
+});
diff --git a/pos_picking_load/static/src/xml/pos_picking_load.xml b/pos_picking_load/static/src/xml/pos_picking_load.xml
new file mode 100644
index 00000000..da4c1368
--- /dev/null
+++ b/pos_picking_load/static/src/xml/pos_picking_load.xml
@@ -0,0 +1,85 @@
+
+