from odoo import models, fields, api from datetime import datetime, timedelta from odoo.exceptions import ValidationError class Voucher(models.Model): _name = 'voucher' _rec_name = 'display_name' display_name = fields.Char(string='Display Name', compute='_compute_display_name') name = fields.Char(string='Name') image = fields.Binary(string='Image') code = fields.Char(string='Code', help='Kode voucher yang akan berlaku untuk pengguna') voucher_category = fields.Many2many('product.public.category', string='Category Voucher', help='Kategori Produk yang dapat menggunakan voucher ini') description = fields.Text(string='Description') discount_amount = fields.Float(string='Discount Amount') discount_type = fields.Selection(string='Discount Type', selection=[ ('percentage', 'Percentage'), ('fixed_price', 'Fixed Price'), ], help='Select the type of discount:\n' '- Percentage: Persentase dari total harga.\n' '- Fixed Price: Jumlah tetap yang dikurangi dari harga total.' ) visibility = fields.Selection(string='Visibility', selection=[ ('public', 'Public'), ('private', 'Private') ], help='Select the visibility:\n' '- Public: Ditampilkan kepada seluruh pengguna.\n' '- Private: Tidak ditampilkan kepada seluruh pengguna.' ) start_time = fields.Datetime(string='Start Time') end_time = fields.Datetime(string='End Time') min_purchase_amount = fields.Integer(string='Min. Purchase Amount', help='Nominal minimum untuk dapat menggunakan voucher. Isi 0 jika tidak ada minimum purchase amount') max_discount_amount = fields.Integer(string='Max. Discount Amount', help='Max nominal terhadap persentase diskon') order_ids = fields.One2many('sale.order', 'applied_voucher_id', string='Order') limit = fields.Integer( string='Limit', default=0, help='Batas penggunaan voucher keseluruhan. Isi dengan angka 0 untuk penggunaan tanpa batas' ) limit_user = fields.Integer( string='Limit User', default=0, help='Batas penggunaan voucher per pengguna. Misalnya, jika diisi dengan angka 1, maka setiap pengguna hanya dapat menggunakan voucher ini satu kali. Isi dengan angka 0 untuk penggunaan tanpa batas' ) manufacture_ids = fields.Many2many('x_manufactures', string='Brands', help='Voucher appplied only for brand') excl_pricelist_ids = fields.Many2many('product.pricelist', string='Excluded Pricelists', help='Hide voucher from selected exclude pricelist') voucher_line = fields.One2many('voucher.line', 'voucher_id', 'Voucher Line') terms_conditions = fields.Html('Terms and Conditions') apply_type = fields.Selection(string='Apply Type', default="all", selection=[ ('all', "All product"), ('shipping', "Shipping"), ('brand', "Selected product brand"), ]) count_order = fields.Integer(string='Count Order', compute='_compute_count_order') show_on_email = fields.Selection([ ('user_activation', 'User Activation') ], 'Show on Email') account_type = fields.Selection(string='Account Type', default="all", selection=[ ('all', "All Account"), ('person', "Account Individu"), ('company', "Account Company"), ]) def is_voucher_applicable(self, product_id): if not self.voucher_category: return True public_categories = product_id.public_categ_ids return bool(set(public_categories.ids) & set(self.voucher_category.ids)) def is_voucher_applicable_for_category(self, category): if not self.voucher_category: return True if category.id in self.voucher_category.ids: return True category_path = [] current_cat = category while current_cat: category_path.append(current_cat.id) current_cat = current_cat.parent_id for voucher_cat in self.voucher_category: if voucher_cat.id in category_path: return True return False @api.constrains('description') def _check_description_length(self): for record in self: if record.description and len(record.description) > 120: raise ValidationError('Deskripsi tidak boleh lebih dari 120 karakter') @api.constrains('limit', 'limit_user') def _check_limit(self): for rec in self: if rec.limit_user > rec.limit: raise ValidationError('Limit user tidak boleh lebih besar dari limit keseluruhan') def _compute_display_name(self): for voucher in self: voucher.display_name = f'{voucher.name} ({voucher.code})' def _compute_count_order(self): for rec in self: rec.count_order = len([x for x in rec.order_ids if x.state != 'cancel']) def res_format(self): datas = [voucher.format() for voucher in self] return datas def format(self): ir_attachment = self.env['ir.attachment'] data = { 'id': self.id, 'image': ir_attachment.api_image('voucher', 'image', self.id), 'name': self.name, 'code': self.code, 'apply_type': self.apply_type, 'description': self.description, 'remaining_time': self._res_remaining_time(), } return data def _res_remaining_time(self): seconds = self._get_remaining_time() remaining_time = timedelta(seconds=seconds) hours = remaining_time.seconds // 3600 minutes = remaining_time.seconds // 60 if remaining_time.days > 0: time = remaining_time.days unit = 'hari' elif hours > 0: time = hours unit = 'jam' else: time = minutes unit = 'menit' return f'{time} {unit}' def _get_remaining_time(self): calculate_time = self.end_time - datetime.now() return round(calculate_time.total_seconds()) def filter_order_line(self, order_line): # import logging # _logger = logging.getLogger(__name__) voucher_manufacture_ids = self.collect_manufacture_ids() results = [] if self.voucher_category and len(order_line) > 0: for line in order_line: category_applicable = False for category in line['product_id'].public_categ_ids: if self.is_voucher_applicable_for_category(category): category_applicable = True break if not category_applicable: # _logger.info("Cart contains product %s with non-applicable category - voucher %s cannot be used", # line['product_id'].name, self.code) return [] for line in order_line: manufacture_id = line['product_id'].x_manufacture.id or None if self.apply_type == 'brand' and manufacture_id not in voucher_manufacture_ids: continue product_flashsale = line['product_id']._get_active_flash_sale() if len(product_flashsale) > 0: continue results.append(line) return results def calc_total_order_line(self, order_line): result = {'all': 0, 'brand': {}} for line in order_line: manufacture_id = line['product_id'].x_manufacture.id or None manufacture_total = result['brand'].get(manufacture_id, 0) result['brand'][manufacture_id] = manufacture_total + line['subtotal'] result['all'] += line['subtotal'] return result def calc_discount_amount(self, total): result = {'all': 0, 'brand': {}} if self.apply_type in ['all', 'shipping']: if total['all'] < self.min_purchase_amount: return result if self.discount_type == 'percentage': decimal_discount = self.discount_amount / 100 discount_all = total['all'] * decimal_discount result['all'] = min(discount_all, self.max_discount_amount) if self.max_discount_amount > 0 else discount_all else: result['all'] = min(self.discount_amount, total['all']) return result for line in self.voucher_line: manufacture_id = line.manufacture_id.id total_brand = total['brand'].get(manufacture_id, 0) discount_brand = 0 if total_brand < line.min_purchase_amount: discount_brand = 0 elif line.discount_type == 'percentage': decimal_discount = line.discount_amount / 100 discount_brand = total_brand * decimal_discount discount_brand = min(discount_brand, line.max_discount_amount) if line.max_discount_amount > 0 else discount_brand else: discount_brand = min(line.discount_amount, total_brand) result['brand'][manufacture_id] = round(discount_brand, 2) result['all'] += discount_brand result['all'] = round(result['all'], 2) return result def apply(self, order_line): filtered_order_line = self.filter_order_line(order_line) amount_total = self.calc_total_order_line(filtered_order_line) discount = self.calc_discount_amount(amount_total) return { 'discount': discount, 'total': amount_total, 'type': self.apply_type, 'valid_order': filtered_order_line, } def collect_manufacture_ids(self): return [x.manufacture_id.id for x in self.voucher_line] def calculate_discount(self, price): if price < self.min_purchase_amount: return 0 if self.discount_type == 'fixed_price': return self.discount_amount if self.discount_type == 'percentage': discount = price * self.discount_amount / 100 max_disc = self.max_discount_amount return discount if max_disc == 0 else min(discount, max_disc) return 0 def get_active_voucher(self, domain): current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') domain += [ ('start_time', '<=', current_time), ('end_time', '>=', current_time), ] vouchers = self.search(domain, order='min_purchase_amount ASC') return vouchers def generate_tnc(self): def format_currency(amount): formatted_number = '{:,.0f}'.format(amount).replace(',', '.') return f'Rp{formatted_number}' tnc = [] tnc.append('
    ') tnc.append( '
  1. Voucher hanya berlaku apabila pembelian Pengguna sudah memenuhi syarat dan ketentuan yang tertera pada voucher
  2. ') tnc.append(f'
  3. Voucher berlaku {self._res_remaining_time()} lagi
  4. ') tnc.append(f'
  5. Voucher tidak bisa digunakan apabila terdapat produk flash sale
  6. ') if self.apply_type == 'brand': tnc.append(f'
  7. Voucher berlaku untuk produk dari brand terpilih
  8. ') tnc.append( f'
  9. Nominal potongan produk yang bisa didapatkan hingga 10 Juta dengan minimum pembelian 10 Ribu.
  10. ') elif self.apply_type == 'all' or self.apply_type == 'shipping': if self.voucher_category: category_names = ', '.join([cat.name for cat in self.voucher_category]) tnc.append( f'
  11. Voucher hanya berlaku untuk produk dalam kategori {category_names} dan sub-kategorinya
  12. ') tnc.append( f'
  13. Voucher tidak dapat digunakan jika ada produk di keranjang yang tidak termasuk dalam kategori tersebut
  14. ') if self.discount_type == 'percentage' and self.apply_type != 'brand': tnc.append( f'
  15. Nominal potongan produk yang bisa didapatkan sebesar {self.max_discount_amount}% dengan minimum pembelian {self.min_purchase_amount}
  16. ') elif self.discount_type == 'percentage' and self.apply_type != 'brand': tnc.append( f'
  17. Nominal potongan produk yang bisa didapatkan sebesar {format_currency(self.discount_amount)} dengan minimum pembelian {format_currency(self.min_purchase_amount)}
  18. ') tnc.append('
') # tnc.append(self.generate_detail_tnc()) return ' '.join(tnc) # def generate_detail_tnc(self): # def format_currency(amount): # formatted_number = '{:,.0f}'.format(amount).replace(',', '.') # return f'Rp{formatted_number}' # # tnc = [] # if self.apply_type == 'all': # tnc.append('
  • ') # tnc.append('Nominal potongan yang bisa didapatkan sebesar') # tnc.append(f'{self.discount_amount}%' if self.discount_type == 'percentage' else format_currency( # self.discount_amount)) # # if self.discount_type == 'percentage' and self.max_discount_amount > 0: # tnc.append(f'hingga {format_currency(self.max_discount_amount)}') # # tnc.append( # f'dengan minimum pembelian {format_currency(self.min_purchase_amount)}' if self.min_purchase_amount > 0 else 'tanpa minimum pembelian') # tnc.append('
  • ') # else: # for line in self.voucher_line: # line_tnc = [] # line_tnc.append(f'Nominal potongan produk {line.manufacture_id.x_name} yang bisa didapatkan sebesar') # line_tnc.append(f'{line.discount_amount}%' if line.discount_type == 'percentage' else format_currency( # line.discount_amount)) # # if line.discount_type == 'percentage' and line.max_discount_amount > 0: # line_tnc.append(f'hingga {format_currency(line.max_discount_amount)}') # # line_tnc.append( # f'dengan minimum pembelian {format_currency(line.min_purchase_amount)}' if line.min_purchase_amount > 0 else 'tanpa minimum pembelian') # line_tnc = ' '.join(line_tnc) # tnc.append(f'
  • {line_tnc}
  • ') # return ' '.join(tnc) # copy semua data kalau diduplicate def copy(self, default=None): default = dict(default or {}) voucher_lines = [] for line in self.voucher_line: voucher_lines.append((0, 0, { 'manufacture_id': line.manufacture_id.id, 'discount_amount': line.discount_amount, 'discount_type': line.discount_type, 'min_purchase_amount': line.min_purchase_amount, 'max_discount_amount': line.max_discount_amount, })) default['voucher_line'] = voucher_lines return super(Voucher, self).copy(default)