From 1420b44d716d99914b11c04c859a190f0f571d64 Mon Sep 17 00:00:00 2001 From: Nguyen Minh Chien Date: Thu, 21 Sep 2023 03:41:01 +0000 Subject: [PATCH 1/7] [ADD] F#T61547 - [TMT] System to deal with the scraps --- pos_scrap_order/__init__.py | 2 + pos_scrap_order/__manifest__.py | 22 +++ pos_scrap_order/i18n/fr.po | 190 +++++++++++++++++++ pos_scrap_order/models/__init__.py | 2 + pos_scrap_order/models/pos_config.py | 14 ++ pos_scrap_order/models/pos_order.py | 80 ++++++++ pos_scrap_order/security/ir.model.access.csv | 3 + pos_scrap_order/static/src/css/scrap.css | 93 +++++++++ pos_scrap_order/static/src/js/pos_model.js | 10 + pos_scrap_order/static/src/js/screen.js | 144 ++++++++++++++ pos_scrap_order/static/src/xml/screen.xml | 67 +++++++ pos_scrap_order/views/assets.xml | 15 ++ pos_scrap_order/views/pos_config.xml | 25 +++ 13 files changed, 667 insertions(+) create mode 100644 pos_scrap_order/__init__.py create mode 100644 pos_scrap_order/__manifest__.py create mode 100644 pos_scrap_order/i18n/fr.po create mode 100644 pos_scrap_order/models/__init__.py create mode 100644 pos_scrap_order/models/pos_config.py create mode 100644 pos_scrap_order/models/pos_order.py create mode 100644 pos_scrap_order/security/ir.model.access.csv create mode 100644 pos_scrap_order/static/src/css/scrap.css create mode 100644 pos_scrap_order/static/src/js/pos_model.js create mode 100644 pos_scrap_order/static/src/js/screen.js create mode 100644 pos_scrap_order/static/src/xml/screen.xml create mode 100644 pos_scrap_order/views/assets.xml create mode 100644 pos_scrap_order/views/pos_config.xml diff --git a/pos_scrap_order/__init__.py b/pos_scrap_order/__init__.py new file mode 100644 index 0000000000..899bcc97f0 --- /dev/null +++ b/pos_scrap_order/__init__.py @@ -0,0 +1,2 @@ +from . import models + diff --git a/pos_scrap_order/__manifest__.py b/pos_scrap_order/__manifest__.py new file mode 100644 index 0000000000..2a8c8f96c2 --- /dev/null +++ b/pos_scrap_order/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright (C) Nguyen Minh Chien (chien@trobz.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Pos Scrap Order", + "version": "12.0.1.0.0", + "category": "Point Of Sale", + "summary": """Create scrap order from POS screen""", + "author": "Trobz", + "website": "https://trobz.com", + "license": "AGPL-3", + "depends": [ + "point_of_sale", + "stock" + ], + "data": [ + "security/ir.model.access.csv", + "views/assets.xml", + "views/pos_config.xml", + ], + 'qweb': ['static/src/xml/screen.xml'], +} diff --git a/pos_scrap_order/i18n/fr.po b/pos_scrap_order/i18n/fr.po new file mode 100644 index 0000000000..cfc543fc95 --- /dev/null +++ b/pos_scrap_order/i18n/fr.po @@ -0,0 +1,190 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pos_scrap_order +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-09-26 05:28+0000\n" +"PO-Revision-Date: 2023-09-26 05:28+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: pos_scrap_order +#. openerp-web +#: code:addons/pos_scrap_order/static/src/xml/screen.xml:30 +#, python-format +msgid "ATTENTION" +msgstr "" + +#. module: pos_scrap_order +#: code:addons/pos_scrap_order/models/pos_order.py:70 +#, python-format +msgid "Access Error!" +msgstr "" + +#. module: pos_scrap_order +#: selection:pos.config,scrap_order_option:0 +msgid "Always create Scrap Order, validate if has stock" +msgstr "Toujours créer la mise au rebut, valider seulement si le produit est en stock" + +#. module: pos_scrap_order +#. openerp-web +#: code:addons/pos_scrap_order/static/src/xml/screen.xml:15 +#, python-format +msgid "Back" +msgstr "" + +#. module: pos_scrap_order +#: model:ir.model,name:pos_scrap_order.model_res_config_settings +msgid "Config Settings" +msgstr "Paramètres de config" + +#. module: pos_scrap_order +#. openerp-web +#: code:addons/pos_scrap_order/static/src/xml/screen.xml:19 +#, python-format +msgid "Create Scrap Order" +msgstr "Valider la mise au rebut" + +#. module: pos_scrap_order +#: selection:pos.config,scrap_order_option:0 +msgid "Create and validate Scrap Order for product has stock only" +msgstr "Créer et valider la mise au rebut seulement pour les produit en stock" + +#. module: pos_scrap_order +#: code:addons/pos_scrap_order/models/pos_order.py:78 +#, python-format +msgid "Data is incorrect." +msgstr "" + +#. module: pos_scrap_order +#: code:addons/pos_scrap_order/models/pos_order.py:77 +#, python-format +msgid "Error!" +msgstr "" + +#. module: pos_scrap_order +#. openerp-web +#: code:addons/pos_scrap_order/static/src/js/screen.js:72 +#, python-format +msgid "It seems that you do not have a network connection at the moment. Try again later." +msgstr "" + +#. module: pos_scrap_order +#. openerp-web +#: code:addons/pos_scrap_order/static/src/xml/screen.xml:6 +#, python-format +msgid "Make Scrap Order" +msgstr "Envoyer au rebut" + +#. module: pos_scrap_order +#. openerp-web +#: code:addons/pos_scrap_order/static/src/js/screen.js:71 +#, python-format +msgid "Network Connection Lost" +msgstr "" + +#. module: pos_scrap_order +#: code:addons/pos_scrap_order/models/pos_order.py:65 +#, python-format +msgid "No Enough Stock!" +msgstr "" + +#. module: pos_scrap_order +#: selection:pos.config,scrap_order_option:0 +msgid "No Scrap Order" +msgstr "Pas de rebut" + +#. module: pos_scrap_order +#. openerp-web +#: code:addons/pos_scrap_order/static/src/js/screen.js:86 +#, python-format +msgid "Not available" +msgstr "" + +#. module: pos_scrap_order +#. openerp-web +#: code:addons/pos_scrap_order/static/src/js/screen.js:126 +#, python-format +msgid "Nothing to Scrap" +msgstr "" + +#. module: pos_scrap_order +#: code:addons/pos_scrap_order/models/pos_order.py:30 +#, python-format +msgid "POS Session: " +msgstr "" + +#. module: pos_scrap_order +#: model:ir.model,name:pos_scrap_order.model_pos_config +msgid "Point of Sale Configuration" +msgstr "" + +#. module: pos_scrap_order +#: model:ir.model,name:pos_scrap_order.model_pos_order +msgid "Point of Sale Orders" +msgstr "Commandes du point de vente" + +#. module: pos_scrap_order +#. openerp-web +#: code:addons/pos_scrap_order/static/src/xml/screen.xml:18 +#, python-format +msgid "Scrap Order" +msgstr "Récapitulatif de la liste de rebuts" + +#. module: pos_scrap_order +#: model:ir.model.fields,field_description:pos_scrap_order.field_pos_config__scrap_order_option +msgid "Scrap Order Option" +msgstr "Mises au rebut" + +#. module: pos_scrap_order +#: code:addons/pos_scrap_order/models/pos_order.py:59 +#, python-format +msgid "Successful!" +msgstr "" + +#. module: pos_scrap_order +#: code:addons/pos_scrap_order/models/pos_order.py:55 +#, python-format +msgid "The product {} has no enough stock." +msgstr "" + +#. module: pos_scrap_order +#: code:addons/pos_scrap_order/models/pos_order.py:60 +#, python-format +msgid "The product(s) has been sent to scrap location" +msgstr "Le(s) produit(s) a/ont été mis au rebut" + +#. module: pos_scrap_order +#. openerp-web +#: code:addons/pos_scrap_order/static/src/js/screen.js:127 +#, python-format +msgid "There is no product to scrap." +msgstr "" + +#. module: pos_scrap_order +#. openerp-web +#: code:addons/pos_scrap_order/static/src/xml/screen.xml:31 +#, python-format +msgid "This action will move these products below into the scrap location" +msgstr "Cette option jettera l'ensemble des produits listés ci-dessous de l'inventaire" + +#. module: pos_scrap_order +#. openerp-web +#: code:addons/pos_scrap_order/static/src/js/screen.js:87 +#, python-format +msgid "This order has been paid or has no line." +msgstr "" + +#. module: pos_scrap_order +#: code:addons/pos_scrap_order/models/pos_order.py:71 +#, python-format +msgid "You have no right to make the scrap order." +msgstr "" + diff --git a/pos_scrap_order/models/__init__.py b/pos_scrap_order/models/__init__.py new file mode 100644 index 0000000000..7723e42ab6 --- /dev/null +++ b/pos_scrap_order/models/__init__.py @@ -0,0 +1,2 @@ +from . import pos_order +from . import pos_config diff --git a/pos_scrap_order/models/pos_config.py b/pos_scrap_order/models/pos_config.py new file mode 100644 index 0000000000..7564b47ea4 --- /dev/null +++ b/pos_scrap_order/models/pos_config.py @@ -0,0 +1,14 @@ +# Copyright (C) Nguyen Minh Chien (chien@trobz.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html + +from odoo import models, fields, api + + +class PosConfig(models.Model): + _inherit = 'pos.config' + + scrap_order_option = fields.Selection([ + ("no", "No Scrap Order"), + ("onhand", "Create and validate Scrap Order for product has stock only"), + ("always", "Always create Scrap Order, validate if has stock") + ], default="onhand") diff --git a/pos_scrap_order/models/pos_order.py b/pos_scrap_order/models/pos_order.py new file mode 100644 index 0000000000..53c34a2d68 --- /dev/null +++ b/pos_scrap_order/models/pos_order.py @@ -0,0 +1,80 @@ +# Copyright (C) Nguyen Minh Chien (chien@trobz.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models, api, _ +from odoo.exceptions import AccessError +import logging + +_logger = logging.getLogger(__name__) + +class OutofStockError(AccessError): + """ Out of stock exception """ + def __init__(self, msg): + super().__init__(msg) + +class PosOrder(models.Model): + _inherit = "pos.order" + + @api.model + def _get_scrap_vals(self, order, line, default_vals): + vals = {} + session = self.env["pos.session"].browse(order.get("pos_session_id")) + location = session.config_id.stock_location_id + if len(line) == 3: + product = self.env["product.product"].browse(line[2].get("product_id")) + vals = { + "product_id": product.id, + "scrap_qty": line[2].get("qty"), + "location_id": location.id, + "product_uom_id": product.uom_id.id, + "origin": _("POS Session: ") + session.display_name + } + vals.update(default_vals) + return vals + + @api.model + def create_scrap_from_ui(self, order, default_vals={}): + scrap_ids = [] + msg = {} + vals = [] + session = self.env["pos.session"].browse(order.get("pos_session_id")) + scrap_order_option = session.config_id.scrap_order_option + lines = order.get("lines") + Scrap = self.env["stock.scrap"] + try: + if scrap_order_option == "no": + raise AccessError("") + for line in lines: + if len(line) == 3: + vals.append(self._get_scrap_vals(order, line, default_vals)) + if len(vals) == len(lines): + scraps = Scrap.create(vals) + for scrap in scraps: + res = scrap.action_validate() + if scrap_order_option == "onhand" and res is not True: + raise OutofStockError(_("The product {} has no enough stock.").format( + scrap.product_id.display_name)) + scrap_ids = scraps.ids + msg = { + "title": _("Successful!"), + "body": _("The product(s) has been sent to scrap location") + } + except OutofStockError as e: + self.env.cr.rollback() + msg = { + "title": _("No Enough Stock!"), + "body": e.args[0] + } + except AccessError as e: + msg = { + "title": _("Access Error!"), + "body": _("You have no right to make the scrap order.") + } + except Exception as err: + _logger.error("====================================") + _logger.error(str(err)) + msg = { + "title": _("Error!"), + "body": _("Data is incorrect.") + } + return {"scrap_ids": scrap_ids, "msg": msg} diff --git a/pos_scrap_order/security/ir.model.access.csv b/pos_scrap_order/security/ir.model.access.csv new file mode 100644 index 0000000000..6d15879140 --- /dev/null +++ b/pos_scrap_order/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +stock_scrap_pos_user,stock_scrap_pos_user,stock.model_stock_scrap,point_of_sale.group_pos_user,1,1,1,0 +stock_scrap_origin_pos_user,stock_scrap_origin_pos_user,stock_scrap_origin.model_stock_scrap_origin,point_of_sale.group_pos_user,1,0,0,0 diff --git a/pos_scrap_order/static/src/css/scrap.css b/pos_scrap_order/static/src/css/scrap.css new file mode 100644 index 0000000000..a1f810272a --- /dev/null +++ b/pos_scrap_order/static/src/css/scrap.css @@ -0,0 +1,93 @@ +/* This is a copy of the pos scrap screen CSS + * This allows to use it in the backend */ + +/* ********* Generic element styling ********* */ + +.pos a { + text-decoration: none; + color: #555555; +} +.pos button, .pos a.button { + display: inline-block; + cursor: pointer; + padding: 4px 10px; + font-size: 11px; + border: 1px solid #cacaca; + background: #e2e2e2; + border-radius: 3px; +} +.pos ul, .pos ol { + padding: 0; + margin: 0; +} +.pos li { + list-style-type: none; +} +.pos .pos-right-align { + text-align: right; +} +.pos .pos-center-align { + text-align: center; +} +.pos .pos-disc-font { + font-size: 12px; + font-style:italic; + color: #808080; +} + +/* c) The scrap screen */ + +.pos .scrap-screen .centered-content .button { + line-height: 40px; + padding: 3px 13px; + font-size: 20px; + text-align: center; + background: rgb(230, 230, 230); + margin: 16px; + margin-bottom: 0px; + border-radius: 3px; + border: solid 1px rgb(209, 209, 209); + cursor: pointer; +} + +.pos .pos-scrap-container { + font-size: 0.75em; + text-align: center; + direction: ltr; +} + +.pos .pos-scraps { + text-align: left; + background-color: white; + margin: 20px; + padding: 15px; + font-size: 14px; + padding-bottom:30px; + display: inline-block; + font-family: "Inconsolata"; + border: solid 1px rgb(220,220,220); + border-radius: 3px; + overflow: hidden; +} +.pos .pos-scraps pre{ + font-family: "Inconsolata"; +} +.pos .pos-scraps .emph{ + font-size: 20px; + margin:5px; +} +.pos .pos-scraps table { + width: 100%; + border: 0; + table-layout: fixed; +} +.pos .pos-scraps table td { + border: 0; + word-wrap: break-word; +} +.pos .pos-scrap-note{ + font-size: 16px; + text-align: center; + margin-top: 15px; + color: red; +} \ No newline at end of file diff --git a/pos_scrap_order/static/src/js/pos_model.js b/pos_scrap_order/static/src/js/pos_model.js new file mode 100644 index 0000000000..0abd8dc9e8 --- /dev/null +++ b/pos_scrap_order/static/src/js/pos_model.js @@ -0,0 +1,10 @@ +/* Copyright (C) Nguyen Minh Chien (chien@trobz.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html +*/ + + +odoo.define('pos_scrap_order.pos_model', function (require) { + "use strict"; + var pos_model = require('point_of_sale.models'); + pos_model.load_fields("pos.config", ['scrap_order_option']); +}); diff --git a/pos_scrap_order/static/src/js/screen.js b/pos_scrap_order/static/src/js/screen.js new file mode 100644 index 0000000000..d36fd19208 --- /dev/null +++ b/pos_scrap_order/static/src/js/screen.js @@ -0,0 +1,144 @@ +/* Copyright (C) Nguyen Minh Chien (chien@trobz.com) + License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). */ + +odoo.define("pos_scrap_order.screens", function(require) { + "use strict"; + var rpc = require('web.rpc'); + var gui = require('point_of_sale.gui'); + var screens = require('point_of_sale.screens'); + var core = require('web.core'); + var session = require('web.session'); + var QWeb = core.qweb; + + var ScrapScreenWidget = screens.ScreenWidget.extend({ + template: 'ScrapScreenWidget', + show: function(){ + this._super(); + var self = this; + this.render_scrap(); + }, + + lock_screen: function(locked) { + this._locked = locked; + if (locked) { + this.$('.next').removeClass('highlight'); + } else { + this.$('.next').addClass('highlight'); + } + }, + get_scrap_render_env: function() { + var order = this.pos.get_order(); + return { + widget: this, + pos: this.pos, + order: order, + orderlines: order.get_orderlines(), + }; + }, + _get_vals: function(order_vals){ + return order_vals; + }, + make_scrap: function() { + var self = this; + var order = this.pos.get_order() + if (order && !order.finalized && order.get_orderlines().length > 0) { + var order_vals = this._get_vals(order.export_as_JSON()); + if (order_vals){ + this.lock_screen(true); + var params = { + model: 'pos.order', + method: 'create_scrap_from_ui', + args: [order_vals], + kwargs: {context: session.user_context}, + }; + + rpc.query(params) + .then(function({scrap_ids, msg}){ + if (scrap_ids && scrap_ids.length > 0){ + // self.pos.db.remove_unpaid_order(order); + order.finalize(); + } + if(msg){ + self.gui.show_popup("alert", { + "title": msg.title, + "body": msg.body + }); + } + }) + .fail(function(error, event){ + event.preventDefault(); + self.gui.show_popup("error", { + "title": _t("Network Connection Lost"), + "body": _t( + "It seems that you do not have a network connection at the moment." + + " Try again later."), + // "confirm": function() { + // self.click_back(); + // }, + }); + }) + .always(function(){ + self.lock_screen(false); + }); + } + } else { + self.gui.show_popup("error", { + "title": _t("Not available"), + "body": _t( + "This order has been paid or has no line.") + }); + } + }, + click_next: function() { + // this.pos.get_order().finalize(); + }, + click_back: function() { + this.gui.show_screen("products"); + }, + renderElement: function() { + var self = this; + this._super(); + this.$('.next').click(function(){ + if (!self._locked) { + self.make_scrap(); + } + }); + this.$('.back').click(function(){ + if (!self._locked) { + self.click_back(); + } + }); + }, + + render_scrap: function() { + this.$('.pos-scrap-container').html(QWeb.render('ScrapLines', this.get_scrap_render_env())); + }, + }); + gui.define_screen({name:'scrap', widget: ScrapScreenWidget}); + + var ScrapButton = screens.ActionButtonWidget.extend({ + template: 'ScrapButton', + button_click: function() { + if (this.pos.get_order() && this.pos.get_order().get_orderlines().length > 0) { + this.gui.show_screen('scrap'); + } else { + this.gui.show_popup('error', { + 'title': _t('Nothing to Scrap'), + 'body': _t('There is no product to scrap.'), + }); + } + }, + }); + + screens.define_action_button({ + 'name': 'scrap', + 'widget': ScrapButton, + 'condition': function(){ + var opt = this.pos.config.scrap_order_option; + return opt !== undefined && opt !== 'no'; + }, + }); + return { + ScrapScreenWidget: ScrapScreenWidget, + }; +}); diff --git a/pos_scrap_order/static/src/xml/screen.xml b/pos_scrap_order/static/src/xml/screen.xml new file mode 100644 index 0000000000..490f2fc8bd --- /dev/null +++ b/pos_scrap_order/static/src/xml/screen.xml @@ -0,0 +1,67 @@ + + + + +
+ Make Scrap Order +
+
+ + +
+
+
+ + + Back + +

Scrap Order

+ + Create Scrap Order + + +
+
+
+
+
+
+
+
ATTENTION
+ This action will move these products below into the scrap location +
+
+
+
+
+
+
+ + +
+ +
+
+ + + + + + + + + + + + +
+ + + + + +
+
+
+
+
diff --git a/pos_scrap_order/views/assets.xml b/pos_scrap_order/views/assets.xml new file mode 100644 index 0000000000..530e59dbee --- /dev/null +++ b/pos_scrap_order/views/assets.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/pos_scrap_order/views/pos_config.xml b/pos_scrap_order/views/pos_config.xml new file mode 100644 index 0000000000..ad488d2686 --- /dev/null +++ b/pos_scrap_order/views/pos_config.xml @@ -0,0 +1,25 @@ + + + + + pos_config_view_form_inherit + pos.config + + + +
+
+
+
+
+
+
+
+
+ +
From 02ff9be579f1aef1081a3b20a6717ac71895c847 Mon Sep 17 00:00:00 2001 From: Nguyen Minh Chien Date: Tue, 5 Dec 2023 11:10:57 +0000 Subject: [PATCH 2/7] F#T62242 - [SQQ] Add another option to scrap registering on POS --- pos_scrap_order/models/pos_config.py | 3 ++- pos_scrap_order/models/pos_order.py | 13 ++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/pos_scrap_order/models/pos_config.py b/pos_scrap_order/models/pos_config.py index 7564b47ea4..296a7e3a31 100644 --- a/pos_scrap_order/models/pos_config.py +++ b/pos_scrap_order/models/pos_config.py @@ -10,5 +10,6 @@ class PosConfig(models.Model): scrap_order_option = fields.Selection([ ("no", "No Scrap Order"), ("onhand", "Create and validate Scrap Order for product has stock only"), - ("always", "Always create Scrap Order, validate if has stock") + ("always", "Always create Scrap Order, validate if has stock"), + ("force", "Always create Scrap Order, validate regardless of no stock") ], default="onhand") diff --git a/pos_scrap_order/models/pos_order.py b/pos_scrap_order/models/pos_order.py index 53c34a2d68..bb8ccac508 100644 --- a/pos_scrap_order/models/pos_order.py +++ b/pos_scrap_order/models/pos_order.py @@ -49,11 +49,14 @@ def create_scrap_from_ui(self, order, default_vals={}): vals.append(self._get_scrap_vals(order, line, default_vals)) if len(vals) == len(lines): scraps = Scrap.create(vals) - for scrap in scraps: - res = scrap.action_validate() - if scrap_order_option == "onhand" and res is not True: - raise OutofStockError(_("The product {} has no enough stock.").format( - scrap.product_id.display_name)) + if scrap_order_option == "force": + scraps.do_scrap() + else: + for scrap in scraps: + res = scrap.action_validate() + if scrap_order_option == "onhand" and res is not True: + raise OutofStockError(_("The product {} has no enough stock.").format( + scrap.product_id.display_name)) scrap_ids = scraps.ids msg = { "title": _("Successful!"), From b997915bb082a385e79ffaae29b5f15721f6a790 Mon Sep 17 00:00:00 2001 From: Nguyen Minh Chien Date: Thu, 7 Dec 2023 08:13:22 +0000 Subject: [PATCH 3/7] F#T62242 - [SQQ] Add another option to scrap registering on POS --- pos_scrap_order/i18n/fr.po | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pos_scrap_order/i18n/fr.po b/pos_scrap_order/i18n/fr.po index cfc543fc95..b06d44ec77 100644 --- a/pos_scrap_order/i18n/fr.po +++ b/pos_scrap_order/i18n/fr.po @@ -33,6 +33,11 @@ msgstr "" msgid "Always create Scrap Order, validate if has stock" msgstr "Toujours créer la mise au rebut, valider seulement si le produit est en stock" +#. module: pos_scrap_order +#: selection:pos.config,scrap_order_option:0 +msgid "Always create Scrap Order, validate regardless of no stock" +msgstr "Toujours créer la mise au rebut, valider même si le produit n'est pas en stock" + #. module: pos_scrap_order #. openerp-web #: code:addons/pos_scrap_order/static/src/xml/screen.xml:15 From 588651427fcd24eebcc765513d4b35cf21386f46 Mon Sep 17 00:00:00 2001 From: Nguyen Minh Chien Date: Mon, 20 May 2024 07:32:09 +0000 Subject: [PATCH 4/7] F#T63416 - Issue with the module dealing with scrap orders through the POS --- pos_scrap_order/models/pos_order.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/pos_scrap_order/models/pos_order.py b/pos_scrap_order/models/pos_order.py index bb8ccac508..6aa1875b6a 100644 --- a/pos_scrap_order/models/pos_order.py +++ b/pos_scrap_order/models/pos_order.py @@ -2,7 +2,7 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import models, api, _ -from odoo.exceptions import AccessError +from odoo.exceptions import AccessError, UserError import logging _logger = logging.getLogger(__name__) @@ -64,16 +64,30 @@ def create_scrap_from_ui(self, order, default_vals={}): } except OutofStockError as e: self.env.cr.rollback() + scrap_ids = [] msg = { "title": _("No Enough Stock!"), "body": e.args[0] } except AccessError as e: + self.env.cr.rollback() + scrap_ids = [] msg = { "title": _("Access Error!"), "body": _("You have no right to make the scrap order.") } + except UserError as err: + self.env.cr.rollback() + scrap_ids = [] + _logger.error("====================================") + _logger.error(str(err)) + msg = { + "title": _("User Error!"), + "body": _("Stock data is incorrect. Please contact the administrator.") + } except Exception as err: + self.env.cr.rollback() + scrap_ids = [] _logger.error("====================================") _logger.error(str(err)) msg = { From 3931de72cd1bf25773a72fb8c797be568fb575b6 Mon Sep 17 00:00:00 2001 From: Nguyen Minh Chien Date: Fri, 1 Aug 2025 07:48:22 +0000 Subject: [PATCH 5/7] F#T65965 - [TMT] List of recent scraps accessible in the POS --- pos_scrap_order/__manifest__.py | 5 +- pos_scrap_order/i18n/fr.po | 53 ++++- pos_scrap_order/models/__init__.py | 1 + pos_scrap_order/models/stock_scrap.py | 35 ++++ pos_scrap_order/static/src/css/list.css | 191 ++++++++++++++++++ pos_scrap_order/static/src/js/screen.js | 81 ++++++++ pos_scrap_order/static/src/xml/screen.xml | 4 +- .../static/src/xml/screen_show_scrap_list.xml | 69 +++++++ pos_scrap_order/views/assets.xml | 1 + 9 files changed, 436 insertions(+), 4 deletions(-) create mode 100644 pos_scrap_order/models/stock_scrap.py create mode 100644 pos_scrap_order/static/src/css/list.css create mode 100644 pos_scrap_order/static/src/xml/screen_show_scrap_list.xml diff --git a/pos_scrap_order/__manifest__.py b/pos_scrap_order/__manifest__.py index 2a8c8f96c2..148811f0b6 100644 --- a/pos_scrap_order/__manifest__.py +++ b/pos_scrap_order/__manifest__.py @@ -18,5 +18,8 @@ "views/assets.xml", "views/pos_config.xml", ], - 'qweb': ['static/src/xml/screen.xml'], + 'qweb': [ + 'static/src/xml/screen_show_scrap_list.xml', + 'static/src/xml/screen.xml', + ], } diff --git a/pos_scrap_order/i18n/fr.po b/pos_scrap_order/i18n/fr.po index b06d44ec77..021f5766d9 100644 --- a/pos_scrap_order/i18n/fr.po +++ b/pos_scrap_order/i18n/fr.po @@ -41,9 +41,17 @@ msgstr "Toujours créer la mise au rebut, valider même si le produit n'est pas #. module: pos_scrap_order #. openerp-web #: code:addons/pos_scrap_order/static/src/xml/screen.xml:15 +#: code:addons/pos_scrap_order/static/src/xml/screen_show_scrap_list.xml:14 #, python-format msgid "Back" -msgstr "" +msgstr "Retour" + +#. module: pos_scrap_order +#. openerp-web +#: code:addons/pos_scrap_order/static/src/xml/screen_show_scrap_list.xml:34 +#, python-format +msgid "Create Date" +msgstr "Date de création" #. module: pos_scrap_order #: model:ir.model,name:pos_scrap_order.model_res_config_settings @@ -81,9 +89,17 @@ msgstr "" msgid "It seems that you do not have a network connection at the moment. Try again later." msgstr "" +#. module: pos_scrap_order +#. openerp-web +#: code:addons/pos_scrap_order/static/src/xml/screen_show_scrap_list.xml:5 +#, python-format +msgid "List" +msgstr "Liste" + #. module: pos_scrap_order #. openerp-web #: code:addons/pos_scrap_order/static/src/xml/screen.xml:6 +#: code:addons/pos_scrap_order/static/src/xml/screen_show_scrap_list.xml:18 #, python-format msgid "Make Scrap Order" msgstr "Envoyer au rebut" @@ -120,6 +136,13 @@ msgstr "" msgid "Nothing to Scrap" msgstr "" +#. module: pos_scrap_order +#. openerp-web +#: code:addons/pos_scrap_order/static/src/xml/screen_show_scrap_list.xml:35 +#, python-format +msgid "Origin" +msgstr "Origine" + #. module: pos_scrap_order #: code:addons/pos_scrap_order/models/pos_order.py:30 #, python-format @@ -136,6 +159,20 @@ msgstr "" msgid "Point of Sale Orders" msgstr "Commandes du point de vente" +#. module: pos_scrap_order +#. openerp-web +#: code:addons/pos_scrap_order/static/src/xml/screen_show_scrap_list.xml:32 +#, python-format +msgid "Product" +msgstr "Article" + +#. module: pos_scrap_order +#. openerp-web +#: code:addons/pos_scrap_order/static/src/xml/screen_show_scrap_list.xml:33 +#, python-format +msgid "Quantity" +msgstr "Quantité" + #. module: pos_scrap_order #. openerp-web #: code:addons/pos_scrap_order/static/src/xml/screen.xml:18 @@ -148,6 +185,20 @@ msgstr "Récapitulatif de la liste de rebuts" msgid "Scrap Order Option" msgstr "Mises au rebut" +#. module: pos_scrap_order +#. openerp-web +#: code:addons/pos_scrap_order/static/src/xml/screen_show_scrap_list.xml:17 +#, python-format +msgid "Scrap Orders" +msgstr "Bons de mise au rebut" + +#. module: pos_scrap_order +#. openerp-web +#: code:addons/pos_scrap_order/static/src/xml/screen_show_scrap_list.xml:34 +#, python-format +msgid "State" +msgstr "Statut" + #. module: pos_scrap_order #: code:addons/pos_scrap_order/models/pos_order.py:59 #, python-format diff --git a/pos_scrap_order/models/__init__.py b/pos_scrap_order/models/__init__.py index 7723e42ab6..e5df60392c 100644 --- a/pos_scrap_order/models/__init__.py +++ b/pos_scrap_order/models/__init__.py @@ -1,2 +1,3 @@ from . import pos_order from . import pos_config +from . import stock_scrap diff --git a/pos_scrap_order/models/stock_scrap.py b/pos_scrap_order/models/stock_scrap.py new file mode 100644 index 0000000000..302a57737d --- /dev/null +++ b/pos_scrap_order/models/stock_scrap.py @@ -0,0 +1,35 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html + +from odoo import api, fields, models, _ +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT as DTF + + +class StockScrap(models.Model): + _inherit = 'stock.scrap' + + @api.model + def get_list_for_ui(self): + result = [] + records = self.sudo().search([ + "|", + ("origin", "like", _("POS Session: ")), + ("origin", "like", "POS Session: ") + ], order="id DESC", limit=50) + for scrap in records: + # Format quantity with unit of measure + quantity_str = '{} {}'.format(scrap.scrap_qty, scrap.product_uom_id.name) + + # Get state label from selection field + state_label = dict(self._fields['state'].selection).get(scrap.state, scrap.state) + + result.append({ + 'name': scrap.name, + 'product_display_name': scrap.product_id.display_name, + 'qty_str': quantity_str, + 'state_label': state_label, + 'origin': scrap.origin.replace( + "POS Session: ", "" + ).replace(_("POS Session: "), ""), + "date": scrap.create_date and fields.Datetime.to_string(scrap.create_date) or "" + }) + return result diff --git a/pos_scrap_order/static/src/css/list.css b/pos_scrap_order/static/src/css/list.css new file mode 100644 index 0000000000..c23693cd28 --- /dev/null +++ b/pos_scrap_order/static/src/css/list.css @@ -0,0 +1,191 @@ +/* firefox seems to ignore the relative positionning of the subwindow-container + * putting this inside subwindow-container fixes it. + */ +.pos .scraplist-screen .window, +.pos .scraplist-screen .full-content .subwindow{ + display: block; +} +.pos .scraplist-screen .full-content .subwindow-container{ + display: block; + height: 100%; +} +.pos .scraplist-screen .full-content .subwindow.collapsed, +.pos .scraplist-screen .full-content .subwindow-container.collapsed{ + height: auto; +} + +/* The Scale Container Screen */ +.pos .scale-screen .add-container{ + text-align: center; + font-size: 32px; + background: rgb(110,200,155); + color: white; + border-radius: 3px; + padding: 16px; + margin: 16px; + cursor: pointer; +} + +.pos .scale-screen .container-name,.container-barcode,.deposit-value { + text-align: center; + font-size: 25px; + border-radius: 3px; + padding-top: 10px; + padding-bottom:10px; + margin-top: 5px; +} + +/* e) The Container List Screen */ + +.pos .scraplist-screen .container-list{ + font-size: 16px; + width: 100%; + line-height: 40px; +} +.pos .scraplist-screen .container-list th, +.pos .scraplist-screen .container-list td { + padding: 0px 8px; +} +.pos .scraplist-screen .container-list tr{ + transition: all 150ms linear; + background: rgb(230,230,230); +} +.pos .scraplist-screen .container-list thead > tr, +.pos .scraplist-screen .container-list tr:nth-child(even) { + background: rgb(247,247,247); +} +.pos .scraplist-screen .container-list tr.highlight{ + transition: all 150ms linear; + background: rgb(110,200,155) !important; + color: white; +} +.pos .scraplist-screen .container-list tr.lowlight{ + transition: all 150ms linear; + background: rgb(216, 238, 227); +} +.pos .scraplist-screen .container-list tr.lowlight:nth-child(even){ + transition: all 150ms linear; + background: rgb(227, 246, 237); +} +.pos .scraplist-screen .container-details{ + padding: 16px; + border-bottom: solid 5px rgb(110,200,155); +} +.pos .scraplist-screen .container-picture{ + height: 64px; + width: 64px; + border-radius: 32px; + overflow: hidden; + text-align: center; + float: left; + margin-right: 16px; + background: white; + position: relative; +} +.pos .scraplist-screen .container-picture > img { + position: absolute; + top: -9999px; + bottom: -9999px; + right: -9999px; + left: -9999px; + max-height: 64px; + margin: auto; +} +.pos .scraplist-screen .container-picture > .fa { + line-height: 64px; + font-size: 32px; +} +.pos .scraplist-screen .container-picture .image-uploader { + position: absolute; + z-index: 1000; + top: 0; + left: 0; + right: 0; + bottom: 0; + opacity: 0; + cursor: pointer; +} +.pos .scraplist-screen .container-name{ + font-size: 32px; + line-height: 64px; + margin-bottom:16px; +} +.pos .scraplist-screen .edit-buttons { + position: absolute; + right: 16px; + top: 10px; +} +.pos .scraplist-screen .edit-buttons .button{ + display: inline-block; + margin-left: 16px; + color: rgb(128,128,128); + cursor: pointer; + font-size: 36px; +} +.pos .scraplist-screen .container-details-box{ + position: relative; + font-size: 16px; +} +.pos .scraplist-screen .container-details-left{ + width: 50%; + float: left; +} +.pos .scraplist-screen .container-details-right{ + width: 50%; + float: left; +} +.pos .scraplist-screen .container-detail{ + line-height: 24px; +} +.pos .scraplist-screen .container-detail > .label{ + font-weight: bold; + display: inline-block; + width: 75px; + text-align: right; + margin-right: 8px; +} +.pos .scraplist-screen .container-details input, +.pos .scraplist-screen .container-details select +{ + padding: 4px; + border-radius: 3px; + border: solid 1px #cecbcb; + margin-bottom: 4px; + background: white; + font-family: "Lato","Lucida Grande", Helvetica, Verdana, Arial; + color: #555555; + width: 340px; + font-size: 14px; + box-sizing: border-box; +} +.pos .scraplist-screen .container-details input.container-name{ + font-size: 24px; + line-height: 24px; + margin: 18px 6px; + width: 340px; +} +.pos .scraplist-screen .container-detail > .empty{ + opacity: 0.3; +} +.pos .scraplist-screen .searchbox{ + right: auto; + margin-left: -90px; + margin-top:8px; + left: 45%; +} +.pos .scraplist-screen .searchbox input{ + width: 120px; +} +.pos .scraplist-screen .button.delete-container { + left: 50%; + margin-left: 120px; +} + +/* Container Action buttons */ +.pos .control-button.main { + width: 75%; +} +.pos .control-button.second { + width: 15%; + flex-grow: 0; +} diff --git a/pos_scrap_order/static/src/js/screen.js b/pos_scrap_order/static/src/js/screen.js index d36fd19208..b1a70b7b86 100644 --- a/pos_scrap_order/static/src/js/screen.js +++ b/pos_scrap_order/static/src/js/screen.js @@ -129,7 +129,86 @@ odoo.define("pos_scrap_order.screens", function(require) { } }, }); + + // Scrap List + var ScrapListScreenWidget = screens.ScreenWidget.extend({ + template: 'ScrapListScreenWidget', + show: function(){ + this._super(); + self = this; + this.fetch_scrap_orders().then(function(scrapOrders){ + self.render_list(scrapOrders); + }) + .fail(function(error, event){ + event.preventDefault(); + self.gui.show_popup("error", { + "title": _t("Network Connection Lost"), + "body": _t( + "It seems that you do not have a network connection at the moment." + + " Try again later."), + }); + }); + + }, + click_next: function() { + this.gui.show_screen('scrap'); + }, + click_back: function() { + this.gui.show_screen("products"); + }, + renderElement: function() { + var self = this; + this._super(); + this.$('.next').click(function(){ + self.click_next(); + }); + this.$('.back').click(function(){ + self.click_back(); + }); + }, + render_list: function(scrapOrders) { + if (!scrapOrders) { + return; + } + var contents = this.$el[0].querySelector('.scrap-list-contents'); + contents.innerHTML = ""; + for(var i = 0, len = scrapOrders.length; i < len; i++){ + var scrapLine_html = QWeb.render('ScrapListLine',{widget: this, scrapOrder:scrapOrders[i]}); + var scrapLine = document.createElement('tbody'); + scrapLine.innerHTML = scrapLine_html; + scrapLine = scrapLine.childNodes[1]; + contents.appendChild(scrapLine); + } + }, + fetch_scrap_orders: function() { + var params = { + model: 'stock.scrap', + method: 'get_list_for_ui', + args: [], + kwargs: {context: session.user_context}, + }; + + return rpc.query(params); + } + }); + gui.define_screen({name:'scrapList', widget: ScrapListScreenWidget}); + + var ShowScrapListButton = screens.ActionButtonWidget.extend({ + template: 'ShowScrapListButton', + button_click: function() { + this.gui.show_screen('scrapList'); + }, + }); + screens.define_action_button({ + 'name': 'scrapList', + 'widget': ShowScrapListButton, + 'condition': function(){ + var opt = this.pos.config.scrap_order_option; + return opt !== undefined && opt !== 'no'; + }, + }); + screens.define_action_button({ 'name': 'scrap', 'widget': ScrapButton, @@ -138,7 +217,9 @@ odoo.define("pos_scrap_order.screens", function(require) { return opt !== undefined && opt !== 'no'; }, }); + return { ScrapScreenWidget: ScrapScreenWidget, + ScrapListScreenWidget: ScrapListScreenWidget, }; }); diff --git a/pos_scrap_order/static/src/xml/screen.xml b/pos_scrap_order/static/src/xml/screen.xml index 490f2fc8bd..a3388ecd28 100644 --- a/pos_scrap_order/static/src/xml/screen.xml +++ b/pos_scrap_order/static/src/xml/screen.xml @@ -2,9 +2,9 @@ -
+
+
diff --git a/pos_scrap_order/static/src/xml/screen_show_scrap_list.xml b/pos_scrap_order/static/src/xml/screen_show_scrap_list.xml new file mode 100644 index 0000000000..df98d2cf8f --- /dev/null +++ b/pos_scrap_order/static/src/xml/screen_show_scrap_list.xml @@ -0,0 +1,69 @@ + + + + + + + +
+
+
+ + + Back + +

Scrap Orders

+ + Make Scrap Order + + +
+
+
+
+
+
+ + + + + + + + + + + + +
ReferenceProductQuantityCreate DateOrigin
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + +
diff --git a/pos_scrap_order/views/assets.xml b/pos_scrap_order/views/assets.xml index 530e59dbee..a27733f84c 100644 --- a/pos_scrap_order/views/assets.xml +++ b/pos_scrap_order/views/assets.xml @@ -10,6 +10,7 @@ + From 61a67961be9df07e4f263979aeb5c7fd3661fbaa Mon Sep 17 00:00:00 2001 From: khoi Date: Tue, 21 Apr 2026 12:32:46 +0700 Subject: [PATCH 6/7] [IMP] pos_scrap_order: pre-commit auto fixes --- pos_scrap_order/__init__.py | 1 - pos_scrap_order/__manifest__.py | 13 +- pos_scrap_order/models/pos_config.py | 21 +- pos_scrap_order/models/pos_order.py | 37 +-- pos_scrap_order/models/stock_scrap.py | 49 ++-- pos_scrap_order/pyproject.toml | 3 + pos_scrap_order/static/src/css/list.css | 71 +++--- pos_scrap_order/static/src/css/scrap.css | 22 +- pos_scrap_order/static/src/js/pos_model.js | 7 +- pos_scrap_order/static/src/js/screen.js | 225 +++++++++--------- pos_scrap_order/static/src/xml/screen.xml | 31 +-- .../static/src/xml/screen_show_scrap_list.xml | 23 +- pos_scrap_order/views/assets.xml | 17 +- pos_scrap_order/views/pos_config.xml | 11 +- 14 files changed, 280 insertions(+), 251 deletions(-) create mode 100644 pos_scrap_order/pyproject.toml diff --git a/pos_scrap_order/__init__.py b/pos_scrap_order/__init__.py index 899bcc97f0..0650744f6b 100644 --- a/pos_scrap_order/__init__.py +++ b/pos_scrap_order/__init__.py @@ -1,2 +1 @@ from . import models - diff --git a/pos_scrap_order/__manifest__.py b/pos_scrap_order/__manifest__.py index 148811f0b6..206c803307 100644 --- a/pos_scrap_order/__manifest__.py +++ b/pos_scrap_order/__manifest__.py @@ -7,19 +7,16 @@ "category": "Point Of Sale", "summary": """Create scrap order from POS screen""", "author": "Trobz", - "website": "https://trobz.com", + "website": "https://github.com/AwesomeFoodCoops/odoo-production", "license": "AGPL-3", - "depends": [ - "point_of_sale", - "stock" - ], + "depends": ["point_of_sale", "stock"], "data": [ "security/ir.model.access.csv", "views/assets.xml", "views/pos_config.xml", ], - 'qweb': [ - 'static/src/xml/screen_show_scrap_list.xml', - 'static/src/xml/screen.xml', + "qweb": [ + "static/src/xml/screen_show_scrap_list.xml", + "static/src/xml/screen.xml", ], } diff --git a/pos_scrap_order/models/pos_config.py b/pos_scrap_order/models/pos_config.py index 296a7e3a31..1641427e81 100644 --- a/pos_scrap_order/models/pos_config.py +++ b/pos_scrap_order/models/pos_config.py @@ -1,15 +1,18 @@ # Copyright (C) Nguyen Minh Chien (chien@trobz.com) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html -from odoo import models, fields, api +from odoo import fields, models class PosConfig(models.Model): - _inherit = 'pos.config' - - scrap_order_option = fields.Selection([ - ("no", "No Scrap Order"), - ("onhand", "Create and validate Scrap Order for product has stock only"), - ("always", "Always create Scrap Order, validate if has stock"), - ("force", "Always create Scrap Order, validate regardless of no stock") - ], default="onhand") + _inherit = "pos.config" + + scrap_order_option = fields.Selection( + [ + ("no", "No Scrap Order"), + ("onhand", "Create and validate Scrap Order for product has stock only"), + ("always", "Always create Scrap Order, validate if has stock"), + ("force", "Always create Scrap Order, validate regardless of no stock"), + ], + default="onhand", + ) diff --git a/pos_scrap_order/models/pos_order.py b/pos_scrap_order/models/pos_order.py index 6aa1875b6a..d59863a76a 100644 --- a/pos_scrap_order/models/pos_order.py +++ b/pos_scrap_order/models/pos_order.py @@ -1,17 +1,21 @@ # Copyright (C) Nguyen Minh Chien (chien@trobz.com) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import models, api, _ -from odoo.exceptions import AccessError, UserError import logging +from odoo import _, api, models +from odoo.exceptions import AccessError, UserError + _logger = logging.getLogger(__name__) + class OutofStockError(AccessError): - """ Out of stock exception """ + """Out of stock exception""" + def __init__(self, msg): super().__init__(msg) + class PosOrder(models.Model): _inherit = "pos.order" @@ -27,7 +31,7 @@ def _get_scrap_vals(self, order, line, default_vals): "scrap_qty": line[2].get("qty"), "location_id": location.id, "product_uom_id": product.uom_id.id, - "origin": _("POS Session: ") + session.display_name + "origin": _("POS Session: ") + session.display_name, } vals.update(default_vals) return vals @@ -55,26 +59,26 @@ def create_scrap_from_ui(self, order, default_vals={}): for scrap in scraps: res = scrap.action_validate() if scrap_order_option == "onhand" and res is not True: - raise OutofStockError(_("The product {} has no enough stock.").format( - scrap.product_id.display_name)) + raise OutofStockError( + _("The product {} has no enough stock.").format( + scrap.product_id.display_name + ) + ) scrap_ids = scraps.ids msg = { "title": _("Successful!"), - "body": _("The product(s) has been sent to scrap location") + "body": _("The product(s) has been sent to scrap location"), } except OutofStockError as e: self.env.cr.rollback() scrap_ids = [] - msg = { - "title": _("No Enough Stock!"), - "body": e.args[0] - } - except AccessError as e: + msg = {"title": _("No Enough Stock!"), "body": e.args[0]} + except AccessError: self.env.cr.rollback() scrap_ids = [] msg = { "title": _("Access Error!"), - "body": _("You have no right to make the scrap order.") + "body": _("You have no right to make the scrap order."), } except UserError as err: self.env.cr.rollback() @@ -83,15 +87,12 @@ def create_scrap_from_ui(self, order, default_vals={}): _logger.error(str(err)) msg = { "title": _("User Error!"), - "body": _("Stock data is incorrect. Please contact the administrator.") + "body": _("Stock data is incorrect. Please contact the administrator."), } except Exception as err: self.env.cr.rollback() scrap_ids = [] _logger.error("====================================") _logger.error(str(err)) - msg = { - "title": _("Error!"), - "body": _("Data is incorrect.") - } + msg = {"title": _("Error!"), "body": _("Data is incorrect.")} return {"scrap_ids": scrap_ids, "msg": msg} diff --git a/pos_scrap_order/models/stock_scrap.py b/pos_scrap_order/models/stock_scrap.py index 302a57737d..0df9e991b7 100644 --- a/pos_scrap_order/models/stock_scrap.py +++ b/pos_scrap_order/models/stock_scrap.py @@ -1,35 +1,44 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html -from odoo import api, fields, models, _ -from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT as DTF +from odoo import _, api, fields, models class StockScrap(models.Model): - _inherit = 'stock.scrap' + _inherit = "stock.scrap" @api.model def get_list_for_ui(self): result = [] - records = self.sudo().search([ - "|", - ("origin", "like", _("POS Session: ")), - ("origin", "like", "POS Session: ") - ], order="id DESC", limit=50) + records = self.sudo().search( + [ + "|", + ("origin", "like", _("POS Session: ")), + ("origin", "like", "POS Session: "), + ], + order="id DESC", + limit=50, + ) for scrap in records: # Format quantity with unit of measure - quantity_str = '{} {}'.format(scrap.scrap_qty, scrap.product_uom_id.name) + quantity_str = f"{scrap.scrap_qty} {scrap.product_uom_id.name}" # Get state label from selection field - state_label = dict(self._fields['state'].selection).get(scrap.state, scrap.state) + state_label = dict(self._fields["state"].selection).get( + scrap.state, scrap.state + ) - result.append({ - 'name': scrap.name, - 'product_display_name': scrap.product_id.display_name, - 'qty_str': quantity_str, - 'state_label': state_label, - 'origin': scrap.origin.replace( - "POS Session: ", "" - ).replace(_("POS Session: "), ""), - "date": scrap.create_date and fields.Datetime.to_string(scrap.create_date) or "" - }) + result.append( + { + "name": scrap.name, + "product_display_name": scrap.product_id.display_name, + "qty_str": quantity_str, + "state_label": state_label, + "origin": scrap.origin.replace("POS Session: ", "").replace( + _("POS Session: "), "" + ), + "date": scrap.create_date + and fields.Datetime.to_string(scrap.create_date) + or "", + } + ) return result diff --git a/pos_scrap_order/pyproject.toml b/pos_scrap_order/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/pos_scrap_order/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/pos_scrap_order/static/src/css/list.css b/pos_scrap_order/static/src/css/list.css index c23693cd28..cd6fcb85ba 100644 --- a/pos_scrap_order/static/src/css/list.css +++ b/pos_scrap_order/static/src/css/list.css @@ -2,23 +2,23 @@ * putting this inside subwindow-container fixes it. */ .pos .scraplist-screen .window, -.pos .scraplist-screen .full-content .subwindow{ +.pos .scraplist-screen .full-content .subwindow { display: block; } -.pos .scraplist-screen .full-content .subwindow-container{ +.pos .scraplist-screen .full-content .subwindow-container { display: block; height: 100%; } .pos .scraplist-screen .full-content .subwindow.collapsed, -.pos .scraplist-screen .full-content .subwindow-container.collapsed{ +.pos .scraplist-screen .full-content .subwindow-container.collapsed { height: auto; } /* The Scale Container Screen */ -.pos .scale-screen .add-container{ +.pos .scale-screen .add-container { text-align: center; font-size: 32px; - background: rgb(110,200,155); + background: rgb(110, 200, 155); color: white; border-radius: 3px; padding: 16px; @@ -26,18 +26,20 @@ cursor: pointer; } -.pos .scale-screen .container-name,.container-barcode,.deposit-value { +.pos .scale-screen .container-name, +.container-barcode, +.deposit-value { text-align: center; font-size: 25px; border-radius: 3px; padding-top: 10px; - padding-bottom:10px; + padding-bottom: 10px; margin-top: 5px; } /* e) The Container List Screen */ -.pos .scraplist-screen .container-list{ +.pos .scraplist-screen .container-list { font-size: 16px; width: 100%; line-height: 40px; @@ -46,32 +48,32 @@ .pos .scraplist-screen .container-list td { padding: 0px 8px; } -.pos .scraplist-screen .container-list tr{ +.pos .scraplist-screen .container-list tr { transition: all 150ms linear; - background: rgb(230,230,230); + background: rgb(230, 230, 230); } .pos .scraplist-screen .container-list thead > tr, .pos .scraplist-screen .container-list tr:nth-child(even) { - background: rgb(247,247,247); + background: rgb(247, 247, 247); } -.pos .scraplist-screen .container-list tr.highlight{ +.pos .scraplist-screen .container-list tr.highlight { transition: all 150ms linear; - background: rgb(110,200,155) !important; + background: rgb(110, 200, 155) !important; color: white; } -.pos .scraplist-screen .container-list tr.lowlight{ +.pos .scraplist-screen .container-list tr.lowlight { transition: all 150ms linear; background: rgb(216, 238, 227); } -.pos .scraplist-screen .container-list tr.lowlight:nth-child(even){ +.pos .scraplist-screen .container-list tr.lowlight:nth-child(even) { transition: all 150ms linear; background: rgb(227, 246, 237); } -.pos .scraplist-screen .container-details{ +.pos .scraplist-screen .container-details { padding: 16px; - border-bottom: solid 5px rgb(110,200,155); + border-bottom: solid 5px rgb(110, 200, 155); } -.pos .scraplist-screen .container-picture{ +.pos .scraplist-screen .container-picture { height: 64px; width: 64px; border-radius: 32px; @@ -105,39 +107,39 @@ opacity: 0; cursor: pointer; } -.pos .scraplist-screen .container-name{ +.pos .scraplist-screen .container-name { font-size: 32px; line-height: 64px; - margin-bottom:16px; + margin-bottom: 16px; } .pos .scraplist-screen .edit-buttons { position: absolute; right: 16px; top: 10px; } -.pos .scraplist-screen .edit-buttons .button{ +.pos .scraplist-screen .edit-buttons .button { display: inline-block; margin-left: 16px; - color: rgb(128,128,128); + color: rgb(128, 128, 128); cursor: pointer; font-size: 36px; } -.pos .scraplist-screen .container-details-box{ +.pos .scraplist-screen .container-details-box { position: relative; font-size: 16px; } -.pos .scraplist-screen .container-details-left{ +.pos .scraplist-screen .container-details-left { width: 50%; float: left; } -.pos .scraplist-screen .container-details-right{ +.pos .scraplist-screen .container-details-right { width: 50%; float: left; } -.pos .scraplist-screen .container-detail{ +.pos .scraplist-screen .container-detail { line-height: 24px; } -.pos .scraplist-screen .container-detail > .label{ +.pos .scraplist-screen .container-detail > .label { font-weight: bold; display: inline-block; width: 75px; @@ -145,35 +147,34 @@ margin-right: 8px; } .pos .scraplist-screen .container-details input, -.pos .scraplist-screen .container-details select -{ +.pos .scraplist-screen .container-details select { padding: 4px; border-radius: 3px; border: solid 1px #cecbcb; margin-bottom: 4px; background: white; - font-family: "Lato","Lucida Grande", Helvetica, Verdana, Arial; + font-family: "Lato", "Lucida Grande", Helvetica, Verdana, Arial; color: #555555; width: 340px; font-size: 14px; box-sizing: border-box; } -.pos .scraplist-screen .container-details input.container-name{ +.pos .scraplist-screen .container-details input.container-name { font-size: 24px; line-height: 24px; margin: 18px 6px; width: 340px; } -.pos .scraplist-screen .container-detail > .empty{ +.pos .scraplist-screen .container-detail > .empty { opacity: 0.3; } -.pos .scraplist-screen .searchbox{ +.pos .scraplist-screen .searchbox { right: auto; margin-left: -90px; - margin-top:8px; + margin-top: 8px; left: 45%; } -.pos .scraplist-screen .searchbox input{ +.pos .scraplist-screen .searchbox input { width: 120px; } .pos .scraplist-screen .button.delete-container { diff --git a/pos_scrap_order/static/src/css/scrap.css b/pos_scrap_order/static/src/css/scrap.css index a1f810272a..1d9ab686aa 100644 --- a/pos_scrap_order/static/src/css/scrap.css +++ b/pos_scrap_order/static/src/css/scrap.css @@ -7,7 +7,8 @@ text-decoration: none; color: #555555; } -.pos button, .pos a.button { +.pos button, +.pos a.button { display: inline-block; cursor: pointer; padding: 4px 10px; @@ -16,7 +17,8 @@ background: #e2e2e2; border-radius: 3px; } -.pos ul, .pos ol { +.pos ul, +.pos ol { padding: 0; margin: 0; } @@ -31,7 +33,7 @@ } .pos .pos-disc-font { font-size: 12px; - font-style:italic; + font-style: italic; color: #808080; } @@ -62,19 +64,19 @@ margin: 20px; padding: 15px; font-size: 14px; - padding-bottom:30px; + padding-bottom: 30px; display: inline-block; font-family: "Inconsolata"; - border: solid 1px rgb(220,220,220); + border: solid 1px rgb(220, 220, 220); border-radius: 3px; overflow: hidden; } -.pos .pos-scraps pre{ +.pos .pos-scraps pre { font-family: "Inconsolata"; } -.pos .pos-scraps .emph{ +.pos .pos-scraps .emph { font-size: 20px; - margin:5px; + margin: 5px; } .pos .pos-scraps table { width: 100%; @@ -85,9 +87,9 @@ border: 0; word-wrap: break-word; } -.pos .pos-scrap-note{ +.pos .pos-scrap-note { font-size: 16px; text-align: center; margin-top: 15px; color: red; -} \ No newline at end of file +} diff --git a/pos_scrap_order/static/src/js/pos_model.js b/pos_scrap_order/static/src/js/pos_model.js index 0abd8dc9e8..0237ed88ac 100644 --- a/pos_scrap_order/static/src/js/pos_model.js +++ b/pos_scrap_order/static/src/js/pos_model.js @@ -2,9 +2,8 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html */ - -odoo.define('pos_scrap_order.pos_model', function (require) { +odoo.define("pos_scrap_order.pos_model", function (require) { "use strict"; - var pos_model = require('point_of_sale.models'); - pos_model.load_fields("pos.config", ['scrap_order_option']); + var pos_model = require("point_of_sale.models"); + pos_model.load_fields("pos.config", ["scrap_order_option"]); }); diff --git a/pos_scrap_order/static/src/js/screen.js b/pos_scrap_order/static/src/js/screen.js index b1a70b7b86..e12fd3ed58 100644 --- a/pos_scrap_order/static/src/js/screen.js +++ b/pos_scrap_order/static/src/js/screen.js @@ -1,32 +1,32 @@ /* Copyright (C) Nguyen Minh Chien (chien@trobz.com) License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). */ -odoo.define("pos_scrap_order.screens", function(require) { +odoo.define("pos_scrap_order.screens", function (require) { "use strict"; - var rpc = require('web.rpc'); - var gui = require('point_of_sale.gui'); - var screens = require('point_of_sale.screens'); - var core = require('web.core'); - var session = require('web.session'); + var rpc = require("web.rpc"); + var gui = require("point_of_sale.gui"); + var screens = require("point_of_sale.screens"); + var core = require("web.core"); + var session = require("web.session"); var QWeb = core.qweb; var ScrapScreenWidget = screens.ScreenWidget.extend({ - template: 'ScrapScreenWidget', - show: function(){ + template: "ScrapScreenWidget", + show: function () { this._super(); var self = this; this.render_scrap(); }, - lock_screen: function(locked) { + lock_screen: function (locked) { this._locked = locked; if (locked) { - this.$('.next').removeClass('highlight'); + this.$(".next").removeClass("highlight"); } else { - this.$('.next').addClass('highlight'); + this.$(".next").addClass("highlight"); } }, - get_scrap_render_env: function() { + get_scrap_render_env: function () { var order = this.pos.get_order(); return { widget: this, @@ -35,96 +35,101 @@ odoo.define("pos_scrap_order.screens", function(require) { orderlines: order.get_orderlines(), }; }, - _get_vals: function(order_vals){ + _get_vals: function (order_vals) { return order_vals; }, - make_scrap: function() { + make_scrap: function () { var self = this; - var order = this.pos.get_order() + var order = this.pos.get_order(); if (order && !order.finalized && order.get_orderlines().length > 0) { var order_vals = this._get_vals(order.export_as_JSON()); - if (order_vals){ + if (order_vals) { this.lock_screen(true); var params = { - model: 'pos.order', - method: 'create_scrap_from_ui', + model: "pos.order", + method: "create_scrap_from_ui", args: [order_vals], kwargs: {context: session.user_context}, }; - + rpc.query(params) - .then(function({scrap_ids, msg}){ - if (scrap_ids && scrap_ids.length > 0){ - // self.pos.db.remove_unpaid_order(order); - order.finalize(); - } - if(msg){ - self.gui.show_popup("alert", { - "title": msg.title, - "body": msg.body + .then(function ({scrap_ids, msg}) { + if (scrap_ids && scrap_ids.length > 0) { + // Self.pos.db.remove_unpaid_order(order); + order.finalize(); + } + if (msg) { + self.gui.show_popup("alert", { + title: msg.title, + body: msg.body, + }); + } + }) + .fail(function (error, event) { + event.preventDefault(); + self.gui.show_popup("error", { + title: _t("Network Connection Lost"), + body: _t( + "It seems that you do not have a network connection at the moment." + + " Try again later." + ), + // "confirm": function() { + // self.click_back(); + // }, }); - } - }) - .fail(function(error, event){ - event.preventDefault(); - self.gui.show_popup("error", { - "title": _t("Network Connection Lost"), - "body": _t( - "It seems that you do not have a network connection at the moment." + - " Try again later."), - // "confirm": function() { - // self.click_back(); - // }, + }) + .always(function () { + self.lock_screen(false); }); - }) - .always(function(){ - self.lock_screen(false); - }); } } else { self.gui.show_popup("error", { - "title": _t("Not available"), - "body": _t( - "This order has been paid or has no line.") + title: _t("Not available"), + body: _t("This order has been paid or has no line."), }); } }, - click_next: function() { - // this.pos.get_order().finalize(); + click_next: function () { + // This.pos.get_order().finalize(); }, - click_back: function() { + click_back: function () { this.gui.show_screen("products"); }, - renderElement: function() { + renderElement: function () { var self = this; this._super(); - this.$('.next').click(function(){ + this.$(".next").click(function () { if (!self._locked) { self.make_scrap(); } }); - this.$('.back').click(function(){ + this.$(".back").click(function () { if (!self._locked) { self.click_back(); } }); }, - render_scrap: function() { - this.$('.pos-scrap-container').html(QWeb.render('ScrapLines', this.get_scrap_render_env())); + render_scrap: function () { + this.$(".pos-scrap-container").html( + QWeb.render("ScrapLines", this.get_scrap_render_env()) + ); }, }); - gui.define_screen({name:'scrap', widget: ScrapScreenWidget}); - + gui.define_screen({name: "scrap", widget: ScrapScreenWidget}); + var ScrapButton = screens.ActionButtonWidget.extend({ - template: 'ScrapButton', - button_click: function() { - if (this.pos.get_order() && this.pos.get_order().get_orderlines().length > 0) { - this.gui.show_screen('scrap'); + template: "ScrapButton", + button_click: function () { + if ( + this.pos.get_order() && + this.pos.get_order().get_orderlines().length > 0 + ) { + this.gui.show_screen("scrap"); } else { - this.gui.show_popup('error', { - 'title': _t('Nothing to Scrap'), - 'body': _t('There is no product to scrap.'), + this.gui.show_popup("error", { + title: _t("Nothing to Scrap"), + body: _t("There is no product to scrap."), }); } }, @@ -132,89 +137,93 @@ odoo.define("pos_scrap_order.screens", function(require) { // Scrap List var ScrapListScreenWidget = screens.ScreenWidget.extend({ - template: 'ScrapListScreenWidget', - show: function(){ + template: "ScrapListScreenWidget", + show: function () { this._super(); self = this; - this.fetch_scrap_orders().then(function(scrapOrders){ - self.render_list(scrapOrders); - }) - .fail(function(error, event){ - event.preventDefault(); - self.gui.show_popup("error", { - "title": _t("Network Connection Lost"), - "body": _t( - "It seems that you do not have a network connection at the moment." + - " Try again later."), + this.fetch_scrap_orders() + .then(function (scrapOrders) { + self.render_list(scrapOrders); + }) + .fail(function (error, event) { + event.preventDefault(); + self.gui.show_popup("error", { + title: _t("Network Connection Lost"), + body: _t( + "It seems that you do not have a network connection at the moment." + + " Try again later." + ), + }); }); - }); - }, - click_next: function() { - this.gui.show_screen('scrap'); + click_next: function () { + this.gui.show_screen("scrap"); }, - click_back: function() { + click_back: function () { this.gui.show_screen("products"); }, - renderElement: function() { + renderElement: function () { var self = this; this._super(); - this.$('.next').click(function(){ + this.$(".next").click(function () { self.click_next(); }); - this.$('.back').click(function(){ + this.$(".back").click(function () { self.click_back(); }); }, - render_list: function(scrapOrders) { + render_list: function (scrapOrders) { if (!scrapOrders) { return; } - var contents = this.$el[0].querySelector('.scrap-list-contents'); + var contents = this.$el[0].querySelector(".scrap-list-contents"); contents.innerHTML = ""; - for(var i = 0, len = scrapOrders.length; i < len; i++){ - var scrapLine_html = QWeb.render('ScrapListLine',{widget: this, scrapOrder:scrapOrders[i]}); - var scrapLine = document.createElement('tbody'); + for (var i = 0, len = scrapOrders.length; i < len; i++) { + var scrapLine_html = QWeb.render("ScrapListLine", { + widget: this, + scrapOrder: scrapOrders[i], + }); + var scrapLine = document.createElement("tbody"); scrapLine.innerHTML = scrapLine_html; scrapLine = scrapLine.childNodes[1]; contents.appendChild(scrapLine); } }, - fetch_scrap_orders: function() { + fetch_scrap_orders: function () { var params = { - model: 'stock.scrap', - method: 'get_list_for_ui', + model: "stock.scrap", + method: "get_list_for_ui", args: [], kwargs: {context: session.user_context}, }; return rpc.query(params); - } + }, }); - gui.define_screen({name:'scrapList', widget: ScrapListScreenWidget}); - + gui.define_screen({name: "scrapList", widget: ScrapListScreenWidget}); + var ShowScrapListButton = screens.ActionButtonWidget.extend({ - template: 'ShowScrapListButton', - button_click: function() { - this.gui.show_screen('scrapList'); + template: "ShowScrapListButton", + button_click: function () { + this.gui.show_screen("scrapList"); }, }); - + screens.define_action_button({ - 'name': 'scrapList', - 'widget': ShowScrapListButton, - 'condition': function(){ + name: "scrapList", + widget: ShowScrapListButton, + condition: function () { var opt = this.pos.config.scrap_order_option; - return opt !== undefined && opt !== 'no'; + return opt !== undefined && opt !== "no"; }, }); - + screens.define_action_button({ - 'name': 'scrap', - 'widget': ScrapButton, - 'condition': function(){ + name: "scrap", + widget: ScrapButton, + condition: function () { var opt = this.pos.config.scrap_order_option; - return opt !== undefined && opt !== 'no'; + return opt !== undefined && opt !== "no"; }, }); diff --git a/pos_scrap_order/static/src/xml/screen.xml b/pos_scrap_order/static/src/xml/screen.xml index a3388ecd28..058d3e0c66 100644 --- a/pos_scrap_order/static/src/xml/screen.xml +++ b/pos_scrap_order/static/src/xml/screen.xml @@ -1,9 +1,9 @@ - + @@ -12,26 +12,25 @@
- + Back

Scrap Order

Create Scrap Order - +
-
-
+
ATTENTION
- This action will move these products below into the scrap location -
-
+ This action will move these products below into the scrap location
+
@@ -39,8 +38,10 @@
- -
+
+ + +

@@ -51,13 +52,15 @@
- + - + - +
diff --git a/pos_scrap_order/static/src/xml/screen_show_scrap_list.xml b/pos_scrap_order/static/src/xml/screen_show_scrap_list.xml index df98d2cf8f..fff7525ec7 100644 --- a/pos_scrap_order/static/src/xml/screen_show_scrap_list.xml +++ b/pos_scrap_order/static/src/xml/screen_show_scrap_list.xml @@ -1,4 +1,4 @@ - + @@ -11,20 +11,22 @@
- + Back

Scrap Orders

Make Scrap Order - +
-
+
@@ -35,8 +37,7 @@ - - +
Origin
@@ -50,19 +51,19 @@ - + - + - + - + - + diff --git a/pos_scrap_order/views/assets.xml b/pos_scrap_order/views/assets.xml index a27733f84c..95b04189cf 100644 --- a/pos_scrap_order/views/assets.xml +++ b/pos_scrap_order/views/assets.xml @@ -1,16 +1,21 @@ - + -