diff options
| author | Rafi Zadanly <zadanlyr@gmail.com> | 2023-09-26 10:20:59 +0700 |
|---|---|---|
| committer | Rafi Zadanly <zadanlyr@gmail.com> | 2023-09-26 10:20:59 +0700 |
| commit | 25a3271bd14671d57da6b4349745e446e94ebfef (patch) | |
| tree | d5a6554c61a02cd08f41c8068237a926e29874dd /indoteknik_custom/models/promotion | |
| parent | bd8a83a76cea6ef2466c250226f7c95c38b3024c (diff) | |
| parent | 50b5bd7bd984ef108e8bd324440050a222d8262f (diff) | |
Merge branch 'change/feature/promotion-program' into production
# Conflicts:
# indoteknik_custom/models/sale_order.py
Diffstat (limited to 'indoteknik_custom/models/promotion')
9 files changed, 380 insertions, 0 deletions
diff --git a/indoteknik_custom/models/promotion/__init__.py b/indoteknik_custom/models/promotion/__init__.py new file mode 100644 index 00000000..1e15d714 --- /dev/null +++ b/indoteknik_custom/models/promotion/__init__.py @@ -0,0 +1,8 @@ +from . import promotion_program +from . import promotion_program_line +from . import promotion_product +from . import promotion_free_product +from . import sale_order_promotion +from . import sale_order_line +from . import sale_order +from . import promotion_keyword
\ No newline at end of file diff --git a/indoteknik_custom/models/promotion/promotion_free_product.py b/indoteknik_custom/models/promotion/promotion_free_product.py new file mode 100644 index 00000000..c5055562 --- /dev/null +++ b/indoteknik_custom/models/promotion/promotion_free_product.py @@ -0,0 +1,29 @@ +from odoo import fields, models + + +class PromotionFreeProduct(models.Model): + _name = "promotion.free_product" + _rec_name = "product_id" + + product_id = fields.Many2one(comodel_name="product.product", string="Product variant") + qty = fields.Integer(string="Quantity") + program_line_id = fields.Many2one(comodel_name="promotion.program.line", string="Program line") + + def formats(self, purchase_qty=1): + ir_attachment = self.env['ir.attachment'] + + result = [] + for rec in self: + weight = rec.product_id.weight or 0 + result.append({ + 'id': rec.product_id.id, + 'image': ir_attachment.api_image('product.template', 'image_256', rec.product_id.product_tmpl_id.id), + 'display_name': rec.product_id.display_name, + 'name': rec.product_id.name, + 'code': rec.product_id.code, + 'price': rec.product_id.calculate_website_price(), + 'qty': rec.qty * purchase_qty, + 'weight': weight, + 'package_weight': weight * rec.qty + }) + return result
\ No newline at end of file diff --git a/indoteknik_custom/models/promotion/promotion_keyword.py b/indoteknik_custom/models/promotion/promotion_keyword.py new file mode 100644 index 00000000..a4bf90b6 --- /dev/null +++ b/indoteknik_custom/models/promotion/promotion_keyword.py @@ -0,0 +1,8 @@ +from odoo import models, fields + + +class PromotionKeyword(models.Model): + _name = 'promotion.keyword' + + name = fields.Char('Name') + promotion_id = fields.Many2one('promotion.program', 'Program')
\ No newline at end of file diff --git a/indoteknik_custom/models/promotion/promotion_product.py b/indoteknik_custom/models/promotion/promotion_product.py new file mode 100644 index 00000000..2fad0f0d --- /dev/null +++ b/indoteknik_custom/models/promotion/promotion_product.py @@ -0,0 +1,29 @@ +from odoo import fields, models + + +class PromotionProduct(models.Model): + _name = "promotion.product" + _rec_name = "product_id" + + product_id = fields.Many2one(comodel_name="product.product", string="Product variant") + qty = fields.Integer(string="Quantity") + program_line_id = fields.Many2one(comodel_name="promotion.program.line", string="Program line") + + def formats(self, purchase_qty=1): + ir_attachment = self.env['ir.attachment'] + + result = [] + for rec in self: + weight = rec.product_id.weight or 0 + result.append({ + 'id': rec.product_id.id, + 'image': ir_attachment.api_image('product.template', 'image_256', rec.product_id.product_tmpl_id.id), + 'display_name': rec.product_id.display_name, + 'name': rec.product_id.name, + 'code': rec.product_id.code, + 'price': rec.product_id.calculate_website_price(), + 'qty': rec.qty * purchase_qty, + 'weight': weight, + 'package_weight': weight * rec.qty + }) + return result
\ No newline at end of file diff --git a/indoteknik_custom/models/promotion/promotion_program.py b/indoteknik_custom/models/promotion/promotion_program.py new file mode 100644 index 00000000..29aaa753 --- /dev/null +++ b/indoteknik_custom/models/promotion/promotion_program.py @@ -0,0 +1,19 @@ +from odoo import fields, models + + +class PromotionProgram(models.Model): + _name = "promotion.program" + + name = fields.Char(string="Name") + banner = fields.Binary(string="Banner") + icon = fields.Binary(string="Icon", help="Image 1:1 ratio") + icon_top = fields.Binary(string="Icon Top", help="Icon ini ditampilkan sebagai atribut pada atas gambar di product card pada website") + icon_bottom = fields.Binary(string="Icon Bottom", help="Icon ini ditampilkan sebagai atribut pada bawah gambar di product card pada website") + start_time = fields.Datetime(string="Start Time") + end_time = fields.Datetime(string="End Time") + applies_to = fields.Selection(selection=[ + ("all_user", "All User"), + ("login_user", "Login User") + ]) + program_line = fields.One2many('promotion.program.line', 'program_id', 'Program line') + keyword_ids = fields.One2many('promotion.keyword', 'promotion_id', 'Keywords') diff --git a/indoteknik_custom/models/promotion/promotion_program_line.py b/indoteknik_custom/models/promotion/promotion_program_line.py new file mode 100644 index 00000000..34a0fbb2 --- /dev/null +++ b/indoteknik_custom/models/promotion/promotion_program_line.py @@ -0,0 +1,134 @@ +from odoo import models, fields +from datetime import datetime + + +class PromotionProgramLine(models.Model): + _name = 'promotion.program.line' + + program_id = fields.Many2one('promotion.program', 'Program') + name = fields.Char('Name') + promotion_type = fields.Selection([ + ("special_price", "Special Price"), + ("bundling", "Bundling"), + ("discount_loading", "Discount Loading"), + ("merchandise", "Merchandise") + ], 'Type') + image = fields.Binary(string="Image") + + package_limit = fields.Integer('Package limit') + package_limit_user = fields.Integer('Package limit / user') + package_limit_trx = fields.Integer('Package limit / transaction') + + product_ids = fields.One2many('promotion.product', 'program_line_id', 'Product') + free_product_ids = fields.One2many('promotion.free_product', 'program_line_id', 'Free Product') + + price = fields.Float('Price') + discount_type = fields.Selection([ + ("percentage", "Percentage"), + ("fixed", "Fixed") + ], 'Discount Type') + discount_amount = fields.Float('Discount Amount') + + order_promotion_ids = fields.One2many('sale.order.promotion', 'program_line_id', 'Promotions') + + def get_active_promotions(self, product_id): + current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + return self.search([ + ('program_id.start_time', '<=', current_time), + ('program_id.end_time', '>=', current_time), + ('product_ids.product_id', '=', product_id) + ]) + + def _res_limit_qty(self): + return { + 'all': self.package_limit, + 'user': self.package_limit_user, + 'transaction': self.package_limit_trx, + } + + def _get_remaining_qty(self, user): + remaining_qty = { + 'all': self.package_limit, + 'user': self.package_limit_user, + 'transaction': self.package_limit_trx + } + + for promotion in self.order_promotion_ids: + order = promotion.order_id + if order.state != 'cancel': + remaining_qty['all'] -= promotion.quantity + if user and order.partner_id.id == user['partner_id']: + remaining_qty['user'] -= promotion.quantity + + if remaining_qty['all'] < remaining_qty['user']: + remaining_qty['user'] = remaining_qty['all'] + if remaining_qty['user'] < remaining_qty['transaction']: + remaining_qty['transaction'] = remaining_qty['user'] + + return remaining_qty + + def _res_promotion_type(self): + return { + 'value': self.promotion_type, + 'label': dict(self._fields['promotion_type'].selection).get(self.promotion_type) + } + + def _get_remaining_time(self): + calculate_time = self.program_id.end_time - datetime.now() + return round(calculate_time.total_seconds()) + + def formats(self, user): + return [x.format(user) for x in self] + + def format(self, user = None, qty = 1): + ir_attachment = self.env['ir.attachment'] + limit_qty = self._res_limit_qty() + remaining_qty = self._get_remaining_qty(user) + + percent_remaining = 0 + if limit_qty['all'] > 0: + percent_remaining = (limit_qty['all'] - remaining_qty['all']) / limit_qty['all'] * 100 + + products = self.product_ids.formats(purchase_qty=qty) + free_products = self.free_product_ids.formats(purchase_qty=qty) + + merged_products = products + free_products + weight = 0 + if not any(x['package_weight'] == 0 for x in merged_products): + weight = sum(x['package_weight'] for x in merged_products) + + response = { + 'id': self.id, + 'name': self.name, + 'image': ir_attachment.api_image(self._name, 'image', self.id), + 'remaining_time': self._get_remaining_time(), + 'promotion_type': self._res_promotion_type(), + 'limit_qty': limit_qty, + 'remaining_qty': remaining_qty, + 'used_percentage': percent_remaining, + 'price': { + 'price': self.price, + 'price_discount': self.price, + 'discount_percentage': 0 + }, + 'products': products, + 'free_products': free_products, + 'weight': weight + } + + if self.promotion_type == 'special_price': + response['price'] = self._calc_special_price_price(products[0]) + + return response + + def _calc_special_price_price(self, product): + result = product['price'] + price = result['price'] + if self.discount_type == 'percentage': + result['discount_percentage'] = self.discount_amount + result['price_discount'] = price * (1 - self.discount_amount / 100) + elif self.discount_type == 'fixed': + final_price = price - self.discount_amount + result['price_discount'] = final_price + result['discount_percentage'] = (price - final_price) / price * 100 + return result
\ No newline at end of file diff --git a/indoteknik_custom/models/promotion/sale_order.py b/indoteknik_custom/models/promotion/sale_order.py new file mode 100644 index 00000000..42e972d0 --- /dev/null +++ b/indoteknik_custom/models/promotion/sale_order.py @@ -0,0 +1,130 @@ +from odoo import fields, models + + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + order_promotion_ids = fields.One2many('sale.order.promotion', 'order_id', 'Promotions') + + def apply_promotion_program(self): + userdata = { + 'user_id': self.partner_id.user_id.id, + 'partner_id': self.partner_id.id + } + + order_lines = [] + iu_products = [] # Internal Use products + for line in self.order_promotion_ids: + promotion = line.program_line_id.format(user=userdata, qty=line.quantity) + + products = promotion['products'] + free_products = promotion['free_products'] + all_products = products.copy() + + if promotion['promotion_type']['value'] == 'merchandise': + iu_products += filter(lambda x: x['qty'] > 0, free_products) + else: + all_products = self._merge_promotion_products(promotion) + + promotion_price = promotion['price']['price_discount'] + promotion_amt_total = sum(product['price']['price'] * product['qty'] for product in all_products) + + promotion_used_price = 0 + for index, product in enumerate(all_products): + order_line_default = { + 'company_id': 1, + 'order_id': self.id, + 'order_promotion_id': line.id, + 'product_id': product['id'], + 'product_uom_qty': product['qty'], + } + if promotion['promotion_type']['value'] == 'special_price': + order_lines.append({ + **order_line_default, + 'price_unit': product['price']['price'], + 'discount': product['price']['discount_percentage'] + }) + continue + + product_price = product['price']['price'] + contrib_decimal = product_price * product['qty'] / promotion_amt_total + + if index < len(all_products) - 1: + contrib_price = contrib_decimal * promotion_price + else: + contrib_price = promotion_price - promotion_used_price + + contrib_price_unit = contrib_price / product['qty'] + contrib_disc_unit = round((product_price - contrib_price_unit) / product_price * 100, 2) + + product_subtotal = round((100 - contrib_disc_unit) / 100 * product_price, 2) * product['qty'] + promotion_used_price += product_subtotal + + order_lines.append({ + **order_line_default, + 'price_unit': product_price, + 'discount': contrib_disc_unit + }) + + self.env['sale.order.line'].create(order_lines) + self._create_promotion_program_iu_docs(iu_products) + + def _merge_promotion_products(self, promotion): + """ + Merges the products and free products from a promotion into a single dictionary. + + Parameters: + promotion (dict): A dictionary representing a promotion with 'products' and 'free_products' keys. + + Returns: + list: A list containing the merged products from the promotion. + """ + merged_products = {} + + for product in promotion['products'] + promotion['free_products']: + product_id = product['id'] + product_qty = product['qty'] + if product_id in merged_products: + merged_products[product_id]['qty'] += product_qty + else: + merged_products[product_id] = product + + return list(merged_products.values()) + + def _create_promotion_program_iu_docs(self, products): + if len(products) == 0: + return False + + default = { + 'picking_type_id': 33, # PT Indoteknik (Bandengan): Internal Transfers + 'location_id': 57, # BU/Stock + 'location_dest_id': 49, # Virtual Locations/Internal Use + 'account_id': 596, # Biaya awareness + 'product_uom': 1 # Unit + } + + picking_type = self.env['stock.picking.type'].browse(default['picking_type_id']) + picking = self.env['stock.picking'].create({ + 'name': picking_type.sequence_id.next_by_id(), + 'picking_type_id': default['picking_type_id'], + 'partner_id': self.partner_id.id, + 'real_shipping_id': self.real_shipping_id.id, + 'location_id': default['location_id'], + 'location_dest_id': default['location_dest_id'], + 'account_id': default['account_id'], + 'origin': self.display_name, + 'is_internal_use': True + }) + + for product in products: + picking.move_ids_without_package.create({ + 'product_id': product['id'], + 'name': product['display_name'], + 'product_uom_qty': product['qty'], + 'product_uom': default['product_uom'], + 'location_id': default['location_id'], + 'location_dest_id': default['location_dest_id'], + 'picking_id': picking.id + }) + + self.picking_iu_id = picking.id diff --git a/indoteknik_custom/models/promotion/sale_order_line.py b/indoteknik_custom/models/promotion/sale_order_line.py new file mode 100644 index 00000000..ba3f55f5 --- /dev/null +++ b/indoteknik_custom/models/promotion/sale_order_line.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + order_promotion_id = fields.Many2one('sale.order.promotion', 'Order Promotion')
\ No newline at end of file diff --git a/indoteknik_custom/models/promotion/sale_order_promotion.py b/indoteknik_custom/models/promotion/sale_order_promotion.py new file mode 100644 index 00000000..a3cd8d01 --- /dev/null +++ b/indoteknik_custom/models/promotion/sale_order_promotion.py @@ -0,0 +1,16 @@ +from odoo import models, fields + + +class SaleOrderPromotion(models.Model): + _name = 'sale.order.promotion' + _rec_name = "program_line_id" + + order_id = fields.Many2one('sale.order', 'Order') + order_line_ids = fields.One2many('sale.order.line', 'order_promotion_id', 'Order Line') + program_line_id = fields.Many2one('promotion.program.line', 'Program Line') + quantity = fields.Integer('Quantity') + is_applied = fields.Boolean('Is Applied', compute='_compute_is_applied') + + def _compute_is_applied(self): + for rec in self: + rec.is_applied = len(rec.order_line_ids) > 0 # True if order line ids greater than 0 |
