diff --git a/.gitignore b/.gitignore index 6b757b0..239a50b 100644 --- a/.gitignore +++ b/.gitignore @@ -159,3 +159,4 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. .idea/ .vscode +.DS_Store diff --git a/nfu_sale_product_min_max_qty/README.rst b/nfu_sale_product_min_max_qty/README.rst new file mode 100644 index 0000000..925bc7c --- /dev/null +++ b/nfu_sale_product_min_max_qty/README.rst @@ -0,0 +1,66 @@ +========================== +NFU Sale Min Max Quantitiy +========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:4dfaea54e4d01502b433eb5c0dcca409a5fbe2c3f175e29382a19230343183c9 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-bruecksen%2Fife_nfu-lightgray.png?logo=github + :target: https://github.com/bruecksen/ife_nfu/tree/16.0/nfu_sale_product_min_max_qty + :alt: bruecksen/ife_nfu + +|badge1| |badge2| |badge3| + +This module adds a minium and a maximum order quantity field to Sale Orders and adds it to checkout of the website shop. + +**Table of contents** + +.. contents:: + :local: + +Changelog +========= + +:1.0.0: Initial module. + +Bug Tracker +=========== + +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 to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* BAKEUP + +Contributors +~~~~~~~~~~~~ + +* Niels Göttsch +* Matthias Brück + +Maintainers +~~~~~~~~~~~ + +This module is part of the `bruecksen/ife_nfu `_ project on GitHub. + +You are welcome to contribute. diff --git a/nfu_sale_product_min_max_qty/__init__.py b/nfu_sale_product_min_max_qty/__init__.py new file mode 100644 index 0000000..48d9904 --- /dev/null +++ b/nfu_sale_product_min_max_qty/__init__.py @@ -0,0 +1,3 @@ +from . import models +from . import controllers +from . import wizard diff --git a/nfu_sale_product_min_max_qty/__manifest__.py b/nfu_sale_product_min_max_qty/__manifest__.py new file mode 100644 index 0000000..01a3732 --- /dev/null +++ b/nfu_sale_product_min_max_qty/__manifest__.py @@ -0,0 +1,19 @@ +{ + "name": "NFU Sale Min Max Quantitiy", + "summary": "Add minimum and maximum order quantities to sale orders", + "author": "BAKEUP", + "website": "https://www.bakeup.org", + "category": "Stock", + "version": "16.0.0.0.1", + "depends": ["sale_management", "website_sale"], + "data": [ + "views/sale_order_views.xml", + "views/sale_order_line_views.xml", + "wizard/sale_max_qty_line_views.xml", + "wizard/sale_max_qty_chooser_views.xml", + "views/templates.xml", + "security/ir.model.access.csv", + ], + "assets": {"web.assets_frontend": ["nfu_sale_product_min_max_qty/static/src/js/website_sale.js"]}, + "license": "LGPL-3", +} diff --git a/nfu_sale_product_min_max_qty/controllers/__init__.py b/nfu_sale_product_min_max_qty/controllers/__init__.py new file mode 100644 index 0000000..deec4a8 --- /dev/null +++ b/nfu_sale_product_min_max_qty/controllers/__init__.py @@ -0,0 +1 @@ +from . import main \ No newline at end of file diff --git a/nfu_sale_product_min_max_qty/controllers/main.py b/nfu_sale_product_min_max_qty/controllers/main.py new file mode 100644 index 0000000..a8a56f5 --- /dev/null +++ b/nfu_sale_product_min_max_qty/controllers/main.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +from odoo.http import request +from odoo import fields, http + +from odoo.addons.website_sale.controllers.main import WebsiteSale + + +class WebsiteSaleMinMax(WebsiteSale): + + @http.route() + def cart_update(self, *args, min_qty=None, max_qty=None, **kw): + """ Override to get min_qty and max_qty from the product. + """ + product_uom_min_qty = fields.Float(min_qty) + product_uom_max_qty = fields.Float(max_qty) + return super().cart_update(*args, min_qty=product_uom_min_qty, max_qty=product_uom_max_qty, **kw) \ No newline at end of file diff --git a/nfu_sale_product_min_max_qty/models/__init__.py b/nfu_sale_product_min_max_qty/models/__init__.py new file mode 100644 index 0000000..91f1542 --- /dev/null +++ b/nfu_sale_product_min_max_qty/models/__init__.py @@ -0,0 +1,2 @@ +from . import sale_order_line +from . import sale_order \ No newline at end of file diff --git a/nfu_sale_product_min_max_qty/models/sale_order.py b/nfu_sale_product_min_max_qty/models/sale_order.py new file mode 100644 index 0000000..5b0fe98 --- /dev/null +++ b/nfu_sale_product_min_max_qty/models/sale_order.py @@ -0,0 +1,80 @@ +from odoo import _, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + def action_min_max_qty_wizard(self): + """ + Here you need to create the wizzard objects first and then call the wizzard with the newly created id + """ + sale_max_qty_chooser = self.env["sale.max.qty.chooser"].create({}) + draft_sale_orders = self.filtered(lambda x: x.state == "draft") + for sale_order in draft_sale_orders: + sale_max_qty_chooser.sale_order_ids += sale_order + for line in sale_order.order_line.filtered(lambda x: not x.display_type): + self.env["sale.max.qty.line"].create( + { + "sale_line_id": line.id, + "sale_max_qty_chooser": sale_max_qty_chooser.id, + "qty": line.product_uom_qty, + } + ) + + action = { + "name": _("Min max qty wizard"), + "type": "ir.actions.act_window", + "res_model": "sale.max.qty.line", + "view_mode": "tree", + "target": "current", + "editable": True, + "context": {"search_default_group_by_product": 1}, + "domain": [("id", "in", sale_max_qty_chooser.sale_max_qty_ids.ids)], + } + return action + + def action_sale_order_lines(self): + """ + Here you need to create the wizzard objects first and then call the wizzard with the newly created id + """ + return { + "name": _("Min max qty wizard"), + "type": "ir.actions.act_window", + "res_model": "sale.order.line", + "view_mode": "tree,form", + "target": "current", + "context": {"group_by": "product_id"}, + "domain": [("id", "in", self.order_line.ids)], + } + + def _prepare_order_line_values( + self, + product_id, + quantity, + linked_line_id=False, + no_variant_attribute_values=None, + product_custom_attribute_values=None, + **kwargs + ): + values = super()._prepare_order_line_values( + product_id, + quantity, + linked_line_id=linked_line_id, + no_variant_attribute_values=no_variant_attribute_values, + product_custom_attribute_values=product_custom_attribute_values, + **kwargs + ) + values.update({"product_uom_min_qty": quantity, "product_uom_max_qty": kwargs.get("max_qty", quantity)}) + return values + + def _prepare_order_line_update_values(self, order_line, quantity, linked_line_id=False, **kwargs): + values = super()._prepare_order_line_update_values( + order_line, quantity, linked_line_id=linked_line_id, **kwargs + ) + max_qty = kwargs.get("max_qty", quantity) + if quantity != order_line.product_uom_min_qty: + values["product_uom_min_qty"] = quantity + if max_qty and max_qty != order_line.product_uom_max_qty: + values["product_uom_max_qty"] = max_qty + + return values diff --git a/nfu_sale_product_min_max_qty/models/sale_order_line.py b/nfu_sale_product_min_max_qty/models/sale_order_line.py new file mode 100644 index 0000000..b8d42a6 --- /dev/null +++ b/nfu_sale_product_min_max_qty/models/sale_order_line.py @@ -0,0 +1,15 @@ +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + product_uom_min_qty = fields.Float(string="Min qty.", digits="Product Unit of Measure") + product_uom_max_qty = fields.Float(string="Max qty.", digits="Product Unit of Measure") + + @api.constrains("product_uom_qty", "product_uom_max_qty") + def _check_product_uom_qty(self): + for record in self: + if record.product_uom_max_qty != 0 and record.product_uom_qty > record.product_uom_max_qty: + raise UserError(_("The quantity must be less than or equal to the maximum quantity.")) diff --git a/nfu_sale_product_min_max_qty/readme/CONTRIBUTORS.rst b/nfu_sale_product_min_max_qty/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..a4ec453 --- /dev/null +++ b/nfu_sale_product_min_max_qty/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Niels Göttsch +* Matthias Brück \ No newline at end of file diff --git a/nfu_sale_product_min_max_qty/readme/DESCRIPTION.rst b/nfu_sale_product_min_max_qty/readme/DESCRIPTION.rst new file mode 100644 index 0000000..754f3e6 --- /dev/null +++ b/nfu_sale_product_min_max_qty/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module adds a minium and a maximum order quantity field to Sale Orders and adds it to checkout of the website shop. \ No newline at end of file diff --git a/nfu_sale_product_min_max_qty/readme/HISTORY.rst b/nfu_sale_product_min_max_qty/readme/HISTORY.rst new file mode 100644 index 0000000..765ab9c --- /dev/null +++ b/nfu_sale_product_min_max_qty/readme/HISTORY.rst @@ -0,0 +1 @@ +:1.0.0: Initial module. \ No newline at end of file diff --git a/nfu_sale_product_min_max_qty/security/ir.model.access.csv b/nfu_sale_product_min_max_qty/security/ir.model.access.csv new file mode 100644 index 0000000..04aeaf3 --- /dev/null +++ b/nfu_sale_product_min_max_qty/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 +access_sale_max_qty_chooser,sale.max.qty.chooser,model_sale_max_qty_chooser,sales_team.group_sale_salesman,1,1,1,1 +access_sale_max_qty_line,sale.max.qty.line,model_sale_max_qty_line,sales_team.group_sale_salesman,1,1,1,1 diff --git a/nfu_sale_product_min_max_qty/static/description/icon.png b/nfu_sale_product_min_max_qty/static/description/icon.png new file mode 100644 index 0000000..1267712 Binary files /dev/null and b/nfu_sale_product_min_max_qty/static/description/icon.png differ diff --git a/nfu_sale_product_min_max_qty/static/description/index.html b/nfu_sale_product_min_max_qty/static/description/index.html new file mode 100644 index 0000000..4ab2ef6 --- /dev/null +++ b/nfu_sale_product_min_max_qty/static/description/index.html @@ -0,0 +1,429 @@ + + + + + +NFU Sale Min Max Quantitiy + + + +
+

NFU Sale Min Max Quantitiy

+ + +

Beta License: LGPL-3 bruecksen/ife_nfu

+

This module adds a minium and a maximum order quantity field to Sale Orders and adds it to checkout of the website shop.

+

Table of contents

+ +
+

Changelog

+ +++ + + + +
1.0.0:Initial module.
+
+
+

Bug Tracker

+

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 to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • BAKEUP
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is part of the bruecksen/ife_nfu project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/nfu_sale_product_min_max_qty/static/src/js/website_sale.js b/nfu_sale_product_min_max_qty/static/src/js/website_sale.js new file mode 100644 index 0000000..bcb77dc --- /dev/null +++ b/nfu_sale_product_min_max_qty/static/src/js/website_sale.js @@ -0,0 +1,83 @@ +odoo.define("nfu_sale_product_min_max_qty.website_min_max_qty", function (require) { + "use strict"; + var core = require("web.core"); + var _t = core._t; + var wSaleUtils = require("website_sale.utils"); + var publicWidget = require("web.public.widget"); + require("website_sale.website_sale"); + publicWidget.registry.WebsiteSale.include({ + onClickAddCartJSON: function (ev) { + ev.preventDefault(); + $(".css_quantity").popover("dispose"); + var $link = $(ev.currentTarget); + var $input = $link.closest(".input-group").find("input"); + var min = parseFloat($input.data("min") || 0); + var previousQty = parseFloat($input.val() || 0, 10); + var quantity = ($link.has(".fa-minus").length ? -1 : 1) + previousQty; + if (quantity < min) { + $input.closest(".css_quantity").popover({ + content: _t(`Minimum Quantity is ${min}.`), + title: _t("Warning"), + placement: "left", + trigger: "focus", + html: true, + }); + $input.closest(".css_quantity").popover("show"); + + setTimeout(function () { + $(".css_quantity").popover("dispose"); + }, 3000); + } + this._super(ev); + }, + + /** + * Adds the max qty to the POST request when adding a product to the cart. + * @override + */ + _changeCartQuantity: function ($input, value, $dom_optional, line_id, productIDs) { + _.each($dom_optional, function (elem) { + $(elem).find(".js_quantity").text(value); + productIDs.push($(elem).find("span[data-product-id]").data("product-id")); + }); + $input.data("update_change", true); + var max_qty = parseInt($input.closest("tr").find('input[name="max-qty"]').val(), 10); + var set_qty = parseInt($input.closest("tr").find('input[name!="max-qty"]').val(), 10); + if (max_qty < set_qty) { + max_qty = set_qty; + } + this._rpc({ + route: "/shop/cart/update_json", + params: { + line_id: line_id, + product_id: parseInt($input.data("product-id"), 10), + set_qty: set_qty, + max_qty: max_qty, + }, + }).then(function (data) { + $input.data("update_change", false); + var check_value = parseInt($input.val() || 0, 10); + if (isNaN(check_value)) { + check_value = 1; + } + if (value !== check_value) { + $input.trigger("change"); + return; + } + sessionStorage.setItem("website_sale_cart_quantity", data.cart_quantity); + if (!data.cart_quantity) { + return (window.location = "/shop/cart"); + } + $input.val(data.quantity); + $(".js_quantity[data-line-id=" + line_id + "]") + .val(data.quantity) + .text(data.quantity); + + wSaleUtils.updateCartNavBar(data); + wSaleUtils.showWarning(data.warning); + // Propagating the change to the express checkout forms + core.bus.trigger("cart_amount_changed", data.amount, data.minor_amount); + }); + }, + }); +}); diff --git a/nfu_sale_product_min_max_qty/views/sale_order_line_views.xml b/nfu_sale_product_min_max_qty/views/sale_order_line_views.xml new file mode 100644 index 0000000..0c93010 --- /dev/null +++ b/nfu_sale_product_min_max_qty/views/sale_order_line_views.xml @@ -0,0 +1,24 @@ + + + + sale.order.line.tree + sale.order.line + + + + + + + + + + + + + + + + + + + diff --git a/nfu_sale_product_min_max_qty/views/sale_order_views.xml b/nfu_sale_product_min_max_qty/views/sale_order_views.xml new file mode 100644 index 0000000..9380572 --- /dev/null +++ b/nfu_sale_product_min_max_qty/views/sale_order_views.xml @@ -0,0 +1,27 @@ + + + + sale.order.form.product.min_max_qty + sale.order + + + + + + + + + + sale.order.list.view.inherited + sale.order + + + +
+
+
+
+
+
diff --git a/nfu_sale_product_min_max_qty/views/templates.xml b/nfu_sale_product_min_max_qty/views/templates.xml new file mode 100644 index 0000000..611340f --- /dev/null +++ b/nfu_sale_product_min_max_qty/views/templates.xml @@ -0,0 +1,51 @@ + + + + + \ No newline at end of file diff --git a/nfu_sale_product_min_max_qty/wizard/__init__.py b/nfu_sale_product_min_max_qty/wizard/__init__.py new file mode 100644 index 0000000..236a111 --- /dev/null +++ b/nfu_sale_product_min_max_qty/wizard/__init__.py @@ -0,0 +1,2 @@ +from . import sale_max_qty_chooser +from . import sale_max_qty_line diff --git a/nfu_sale_product_min_max_qty/wizard/sale_max_qty_chooser.py b/nfu_sale_product_min_max_qty/wizard/sale_max_qty_chooser.py new file mode 100644 index 0000000..04c48a0 --- /dev/null +++ b/nfu_sale_product_min_max_qty/wizard/sale_max_qty_chooser.py @@ -0,0 +1,54 @@ +from odoo import fields, models + + +class SaleMaxQtyChooser(models.TransientModel): + """Wizard to Allow user to adjust the sale order amout to fit to a packaging size""" + + _name = "sale.max.qty.chooser" + _description = "Sale Max Quantity Chhooser" + + # fill from init or compute from given sale order lines + sale_order_ids = fields.Many2many("sale.order") + sale_max_qty_ids = fields.One2many("sale.max.qty.line", "sale_max_qty_chooser") + + def update_and_confirm_sale_order(self): + """ + Confirm sale orders where the quantity is fine. Create new Sale Orders for SO Lines with reset to draft + Through user error when total order amount of a product is not equal to packaging size (first package in the + list or smalles package sale_order_line.product_id.product.packaging_ids.qty) + we can use write({'key':'value'}) to update all sale order lines and action_confirm() to validate all sale + orders + + Also see this methode on how to create new records. It creates a invoice with invoice lines. we need to do the + same for sale orders + + def action_sold(self): + res = super().action_sold() + self.env["account.move"].create( + { + "property_id": self.id, + "partner_id": self.buyer_id.id, + "move_type": "out_invoice", + "line_ids": [ + fields.Command.create( + { + "name": f"Property {self.name} - 10% Tax", + "quantity": 1, + "price_unit": self.selling_price * 1.1, + } + ), + fields.Command.create({"name": "Administration Fees", "quantity": 1, "price_unit": 1000}), + ], + } + ) + return res + """ + + for sale_order in self.sale_order_ids: + can_confirm = True + for line in self.sale_max_qty_ids.filtered(lambda line: line.sale_line_id.order_id == sale_order): + line.sale_line_id.write({"product_uom_qty": line.qty}) + if not line.quantity_fits_packaging: + can_confirm = False + if can_confirm: + sale_order.action_confirm() diff --git a/nfu_sale_product_min_max_qty/wizard/sale_max_qty_chooser_views.xml b/nfu_sale_product_min_max_qty/wizard/sale_max_qty_chooser_views.xml new file mode 100644 index 0000000..0d4cd9e --- /dev/null +++ b/nfu_sale_product_min_max_qty/wizard/sale_max_qty_chooser_views.xml @@ -0,0 +1,47 @@ + + + + Sale Max Quantity Chhooser + sale.max.qty.chooser + +
+
+

Sale Orders

+ +
+ + + +
+
+
+
+
+ + + Set Quantities + sale.max.qty.chooser + form + current + + { + 'search_default_group_by_product': 1, + } + + +
diff --git a/nfu_sale_product_min_max_qty/wizard/sale_max_qty_line.py b/nfu_sale_product_min_max_qty/wizard/sale_max_qty_line.py new file mode 100644 index 0000000..ab4ff6b --- /dev/null +++ b/nfu_sale_product_min_max_qty/wizard/sale_max_qty_line.py @@ -0,0 +1,46 @@ +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class SaleMaxQtyLine(models.TransientModel): + """ + Lines to shown in Wizard + """ + + _name = "sale.max.qty.line" + _description = "Max qty Sale Order Lines" + + name = fields.Char() + sale_max_qty_chooser = fields.Many2one("sale.max.qty.chooser") + qty = fields.Float() + max_qty = fields.Float(related="sale_line_id.product_uom_max_qty") + packaging_size = fields.Float(compute="_compute_packaging_size") + quantity_fits_packaging = fields.Boolean(compute="_compute_quantity_fits_packaging") + total_quantity = fields.Float(compute="_compute_quantity_fits_packaging") + + sale_line_id = fields.Many2one("sale.order.line") + product_id = fields.Many2one(related="sale_line_id.product_id", store=True) + reset_to_draft = fields.Boolean() + + def _compute_packaging_size(self): + for max_qty_line in self: + max_qty_line.packaging_size = min( + max_qty_line.sale_line_id.product_id.packaging_ids.mapped("qty"), default=1.0 + ) + + @api.onchange("qty") + def _compute_quantity_fits_packaging(self): + for max_qty_line in self: + order_lines = max_qty_line.sale_max_qty_chooser.sale_max_qty_ids.filtered( + lambda line: line.sale_line_id.product_id == max_qty_line.product_id + ) + max_qty_line.total_quantity = sum(order_lines.mapped("qty")) + max_qty_line.quantity_fits_packaging = max_qty_line.total_quantity % max_qty_line.packaging_size == 0 + # for order_line in order_lines: + # order_line.quantity_fits_packaging = max_qty_line.quantity_fits_packaging + + @api.constrains("qty") + def _check_product_uom_qty(self): + for max_qty_line in self: + if max_qty_line.max_qty != 0 and max_qty_line.qty > max_qty_line.max_qty: + raise UserError(_("The quantity must be less than or equal to the maximum quantity.")) diff --git a/nfu_sale_product_min_max_qty/wizard/sale_max_qty_line_views.xml b/nfu_sale_product_min_max_qty/wizard/sale_max_qty_line_views.xml new file mode 100644 index 0000000..67e32e6 --- /dev/null +++ b/nfu_sale_product_min_max_qty/wizard/sale_max_qty_line_views.xml @@ -0,0 +1,28 @@ + + + + sale.max.qty.line.search + sale.max.qty.line + + + + + + + + + sale.max.qty.line.tree + sale.max.qty.line + + + + + + + + + + + + +