odoo-addon-sale-blanket-order 16.0.2.0.0.2__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 +111 -63
- 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 +65 -80
- odoo/addons/sale_blanket_order/i18n/es.po +30 -59
- odoo/addons/sale_blanket_order/i18n/fr.po +30 -59
- odoo/addons/sale_blanket_order/i18n/fr_FR.po +30 -59
- odoo/addons/sale_blanket_order/i18n/it.po +68 -88
- odoo/addons/sale_blanket_order/i18n/pt.po +15 -59
- odoo/addons/sale_blanket_order/i18n/sale_blanket_order.pot +16 -60
- odoo/addons/sale_blanket_order/models/blanket_orders.py +77 -143
- odoo/addons/sale_blanket_order/models/sale_config_settings.py +0 -1
- odoo/addons/sale_blanket_order/models/sale_orders.py +8 -14
- 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 +87 -62
- 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 -80
- odoo/addons/sale_blanket_order/views/sale_blanket_order_line_views.xml +7 -16
- odoo/addons/sale_blanket_order/views/sale_blanket_order_views.xml +31 -57
- odoo/addons/sale_blanket_order/views/sale_config_settings.xml +11 -19
- odoo/addons/sale_blanket_order/views/sale_order_views.xml +5 -8
- odoo/addons/sale_blanket_order/wizard/create_sale_orders.py +28 -26
- 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.2.0.0.2.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.2.0.0.2.dist-info/METADATA +0 -168
- odoo_addon_sale_blanket_order-16.0.2.0.0.2.dist-info/RECORD +0 -46
- odoo_addon_sale_blanket_order-16.0.2.0.0.2.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,21 @@ 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
|
-
states=READONLY_FIELD_STATES,
|
|
114
96
|
)
|
|
115
97
|
tag_ids = fields.Many2many(
|
|
116
98
|
comodel_name="crm.tag",
|
|
@@ -128,7 +110,9 @@ class BlanketOrder(models.Model):
|
|
|
128
110
|
sale_count = fields.Integer(compute="_compute_sale_count")
|
|
129
111
|
|
|
130
112
|
fiscal_position_id = fields.Many2one(
|
|
131
|
-
"account.fiscal.position",
|
|
113
|
+
"account.fiscal.position",
|
|
114
|
+
string="Fiscal Position",
|
|
115
|
+
check_company=True,
|
|
132
116
|
)
|
|
133
117
|
|
|
134
118
|
amount_untaxed = fields.Monetary(
|
|
@@ -205,7 +189,7 @@ class BlanketOrder(models.Model):
|
|
|
205
189
|
order.state = "expired"
|
|
206
190
|
elif float_is_zero(
|
|
207
191
|
sum(
|
|
208
|
-
order.line_ids.filtered(lambda
|
|
192
|
+
order.line_ids.filtered(lambda line: not line.display_type).mapped(
|
|
209
193
|
"remaining_uom_qty"
|
|
210
194
|
)
|
|
211
195
|
),
|
|
@@ -254,15 +238,15 @@ class BlanketOrder(models.Model):
|
|
|
254
238
|
|
|
255
239
|
if self.partner_id.user_id:
|
|
256
240
|
values["user_id"] = self.partner_id.user_id.id
|
|
257
|
-
if self.partner_id.
|
|
258
|
-
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
|
|
259
243
|
self.update(values)
|
|
260
244
|
|
|
261
245
|
def unlink(self):
|
|
262
246
|
for order in self:
|
|
263
247
|
if order.state not in ("draft", "expired") or order._check_active_orders():
|
|
264
248
|
raise UserError(
|
|
265
|
-
_(
|
|
249
|
+
self.env._(
|
|
266
250
|
"You can not delete an open blanket or "
|
|
267
251
|
"with active sale orders! "
|
|
268
252
|
"Try to cancel it before."
|
|
@@ -274,12 +258,12 @@ class BlanketOrder(models.Model):
|
|
|
274
258
|
try:
|
|
275
259
|
today = fields.Date.today()
|
|
276
260
|
for order in self:
|
|
277
|
-
assert order.validity_date, _("Validity date is mandatory")
|
|
278
|
-
assert order.validity_date > today, _(
|
|
261
|
+
assert order.validity_date, self.env._("Validity date is mandatory")
|
|
262
|
+
assert order.validity_date > today, self.env._(
|
|
279
263
|
"Validity date must be in the future"
|
|
280
264
|
)
|
|
281
|
-
assert order.partner_id, _("Partner is mandatory")
|
|
282
|
-
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")
|
|
283
267
|
order.line_ids._validate()
|
|
284
268
|
except AssertionError as e:
|
|
285
269
|
raise UserError(e) from e
|
|
@@ -310,7 +294,7 @@ class BlanketOrder(models.Model):
|
|
|
310
294
|
for order in self:
|
|
311
295
|
if order._check_active_orders():
|
|
312
296
|
raise UserError(
|
|
313
|
-
_(
|
|
297
|
+
self.env._(
|
|
314
298
|
"You can not delete a blanket order with opened "
|
|
315
299
|
"sale orders! "
|
|
316
300
|
"Try to cancel them before."
|
|
@@ -437,9 +421,9 @@ class BlanketOrderLine(models.Model):
|
|
|
437
421
|
product_uom = fields.Many2one("uom.uom", string="Unit of Measure")
|
|
438
422
|
price_unit = fields.Float(string="Price", digits="Product Price")
|
|
439
423
|
taxes_id = fields.Many2many(
|
|
440
|
-
"account.tax",
|
|
441
|
-
|
|
442
|
-
|
|
424
|
+
comodel_name="account.tax",
|
|
425
|
+
context={"active_test": False},
|
|
426
|
+
check_company=True,
|
|
443
427
|
)
|
|
444
428
|
date_schedule = fields.Date(string="Scheduled Date")
|
|
445
429
|
original_uom_qty = fields.Float(
|
|
@@ -490,115 +474,53 @@ class BlanketOrderLine(models.Model):
|
|
|
490
474
|
default=False,
|
|
491
475
|
help="Technical field for UX purpose.",
|
|
492
476
|
)
|
|
477
|
+
pricelist_item_id = fields.Many2one(
|
|
478
|
+
comodel_name="product.pricelist.item", compute="_compute_pricelist_item_id"
|
|
479
|
+
)
|
|
493
480
|
|
|
494
|
-
|
|
495
|
-
|
|
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):
|
|
496
486
|
if self.env.context.get("from_sale_order"):
|
|
497
487
|
for record in self:
|
|
498
|
-
|
|
488
|
+
name = f"[{record.order_id.name}]"
|
|
499
489
|
if record.date_schedule:
|
|
500
490
|
formatted_date = format_date(record.env, record.date_schedule)
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
491
|
+
name += " - {}: {}".format(
|
|
492
|
+
self.env._("Date Scheduled"), formatted_date
|
|
493
|
+
)
|
|
494
|
+
name += " ({}: {} {})".format(
|
|
495
|
+
self.env._("remaining"),
|
|
504
496
|
record.remaining_uom_qty,
|
|
505
497
|
record.product_uom.name,
|
|
506
498
|
)
|
|
507
|
-
|
|
508
|
-
return result
|
|
509
|
-
return super().name_get()
|
|
510
|
-
|
|
511
|
-
def _get_real_price_currency(self, product, rule_id, qty, uom, pricelist_id):
|
|
512
|
-
"""Retrieve the price before applying the pricelist
|
|
513
|
-
:param obj product: object of current product record
|
|
514
|
-
:param float qty: total quentity of product
|
|
515
|
-
:param tuple price_and_rule: tuple(price, suitable_rule) coming
|
|
516
|
-
from pricelist computation
|
|
517
|
-
:param obj uom: unit of measure of current order line
|
|
518
|
-
:param integer pricelist_id: pricelist id of sale order"""
|
|
519
|
-
# Copied and adapted from the sale module
|
|
520
|
-
PricelistItem = self.env["product.pricelist.item"]
|
|
521
|
-
field_name = "lst_price"
|
|
522
|
-
currency_id = None
|
|
523
|
-
product_currency = None
|
|
524
|
-
if rule_id:
|
|
525
|
-
pricelist_item = PricelistItem.browse(rule_id)
|
|
526
|
-
if pricelist_item.pricelist_id.discount_policy == "without_discount":
|
|
527
|
-
while (
|
|
528
|
-
pricelist_item.base == "pricelist"
|
|
529
|
-
and pricelist_item.base_pricelist_id
|
|
530
|
-
and pricelist_item.base_pricelist_id.discount_policy
|
|
531
|
-
== "without_discount"
|
|
532
|
-
):
|
|
533
|
-
price, rule_id = pricelist_item.base_pricelist_id.with_context(
|
|
534
|
-
uom=uom.id
|
|
535
|
-
)._get_product_price_rule(product, qty, uom)
|
|
536
|
-
pricelist_item = PricelistItem.browse(rule_id)
|
|
537
|
-
|
|
538
|
-
if pricelist_item.base == "standard_price":
|
|
539
|
-
field_name = "standard_price"
|
|
540
|
-
if pricelist_item.base == "pricelist" and pricelist_item.base_pricelist_id:
|
|
541
|
-
field_name = "price"
|
|
542
|
-
product = product.with_context(
|
|
543
|
-
pricelist=pricelist_item.base_pricelist_id.id
|
|
544
|
-
)
|
|
545
|
-
product_currency = pricelist_item.base_pricelist_id.currency_id
|
|
546
|
-
currency_id = pricelist_item.pricelist_id.currency_id
|
|
547
|
-
|
|
548
|
-
product_currency = (
|
|
549
|
-
product_currency
|
|
550
|
-
or (product.company_id and product.company_id.currency_id)
|
|
551
|
-
or self.env.company.currency_id
|
|
552
|
-
)
|
|
553
|
-
if not currency_id:
|
|
554
|
-
currency_id = product_currency
|
|
555
|
-
cur_factor = 1.0
|
|
556
|
-
else:
|
|
557
|
-
if currency_id.id == product_currency.id:
|
|
558
|
-
cur_factor = 1.0
|
|
559
|
-
else:
|
|
560
|
-
cur_factor = currency_id._get_conversion_rate(
|
|
561
|
-
product_currency, currency_id
|
|
562
|
-
)
|
|
563
|
-
|
|
564
|
-
product_uom = product.uom_id.id
|
|
565
|
-
if uom and uom.id != product_uom:
|
|
566
|
-
# the unit price is in a different uom
|
|
567
|
-
uom_factor = uom._compute_price(1.0, product.uom_id)
|
|
499
|
+
record.display_name = name
|
|
568
500
|
else:
|
|
569
|
-
|
|
501
|
+
return super()._compute_display_name()
|
|
570
502
|
|
|
571
|
-
|
|
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
|
|
572
511
|
|
|
573
|
-
def
|
|
512
|
+
def _get_pricelist_price(self):
|
|
574
513
|
# Copied and adapted from the sale module
|
|
575
514
|
self.ensure_one()
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
context_partner = dict(
|
|
584
|
-
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,
|
|
585
522
|
)
|
|
586
|
-
|
|
587
|
-
**context_partner
|
|
588
|
-
)._get_real_price_currency(
|
|
589
|
-
self.product_id,
|
|
590
|
-
rule_id,
|
|
591
|
-
self.original_uom_qty,
|
|
592
|
-
self.product_uom,
|
|
593
|
-
pricelist.id,
|
|
594
|
-
)
|
|
595
|
-
if currency_id != pricelist.currency_id.id:
|
|
596
|
-
currency = self.env["res.currency"].browse(currency_id)
|
|
597
|
-
base_price = currency.with_context(**context_partner).compute(
|
|
598
|
-
base_price, pricelist.currency_id
|
|
599
|
-
)
|
|
600
|
-
# negative discounts (= surcharge) are included in the display price
|
|
601
|
-
return max(base_price, final_price)
|
|
523
|
+
return price
|
|
602
524
|
|
|
603
525
|
@api.onchange("product_id", "original_uom_qty")
|
|
604
526
|
def onchange_product(self):
|
|
@@ -612,23 +534,17 @@ class BlanketOrderLine(models.Model):
|
|
|
612
534
|
if self.order_id.partner_id and float_is_zero(
|
|
613
535
|
self.price_unit, precision_digits=precision
|
|
614
536
|
):
|
|
615
|
-
self.price_unit = self._get_display_price(
|
|
537
|
+
self.price_unit = self._get_display_price()
|
|
616
538
|
if self.product_id.code:
|
|
617
|
-
name = "[{}] {
|
|
539
|
+
name = f"[{name}] {self.product_id.code}"
|
|
618
540
|
if self.product_id.description_sale:
|
|
619
541
|
name += "\n" + self.product_id.description_sale
|
|
620
542
|
self.name = name
|
|
621
543
|
|
|
622
544
|
fpos = self.order_id.fiscal_position_id
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
self.product_id.taxes_id.filtered(
|
|
627
|
-
lambda r: r.company_id.id == company_id
|
|
628
|
-
)
|
|
629
|
-
)
|
|
630
|
-
else:
|
|
631
|
-
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
|
+
)
|
|
632
548
|
|
|
633
549
|
@api.depends(
|
|
634
550
|
"sale_lines.order_id.state",
|
|
@@ -663,12 +579,30 @@ class BlanketOrderLine(models.Model):
|
|
|
663
579
|
line.remaining_uom_qty, line.product_id.uom_id
|
|
664
580
|
)
|
|
665
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
|
+
|
|
666
600
|
def _validate(self):
|
|
667
601
|
try:
|
|
668
602
|
for line in self:
|
|
669
603
|
assert (
|
|
670
604
|
not line.display_type and line.original_uom_qty > 0.0
|
|
671
|
-
) or line.display_type, _("Quantity must be greater than zero")
|
|
605
|
+
) or line.display_type, self.env._("Quantity must be greater than zero")
|
|
672
606
|
except AssertionError as e:
|
|
673
607
|
raise UserError(e) from e
|
|
674
608
|
|
|
@@ -712,7 +646,7 @@ class BlanketOrderLine(models.Model):
|
|
|
712
646
|
lambda line: line.display_type != values.get("display_type")
|
|
713
647
|
):
|
|
714
648
|
raise UserError(
|
|
715
|
-
_(
|
|
649
|
+
self.env._(
|
|
716
650
|
"""
|
|
717
651
|
You cannot change the type of a sale order line.
|
|
718
652
|
Instead you should delete the current line and create a new line
|
|
@@ -720,4 +654,4 @@ class BlanketOrderLine(models.Model):
|
|
|
720
654
|
"""
|
|
721
655
|
)
|
|
722
656
|
)
|
|
723
|
-
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
|
|
|
@@ -13,7 +13,7 @@ class SaleOrder(models.Model):
|
|
|
13
13
|
blanket_order_id = fields.Many2one(
|
|
14
14
|
"sale.blanket.order",
|
|
15
15
|
string="Origin blanket order",
|
|
16
|
-
|
|
16
|
+
related="order_line.blanket_order_line.order_id",
|
|
17
17
|
)
|
|
18
18
|
disable_adding_lines = fields.Boolean(
|
|
19
19
|
compute="_compute_disable_adding_lines",
|
|
@@ -30,7 +30,7 @@ class SaleOrder(models.Model):
|
|
|
30
30
|
for order in self:
|
|
31
31
|
if order._check_exchausted_blanket_order_line():
|
|
32
32
|
raise ValidationError(
|
|
33
|
-
_(
|
|
33
|
+
self.env._(
|
|
34
34
|
"Cannot confirm order %s as one of the lines refers "
|
|
35
35
|
"to a blanket order that has no remaining quantity."
|
|
36
36
|
)
|
|
@@ -44,18 +44,12 @@ class SaleOrder(models.Model):
|
|
|
44
44
|
if line.blanket_order_line:
|
|
45
45
|
if line.blanket_order_line.partner_id != self.partner_id:
|
|
46
46
|
raise ValidationError(
|
|
47
|
-
_(
|
|
47
|
+
self.env._(
|
|
48
48
|
"The customer must be equal to the "
|
|
49
49
|
"blanket order lines customer"
|
|
50
50
|
)
|
|
51
51
|
)
|
|
52
52
|
|
|
53
|
-
@api.depends("order_line.blanket_order_line.order_id")
|
|
54
|
-
def _compute_blanket_order_id(self):
|
|
55
|
-
for order in self:
|
|
56
|
-
blanket_order = order.order_line.mapped("blanket_order_line.order_id")
|
|
57
|
-
order.blanket_order_id = blanket_order[:1]
|
|
58
|
-
|
|
59
53
|
@api.depends("blanket_order_id")
|
|
60
54
|
@api.depends_context("uid")
|
|
61
55
|
def _compute_disable_adding_lines(self):
|
|
@@ -80,14 +74,14 @@ class SaleOrderLine(models.Model):
|
|
|
80
74
|
assigned_bo_line = False
|
|
81
75
|
date_planned = date.today()
|
|
82
76
|
date_delta = timedelta(days=365)
|
|
83
|
-
for line in bo_lines.filtered(lambda
|
|
77
|
+
for line in bo_lines.filtered(lambda bo_line: bo_line.date_schedule):
|
|
84
78
|
date_schedule = line.date_schedule
|
|
85
79
|
if date_schedule and abs(date_schedule - date_planned) < date_delta:
|
|
86
80
|
assigned_bo_line = line
|
|
87
81
|
date_delta = abs(date_schedule - date_planned)
|
|
88
82
|
if assigned_bo_line:
|
|
89
83
|
return assigned_bo_line
|
|
90
|
-
non_date_bo_lines = bo_lines.filtered(lambda
|
|
84
|
+
non_date_bo_lines = bo_lines.filtered(lambda bo_line: not bo_line.date_schedule)
|
|
91
85
|
if non_date_bo_lines:
|
|
92
86
|
return non_date_bo_lines[0]
|
|
93
87
|
|
|
@@ -185,7 +179,7 @@ class SaleOrderLine(models.Model):
|
|
|
185
179
|
and line.product_id != line.blanket_order_line.product_id
|
|
186
180
|
):
|
|
187
181
|
raise ValidationError(
|
|
188
|
-
_(
|
|
182
|
+
self.env._(
|
|
189
183
|
"The product in the blanket order and in the "
|
|
190
184
|
"sales order must match"
|
|
191
185
|
)
|
|
@@ -197,7 +191,7 @@ class SaleOrderLine(models.Model):
|
|
|
197
191
|
if line.blanket_order_line:
|
|
198
192
|
if line.currency_id != line.blanket_order_line.order_id.currency_id:
|
|
199
193
|
raise ValidationError(
|
|
200
|
-
_(
|
|
194
|
+
self.env._(
|
|
201
195
|
"The currency of the blanket order must match with "
|
|
202
196
|
"that of the sale order."
|
|
203
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>
|