odoo-addon-sale-blanket-order 16.0.1.0.0.5__py3-none-any.whl → 18.0.1.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- odoo/addons/sale_blanket_order/README.rst +116 -64
- odoo/addons/sale_blanket_order/__manifest__.py +8 -3
- odoo/addons/sale_blanket_order/data/ir_cron.xml +0 -2
- odoo/addons/sale_blanket_order/i18n/de.po +117 -109
- odoo/addons/sale_blanket_order/i18n/es.po +91 -91
- odoo/addons/sale_blanket_order/i18n/fr.po +99 -97
- odoo/addons/sale_blanket_order/i18n/fr_FR.po +91 -91
- odoo/addons/sale_blanket_order/i18n/it.po +69 -80
- odoo/addons/sale_blanket_order/i18n/pt.po +51 -88
- odoo/addons/sale_blanket_order/i18n/sale_blanket_order.pot +26 -67
- odoo/addons/sale_blanket_order/models/blanket_orders.py +84 -146
- odoo/addons/sale_blanket_order/models/sale_config_settings.py +0 -1
- odoo/addons/sale_blanket_order/models/sale_orders.py +20 -7
- odoo/addons/sale_blanket_order/readme/CONTEXT.md +5 -0
- odoo/addons/sale_blanket_order/readme/CONTRIBUTORS.md +17 -0
- odoo/addons/sale_blanket_order/readme/{CREDITS.rst → CREDITS.md} +2 -1
- odoo/addons/sale_blanket_order/readme/DESCRIPTION.md +5 -0
- odoo/addons/sale_blanket_order/readme/ROADMAP.md +3 -0
- odoo/addons/sale_blanket_order/readme/USAGE.md +58 -0
- odoo/addons/sale_blanket_order/report/report.xml +0 -2
- odoo/addons/sale_blanket_order/report/templates.xml +32 -35
- odoo/addons/sale_blanket_order/static/description/index.html +103 -69
- odoo/addons/sale_blanket_order/static/src/js/disable_add_order_line.esm.js +22 -0
- odoo/addons/sale_blanket_order/tests/test_blanket_orders.py +99 -50
- odoo/addons/sale_blanket_order/tests/test_sale_order.py +7 -21
- odoo/addons/sale_blanket_order/views/sale_blanket_order_line_views.xml +17 -28
- odoo/addons/sale_blanket_order/views/sale_blanket_order_views.xml +36 -58
- odoo/addons/sale_blanket_order/views/sale_config_settings.xml +11 -19
- odoo/addons/sale_blanket_order/views/sale_order_views.xml +7 -9
- odoo/addons/sale_blanket_order/wizard/create_sale_orders.py +58 -41
- odoo/addons/sale_blanket_order/wizard/create_sale_orders.xml +12 -2
- odoo_addon_sale_blanket_order-18.0.1.2.1.dist-info/METADATA +216 -0
- odoo_addon_sale_blanket_order-18.0.1.2.1.dist-info/RECORD +49 -0
- {odoo_addon_sale_blanket_order-16.0.1.0.0.5.dist-info → odoo_addon_sale_blanket_order-18.0.1.2.1.dist-info}/WHEEL +1 -1
- odoo_addon_sale_blanket_order-18.0.1.2.1.dist-info/top_level.txt +1 -0
- odoo/addons/sale_blanket_order/readme/CONTRIBUTORS.rst +0 -8
- odoo/addons/sale_blanket_order/readme/DESCRIPTION.rst +0 -4
- odoo/addons/sale_blanket_order/readme/USAGE.rst +0 -53
- odoo_addon_sale_blanket_order-16.0.1.0.0.5.dist-info/METADATA +0 -167
- odoo_addon_sale_blanket_order-16.0.1.0.0.5.dist-info/RECORD +0 -46
- odoo_addon_sale_blanket_order-16.0.1.0.0.5.dist-info/top_level.txt +0 -1
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
# Copyright 2018 ACSONE SA/NV
|
|
2
2
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
3
3
|
|
|
4
|
-
from odoo import
|
|
4
|
+
from odoo import api, fields, models
|
|
5
5
|
from odoo.exceptions import UserError
|
|
6
6
|
from odoo.tools import float_is_zero
|
|
7
7
|
from odoo.tools.misc import format_date
|
|
8
8
|
|
|
9
|
-
from odoo.addons.sale.models.sale_order import READONLY_FIELD_STATES
|
|
10
|
-
|
|
11
9
|
|
|
12
10
|
class BlanketOrder(models.Model):
|
|
13
11
|
_name = "sale.blanket.order"
|
|
@@ -44,7 +42,6 @@ class BlanketOrder(models.Model):
|
|
|
44
42
|
partner_id = fields.Many2one(
|
|
45
43
|
"res.partner",
|
|
46
44
|
string="Partner",
|
|
47
|
-
states=READONLY_FIELD_STATES,
|
|
48
45
|
)
|
|
49
46
|
line_ids = fields.One2many(
|
|
50
47
|
"sale.blanket.order.line", "order_id", string="Order lines", copy=True
|
|
@@ -63,21 +60,11 @@ class BlanketOrder(models.Model):
|
|
|
63
60
|
"product.pricelist",
|
|
64
61
|
string="Pricelist",
|
|
65
62
|
required=True,
|
|
66
|
-
states=READONLY_FIELD_STATES,
|
|
67
63
|
)
|
|
68
64
|
currency_id = fields.Many2one("res.currency", related="pricelist_id.currency_id")
|
|
69
|
-
analytic_account_id = fields.Many2one(
|
|
70
|
-
comodel_name="account.analytic.account",
|
|
71
|
-
string="Analytic Account",
|
|
72
|
-
copy=False,
|
|
73
|
-
check_company=True,
|
|
74
|
-
states=READONLY_FIELD_STATES,
|
|
75
|
-
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
|
|
76
|
-
)
|
|
77
65
|
payment_term_id = fields.Many2one(
|
|
78
66
|
"account.payment.term",
|
|
79
67
|
string="Payment Terms",
|
|
80
|
-
states=READONLY_FIELD_STATES,
|
|
81
68
|
)
|
|
82
69
|
confirmed = fields.Boolean(copy=False)
|
|
83
70
|
state = fields.Selection(
|
|
@@ -91,26 +78,28 @@ class BlanketOrder(models.Model):
|
|
|
91
78
|
store=True,
|
|
92
79
|
copy=False,
|
|
93
80
|
)
|
|
94
|
-
validity_date = fields.Date(
|
|
95
|
-
states=READONLY_FIELD_STATES,
|
|
96
|
-
)
|
|
81
|
+
validity_date = fields.Date()
|
|
97
82
|
client_order_ref = fields.Char(
|
|
98
83
|
string="Customer Reference",
|
|
99
84
|
copy=False,
|
|
100
|
-
states=READONLY_FIELD_STATES,
|
|
101
85
|
)
|
|
102
|
-
note = fields.Text(default=_default_note
|
|
86
|
+
note = fields.Text(default=_default_note)
|
|
103
87
|
user_id = fields.Many2one(
|
|
104
88
|
"res.users",
|
|
105
89
|
string="Salesperson",
|
|
106
|
-
states=READONLY_FIELD_STATES,
|
|
107
90
|
)
|
|
108
91
|
team_id = fields.Many2one(
|
|
109
92
|
"crm.team",
|
|
110
93
|
string="Sales Team",
|
|
111
94
|
change_default=True,
|
|
112
95
|
default=lambda self: self.env["crm.team"]._get_default_team_id(),
|
|
113
|
-
|
|
96
|
+
)
|
|
97
|
+
tag_ids = fields.Many2many(
|
|
98
|
+
comodel_name="crm.tag",
|
|
99
|
+
relation="sale_blanket_order_tag_rel",
|
|
100
|
+
column1="blanket_order_id",
|
|
101
|
+
column2="tag_id",
|
|
102
|
+
string="Tags",
|
|
114
103
|
)
|
|
115
104
|
company_id = fields.Many2one(
|
|
116
105
|
comodel_name="res.company",
|
|
@@ -121,7 +110,9 @@ class BlanketOrder(models.Model):
|
|
|
121
110
|
sale_count = fields.Integer(compute="_compute_sale_count")
|
|
122
111
|
|
|
123
112
|
fiscal_position_id = fields.Many2one(
|
|
124
|
-
"account.fiscal.position",
|
|
113
|
+
"account.fiscal.position",
|
|
114
|
+
string="Fiscal Position",
|
|
115
|
+
check_company=True,
|
|
125
116
|
)
|
|
126
117
|
|
|
127
118
|
amount_untaxed = fields.Monetary(
|
|
@@ -198,7 +189,7 @@ class BlanketOrder(models.Model):
|
|
|
198
189
|
order.state = "expired"
|
|
199
190
|
elif float_is_zero(
|
|
200
191
|
sum(
|
|
201
|
-
order.line_ids.filtered(lambda
|
|
192
|
+
order.line_ids.filtered(lambda line: not line.display_type).mapped(
|
|
202
193
|
"remaining_uom_qty"
|
|
203
194
|
)
|
|
204
195
|
),
|
|
@@ -247,15 +238,15 @@ class BlanketOrder(models.Model):
|
|
|
247
238
|
|
|
248
239
|
if self.partner_id.user_id:
|
|
249
240
|
values["user_id"] = self.partner_id.user_id.id
|
|
250
|
-
if self.partner_id.
|
|
251
|
-
values["team_id"] = self.partner_id.
|
|
241
|
+
if self.partner_id.user_id.sale_team_id:
|
|
242
|
+
values["team_id"] = self.partner_id.user_id.sale_team_id.id
|
|
252
243
|
self.update(values)
|
|
253
244
|
|
|
254
245
|
def unlink(self):
|
|
255
246
|
for order in self:
|
|
256
247
|
if order.state not in ("draft", "expired") or order._check_active_orders():
|
|
257
248
|
raise UserError(
|
|
258
|
-
_(
|
|
249
|
+
self.env._(
|
|
259
250
|
"You can not delete an open blanket or "
|
|
260
251
|
"with active sale orders! "
|
|
261
252
|
"Try to cancel it before."
|
|
@@ -267,12 +258,12 @@ class BlanketOrder(models.Model):
|
|
|
267
258
|
try:
|
|
268
259
|
today = fields.Date.today()
|
|
269
260
|
for order in self:
|
|
270
|
-
assert order.validity_date, _("Validity date is mandatory")
|
|
271
|
-
assert order.validity_date > today, _(
|
|
261
|
+
assert order.validity_date, self.env._("Validity date is mandatory")
|
|
262
|
+
assert order.validity_date > today, self.env._(
|
|
272
263
|
"Validity date must be in the future"
|
|
273
264
|
)
|
|
274
|
-
assert order.partner_id, _("Partner is mandatory")
|
|
275
|
-
assert len(order.line_ids) > 0, _("Must have some lines")
|
|
265
|
+
assert order.partner_id, self.env._("Partner is mandatory")
|
|
266
|
+
assert len(order.line_ids) > 0, self.env._("Must have some lines")
|
|
276
267
|
order.line_ids._validate()
|
|
277
268
|
except AssertionError as e:
|
|
278
269
|
raise UserError(e) from e
|
|
@@ -303,7 +294,7 @@ class BlanketOrder(models.Model):
|
|
|
303
294
|
for order in self:
|
|
304
295
|
if order._check_active_orders():
|
|
305
296
|
raise UserError(
|
|
306
|
-
_(
|
|
297
|
+
self.env._(
|
|
307
298
|
"You can not delete a blanket order with opened "
|
|
308
299
|
"sale orders! "
|
|
309
300
|
"Try to cancel them before."
|
|
@@ -430,9 +421,9 @@ class BlanketOrderLine(models.Model):
|
|
|
430
421
|
product_uom = fields.Many2one("uom.uom", string="Unit of Measure")
|
|
431
422
|
price_unit = fields.Float(string="Price", digits="Product Price")
|
|
432
423
|
taxes_id = fields.Many2many(
|
|
433
|
-
"account.tax",
|
|
434
|
-
|
|
435
|
-
|
|
424
|
+
comodel_name="account.tax",
|
|
425
|
+
context={"active_test": False},
|
|
426
|
+
check_company=True,
|
|
436
427
|
)
|
|
437
428
|
date_schedule = fields.Date(string="Scheduled Date")
|
|
438
429
|
original_uom_qty = fields.Float(
|
|
@@ -483,115 +474,53 @@ class BlanketOrderLine(models.Model):
|
|
|
483
474
|
default=False,
|
|
484
475
|
help="Technical field for UX purpose.",
|
|
485
476
|
)
|
|
477
|
+
pricelist_item_id = fields.Many2one(
|
|
478
|
+
comodel_name="product.pricelist.item", compute="_compute_pricelist_item_id"
|
|
479
|
+
)
|
|
486
480
|
|
|
487
|
-
|
|
488
|
-
|
|
481
|
+
@api.depends(
|
|
482
|
+
"order_id.name", "date_schedule", "remaining_uom_qty", "product_uom.name"
|
|
483
|
+
)
|
|
484
|
+
@api.depends_context("from_sale_order")
|
|
485
|
+
def _compute_display_name(self):
|
|
489
486
|
if self.env.context.get("from_sale_order"):
|
|
490
487
|
for record in self:
|
|
491
|
-
|
|
488
|
+
name = f"[{record.order_id.name}]"
|
|
492
489
|
if record.date_schedule:
|
|
493
490
|
formatted_date = format_date(record.env, record.date_schedule)
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
491
|
+
name += " - {}: {}".format(
|
|
492
|
+
self.env._("Date Scheduled"), formatted_date
|
|
493
|
+
)
|
|
494
|
+
name += " ({}: {} {})".format(
|
|
495
|
+
self.env._("remaining"),
|
|
497
496
|
record.remaining_uom_qty,
|
|
498
497
|
record.product_uom.name,
|
|
499
498
|
)
|
|
500
|
-
|
|
501
|
-
return result
|
|
502
|
-
return super().name_get()
|
|
503
|
-
|
|
504
|
-
def _get_real_price_currency(self, product, rule_id, qty, uom, pricelist_id):
|
|
505
|
-
"""Retrieve the price before applying the pricelist
|
|
506
|
-
:param obj product: object of current product record
|
|
507
|
-
:param float qty: total quentity of product
|
|
508
|
-
:param tuple price_and_rule: tuple(price, suitable_rule) coming
|
|
509
|
-
from pricelist computation
|
|
510
|
-
:param obj uom: unit of measure of current order line
|
|
511
|
-
:param integer pricelist_id: pricelist id of sale order"""
|
|
512
|
-
# Copied and adapted from the sale module
|
|
513
|
-
PricelistItem = self.env["product.pricelist.item"]
|
|
514
|
-
field_name = "lst_price"
|
|
515
|
-
currency_id = None
|
|
516
|
-
product_currency = None
|
|
517
|
-
if rule_id:
|
|
518
|
-
pricelist_item = PricelistItem.browse(rule_id)
|
|
519
|
-
if pricelist_item.pricelist_id.discount_policy == "without_discount":
|
|
520
|
-
while (
|
|
521
|
-
pricelist_item.base == "pricelist"
|
|
522
|
-
and pricelist_item.base_pricelist_id
|
|
523
|
-
and pricelist_item.base_pricelist_id.discount_policy
|
|
524
|
-
== "without_discount"
|
|
525
|
-
):
|
|
526
|
-
price, rule_id = pricelist_item.base_pricelist_id.with_context(
|
|
527
|
-
uom=uom.id
|
|
528
|
-
)._get_product_price_rule(product, qty, uom)
|
|
529
|
-
pricelist_item = PricelistItem.browse(rule_id)
|
|
530
|
-
|
|
531
|
-
if pricelist_item.base == "standard_price":
|
|
532
|
-
field_name = "standard_price"
|
|
533
|
-
if pricelist_item.base == "pricelist" and pricelist_item.base_pricelist_id:
|
|
534
|
-
field_name = "price"
|
|
535
|
-
product = product.with_context(
|
|
536
|
-
pricelist=pricelist_item.base_pricelist_id.id
|
|
537
|
-
)
|
|
538
|
-
product_currency = pricelist_item.base_pricelist_id.currency_id
|
|
539
|
-
currency_id = pricelist_item.pricelist_id.currency_id
|
|
540
|
-
|
|
541
|
-
product_currency = (
|
|
542
|
-
product_currency
|
|
543
|
-
or (product.company_id and product.company_id.currency_id)
|
|
544
|
-
or self.env.company.currency_id
|
|
545
|
-
)
|
|
546
|
-
if not currency_id:
|
|
547
|
-
currency_id = product_currency
|
|
548
|
-
cur_factor = 1.0
|
|
549
|
-
else:
|
|
550
|
-
if currency_id.id == product_currency.id:
|
|
551
|
-
cur_factor = 1.0
|
|
552
|
-
else:
|
|
553
|
-
cur_factor = currency_id._get_conversion_rate(
|
|
554
|
-
product_currency, currency_id
|
|
555
|
-
)
|
|
556
|
-
|
|
557
|
-
product_uom = product.uom_id.id
|
|
558
|
-
if uom and uom.id != product_uom:
|
|
559
|
-
# the unit price is in a different uom
|
|
560
|
-
uom_factor = uom._compute_price(1.0, product.uom_id)
|
|
499
|
+
record.display_name = name
|
|
561
500
|
else:
|
|
562
|
-
|
|
501
|
+
return super()._compute_display_name()
|
|
563
502
|
|
|
564
|
-
|
|
503
|
+
def _get_display_price(self):
|
|
504
|
+
# Copied and adapted from the sale module
|
|
505
|
+
# No need to call _get_pricelist_price_before_discount()
|
|
506
|
+
# since BO lines cannot have discounts
|
|
507
|
+
# TODO: handle combos the way Odoo does in the sale module
|
|
508
|
+
self.ensure_one()
|
|
509
|
+
pricelist_price = self._get_pricelist_price()
|
|
510
|
+
return pricelist_price
|
|
565
511
|
|
|
566
|
-
def
|
|
512
|
+
def _get_pricelist_price(self):
|
|
567
513
|
# Copied and adapted from the sale module
|
|
568
514
|
self.ensure_one()
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
context_partner = dict(
|
|
577
|
-
self.env.context, partner_id=partner.id, date=fields.Date.today()
|
|
515
|
+
self.product_id.ensure_one()
|
|
516
|
+
price = self.pricelist_item_id._compute_price(
|
|
517
|
+
product=self.product_id,
|
|
518
|
+
quantity=self.original_uom_qty or 1.0,
|
|
519
|
+
uom=self.product_uom,
|
|
520
|
+
date=fields.Date.today(),
|
|
521
|
+
currency=self.currency_id,
|
|
578
522
|
)
|
|
579
|
-
|
|
580
|
-
**context_partner
|
|
581
|
-
)._get_real_price_currency(
|
|
582
|
-
self.product_id,
|
|
583
|
-
rule_id,
|
|
584
|
-
self.original_uom_qty,
|
|
585
|
-
self.product_uom,
|
|
586
|
-
pricelist.id,
|
|
587
|
-
)
|
|
588
|
-
if currency_id != pricelist.currency_id.id:
|
|
589
|
-
currency = self.env["res.currency"].browse(currency_id)
|
|
590
|
-
base_price = currency.with_context(**context_partner).compute(
|
|
591
|
-
base_price, pricelist.currency_id
|
|
592
|
-
)
|
|
593
|
-
# negative discounts (= surcharge) are included in the display price
|
|
594
|
-
return max(base_price, final_price)
|
|
523
|
+
return price
|
|
595
524
|
|
|
596
525
|
@api.onchange("product_id", "original_uom_qty")
|
|
597
526
|
def onchange_product(self):
|
|
@@ -605,23 +534,17 @@ class BlanketOrderLine(models.Model):
|
|
|
605
534
|
if self.order_id.partner_id and float_is_zero(
|
|
606
535
|
self.price_unit, precision_digits=precision
|
|
607
536
|
):
|
|
608
|
-
self.price_unit = self._get_display_price(
|
|
537
|
+
self.price_unit = self._get_display_price()
|
|
609
538
|
if self.product_id.code:
|
|
610
|
-
name = "[{}] {
|
|
539
|
+
name = f"[{name}] {self.product_id.code}"
|
|
611
540
|
if self.product_id.description_sale:
|
|
612
541
|
name += "\n" + self.product_id.description_sale
|
|
613
542
|
self.name = name
|
|
614
543
|
|
|
615
544
|
fpos = self.order_id.fiscal_position_id
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
self.product_id.taxes_id.filtered(
|
|
620
|
-
lambda r: r.company_id.id == company_id
|
|
621
|
-
)
|
|
622
|
-
)
|
|
623
|
-
else:
|
|
624
|
-
self.taxes_id = fpos.map_tax(self.product_id.taxes_id)
|
|
545
|
+
self.taxes_id = fpos.map_tax(
|
|
546
|
+
self.product_id.taxes_id._filter_taxes_by_company(self.company_id)
|
|
547
|
+
)
|
|
625
548
|
|
|
626
549
|
@api.depends(
|
|
627
550
|
"sale_lines.order_id.state",
|
|
@@ -656,15 +579,30 @@ class BlanketOrderLine(models.Model):
|
|
|
656
579
|
line.remaining_uom_qty, line.product_id.uom_id
|
|
657
580
|
)
|
|
658
581
|
|
|
582
|
+
@api.depends("product_id", "product_uom", "original_uom_qty")
|
|
583
|
+
def _compute_pricelist_item_id(self):
|
|
584
|
+
# Copied and adapted from the sale module
|
|
585
|
+
for line in self:
|
|
586
|
+
if (
|
|
587
|
+
not line.product_id
|
|
588
|
+
or line.display_type
|
|
589
|
+
or not line.order_id.pricelist_id
|
|
590
|
+
):
|
|
591
|
+
line.pricelist_item_id = False
|
|
592
|
+
else:
|
|
593
|
+
line.pricelist_item_id = line.order_id.pricelist_id._get_product_rule(
|
|
594
|
+
line.product_id,
|
|
595
|
+
quantity=line.original_uom_qty or 1.0,
|
|
596
|
+
uom=line.product_uom,
|
|
597
|
+
date=fields.Date.today(),
|
|
598
|
+
)
|
|
599
|
+
|
|
659
600
|
def _validate(self):
|
|
660
601
|
try:
|
|
661
602
|
for line in self:
|
|
662
|
-
assert (
|
|
663
|
-
not line.display_type and line.price_unit > 0.0
|
|
664
|
-
) or line.display_type, _("Price must be greater than zero")
|
|
665
603
|
assert (
|
|
666
604
|
not line.display_type and line.original_uom_qty > 0.0
|
|
667
|
-
) or line.display_type, _("Quantity must be greater than zero")
|
|
605
|
+
) or line.display_type, self.env._("Quantity must be greater than zero")
|
|
668
606
|
except AssertionError as e:
|
|
669
607
|
raise UserError(e) from e
|
|
670
608
|
|
|
@@ -708,7 +646,7 @@ class BlanketOrderLine(models.Model):
|
|
|
708
646
|
lambda line: line.display_type != values.get("display_type")
|
|
709
647
|
):
|
|
710
648
|
raise UserError(
|
|
711
|
-
_(
|
|
649
|
+
self.env._(
|
|
712
650
|
"""
|
|
713
651
|
You cannot change the type of a sale order line.
|
|
714
652
|
Instead you should delete the current line and create a new line
|
|
@@ -716,4 +654,4 @@ class BlanketOrderLine(models.Model):
|
|
|
716
654
|
"""
|
|
717
655
|
)
|
|
718
656
|
)
|
|
719
|
-
return super(
|
|
657
|
+
return super().write(values)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
4
4
|
from datetime import date, timedelta
|
|
5
5
|
|
|
6
|
-
from odoo import
|
|
6
|
+
from odoo import api, fields, models
|
|
7
7
|
from odoo.exceptions import ValidationError
|
|
8
8
|
|
|
9
9
|
|
|
@@ -15,6 +15,9 @@ class SaleOrder(models.Model):
|
|
|
15
15
|
string="Origin blanket order",
|
|
16
16
|
related="order_line.blanket_order_line.order_id",
|
|
17
17
|
)
|
|
18
|
+
disable_adding_lines = fields.Boolean(
|
|
19
|
+
compute="_compute_disable_adding_lines",
|
|
20
|
+
)
|
|
18
21
|
|
|
19
22
|
@api.model
|
|
20
23
|
def _check_exchausted_blanket_order_line(self):
|
|
@@ -27,7 +30,7 @@ class SaleOrder(models.Model):
|
|
|
27
30
|
for order in self:
|
|
28
31
|
if order._check_exchausted_blanket_order_line():
|
|
29
32
|
raise ValidationError(
|
|
30
|
-
_(
|
|
33
|
+
self.env._(
|
|
31
34
|
"Cannot confirm order %s as one of the lines refers "
|
|
32
35
|
"to a blanket order that has no remaining quantity."
|
|
33
36
|
)
|
|
@@ -41,12 +44,22 @@ class SaleOrder(models.Model):
|
|
|
41
44
|
if line.blanket_order_line:
|
|
42
45
|
if line.blanket_order_line.partner_id != self.partner_id:
|
|
43
46
|
raise ValidationError(
|
|
44
|
-
_(
|
|
47
|
+
self.env._(
|
|
45
48
|
"The customer must be equal to the "
|
|
46
49
|
"blanket order lines customer"
|
|
47
50
|
)
|
|
48
51
|
)
|
|
49
52
|
|
|
53
|
+
@api.depends("blanket_order_id")
|
|
54
|
+
@api.depends_context("uid")
|
|
55
|
+
def _compute_disable_adding_lines(self):
|
|
56
|
+
self.disable_adding_lines = False
|
|
57
|
+
if self.env.user.has_group(
|
|
58
|
+
"sale_blanket_order.blanket_orders_disable_adding_lines"
|
|
59
|
+
):
|
|
60
|
+
for order in self:
|
|
61
|
+
order.disable_adding_lines = order.blanket_order_id
|
|
62
|
+
|
|
50
63
|
|
|
51
64
|
class SaleOrderLine(models.Model):
|
|
52
65
|
_inherit = "sale.order.line"
|
|
@@ -61,14 +74,14 @@ class SaleOrderLine(models.Model):
|
|
|
61
74
|
assigned_bo_line = False
|
|
62
75
|
date_planned = date.today()
|
|
63
76
|
date_delta = timedelta(days=365)
|
|
64
|
-
for line in bo_lines.filtered(lambda
|
|
77
|
+
for line in bo_lines.filtered(lambda bo_line: bo_line.date_schedule):
|
|
65
78
|
date_schedule = line.date_schedule
|
|
66
79
|
if date_schedule and abs(date_schedule - date_planned) < date_delta:
|
|
67
80
|
assigned_bo_line = line
|
|
68
81
|
date_delta = abs(date_schedule - date_planned)
|
|
69
82
|
if assigned_bo_line:
|
|
70
83
|
return assigned_bo_line
|
|
71
|
-
non_date_bo_lines = bo_lines.filtered(lambda
|
|
84
|
+
non_date_bo_lines = bo_lines.filtered(lambda bo_line: not bo_line.date_schedule)
|
|
72
85
|
if non_date_bo_lines:
|
|
73
86
|
return non_date_bo_lines[0]
|
|
74
87
|
|
|
@@ -166,7 +179,7 @@ class SaleOrderLine(models.Model):
|
|
|
166
179
|
and line.product_id != line.blanket_order_line.product_id
|
|
167
180
|
):
|
|
168
181
|
raise ValidationError(
|
|
169
|
-
_(
|
|
182
|
+
self.env._(
|
|
170
183
|
"The product in the blanket order and in the "
|
|
171
184
|
"sales order must match"
|
|
172
185
|
)
|
|
@@ -178,7 +191,7 @@ class SaleOrderLine(models.Model):
|
|
|
178
191
|
if line.blanket_order_line:
|
|
179
192
|
if line.currency_id != line.blanket_order_line.order_id.currency_id:
|
|
180
193
|
raise ValidationError(
|
|
181
|
-
_(
|
|
194
|
+
self.env._(
|
|
182
195
|
"The currency of the blanket order must match with "
|
|
183
196
|
"that of the sale order."
|
|
184
197
|
)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Others modules provide similar features. The module (sale_order_blanket_order)[https://pypi.org/project/odoo-addon-sale-order-blanket-order] also defines the concept of sale blanket order. The main differences are:
|
|
2
|
+
|
|
3
|
+
* This module integrates Blanket Orders and Call-Off Orders into the sale.blanket.order object, whereas the other module extends the sale.order object. This means that any extensions made to the sale order model can also apply to blanket orders.
|
|
4
|
+
|
|
5
|
+
* In the other module, you can deliver and invoice directly from the blanket order. You can also create a separate call-off order to partially deliver the blanket order.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
- André Pereira \<<github@andreparames.com>\> (<https://www.acsone.eu/>)
|
|
2
|
+
|
|
3
|
+
- Adrià Gil Sorribes \<<adria.gil@eficent.com>\>
|
|
4
|
+
(<https://www.eficent.com/>)
|
|
5
|
+
|
|
6
|
+
- Jordi Ballester Alomar \<<jordi.ballester@eficent.com>\>
|
|
7
|
+
|
|
8
|
+
- Alex Comba \<<alex.comba@agilebg.com>\> (<https://www.agilebg.com/>)
|
|
9
|
+
|
|
10
|
+
- Codeforward (https://www.codeforward.nl/):
|
|
11
|
+
|
|
12
|
+
> - Jasper Jumelet \<<jasper.jumelet@codeforward.nl>\>
|
|
13
|
+
> - Chris Bergman \<<chris.bergman@codeforward.nl>\>
|
|
14
|
+
|
|
15
|
+
- [Trobz](https://trobz.com):
|
|
16
|
+
|
|
17
|
+
> - Nguyễn Minh Chiến \<<chien@trobz.com>\>
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
The migration of this module from 15.0 to 16.0 was financially supported
|
|
1
|
+
The migration of this module from 15.0 to 16.0 was financially supported
|
|
2
|
+
by Camptocamp
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
A blanket order is a pre-agreement to sell a certain number of
|
|
2
|
+
quantities of products at a specific price. From a confirmed blanket
|
|
3
|
+
order, the users can create new sale orders at such price, until the
|
|
4
|
+
blanket order expires, either due to reaching the validity date or
|
|
5
|
+
exhausting all the quantities of products.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
A new menu in the Sales area is created, allowing users to create new
|
|
2
|
+
blanket orders.
|
|
3
|
+
|
|
4
|
+
To create a new Sale Blanket Order go to the sale menu in the Sales
|
|
5
|
+
section:
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
Hitting the button create will open the form view in which we can
|
|
10
|
+
introduce the following information:
|
|
11
|
+
|
|
12
|
+
- Vendor
|
|
13
|
+
|
|
14
|
+
- Salesperson
|
|
15
|
+
|
|
16
|
+
- Payment Terms
|
|
17
|
+
|
|
18
|
+
- Validity date
|
|
19
|
+
|
|
20
|
+
- Order lines:
|
|
21
|
+
- Product
|
|
22
|
+
- Accorded price
|
|
23
|
+
- Original, Ordered, Invoiced, Received and Remaining quantities
|
|
24
|
+
|
|
25
|
+
- Terms and Conditions of the Blanket Order
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
From the form, once the Blanket Order has been confirmed and its state
|
|
30
|
+
is open, the user can create a Sale Order, check the Sale Orders
|
|
31
|
+
associated to the Blanket Order and/or see the Blanket Order lines
|
|
32
|
+
associated to the BO.
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+
|
|
36
|
+
Hitting the button Create Sale Order will open a wizard that will ask
|
|
37
|
+
for the amount of each product in the BO lines for which the Sale Order
|
|
38
|
+
will be created.
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
Installing this module will add an additional menu which will show all
|
|
43
|
+
the blanket order lines currently defined in the system. From this list
|
|
44
|
+
the user can create customized Sale Orders selecting the lines for which
|
|
45
|
+
the PO (or POs if the customers are different) is (are) created.
|
|
46
|
+
|
|
47
|
+

|
|
48
|
+
|
|
49
|
+
In the Sale Order form one field is added in the PO lines, the Blanket
|
|
50
|
+
Order line field. This field keeps track to which Blanket Order line the
|
|
51
|
+
PO line is associated. Upon adding a new product in a newly created Sale
|
|
52
|
+
Order a blanket order line will be suggested depending on the following
|
|
53
|
+
factors:
|
|
54
|
+
|
|
55
|
+
- Closer Validity date
|
|
56
|
+
- Remaining quantity \> Quantity introduced in the Sale Order line
|
|
57
|
+
|
|
58
|
+

|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="utf-8" ?>
|
|
2
2
|
<odoo>
|
|
3
|
-
|
|
4
3
|
<record id="report_blanket_order" model="ir.actions.report">
|
|
5
4
|
<field name="name">Blanket Order</field>
|
|
6
5
|
<field name="model">sale.blanket.order</field>
|
|
@@ -14,5 +13,4 @@
|
|
|
14
13
|
/>
|
|
15
14
|
<field name="binding_type">report</field>
|
|
16
15
|
</record>
|
|
17
|
-
|
|
18
16
|
</odoo>
|