Skip to content

Commit

Permalink
[ADD] pos_stock_available_online: Add new module to sync product avai…
Browse files Browse the repository at this point in the history
…lability with POS
  • Loading branch information
GabbasovDinar authored and ivs-cetmix committed Feb 1, 2023
1 parent dbf3aa3 commit 5a6ecf0
Show file tree
Hide file tree
Showing 23 changed files with 480 additions and 0 deletions.
1 change: 1 addition & 0 deletions pos_stock_available_online/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
wait a bot :)
1 change: 1 addition & 0 deletions pos_stock_available_online/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
20 changes: 20 additions & 0 deletions pos_stock_available_online/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "Point of Sale Stock Available Online",
"version": "16.0.1.0.0",
"category": "Sales/Point of Sale",
"summary": "Show the available quantity of products in the Point of Sale ",
"depends": ["point_of_sale", "stock_available", "base_automation"],
"website": "https://github.com/OCA/pos",
"author": "Cetmix, Odoo Community Association (OCA)",
"maintainers": ["GabbasovDinar", "CetmixGitDrone"],
"installable": True,
"data": ["views/res_config_settings_view.xml"],
"assets": {
"point_of_sale.assets": [
"pos_stock_available_online/static/src/css/**/*.css",
"pos_stock_available_online/static/src/js/**/*.js",
"pos_stock_available_online/static/src/xml/**/*.xml",
],
},
"license": "AGPL-3",
}
5 changes: 5 additions & 0 deletions pos_stock_available_online/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from . import pos_config
from . import pos_session
from . import res_config_settings
from . import stock_quant
from . import stock_warehouse
52 changes: 52 additions & 0 deletions pos_stock_available_online/models/pos_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import logging

from odoo import fields, models

_logger = logging.getLogger(__name__)


class PosConfig(models.Model):
_inherit = "pos.config"

display_product_quantity = fields.Boolean(
default=True,
)
main_warehouse_id = fields.Many2one(
"stock.warehouse",
related="picking_type_id.warehouse_id",
store=True,
)
additional_warehouse_ids = fields.Many2many(
"stock.warehouse",
"pos_config_stock_warehouse_rel",
"pos_config_id",
"warehouse_id",
string="Additional Warehouses",
help="For the selected warehouses will be displayed "
"quantity of available products in the POS",
)
minimum_product_quantity_alert = fields.Float(
default=0.0,
)

def _get_channel_name(self):
"""
Return full channel name as combination, POS Config ID and const CHANNEL
"""
self.ensure_one()
return '["{}","{}"]'.format("pos_stock_available_online", self.id)

def _notify_available_quantity(self, message):
"""
Notify POSes about product updates
"""
if not isinstance(message, list):
message = [message]
notifications = []
for config in self:
notifications.append(
[config._get_channel_name(), "pos.config/product_update", message]
)
if notifications:
self.env["bus.bus"]._sendmany(notifications)
_logger.debug("POS notifications for %s: %s", self.ids, notifications)
22 changes: 22 additions & 0 deletions pos_stock_available_online/models/pos_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from odoo import models


class PosSession(models.Model):
_inherit = "pos.session"

def _process_pos_ui_product_product(self, products):
config = self.config_id
if config.display_product_quantity:
product_obj = self.env["product.product"]
for product_info in products:
product = product_obj.browse(product_info["id"])
# prepared first main warehouse info
warehouse_info = [
config.main_warehouse_id._prepare_vals_for_pos(product)
]
# prepared additional warehouses info
for warehouse in config.additional_warehouse_ids:
warehouse_info.append(warehouse._prepare_vals_for_pos(product))
product_info["warehouse_info"] = warehouse_info

return super()._process_pos_ui_product_product(products)
23 changes: 23 additions & 0 deletions pos_stock_available_online/models/res_config_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from odoo import fields, models


class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"

pos_display_product_quantity = fields.Boolean(
related="pos_config_id.display_product_quantity",
readonly=False,
)
pos_main_warehouse_id = fields.Many2one(
"stock.warehouse",
related="pos_config_id.main_warehouse_id",
)
pos_additional_warehouse_ids = fields.Many2many(
"stock.warehouse",
related="pos_config_id.additional_warehouse_ids",
readonly=False,
)
pos_minimum_product_quantity_alert = fields.Float(
related="pos_config_id.minimum_product_quantity_alert",
readonly=False,
)
40 changes: 40 additions & 0 deletions pos_stock_available_online/models/stock_quant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import logging

from odoo import models

_logger = logging.getLogger(__name__)


class StockQuant(models.Model):
_inherit = "stock.quant"

def _prepare_pos_message(self):
"""
Return prepared message to send to POS
"""
self.ensure_one()
return self.warehouse_id._prepare_vals_for_pos(self.product_id)

def _notify_pos(self):
"""
Send notification to POS
"""
pos_session_obj = self.env["pos.session"]
for quant in self:
warehouse_id = quant.warehouse_id.id
configs = pos_session_obj.search(
[
("state", "=", "opened"),
("config_id.display_product_quantity", "=", True),
"|",
("config_id.additional_warehouse_ids", "in", [warehouse_id]),
("config_id.main_warehouse_id", "=", warehouse_id),
],
).mapped("config_id")
if configs:
configs._notify_available_quantity(quant._prepare_pos_message())

def write(self, vals):
res = super().write(vals)
self._notify_pos()
return res
17 changes: 17 additions & 0 deletions pos_stock_available_online/models/stock_warehouse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from odoo import models


class StockWarehouse(models.Model):
_inherit = "stock.warehouse"

def _prepare_vals_for_pos(self, product):
"""
Prepare warehouse info data to send a POS
"""
self.ensure_one()
return {
"id": self.id,
"name": self.name,
"quantity": product.with_context(warehouse=self.id).immediately_usable_qty,
"product_id": product.id,
}
16 changes: 16 additions & 0 deletions pos_stock_available_online/readme/CONFIGURE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
In "Point of Sale" configuration "Product Quantity" section activate "Display Product Quantity" feature:
.. image:: ../static/img/pos_config.png

By default quantity is displayed for the warehouse that is used in the POS stock operation type.

You can add additional warehouses to show quantity in by adding them into "Additional Warehouses" field.

In this case the following information will be displayed on product tiles:

- Total quantity = quantity in the default warehouse + quantity in the additional warehouses

- Quantity in the default warehouse

- Quantity in two first additional warehouses.

Eg is you add 6 additional warehouses only 2 of them will be displayed on product tile. This is due to the size of tile which is limited.
2 changes: 2 additions & 0 deletions pos_stock_available_online/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
* Cetmix <https://cetmix.com/>
* Dinar Gabbasov
6 changes: 6 additions & 0 deletions pos_stock_available_online/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
This module allows to display product quantities in selected locations in real time. Quantities are displayed directly on product tiles:
.. image:: ../static/img/pos_quantity.png

Once a product quantity is changed it will be simultaneously updated in all active POS.

This module depends on stock_available module which is available in https://github.com/OCA/stock-logistics-availability repo.
3 changes: 3 additions & 0 deletions pos_stock_available_online/readme/ROADMAP.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
This module requires connection to update quantities and doesn't support offline mode.
Add support for more than 2 additional quantities displayed on product tile.
Offline mode support (probably additional module).
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 38 additions & 0 deletions pos_stock_available_online/static/src/css/pos.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
.pos .product-list .warehouse-info {
position: absolute;
right: 0;
top: 0;
padding: 0 5px;
font-weight: bold;
}
.pos .product-list .warehouse-info .warehouse {
padding: 4px 0;
margin-left: auto;
width: fit-content;
}
.pos .product-list .warehouse-info .warehouse .quantity {
color: white;
padding: 4px;
float: right;
}
.pos .product-list .warehouse-info .warehouse .warehouse-name {
display: block;
color: #696969;
font-size: 10px;
text-align: right;
}
.pos .product-list .warehouse-info .warehouse.total .warehouse-name {
font-weight: bold;
}
.pos .product-list .warehouse-info .warehouse .quantity.available {
background: #32a868;
}
.pos .product-list .warehouse-info .warehouse .quantity.not-available {
background: #ef5350;
}
.pos .product-list .warehouse-info .warehouse .quantity {
border-radius: 5px;
}
.pos .product-list .warehouse-info .warehouse.total .quantity {
border-radius: 0px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/** @odoo-module **/

import ProductItem from "point_of_sale.ProductItem";
import Registries from "point_of_sale.Registries";
import {format} from "web.field_utils";
import utils from "web.utils";

const StockProductItem = (ProductItem) =>
class StockProductItem extends ProductItem {
format_quantity(quantity) {
const unit = this.env.pos.units_by_id[this.props.product.uom_id[0]];
var formattedQuantity = `${quantity}`;
if (unit) {
if (unit.rounding) {
var decimals = this.env.pos.dp["Product Unit of Measure"];
formattedQuantity = format.float(quantity, {
digits: [69, decimals],
});
} else {
formattedQuantity = utils.round_precision(quantity, 1).toFixed(0);
}
}
return `${formattedQuantity}`;
}
get display_total_quantity() {
return this.format_quantity(this.total_quantity);
}
get total_quantity() {
return this.warehouses.reduce(
(partialSum, warehouse) => partialSum + warehouse.quantity,
0
);
}
get warehouses() {
// Display only a limited number of warehouses in the POS interface,
// because the product card in the POS is limited to the design
// TODO: Think UI/UX to display information for any number of warehouses
const display_limit = 3;
return this.props.product.warehouse_info.slice(0, display_limit);
}
};

Registries.Component.extend(ProductItem, StockProductItem);
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/** @odoo-module **/

import ProductsWidget from "point_of_sale.ProductsWidget";
import Registries from "point_of_sale.Registries";

const StockProductsWidget = (ProductsWidget) =>
class StockProductsWidget extends ProductsWidget {
setup() {
super.setup();
this.env.services.bus_service.addChannel(this._getChannelName());
this.env.services.bus_service.addEventListener(
"notification",
this._onNotification.bind(this)
);
}
_getChannelName() {
return JSON.stringify([
"pos_stock_available_online",
String(this.env.pos.config.id),
]);
}
_onNotification({detail: notifications}) {
var payloads = [];
for (const {payload, type} of notifications) {
if (type === "pos.config/product_update") {
payloads.push(payload);
}
}
this._handleNotification(payloads);
}
async _handleNotification(payloads) {
if (this.env.isDebug()) {
console.log("Payloads:", payloads);
}
const db = this.env.pos.db;
const ProductIds = [];
for (const payload of payloads) {
for (const message of payload) {
var product = db.get_product_by_id(message.product_id);
if (product) {
// Update warehouse info of the product
var warehouse = product.warehouse_info.find(
(wh) => wh.id === message.id
);
if (warehouse) {
warehouse.quantity = message.quantity;
} else {
product.warehouse_info.push(message);
}
} else {
ProductIds.push(message.id);
}
}
}
if (ProductIds.length) {
await this.env.pos._addProducts([...new Set(ProductIds)], false);
}
// Re-render product list without category switching
this.render(true);
}
};

Registries.Component.extend(ProductsWidget, StockProductsWidget);
Loading

0 comments on commit 5a6ecf0

Please sign in to comment.