# This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. from sql import Null from sql.functions import CharLength from sql.operators import Equal import trytond.config as config from trytond.i18n import gettext from trytond.model import Exclude, ModelSQL, ModelView, Workflow, fields from trytond.modules.company.model import CompanyValueMixin from trytond.modules.currency.fields import Monetary from trytond.pool import Pool, PoolMeta from trytond.pyson import Bool, Eval, Id from trytond.report import Report, get_email from trytond.sendmail import send_message_transactional from trytond.tools.email_ import ( EmailNotValidError, set_from_header, validate_email) from trytond.transaction import Transaction from trytond.wizard import Button, StateTransition, StateView from .exceptions import GiftCardLineValidationError class Configuration(metaclass=PoolMeta): __name__ = 'sale.configuration' gift_card_sequence = fields.MultiValue(fields.Many2One( 'ir.sequence', "Gift Card Sequence", domain=[ ('company', 'in', [ Eval('context', {}).get('company', -1), None]), ('sequence_type', '=', Id( 'sale_gift_card', 'sequence_type_gift_card')), ])) @classmethod def multivalue_model(cls, field): pool = Pool() if field == 'gift_card_sequence': return pool.get('sale.configuration.gift_card.sequence') return super().multivalue_model(field) class ConfigurationGiftCardSequence(ModelSQL, CompanyValueMixin): __name__ = 'sale.configuration.gift_card.sequence' gift_card_sequence = fields.Many2One( 'ir.sequence', "Gift Card Sequence", domain=[ ('company', 'in', [Eval('company', -1), None]), ('sequence_type', '=', Id( 'sale_gift_card', 'sequence_type_gift_card')), ]) class GiftCard(ModelSQL, ModelView): __name__ = 'sale.gift_card' _rec_name = 'number' _states = { 'readonly': Bool(Eval('origin')) | Bool(Eval('spent_on')), } number = fields.Char( "Number", required=True, states=_states) company = fields.Many2One( 'company.company', "Company", required=True, states=_states) product = fields.Many2One( 'product.product', "Product", required=True, domain=[ ('gift_card', '=', True), ], context={ 'company': Eval('company', -1), }, states=_states, depends={'company'}) value = Monetary( "Value", currency='currency', digits='currency', required=True, states=_states) currency = fields.Many2One( 'currency.currency', "Currency", required=True, states=_states) origin = fields.Reference("Origin", selection='get_origin', readonly=True) spent_on = fields.Reference( "Spent On", selection='get_spent_on', readonly=True) del _states @classmethod def __setup__(cls): super().__setup__() t = cls.__table__() cls._sql_constraints = [ ('number_exclude', Exclude(t, (t.number, Equal), (t.company, Equal), where=(t.spent_on == Null)), 'sale_gift_card.msg_gift_card_number_unique'), ] cls._order.insert(0, ('number', 'ASC')) @classmethod def order_number(cls, tables): table, _ = tables[None] return [CharLength(table.number), table.number] @classmethod def default_company(cls): return Transaction().context.get('company') @classmethod def _get_origin(cls): return ['sale.line', 'stock.move'] @classmethod def get_origin(cls): pool = Pool() Model = pool.get('ir.model') return [(None, '')] + [ (m, Model.get_name(m)) for m in cls._get_origin()] @fields.depends('origin', 'value', 'currency') def on_change_origin(self): pool = Pool() SaleLine = pool.get('sale.line') StockMove = pool.get('stock.move') UoM = pool.get('product.uom') if isinstance(self.origin, SaleLine): line = self.origin self.company = line.sale.company self.product = line.product if line.unit and line.unit_price and line.product: self.value = UoM.compute_price( line.unit, line.unit_price, line.product.default_uom) self.currency = line.sale.currency elif isinstance(self.origin, StockMove): move = self.origin self.company = move.company self.product = move.product if move.unit and move.unit_price: self.value = UoM.compute_price( move.unit, move.unit_price, move.product.default_uom) self.currency = move.currency if self.value and self.currency: self.value = self.currency.round(self.value) @classmethod def _get_spent_on(cls): return ['sale.sale'] @classmethod def get_spent_on(cls): pool = Pool() Model = pool.get('ir.model') return [(None, '')] + [ (m, Model.get_name(m)) for m in cls._get_spent_on()] @property def _email(self): pool = Pool() SaleLine = pool.get('sale.line') if isinstance(self.origin, SaleLine): return self.origin.gift_card_email or self.origin.sale.party.email @property def _languages(self): pool = Pool() SaleLine = pool.get('sale.line') languages = [] if isinstance(self.origin, SaleLine): lang = self.origin.sale.party.lang if lang: languages.append(lang) return languages @classmethod def send(cls, gift_cards, from_=None): pool = Pool() Lang = pool.get('ir.lang') from_cfg = config.get('email', 'from') for gift_card in gift_cards: email = gift_card._email if not email: continue languages = gift_card._languages if not languages: languages.append(Lang.get()) msg, title = get_email( 'sale.gift_card.email', gift_card, languages) set_from_header(msg, from_cfg, from_ or from_cfg) msg['To'] = email msg['Subject'] = title send_message_transactional(msg, strict=True) class GiftCardReport(Report): __name__ = 'sale.gift_card' @classmethod def _get_records(cls, ids, model, data): pool = Pool() if model in {'sale.sale', 'sale.point.sale'}: Sale = pool.get(model) sales = Sale.browse(ids) ids = [ g.id for s in sales for line in s.lines for g in line.gift_cards] model = 'sale.gift_card' return super()._get_records(ids, model, data) class GiftCardEmail(Report): __name__ = 'sale.gift_card.email' class GiftCard_POS(metaclass=PoolMeta): __name__ = 'sale.gift_card' @classmethod def _get_origin(cls): return super()._get_origin() + ['sale.point.sale.line'] @fields.depends('origin') def on_change_origin(self): pool = Pool() POSLine = pool.get('sale.point.sale.line') UOM = pool.get('product.uom') if isinstance(self.origin, POSLine): line = self.origin self.company = line.sale.company self.product = line.product if line.unit and line.unit_price and line.product: self.value = UOM.compute_price( line.unit, line.unit_price, line.product.default_uom) self.currency = line.sale.currency super().on_change_origin() @classmethod def _get_spent_on(cls): return super()._get_spent_on() + ['sale.point.sale'] @property def _email(self): pool = Pool() POSLine = pool.get('sale.point.sale.line') email = super()._email if isinstance(self.origin, POSLine): email = self.origin.gift_card_email return email class Sale(metaclass=PoolMeta): __name__ = 'sale.sale' gift_cards = fields.One2Many( 'sale.gift_card', 'spent_on', "Gift Cards", domain=[ ('company', '=', Eval('company', -1)), ('currency', '=', Eval('currency', -1)), ], add_remove=[ ('spent_on', '=', None), ], states={ 'readonly': Eval('state') != 'draft', }) @classmethod @ModelView.button @Workflow.transition('quotation') def quote(cls, sales): for sale in sales: sale.add_return_gift_cards() cls.save(sales) super().quote(sales) def add_return_gift_cards(self): lines = list(self.lines) for line in self.lines: if line.is_gift_card and line.quantity < 0: lines.remove(line) for gift_card in self.gift_cards: lines.append(self.get_return_gift_card_line(gift_card)) self.lines = lines def get_return_gift_card_line(self, gift_card): pool = Pool() Line = pool.get('sale.line') sequence = None if self.lines: last_line = self.lines[-1] if last_line.sequence is not None: sequence = last_line.sequence + 1 return Line( sale=self, sequence=sequence, type='line', product=gift_card.product, quantity=-1, unit=gift_card.product.default_uom, unit_price=gift_card.value, ) @classmethod @ModelView.button def process(cls, sales): pool = Pool() GiftCard = pool.get('sale.gift_card') cls.lock(sales) gift_cards = [] for sale in sales: for line in sale.lines: cards = line.get_gift_cards() if cards: gift_cards.extend(cards) GiftCard.save(gift_cards) GiftCard.send(gift_cards) super().process(sales) class POSSale(metaclass=PoolMeta): __name__ = 'sale.point.sale' gift_cards = fields.One2Many( 'sale.gift_card', 'spent_on', "Gift Cards", domain=[ ('company', '=', Eval('company', -1)), ('currency', '=', Eval('currency', -1)), ], add_remove=[ ('spent_on', '=', None), ], states={ 'readonly': Eval('state') != 'draft', }) @fields.depends('state', 'gift_cards') def on_change_with_total(self, name=None): total = super().on_change_with_total(name=name) if self.state == 'open': total -= sum(c.value for c in self.gift_cards) return total @classmethod @Workflow.transition('done') def do(cls, sales): for sale in sales: sale.add_return_gift_cards() cls.save(sales) super().do(sales) pool = Pool() GiftCard = pool.get('sale.gift_card') cls.lock(sales) gift_cards = [] for sale in sales: for line in sale.lines: cards = line.get_gift_cards() if cards: gift_cards.extend(cards) GiftCard.save(gift_cards) GiftCard.send(gift_cards) # TODO: print gift cards def add_return_gift_cards(self): lines = list(self.lines) for line in self.lines: if line.is_gift_card and line.quantity < 0: lines.remove(line) for gift_card in self.gift_cards: lines.append(self.get_return_gift_card_line(gift_card)) self.lines = lines def get_return_gift_card_line(self, gift_card): pool = Pool() Line = pool.get('sale.point.sale.line') return Line( sale=self, product=gift_card.product, quantity=-1, unit=gift_card.product.default_uom, unit_list_price=gift_card.value, unit_gross_price=gift_card.value, ) class _LineMixin: __slots__ = () gift_cards = fields.One2Many( 'sale.gift_card', 'origin', "Gift Cards", readonly=True, states={ 'invisible': ~Eval('gift_cards', []), }) is_gift_card = fields.Function( fields.Boolean("Is Gift Card"), 'on_change_with_is_gift_card') is_gift_card_service = fields.Function( fields.Boolean("Is Gift Card Service"), 'on_change_with_is_gift_card_service') gift_card_email = fields.Char( "Gift Card Email", states={ 'invisible': ~Eval('is_gift_card_service', False), }, help="Leave empty for the customer email.") @fields.depends('product') def on_change_with_is_gift_card(self, name=None): return self.product and self.product.gift_card @fields.depends('product', methods=['on_change_with_is_gift_card']) def on_change_with_is_gift_card_service(self, name=None): return (self.on_change_with_is_gift_card() and self.product.type == 'service') @classmethod def view_attributes(cls): return super().view_attributes() + [ ('//page[@id="gift_cards"]', 'states', { 'invisible': ~Eval('is_gift_card_service', False), }, ['is_gift_card_service']), ] def get_gift_cards(self): if (not self.is_gift_card_service or self.quantity < 0 or self.gift_cards): return pool = Pool() GiftCard = pool.get('sale.gift_card') UoM = pool.get('product.uom') Config = pool.get('sale.configuration') config = Config(1) cards = [] quantity = UoM.compute_qty( self.unit, self.quantity, self.product.default_uom) quantity -= len(self.gift_cards) quantity = max(quantity, 0) unit_price = UoM.compute_price( self.unit, self.unit_price, self.product.default_uom) unit_price = self.sale.currency.round(unit_price) sequence = config.get_multivalue( 'gift_card_sequence', company=self.sale.company.id) if sequence: numbers = sequence.get_many(int(quantity)) else: numbers = range(int(quantity)) for number in numbers: card = GiftCard() card.company = self.sale.company if sequence: card.number = number card.product = self.product card.value = unit_price card.currency = self.sale.currency card.origin = self cards.append(card) return cards @classmethod def validate_fields(cls, cards, fields_names): super().validate_fields(cards, fields_names) cls.check_valid_email(cards, fields_names) @classmethod def check_valid_email(cls, cards, fields_names=None): if fields_names and 'email' not in fields_names: return for card in cards: if card.gift_card_email: try: validate_email(card.gift_card_email) except EmailNotValidError as e: raise GiftCardLineValidationError(gettext( 'sale_gift_card.msg_gift_card_email_invalid', card=card.rec_name, email=card.gift_card_email), str(e)) from e class Line(_LineMixin, metaclass=PoolMeta): __name__ = 'sale.line' class POSSaleLine(_LineMixin, metaclass=PoolMeta): __name__ = 'sale.point.sale.line' @classmethod def __setup__(cls): super().__setup__() # Prevent selling goods gift card as POS does not manage shipment cls.product.domain = [ cls.product.domain, ['OR', ('gift_card', '!=', True), ('type', '=', 'service'), ], ] class POSPay(metaclass=PoolMeta): __name__ = 'sale.point.sale.pay' gift_card = StateView( 'sale.point.sale.pay.gift_card', 'sale_gift_card.sale_point_sale_pay_gift_card_view_form', [ Button("Cancel", 'end', 'tryton-cancel'), Button("Add", 'add_gift_card', 'tryton-ok', default=True), ]) add_gift_card = StateTransition() @classmethod def __setup__(cls): super().__setup__() cls.payment.buttons.insert(-1, Button( "Gift Card", 'gift_card', states={ 'invisible': Eval('amount', 0) >= 0, }, validate=False)) def default_gift_card(self, fields): return { 'sale': self.record.id, 'amount': -self.record.amount_to_pay, } def transition_add_gift_card(self): self._add_gift_card().save() if self.record.amount_to_pay: return 'payment' else: self.model.process([self.record]) return 'end' def _add_gift_card(self): pool = Pool() Line = pool.get('sale.point.sale.line') return Line( sale=self.record, product=self.gift_card.product, quantity=1, unit=self.gift_card.product.default_uom, unit_list_price=self.gift_card.amount, unit_gross_price=self.gift_card.amount, gift_card_email=self.gift_card.email, ) class POSPayGiftCard(ModelView): __name__ = 'sale.point.sale.pay.gift_card' sale = fields.Many2One('sale.point.sale', "Sale") product = fields.Many2One( 'product.product', "Product", required=True, domain=[ ('salable', '=', True), ('gift_card', '=', True), ]) amount = Monetary( "Amount", currency='currency', digits='currency', required=True) email = fields.Char("Email") currency = fields.Function( fields.Many2One('currency.currency', "Currency"), 'on_change_with_currency') @fields.depends('sale') def on_change_with_currency(self, name=None): if self.sale and self.sale.company: return self.sale.company.currency