diff --git a/modules/sale_promotion_coupon/exceptions.py b/modules/sale_promotion_coupon/exceptions.py
index 246149ad14..104e866c47 100644
--- a/modules/sale_promotion_coupon/exceptions.py
+++ b/modules/sale_promotion_coupon/exceptions.py
@@ -3,9 +3,5 @@
from trytond.model.exceptions import ValidationError
-class DuplicateError(ValidationError):
- pass
-
-
class PromotionCouponNumberDatesError(ValidationError):
pass
diff --git a/modules/sale_promotion_coupon/message.xml b/modules/sale_promotion_coupon/message.xml
index abd5cf22e9..c22f8ba3da 100644
--- a/modules/sale_promotion_coupon/message.xml
+++ b/modules/sale_promotion_coupon/message.xml
@@ -10,7 +10,7 @@ this repository contains the full copyright notices and license terms. -->
The end date of coupons numbered "%(numbers)s" must be before %(start_date)s.
- The coupons numbered "%(numbers)s" are duplicated.
+ The dates of coupon numbers with the same number can not overlap.
diff --git a/modules/sale_promotion_coupon/sale.py b/modules/sale_promotion_coupon/sale.py
index aab7dfe437..0f11322ac7 100644
--- a/modules/sale_promotion_coupon/sale.py
+++ b/modules/sale_promotion_coupon/sale.py
@@ -6,22 +6,29 @@ import datetime as dt
from sql import Literal
from sql.aggregate import Count
from sql.conditionals import Case, Coalesce
+from sql.operators import Equal
from trytond.i18n import gettext
from trytond.model import (
- DeactivableMixin, ModelSQL, ModelView, Workflow, fields)
+ DeactivableMixin, Exclude, ModelSQL, ModelView, Workflow, fields)
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval, If
+from trytond.sql.functions import DateRange
+from trytond.sql.operators import RangeOverlap
from trytond.tools import grouped_slice, reduce_ids
from trytond.transaction import Transaction
-from .exceptions import DuplicateError, PromotionCouponNumberDatesError
+from .exceptions import PromotionCouponNumberDatesError
class Promotion(metaclass=PoolMeta):
__name__ = 'sale.promotion'
- coupons = fields.One2Many('sale.promotion.coupon', 'promotion', "Coupons")
+ coupons = fields.One2Many(
+ 'sale.promotion.coupon', 'promotion', "Coupons",
+ domain=[
+ ('company', '=', Eval('company', -1)),
+ ])
def get_pattern(self, sale):
pattern = super(Promotion, self).get_pattern(sale)
@@ -71,16 +78,49 @@ class PromotionCoupon(ModelSQL, ModelView):
"Per Party", help="Check to count usage per party.")
numbers = fields.One2Many(
'sale.promotion.coupon.number', 'coupon', "Numbers",
+ domain=[
+ ('company', '=', Eval('company', -1)),
+ ],
filter=[
('active', 'in', [True, False]), # Show inactive numbers
])
- promotion = fields.Many2One('sale.promotion', "Promotion", required=True)
+ promotion = fields.Many2One(
+ 'sale.promotion', "Promotion", required=True,
+ domain=[
+ ('company', '=', Eval('company', -1)),
+ ])
+ company = fields.Many2One('company.company', "Company", required=True)
@classmethod
def __setup__(cls):
super().__setup__()
cls.__access__.add('promotion')
+ @classmethod
+ def __register__(cls, module):
+ pool = Pool()
+ Promotion = pool.get('sale.promotion.coupon')
+ table = cls.__table__()
+ promotion = Promotion.__table__()
+ table_h = cls.__table_handler__(module)
+ cursor = Transaction().connection.cursor()
+
+ company_exist = table_h.column_exist('company')
+
+ super().__register__(module)
+
+ # Migration from 7.2: add company
+ if not company_exist:
+ cursor.execute(*table.update(
+ [table.company],
+ [promotion.select(
+ promotion.company,
+ where=promotion.id == table.promotion)]))
+
+ @classmethod
+ def default_company(cls):
+ return Transaction().context.get('company')
+
@classmethod
def default_number_of_use(cls):
return 0
@@ -97,7 +137,11 @@ class PromotionCouponNumber(DeactivableMixin, ModelSQL, ModelView):
number = fields.Char("Number", required=True)
coupon = fields.Many2One(
- 'sale.promotion.coupon', "Coupon", required=True)
+ 'sale.promotion.coupon', "Coupon", required=True,
+ domain=[
+ ('company', '=', Eval('company', -1)),
+ ])
+ company = fields.Many2One('company.company', "Company", required=True)
start_date = fields.Date("Start Date",
domain=['OR',
('start_date', '<=', If(~Eval('end_date', None),
@@ -116,6 +160,15 @@ class PromotionCouponNumber(DeactivableMixin, ModelSQL, ModelView):
@classmethod
def __setup__(cls):
super(PromotionCouponNumber, cls).__setup__()
+ t = cls.__table__()
+ cls._sql_constraints += [
+ ('dates_number_overlap',
+ Exclude(t,
+ (t.company, Equal),
+ (t.number, Equal),
+ (DateRange(t.start_date, t.end_date, '[]'), RangeOverlap)),
+ 'sale_promotion_coupon.msg_duplicate_numbers'),
+ ]
cls.__access__.add('coupon')
cls.active = fields.Function(
cls.active, 'get_active', searcher='search_active')
@@ -133,6 +186,7 @@ class PromotionCouponNumber(DeactivableMixin, ModelSQL, ModelView):
start_date_exists = table_h.column_exist('start_date')
end_date_exists = table_h.column_exist('end_date')
+ company_exist = table_h.column_exist('company')
super().__register__(module)
@@ -156,6 +210,21 @@ class PromotionCouponNumber(DeactivableMixin, ModelSQL, ModelView):
promotion.end_date,
where=coupon.id == table.coupon)]))
+ # Migration from 7.2: add company
+ if not company_exist:
+ cursor.execute(*table.update(
+ [table.company],
+ [coupon
+ .join(promotion,
+ condition=coupon.promotion == promotion.id)
+ .select(
+ promotion.company,
+ where=coupon.id == table.coupon)]))
+
+ @classmethod
+ def default_company(cls):
+ return Transaction().context.get('company')
+
@classmethod
def default_start_date(cls):
return Pool().get('ir.date').today()
@@ -271,61 +340,6 @@ class PromotionCouponNumber(DeactivableMixin, ModelSQL, ModelView):
number=number.rec_name,
end_date=lang.strftime(end_date)))
- @classmethod
- def validate_fields(cls, numbers, field_names):
- super().validate_fields(numbers, field_names)
- cls.check_unique(numbers, field_names)
-
- @classmethod
- def check_unique(cls, numbers, field_names=None):
- if field_names and not (field_names & {
- 'number', 'coupon', 'start_date', 'end_date'}):
- return
- cls.lock()
- duplicates = []
- for sub_numbers in grouped_slice(numbers):
- domain = ['OR']
- for number in sub_numbers:
- start_date = number.start_date or dt.date.min
- end_date = number.end_date or dt.date.max
- domain.append([
- ('number', '=', number.number),
- ('id', '!=', number.id),
- ('coupon.promotion.company', '=',
- number.coupon.promotion.company.id),
- ['OR', [
- ['OR',
- ('start_date', '<=', start_date),
- ('start_date', '=', None),
- ],
- ['OR',
- ('end_date', '>=', end_date),
- ('end_date', '=', None),
- ],
- ], [
- ['OR',
- ('start_date', '<=', end_date),
- ('start_date', '=', None),
- ],
- ['OR',
- ('end_date', '>=', end_date),
- ('end_date', '=', None),
- ],
- ], [
- ('start_date', '>=', number.start_date),
- ('end_date', '<=', number.end_date),
- ],
- ],
- ])
- duplicates.extend(cls.search(domain))
- if duplicates:
- numbers = ', '.join(n.number for n in duplicates[:5])
- if len(duplicates) > 5:
- numbers += '...'
- raise DuplicateError(
- gettext('sale_promotion_coupon.msg_duplicate_numbers',
- numbers=numbers))
-
class Sale(metaclass=PoolMeta):
__name__ = 'sale.sale'
diff --git a/modules/sale_promotion_coupon/sale.xml b/modules/sale_promotion_coupon/sale.xml
index 8ed4bee467..55588c5595 100644
--- a/modules/sale_promotion_coupon/sale.xml
+++ b/modules/sale_promotion_coupon/sale.xml
@@ -20,6 +20,16 @@ this repository contains the full copyright notices and license terms. -->
coupon_list
+
+ User in companies
+ sale.promotion.coupon
+
+
+
+
+
+
+
sale.promotion.coupon.number
form
@@ -31,6 +41,16 @@ this repository contains the full copyright notices and license terms. -->
coupon_number_list
+
+ User in companies
+ sale.promotion.coupon.number
+
+
+
+
+
+
+
sale.sale
diff --git a/modules/sale_promotion_coupon/tests/scenario_sale_promotion_coupon_unique.rst b/modules/sale_promotion_coupon/tests/scenario_sale_promotion_coupon_unique.rst
index 03897eb25d..a41a84f8e4 100644
--- a/modules/sale_promotion_coupon/tests/scenario_sale_promotion_coupon_unique.rst
+++ b/modules/sale_promotion_coupon/tests/scenario_sale_promotion_coupon_unique.rst
@@ -40,7 +40,7 @@ Try to create a second promotion with same coupon number::
>>> promotion2.save()
Traceback (most recent call last):
...
- DuplicateError: ...
+ SQLConstraintError: ...
End the first promotion and start the second after::
diff --git a/modules/sale_promotion_coupon/view/coupon_form.xml b/modules/sale_promotion_coupon/view/coupon_form.xml
index 4bdf45ed98..c002417bf2 100644
--- a/modules/sale_promotion_coupon/view/coupon_form.xml
+++ b/modules/sale_promotion_coupon/view/coupon_form.xml
@@ -2,13 +2,18 @@
diff --git a/modules/sale_promotion_coupon/view/coupon_list.xml b/modules/sale_promotion_coupon/view/coupon_list.xml
index a793418ae1..4d34e385b2 100644
--- a/modules/sale_promotion_coupon/view/coupon_list.xml
+++ b/modules/sale_promotion_coupon/view/coupon_list.xml
@@ -2,6 +2,7 @@
+
diff --git a/modules/sale_promotion_coupon/view/coupon_number_form.xml b/modules/sale_promotion_coupon/view/coupon_number_form.xml
index 7ea4a3ccdb..6a6c5e0e37 100644
--- a/modules/sale_promotion_coupon/view/coupon_number_form.xml
+++ b/modules/sale_promotion_coupon/view/coupon_number_form.xml
@@ -3,7 +3,10 @@
this repository contains the full copyright notices and license terms. -->