first commit
This commit is contained in:
571
modules/sale_gift_card/sale.py
Normal file
571
modules/sale_gift_card/sale.py
Normal file
@@ -0,0 +1,571 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user