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.
Files changed (41) hide show
  1. odoo/addons/sale_blanket_order/README.rst +116 -64
  2. odoo/addons/sale_blanket_order/__manifest__.py +8 -3
  3. odoo/addons/sale_blanket_order/data/ir_cron.xml +0 -2
  4. odoo/addons/sale_blanket_order/i18n/de.po +117 -109
  5. odoo/addons/sale_blanket_order/i18n/es.po +91 -91
  6. odoo/addons/sale_blanket_order/i18n/fr.po +99 -97
  7. odoo/addons/sale_blanket_order/i18n/fr_FR.po +91 -91
  8. odoo/addons/sale_blanket_order/i18n/it.po +69 -80
  9. odoo/addons/sale_blanket_order/i18n/pt.po +51 -88
  10. odoo/addons/sale_blanket_order/i18n/sale_blanket_order.pot +26 -67
  11. odoo/addons/sale_blanket_order/models/blanket_orders.py +84 -146
  12. odoo/addons/sale_blanket_order/models/sale_config_settings.py +0 -1
  13. odoo/addons/sale_blanket_order/models/sale_orders.py +20 -7
  14. odoo/addons/sale_blanket_order/readme/CONTEXT.md +5 -0
  15. odoo/addons/sale_blanket_order/readme/CONTRIBUTORS.md +17 -0
  16. odoo/addons/sale_blanket_order/readme/{CREDITS.rst → CREDITS.md} +2 -1
  17. odoo/addons/sale_blanket_order/readme/DESCRIPTION.md +5 -0
  18. odoo/addons/sale_blanket_order/readme/ROADMAP.md +3 -0
  19. odoo/addons/sale_blanket_order/readme/USAGE.md +58 -0
  20. odoo/addons/sale_blanket_order/report/report.xml +0 -2
  21. odoo/addons/sale_blanket_order/report/templates.xml +32 -35
  22. odoo/addons/sale_blanket_order/static/description/index.html +103 -69
  23. odoo/addons/sale_blanket_order/static/src/js/disable_add_order_line.esm.js +22 -0
  24. odoo/addons/sale_blanket_order/tests/test_blanket_orders.py +99 -50
  25. odoo/addons/sale_blanket_order/tests/test_sale_order.py +7 -21
  26. odoo/addons/sale_blanket_order/views/sale_blanket_order_line_views.xml +17 -28
  27. odoo/addons/sale_blanket_order/views/sale_blanket_order_views.xml +36 -58
  28. odoo/addons/sale_blanket_order/views/sale_config_settings.xml +11 -19
  29. odoo/addons/sale_blanket_order/views/sale_order_views.xml +7 -9
  30. odoo/addons/sale_blanket_order/wizard/create_sale_orders.py +58 -41
  31. odoo/addons/sale_blanket_order/wizard/create_sale_orders.xml +12 -2
  32. odoo_addon_sale_blanket_order-18.0.1.2.1.dist-info/METADATA +216 -0
  33. odoo_addon_sale_blanket_order-18.0.1.2.1.dist-info/RECORD +49 -0
  34. {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
  35. odoo_addon_sale_blanket_order-18.0.1.2.1.dist-info/top_level.txt +1 -0
  36. odoo/addons/sale_blanket_order/readme/CONTRIBUTORS.rst +0 -8
  37. odoo/addons/sale_blanket_order/readme/DESCRIPTION.rst +0 -4
  38. odoo/addons/sale_blanket_order/readme/USAGE.rst +0 -53
  39. odoo_addon_sale_blanket_order-16.0.1.0.0.5.dist-info/METADATA +0 -167
  40. odoo_addon_sale_blanket_order-16.0.1.0.0.5.dist-info/RECORD +0 -46
  41. 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 SUPERUSER_ID, _, api, fields, models
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, states=READONLY_FIELD_STATES)
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,
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", string="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 l: not l.display_type).mapped(
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.team_id:
251
- values["team_id"] = self.partner_id.team_id.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
- string="Taxes",
435
- domain=["|", ("active", "=", False), ("active", "=", True)],
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
- def name_get(self):
488
- result = []
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
- res = "[%s]" % record.order_id.name
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
- res += " - {}: {}".format(_("Date Scheduled"), formatted_date)
495
- res += " ({}: {} {})".format(
496
- _("remaining"),
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
- result.append((record.id, res))
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
- uom_factor = 1.0
501
+ return super()._compute_display_name()
563
502
 
564
- return product[field_name] * uom_factor * cur_factor, currency_id.id
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 _get_display_price(self, product):
512
+ def _get_pricelist_price(self):
567
513
  # Copied and adapted from the sale module
568
514
  self.ensure_one()
569
- pricelist = self.order_id.pricelist_id
570
- partner = self.order_id.partner_id
571
- if self.order_id.pricelist_id.discount_policy == "with_discount":
572
- return product.with_context(pricelist=pricelist.id).lst_price
573
- final_price, rule_id = pricelist._get_product_price_rule(
574
- self.product_id, self.original_uom_qty or 1.0, self.product_uom
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
- base_price, currency_id = self.with_context(
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(self.product_id)
537
+ self.price_unit = self._get_display_price()
609
538
  if self.product_id.code:
610
- name = "[{}] {}".format(name, self.product_id.code)
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
- if self.env.uid == SUPERUSER_ID:
617
- company_id = self.env.company.id
618
- self.taxes_id = fpos.map_tax(
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(BlanketOrderLine, self).write(values)
657
+ return super().write(values)
@@ -5,7 +5,6 @@ from odoo import fields, models
5
5
 
6
6
 
7
7
  class SaleConfigSettings(models.TransientModel):
8
-
9
8
  _inherit = "res.config.settings"
10
9
 
11
10
  group_blanket_disable_adding_lines = fields.Boolean(
@@ -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 _, api, fields, models
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 l: l.date_schedule):
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 l: not l.date_schedule)
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 by Camptocamp
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,3 @@
1
+ - Currently, combo products are not supported in blanket orders nor blanket
2
+ order lines, they are treated as regular products. Future versions of the
3
+ module should include support for these types 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
+ ![](../static/description/BO_menu.png)
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
+ ![](../static/description/BO_form.png)
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
+ ![](../static/description/BO_actions.png)
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
+ ![](../static/description/PO_from_BO.png)
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
+ ![](../static/description/BO_lines.png)
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
+ ![](../static/description/PO_BOLine.png)
@@ -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>