# This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import datetime as dt from collections import defaultdict from decimal import Decimal from sql import Null from sql.functions import CharLength from trytond.i18n import gettext from trytond.model import ( ChatMixin, DeactivableMixin, Index, ModelSQL, ModelView, Workflow, fields) from trytond.model.exceptions import AccessError from trytond.modules.company.model import ( employee_field, reset_employee, set_employee) from trytond.modules.currency.fields import Monetary from trytond.modules.product import price_digits from trytond.pool import Pool, PoolMeta from trytond.pyson import Bool, Eval, If from trytond.tools import grouped_slice from trytond.transaction import Transaction from .exceptions import ComplaintSimilarWarning class Type(DeactivableMixin, ModelSQL, ModelView): __name__ = 'sale.complaint.type' name = fields.Char('Name', required=True) origin = fields.Many2One('ir.model', 'Origin', required=True, domain=[('name', 'in', [ 'sale.sale', 'sale.line', 'account.invoice', 'account.invoice.line'])]) class Complaint(Workflow, ModelSQL, ModelView, ChatMixin): __name__ = 'sale.complaint' _rec_name = 'number' _states = { 'readonly': Eval('state') != 'draft', } number = fields.Char("Number", readonly=True) reference = fields.Char("Reference") date = fields.Date('Date', states=_states) customer = fields.Many2One( 'party.party', "Customer", required=True, states=_states, context={ 'company': Eval('company', -1), }, depends={'company'}) company = fields.Many2One( 'company.company', 'Company', required=True, states={ 'readonly': _states['readonly'] | Eval('origin'), }) type = fields.Many2One('sale.complaint.type', 'Type', required=True, states=_states) origin = fields.Reference('Origin', selection='get_origin', domain={ 'sale.sale': [ If(Eval('customer'), ('party', '=', Eval('customer', -1)), ()), ('company', '=', Eval('company', -1)), ('state', 'in', ['confirmed', 'processing', 'done']), ], 'sale.line': [ ('type', '=', 'line'), If(Eval('customer'), ('sale.party', '=', Eval('customer')), ()), ('sale.company', '=', Eval('company')), ('sale.state', 'in', ['confirmed', 'processing', 'done']), ], 'account.invoice': [ If(Eval('customer'), ('party', '=', Eval('customer', -1)), ()), ('company', '=', Eval('company', -1)), ('type', '=', 'out'), ('state', 'in', ['posted', 'paid']), ], 'account.invoice.line': [ ('type', '=', 'line'), If(Eval('customer'), ('invoice.party', '=', Eval('customer')), ()), ('invoice.company', '=', Eval('company')), ('invoice.type', '=', 'out'), ('invoice.state', 'in', ['posted', 'paid']), ], }, states={ 'readonly': ((Eval('state') != 'draft') | Bool(Eval('actions', [0]))), 'required': Bool(Eval('origin_model')), }, depends={'origin_model'}) origin_id = fields.Function(fields.Integer('Origin ID'), 'on_change_with_origin_id') origin_model = fields.Function(fields.Char('Origin Model'), 'on_change_with_origin_model') description = fields.Text('Description', states=_states) actions = fields.One2Many('sale.complaint.action', 'complaint', 'Actions', states={ 'readonly': ((Eval('state') != 'draft') | (If(~Eval('origin_id', 0), 0, Eval('origin_id', 0)) <= 0)), }, depends={'origin_model'}) submitted_by = employee_field( "Submitted By", states=['waiting', 'approved', 'rejected', 'done', 'cancelled']) approved_by = employee_field( "Approved By", states=['approved', 'rejected', 'done', 'cancelled']) rejected_by = employee_field( "Rejected By", states=['approved', 'rejected', 'done', 'cancelled']) cancelled_by = employee_field( "Cancelled By", states=['cancelled']) state = fields.Selection([ ('draft', 'Draft'), ('waiting', 'Waiting'), ('approved', 'Approved'), ('rejected', 'Rejected'), ('done', 'Done'), ('cancelled', 'Cancelled'), ], "State", readonly=True, required=True, sort=False) @classmethod def __setup__(cls): cls.number.search_unaccented = False cls.reference.search_unaccented = False super().__setup__() t = cls.__table__() cls._sql_indexes.update({ Index(t, (t.reference, Index.Similarity())), Index( t, (t.state, Index.Equality(cardinality='low')), where=t.state.in_(['draft', 'waiting', 'approved'])), }) cls._order.insert(0, ('date', 'DESC')) cls._transitions |= set(( ('draft', 'waiting'), ('waiting', 'draft'), ('waiting', 'approved'), ('waiting', 'rejected'), ('approved', 'done'), ('approved', 'draft'), ('draft', 'cancelled'), ('waiting', 'cancelled'), ('done', 'draft'), ('rejected', 'draft'), ('cancelled', 'draft'), )) cls._buttons.update({ 'cancel': { 'invisible': ~Eval('state').in_(['draft', 'waiting']), 'depends': ['state'], }, 'draft': { 'invisible': ~Eval('state').in_( ['waiting', 'done', 'cancelled']), 'icon': If(Eval('state').in_(['done', 'cancelled']), 'tryton-undo', 'tryton-back'), 'depends': ['state'], }, 'wait': { 'invisible': ~Eval('state').in_(['draft']), 'depends': ['state'], }, 'approve': { 'invisible': ~Eval('state').in_(['waiting']), 'depends': ['state'], }, 'reject': { 'invisible': ~Eval('state').in_(['waiting']), 'depends': ['state'], }, 'process': { 'invisible': ~Eval('state').in_(['approved']), 'depends': ['state'], }, }) actions_domains = cls._actions_domains() actions_domain = [('action', 'in', actions_domains.pop(None))] for model, actions in actions_domains.items(): actions_domain = If(Eval('origin_model') == model, [('action', 'in', actions)], actions_domain) cls.actions.domain = [actions_domain] @classmethod def __register__(cls, module_name): table_h = cls.__table_handler__(module_name) # Migration from 6.4: rename employee into submitted_by if (table_h.column_exist('employee') and not table_h.column_exist('submitted_by')): table_h.column_rename('employee', 'submitted_by') super().__register__(module_name) @classmethod def _actions_domains(cls): return { None: [], 'sale.sale': ['sale_return'], 'sale.line': ['sale_return'], 'account.invoice': ['credit_note'], 'account.invoice.line': ['credit_note'], } @classmethod def order_number(cls, tables): table, _ = tables[None] return [ ~((table.state == 'cancelled') & (table.number == Null)), CharLength(table.number), table.number] @staticmethod def default_date(): pool = Pool() Date = pool.get('ir.date') return Date.today() @staticmethod def default_company(): return Transaction().context.get('company') @staticmethod def default_state(): return 'draft' @fields.depends('type') def get_origin(self): if self.type: origin = self.type.origin return [('', ''), (origin.name, origin.name)] else: return [] @fields.depends('origin', 'customer') def on_change_origin(self): pool = Pool() Sale = pool.get('sale.sale') SaleLine = pool.get('sale.line') Invoice = pool.get('account.invoice') InvoiceLine = pool.get('account.invoice.line') if not self.customer and self.origin and self.origin.id >= 0: if isinstance(self.origin, Sale): self.customer = self.origin.party elif isinstance(self.origin, SaleLine): self.customer = self.origin.sale.party elif isinstance(self.origin, Invoice): self.customer = self.origin.party elif isinstance(self.origin, InvoiceLine) and self.origin.invoice: self.customer = self.origin.invoice.party @fields.depends('origin') def on_change_with_origin_id(self, name=None): if self.origin: return self.origin.id @fields.depends('origin') def on_change_with_origin_model(self, name=None): if self.origin: return self.origin.__class__.__name__ @classmethod def view_attributes(cls): return super().view_attributes() + [ ('/tree', 'visual', If(Eval('state') == 'cancelled', 'muted', '')), ] def chat_language(self, audience='internal'): language = super().chat_language(audience=audience) if audience == 'public': language = self.customer.lang.code if self.customer.lang else None return language @classmethod def preprocess_values(cls, mode, values): pool = Pool() Configuration = pool.get('sale.configuration') values = super().preprocess_values(mode, values) if mode == 'create' and not values.get('number'): company_id = values.get('company', cls.default_company()) if company_id is not None: configuration = Configuration(1) if sequence := configuration.get_multivalue( 'complaint_sequence', company=company_id): values['number'] = sequence.get() return values @classmethod def check_modification(cls, mode, complaints, values=None, external=False): super().check_modification( mode, complaints, values=values, external=external) if mode == 'delete': for complaint in complaints: if complaint.state != 'draft': raise AccessError(gettext( 'sale_complaint.msg_complaint_delete_draft', complaint=complaint.rec_name)) @classmethod def copy(cls, complaints, default=None): if default is None: default = {} else: default = default.copy() default.setdefault('number', None) default.setdefault('reference') default.setdefault('submitted_by') default.setdefault('approved_by') default.setdefault('rejected_by') default.setdefault('cancelled_by') return super().copy(complaints, default=default) @classmethod @ModelView.button @Workflow.transition('cancelled') @set_employee('cancelled_by') def cancel(cls, complaints): pass @classmethod @ModelView.button @Workflow.transition('draft') @reset_employee( 'submitted_by', 'approved_by', 'rejected_by', 'cancelled_by') def draft(cls, complaints): pass @classmethod @ModelView.button @Workflow.transition('waiting') @set_employee('submitted_by') def wait(cls, complaints): cls._check_similar(complaints) @classmethod @ModelView.button @Workflow.transition('approved') @set_employee('approved_by') def approve(cls, complaints): pool = Pool() Configuration = pool.get('sale.configuration') transaction = Transaction() context = transaction.context config = Configuration(1) with transaction.set_context( queue_scheduled_at=config.sale_process_after, queue_batch=context.get('queue_batch', True)): cls.__queue__.process(complaints) @classmethod @ModelView.button @Workflow.transition('rejected') @set_employee('rejected_by') def reject(cls, complaints): pass @classmethod @ModelView.button @Workflow.transition('done') def process(cls, complaints): pool = Pool() Action = pool.get('sale.complaint.action') results = defaultdict(list) actions = defaultdict(list) for complaint in complaints: for action in complaint.actions: if action.result: continue result = action.do() results[result.__class__].append(result) actions[result.__class__].append(action) for kls, records in results.items(): kls.save(records) for action, record in zip(actions[kls], records): action.result = record Action.save(sum(list(actions.values()), [])) @classmethod def _check_similar(cls, complaints): pool = Pool() Warning = pool.get('res.user.warning') for sub_complaints in grouped_slice(complaints): sub_complaints = list(sub_complaints) domain = list(filter(None, (c._similar_domain() for c in sub_complaints))) if not domain: continue if cls.search(['OR'] + domain, order=[]): for complaint in sub_complaints: domain = complaint._similar_domain() if not domain: continue try: similar, = cls.search(domain, limit=1) except ValueError: continue warning_key = Warning.format( 'complaint_similar', [complaint]) if Warning.check(warning_key): raise ComplaintSimilarWarning(warning_key, gettext('sale_complaint.msg_complaint_similar', similar=similar.rec_name, complaint=complaint.rec_name)) def _similar_domain(self): pool = Pool() Sale = pool.get('sale.sale') SaleLine = pool.get('sale.line') Invoice = pool.get('account.invoice') InvoiceLine = pool.get('account.invoice.line') domain = ['OR', ('origin', '=', str(self.origin)), ] if isinstance(self.origin, Sale): domain.append(('origin.sale', '=', self.origin.id, 'sale.line')) elif isinstance(self.origin, SaleLine): domain.append(('origin', '=', str(self.origin.sale))) elif isinstance(self.origin, Invoice): domain.append( ('origin.invoice', '=', self.origin.id, 'account.invoice.line')) elif isinstance(self.origin, InvoiceLine): domain.append(('origin', '=', str(self.origin.invoice))) return [ domain, ('id', '!=', self.id), ] class Action(ModelSQL, ModelView): __name__ = 'sale.complaint.action' _states = { 'readonly': ((Eval('complaint_state') != 'draft') | Bool(Eval('result'))), } _line_states = { 'invisible': ~Eval('_parent_complaint', {} ).get('origin_model', 'sale.line').in_( ['sale.line', 'account.invoice.line']), 'readonly': _states['readonly'], } complaint = fields.Many2One( 'sale.complaint', 'Complaint', required=True, ondelete='CASCADE', states=_states) action = fields.Selection([ ('sale_return', 'Create Sale Return'), ('credit_note', 'Create Credit Note'), ], 'Action', states=_states) sale_lines = fields.One2Many( 'sale.complaint.action-sale.line', 'action', "Sale Lines", states={ 'invisible': Eval('_parent_complaint', {} ).get('origin_model', 'sale.sale') != 'sale.sale', 'readonly': _states['readonly'], }, help='Leave empty for all lines.') invoice_lines = fields.One2Many( 'sale.complaint.action-account.invoice.line', 'action', "Invoice Lines", states={ 'invisible': Eval('_parent_complaint', {} ).get('origin_model', 'account.invoice.line' ) != 'account.invoice', 'readonly': _states['readonly'], }, help='Leave empty for all lines.') quantity = fields.Float( "Quantity", digits='unit', states=_line_states, help='Leave empty for the same quantity.') unit = fields.Function(fields.Many2One('product.uom', 'Unit', states=_line_states), 'on_change_with_unit') unit_price = Monetary( "Unit Price", currency='currency', digits=price_digits, states=_line_states, help='Leave empty for the same price.') amount = fields.Function(Monetary( "Amount", 'currency', digits='currency'), 'on_change_with_amount') currency = fields.Function(fields.Many2One( 'currency.currency', "Currency"), 'on_change_with_currency') result = fields.Reference('Result', selection='get_result', readonly=True) complaint_state = fields.Function( fields.Selection('get_complaint_states', "Complaint State"), 'on_change_with_complaint_state') company = fields.Function( fields.Many2One('company.company', "Company"), 'on_change_with_company') @classmethod def __setup__(cls): super().__setup__() cls.__access__.add('complaint') @fields.depends('complaint', '_parent_complaint.origin_model', '_parent_complaint.origin') def on_change_with_unit(self, name=None): if (self.complaint and self.complaint.origin_model in { 'sale.line', 'account.invoice.line'}): return self.complaint.origin.unit @fields.depends( 'quantity', 'unit_price', 'currency', 'sale_lines', 'invoice_lines', 'complaint', '_parent_complaint.origin_model', '_parent_complaint.origin') def on_change_with_amount(self, name=None): if self.complaint: if self.complaint.origin_model in { 'sale.line', 'account.invoice.line'}: if self.quantity is not None: quantity = self.quantity elif (self.complaint.origin_model == 'sale.line' and self.complaint.origin.actual_quantity is not None): quantity = self.complaint.origin.actual_quantity else: quantity = self.complaint.origin.quantity if self.unit_price is not None: unit_price = self.unit_price else: unit_price = self.complaint.origin.unit_price amount = Decimal(str(quantity)) * unit_price if self.currency: amount = self.currency.round(amount) return amount elif self.complaint.origin_model == 'sale.sale': if not self.sale_lines: if self.complaint and self.complaint.origin: sale = self.complaint.origin amount = 0 for line in sale.lines: if line.type != 'line': continue if line.actual_quantity is not None: quantity = line.actual_quantity else: quantity = line.quantity amount += sale.currency.round( Decimal(str(quantity)) * line.unit_price) return amount else: return sum( getattr(l, 'amount', None) or Decimal(0) for l in self.sale_lines) elif self.complaint.origin_model == 'account.invoice': if not self.invoice_lines: if self.complaint and self.complaint.origin: return self.complaint.origin.untaxed_amount else: return sum( getattr(l, 'amount', None) or Decimal(0) for l in self.invoice_lines) @fields.depends( 'complaint', '_parent_complaint.origin_model', '_parent_complaint.origin') def on_change_with_currency(self, name=None): if (self.complaint and self.complaint.origin_model in { 'sale.sale', 'sale.line', 'account.invoice', 'account.invoice.line'}): return self.complaint.origin.currency @classmethod def get_complaint_states(cls): pool = Pool() Complaint = pool.get('sale.complaint') return Complaint.fields_get(['state'])['state']['selection'] @fields.depends('complaint', '_parent_complaint.state') def on_change_with_complaint_state(self, name=None): if self.complaint: return self.complaint.state @fields.depends('complaint', '_parent_complaint.company') def on_change_with_company(self, name=None): if self.complaint: return self.complaint.company @classmethod def _get_result(cls): 'Return list of Model names for result Reference' return ['sale.sale', 'account.invoice'] @classmethod def get_result(cls): pool = Pool() Model = pool.get('ir.model') get_name = Model.get_name models = cls._get_result() return [(None, '')] + [(m, get_name(m)) for m in models] @classmethod def copy(cls, actions, default=None): if default is None: default = {} else: default = default.copy() default.setdefault('result', None) return super().copy(actions, default=default) def do(self): return getattr(self, 'do_%s' % self.action)() def do_sale_return(self): pool = Pool() Sale = pool.get('sale.sale') Line = pool.get('sale.line') if isinstance(self.complaint.origin, (Sale, Line)): default = {} if isinstance(self.complaint.origin, Sale): sale = self.complaint.origin if self.sale_lines: sale_lines = [l.line for l in self.sale_lines] line2qty = { l.line.id: l.get_quantity() for l in self.sale_lines} line2price = { l.line.id: l.get_unit_price() for l in self.sale_lines} default['quantity'] = lambda o: line2qty.get(o['id']) default['unit_price'] = lambda o: line2price.get(o['id']) else: sale_lines = [l for l in sale.lines if l.type == 'line'] default['quantity'] = lambda o: ( o['actual_quantity'] if o['actual_quantity'] is not None else o['quantity']) elif isinstance(self.complaint.origin, Line): sale_line = self.complaint.origin sale = sale_line.sale sale_lines = [sale_line] if self.quantity is not None: default['quantity'] = self.quantity else: default['quantity'] = ( sale_line.actual_quantity if sale_line.actual_quantity is not None else sale_line.quantity) if self.unit_price is not None: default['unit_price'] = self.unit_price return_sale, = Sale.copy([sale], default={'lines': None}) default['sale'] = return_sale.id Line.copy(sale_lines, default=default) else: return return_sale.origin = self.complaint for line in return_sale.lines: if line.type == 'line': line.quantity *= -1 return_sale.lines = return_sale.lines # Force saving return return_sale def do_credit_note(self): pool = Pool() Invoice = pool.get('account.invoice') Line = pool.get('account.invoice.line') if isinstance(self.complaint.origin, (Invoice, Line)): line2qty = line2price = {} if isinstance(self.complaint.origin, Invoice): invoice = self.complaint.origin if self.invoice_lines: invoice_lines = [l.line for l in self.invoice_lines] line2qty = {l.line: l.quantity for l in self.invoice_lines if l.quantity is not None} line2price = {l.line: l.unit_price for l in self.invoice_lines if l.unit_price is not None} else: invoice_lines = [ l for l in invoice.lines if l.type == 'line'] elif isinstance(self.complaint.origin, Line): invoice_line = self.complaint.origin invoice = invoice_line.invoice invoice_lines = [invoice_line] if self.quantity is not None: line2qty = {invoice_line: self.quantity} if self.unit_price is not None: line2price = {invoice_line: self.unit_price} with Transaction().set_context(_account_invoice_correction=True): credit_note, = Invoice.copy([invoice], default={ 'lines': [], 'taxes': [], }) # Copy each line one by one to get negative and positive lines # following each other for invoice_line in invoice_lines: qty = line2qty.get(invoice_line, invoice_line.quantity) unit_price = invoice_line.unit_price - line2price.get( invoice_line, invoice_line.unit_price) Line.copy([invoice_line], default={ 'invoice': credit_note.id, 'quantity': -qty, 'origin': str(self.complaint), }) credit_line, = Line.copy([invoice_line], default={ 'invoice': credit_note.id, 'quantity': qty, 'unit_price': unit_price, 'origin': str(self.complaint), }) credit_note.update_taxes() else: return return credit_note @classmethod def check_modification(cls, mode, actions, values=None, external=False): super().check_modification( mode, actions, values=values, external=external) if mode == 'delete': for action in actions: if action.result: raise AccessError(gettext( 'sale_complaint.msg_action_delete_result', action=action.rec_name)) class _Action_Line: __slots__ = () _states = { 'readonly': ( (Eval('complaint_state') != 'draft') | Bool(Eval('_parent_action.result', True))), } action = fields.Many2One( 'sale.complaint.action', "Action", ondelete='CASCADE', required=True) quantity = fields.Float( "Quantity", digits='unit', states=_states) unit = fields.Function( fields.Many2One('product.uom', "Unit"), 'on_change_with_unit') unit_price = Monetary( "Unit Price", currency='currency', digits=price_digits, states=_states, help='Leave empty for the same price.') amount = fields.Function(Monetary( "Amount", currency='currency', digits='currency'), 'on_change_with_amount') currency = fields.Function(fields.Many2One( 'currency.currency', "Currency"), 'on_change_with_currency') complaint_state = fields.Function( fields.Selection('get_complaint_states', "Complaint State"), 'on_change_with_complaint_state') complaint_origin_id = fields.Function( fields.Integer("Complaint Origin ID"), 'on_change_with_complaint_origin_id') @classmethod def __setup__(cls): super().__setup__() cls.__access__.add('action') def on_change_with_unit(self, name=None): raise NotImplementedError @fields.depends('currency', methods=['get_quantity', 'get_unit_price']) def on_change_with_amount(self, name=None): quantity = self.get_quantity() or 0 unit_price = self.get_unit_price() or Decimal(0) amount = Decimal(str(quantity)) * unit_price if self.currency: amount = self.currency.round(amount) return amount def get_quantity(self): raise NotImplementedError def get_unit_price(self): raise NotImplementedError @fields.depends('action', '_parent_action.currency') def on_change_with_currency(self, name=None): return self.action.currency if self.action else None @classmethod def get_complaint_states(cls): pool = Pool() Complaint = pool.get('sale.complaint') return Complaint.fields_get(['state'])['state']['selection'] @fields.depends('action', '_parent_action.complaint', '_parent_action._parent_complaint.state') def on_change_with_complaint_state(self, name=None): if self.action and self.action.complaint: return self.action.complaint.state @fields.depends('action', '_parent_action.complaint', '_parent_action._parent_complaint.origin_id') def on_change_with_complaint_origin_id(self, name=None): if self.action and self.action.complaint: return self.action.complaint.origin_id class Action_SaleLine(_Action_Line, ModelView, ModelSQL): __name__ = 'sale.complaint.action-sale.line' line = fields.Many2One( 'sale.line', "Sale Line", ondelete='RESTRICT', required=True, domain=[ ('type', '=', 'line'), ('sale', '=', Eval('complaint_origin_id', -1)), ]) @fields.depends('line') def on_change_with_unit(self, name=None): return self.line.unit if self.line else None @fields.depends('quantity', 'line') def get_quantity(self): if self.quantity is not None: return self.quantity elif self.line: if self.line.actual_quantity is not None: return self.line.actual_quantity else: return self.line.quantity @fields.depends('unit_price', 'line') def get_unit_price(self): if self.unit_price is not None: return self.unit_price elif self.line: return self.line.unit_price class Action_InvoiceLine(_Action_Line, ModelView, ModelSQL): __name__ = 'sale.complaint.action-account.invoice.line' line = fields.Many2One( 'account.invoice.line', 'Invoice Line', ondelete='RESTRICT', required=True, domain=[ ('type', '=', 'line'), ('invoice', '=', Eval('complaint_origin_id', -1)), ]) @fields.depends('line') def on_change_with_unit(self, name=None): return self.line.unit if self.line else None @fields.depends('quantity', 'line') def get_quantity(self): if self.quantity is not None: return self.quantity elif self.line: return self.line.quantity @fields.depends('unit_price', 'line') def get_unit_price(self): if self.unit_price is not None: return self.unit_price elif self.line: return self.line.unit_price class Complaint_PromotionCoupon(metaclass=PoolMeta): __name__ = 'sale.complaint' @classmethod def _actions_domains(cls): domains = super()._actions_domains() for name, domain in domains.items(): domain.append('promotion_coupon') return domains class Action_PromotionCoupon(metaclass=PoolMeta): __name__ = 'sale.complaint.action' _promtion_coupon_states = { 'invisible': Eval('action') != 'promotion_coupon', 'required': Eval('action') == 'promotion_coupon', } promotion_coupon = fields.Many2One( 'sale.promotion.coupon', "Promotion Coupon", domain=[ ('company', '=', Eval('company', -1)), ('number_of_use', '=', 1), ('per_party', '=', False), ], states=_promtion_coupon_states) promotion_coupon_number = fields.Char( "Number", states=_promtion_coupon_states) promotion_coupon_duration = fields.TimeDelta( "Duration", states=_promtion_coupon_states) del _promtion_coupon_states @classmethod def __setup__(cls): super().__setup__() cls.action.selection.append(('promotion_coupon', "Promotion Coupon")) @classmethod def default_promotion_coupon_duration(cls): return dt.timedelta(days=90) @classmethod def _get_result(cls): return super()._get_result() + ['sale.promotion.coupon.number'] def do_promotion_coupon(self): pool = Pool() PromotionCouponNumber = pool.get('sale.promotion.coupon.number') Date = pool.get('ir.date') with Transaction().set_context(company=self.company.id): today = Date.today() return PromotionCouponNumber( number=self.promotion_coupon_number, coupon=self.promotion_coupon, company=self.company, start_date=today, end_date=today + self.promotion_coupon_duration, )