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