Files
2026-03-14 09:42:12 +00:00

572 lines
18 KiB
Python

# 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