Merge pull request #3 from bakeupboys/add-min-max

Add min max
This commit is contained in:
madmooose 2024-07-12 18:34:35 +02:00 committed by GitHub
commit 2fe8b35204
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1001 additions and 0 deletions

1
.gitignore vendored
View file

@ -159,3 +159,4 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/ .idea/
.vscode .vscode
.DS_Store

View file

@ -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 <https://github.com/bruecksen/ife_nfu/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 <https://github.com/bruecksen/ife_nfu/issues/new?body=module:%20nfu_sale_product_min_max_qty%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* BAKEUP
Contributors
~~~~~~~~~~~~
* Niels Göttsch <niels@ziemlichoptimal.de>
* Matthias Brück <hi@brueck.io>
Maintainers
~~~~~~~~~~~
This module is part of the `bruecksen/ife_nfu <https://github.com/bruecksen/ife_nfu/tree/16.0/nfu_sale_product_min_max_qty>`_ project on GitHub.
You are welcome to contribute.

View file

@ -0,0 +1,3 @@
from . import models
from . import controllers
from . import wizard

View file

@ -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",
}

View file

@ -0,0 +1 @@
from . import main

View file

@ -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)

View file

@ -0,0 +1,2 @@
from . import sale_order_line
from . import sale_order

View file

@ -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

View file

@ -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."))

View file

@ -0,0 +1,2 @@
* Niels Göttsch <niels@ziemlichoptimal.de>
* Matthias Brück <hi@brueck.io>

View file

@ -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.

View file

@ -0,0 +1 @@
:1.0.0: Initial module.

View file

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_sale_max_qty_chooser sale.max.qty.chooser model_sale_max_qty_chooser sales_team.group_sale_salesman 1 1 1 1
3 access_sale_max_qty_line sale.max.qty.line model_sale_max_qty_line sales_team.group_sale_salesman 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,429 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>NFU Sale Min Max Quantitiy</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="nfu-sale-min-max-quantitiy">
<h1 class="title">NFU Sale Min Max Quantitiy</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:4dfaea54e4d01502b433eb5c0dcca409a5fbe2c3f175e29382a19230343183c9
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/licence-LGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/bruecksen/ife_nfu/tree/16.0/nfu_sale_product_min_max_qty"><img alt="bruecksen/ife_nfu" src="https://img.shields.io/badge/github-bruecksen%2Fife_nfu-lightgray.png?logo=github" /></a></p>
<p>This module adds a minium and a maximum order quantity field to Sale Orders and adds it to checkout of the website shop.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#changelog" id="toc-entry-1">Changelog</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-2">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-3">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-4">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-5">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-6">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="changelog">
<h1><a class="toc-backref" href="#toc-entry-1">Changelog</a></h1>
<table class="docutils field-list" frame="void" rules="none">
<col class="field-name" />
<col class="field-body" />
<tbody valign="top">
<tr class="field"><th class="field-name">1.0.0:</th><td class="field-body">Initial module.</td>
</tr>
</tbody>
</table>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-2">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/bruecksen/ife_nfu/issues">GitHub Issues</a>.
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
<a class="reference external" href="https://github.com/bruecksen/ife_nfu/issues/new?body=module:%20nfu_sale_product_min_max_qty%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-3">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-4">Authors</a></h2>
<ul class="simple">
<li>BAKEUP</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-5">Contributors</a></h2>
<ul class="simple">
<li>Niels Göttsch &lt;<a class="reference external" href="mailto:niels&#64;ziemlichoptimal.de">niels&#64;ziemlichoptimal.de</a>&gt;</li>
<li>Matthias Brück &lt;<a class="reference external" href="mailto:hi&#64;brueck.io">hi&#64;brueck.io</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-6">Maintainers</a></h2>
<p>This module is part of the <a class="reference external" href="https://github.com/bruecksen/ife_nfu/tree/16.0/nfu_sale_product_min_max_qty">bruecksen/ife_nfu</a> project on GitHub.</p>
<p>You are welcome to contribute.</p>
</div>
</div>
</div>
</body>
</html>

View file

@ -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);
});
},
});
});

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_order_line_tree" model="ir.ui.view">
<field name="name">sale.order.line.tree</field>
<field name="model">sale.order.line</field>
<field name="arch" type="xml">
<tree create="false" expand="1">
<field name="order_id"/>
<field name="order_partner_id"/>
<field name="name"/>
<field name="salesman_id"/>
<field name="product_uom_qty" string="Qty"/>
<field name="product_uom_min_qty" optional="hide"/>
<field name="product_uom_max_qty"/>
<field name="qty_delivered"/>
<field name="qty_invoiced"/>
<field name="qty_to_invoice"/>
<field name="product_uom" string="Unit of Measure" groups="uom.group_uom"/>
<field name="price_subtotal" sum="Total" widget="monetary"/>
<field name="currency_id" invisible="1"/>
</tree>
</field>
</record>
</odoo>

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_order_form_inherit_product_min_max_qty" model="ir.ui.view">
<field name="name">sale.order.form.product.min_max_qty</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='order_line']/tree/field[@name='product_uom_qty']" position="after">
<field name="product_uom_min_qty" optional="hide"/>
<field name="product_uom_max_qty"/>
</xpath>
</field>
</record>
<record id="view_order_list_inherit_product_min_max_qty" model="ir.ui.view">
<field name="name">sale.order.list.view.inherited</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_quotation_tree"/>
<field name="arch" type="xml">
<xpath expr="//tree" position="inside">
<header>
<button type="object" name="action_min_max_qty_wizard" string="Min/Max Wizard"/>
<button type="object" name="action_sale_order_lines" string="Sale order lines"/>
</header>
</xpath>
</field>
</record>
</odoo>

View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="cart_lines_min_max_qty" inherit_id="website_sale.cart_lines" name="Shopping Cart Lines">
<xpath expr="//th[hasclass('td-qty')]" position="after">
<th class="text-center td-qty">
<t t-if="show_qty">
<span>Max qty.</span>
</t>
</th>
</xpath>
<xpath expr="//td[hasclass('td-qty')]" position="after">
<td class="text-center td-qty">
<div class="css_quantity input-group mx-auto justify-content-center">
<t t-if="not line._is_not_sellable_line()">
<t t-if="show_qty">
<a t-attf-href="#" class="btn btn-link js_add_cart_json d-none d-md-inline-block" aria-label="Remove one" title="Remove one">
<i class="fa fa-minus"></i>
</a>
<input type="text" name="max-qty" class="js_quantity form-control quantity" t-att-data-line-id="line.id" t-att-data-product-id="line.product_id.id" t-att-value="line.product_uom_max_qty and int(line.product_uom_max_qty) or int(line.product_uom_qty)" t-att-data-min="int(line.product_uom_qty)" />
<t t-if="line._get_shop_warning(clear=False)">
<a t-attf-href="#" class="btn btn-link">
<i class='fa fa-warning text-warning' t-att-title="line._get_shop_warning()" role="img" aria-label="Warning"/>
</a>
</t>
<a t-else='' t-attf-href="#" class="btn btn-link float_left js_add_cart_json d-none d-md-inline-block" aria-label="Add one" title="Add one">
<i class="fa fa-plus"></i>
</a>
</t>
<t t-else="">
<input type="hidden" class="js_quantity form-control quantity" t-att-data-line-id="line.id" t-att-data-product-id="line.product_id.id" t-att-value="int(line.product_uom_qty) == line.product_uom_qty and int(line.product_uom_qty) or line.product_uom_qty" />
</t>
</t>
<t t-else="">
<span class="text-muted w-100" t-esc="int(line.product_uom_qty)"/>
<input type="hidden" class="js_quantity form-control quantity" t-att-data-line-id="line.id" t-att-data-product-id="line.product_id.id" t-att-value="line.product_uom_qty" />
</t>
</div>
</td>
</xpath>
</template>
<template id="cart_summary_min_max_qty" inherit_id="website_sale.cart_summary" name="Cart right column">
<xpath expr="//th[hasclass('td-qty')]" position="after">
<th class="border-top-0 td-qty">Max qty.</th>
</xpath>
<xpath expr="//td[hasclass('td-qty')]" position="after">
<td class='td-qty'>
<div t-esc="line.product_uom_max_qty" />
</td>
</xpath>
</template>
</odoo>

View file

@ -0,0 +1,2 @@
from . import sale_max_qty_chooser
from . import sale_max_qty_line

View file

@ -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()

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_sale_max_qty_chooser_form" model="ir.ui.view">
<field name="name">Sale Max Quantity Chhooser</field>
<field name="model">sale.max.qty.chooser</field>
<field name="arch" type="xml">
<form>
<div class="oe_grey">
<p>Sale Orders</p>
<field name="sale_order_ids" widget="many2many_tags"/>
</div>
<field name="sale_max_qty_ids" context="{'search_default_group_by_qty': 1}">
<!-- <tree create="0" editable="1" default_order="product_id">
<field name="product_id" context="{'group_by': 'product_id'}" />
<field name="qty"/>
<field name="max_qty"/>
<field name="packaging_size"/>
<field name="quantity_fits_packaging" widget="boolean"/>
<field name="reset_to_draft"/>
</tree> -->
</field>
<footer>
<button
name="update_and_confirm_sale_order"
string="Confirm Sale Orders"
type="object"
class="btn-primary"
data-hotkey="q"
/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
</footer>
</form>
</field>
</record>
<record id="action_sale_max_qty" model="ir.actions.act_window">
<field name="name">Set Quantities</field>
<field name="res_model">sale.max.qty.chooser</field>
<field name="view_mode">form</field>
<field name="target">current</field>
<field name="context">
{
'search_default_group_by_product': 1,
}
</field>
</record>
</odoo>

View file

@ -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."))

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_sale_max_qty_line_search" model="ir.ui.view">
<field name="name">sale.max.qty.line.search</field>
<field name="model">sale.max.qty.line</field>
<field name="arch" type="xml">
<search>
<filter string="Product" name="group_by_product" context="{'group_by':'product_id'}"/>
</search>
</field>
</record>
<record id="viw_sale_max_qty_line_tree" model="ir.ui.view">
<field name="name">sale.max.qty.line.tree</field>
<field name="model">sale.max.qty.line</field>
<field name="arch" type="xml">
<tree editable="bottom" create="0" expand="1">
<field name="product_id"/>
<field name="qty"/>
<field name="max_qty"/>
<field name="packaging_size"/>
<field name="quantity_fits_packaging" widget="boolean"/>
<field name="reset_to_draft"/>
</tree>
</field>
</record>
</odoo>