From adbc9b985f1c5fb2b2f41f79c686b3a573003e62 Mon Sep 17 00:00:00 2001 From: Rafi Zadanly Date: Mon, 18 Sep 2023 16:20:15 +0700 Subject: Update promotion program feature --- indoteknik_api/controllers/api_v1/sale_order.py | 39 ++++++---------- indoteknik_custom/models/promotion_program_line.py | 53 ++++++++++++++------- indoteknik_custom/models/sale_order.py | 54 ++++++++++++++++++++++ indoteknik_custom/models/website_user_cart.py | 14 +++--- indoteknik_custom/views/sale_order.xml | 1 + 5 files changed, 113 insertions(+), 48 deletions(-) diff --git a/indoteknik_api/controllers/api_v1/sale_order.py b/indoteknik_api/controllers/api_v1/sale_order.py index adc89f66..2c6958d8 100644 --- a/indoteknik_api/controllers/api_v1/sale_order.py +++ b/indoteknik_api/controllers/api_v1/sale_order.py @@ -302,43 +302,32 @@ class SaleOrder(controller.Controller): source = params['value']['source'] products = user_cart.get_product_by_user(user_id=user_id, selected=True, source=source) for product in products: - total_qty = product['quantity'] price_unit = product['price']['price'] price_discount =product['price']['discount_percentage'] - if product['program'] and product['program']['type']['value'] != 'special_price': - total_qty += sum(x['quantity'] for x in product['program']['items']) - price_unit = product['subtotal'] / total_qty - price_discount = 0 - - param = { + order_line = { 'company_id': 1, 'order_id': sale_order.id, 'price_unit': price_unit, - 'discount': price_discount - } - - primary_product = { - **param, + 'discount': price_discount, 'product_id': product['id'], 'product_uom_qty': product['quantity'] } + if product['program']: - primary_product.update({ + order_line.update({ 'program_line_id': product['program']['id'] }) - parameters.append(primary_product) - - if not product['program']: - continue - - for item in product['program']['items']: - parameters.append({ - **param, - 'product_id': item['id'], - 'product_uom_qty': item['quantity'], - }) + + parameters.append(order_line) + # if product['program'] and product['program']['type']['value'] != 'special_price': + # total_qty += sum(x['quantity'] for x in product['program']['items']) + # price_unit = product['subtotal'] / total_qty + # price_discount = 0 request.env['sale.order.line'].create(parameters) + + if any(x['program'] for x in products): + sale_order.apply_promotion_program() voucher_code = params['value']['voucher'] voucher = request.env['voucher'].search([('code', '=', voucher_code)]) @@ -347,7 +336,7 @@ class SaleOrder(controller.Controller): sale_order.apply_voucher() cart_ids = [x['cart_id'] for x in products] - user_cart.browse(cart_ids).unlink() + # user_cart.browse(cart_ids).unlink() return self.response({ 'id': sale_order.id, 'name': sale_order.name diff --git a/indoteknik_custom/models/promotion_program_line.py b/indoteknik_custom/models/promotion_program_line.py index 077f7e12..71418d6c 100644 --- a/indoteknik_custom/models/promotion_program_line.py +++ b/indoteknik_custom/models/promotion_program_line.py @@ -1,5 +1,6 @@ from odoo import fields, models, api from datetime import datetime +import math class PromotionProgramLine(models.Model): @@ -14,15 +15,21 @@ class PromotionProgramLine(models.Model): ("fixed_price", "Fixed Price"), ], string="Discount Type") discount_amount = fields.Float(string="Discount Amount") - promotion_type = fields.Selection(selection=[ - ("special_price", "Special Price"), - ("bundling", "Bundling"), - ("discount_loading", "Discount Loading"), - ("merchandise", "Merchandise") - ], string="Promotion Type") - minimum_purchase_qty = fields.Integer(string="Minimum Purchase Qty", help="Minimum Qty to applied discount loading") - applies_multiply = fields.Boolean(string="Applies Multiply", help="Is applies multiply") - limit_qty = fields.Integer(string="Limit Qty", help="Limit Qty product in promotion") + promotion_type = fields.Selection(string="Promotion Type", + selection=[ + ("special_price", "Special Price"), + ("bundling", "Bundling"), + ("discount_loading", "Discount Loading"), + ("merchandise", "Merchandise") + ], + help='- Special Price: Potongan harga barang.\n' + '- Bundling: Menggabungkan beberapa produk menjadi satu paket.\n' + '- Discount Loading: Semakin banyak unit yang dibeli maka semakin murah harga yang dibayar.\n' + '- Merchandise: Pemberian barang gratis.' + ) + minimum_purchase_qty = fields.Integer(string="Minimum Purchase Qty", help="Minimum unit barang yang perlu dibeli untuk dapat menggunakan promosi") + applies_multiply = fields.Boolean(string="Applies Multiply", help="Diterapkan berlipat ganda") + limit_qty = fields.Integer(string="Limit Qty", help="Kuota promosi keseluruhan") limit_qty_user = fields.Integer(string="Limit Qty / User", help="Limit Qty per User") limit_qty_transaction = fields.Integer(string="Limit Qty / Transaction", help="Limit Qty per Transaction") line_free_item = fields.One2many(comodel_name="promotion.program.free_item", inverse_name="line_id", string="Line Free Item") @@ -118,7 +125,7 @@ class PromotionProgramLine(models.Model): 'label': dict(self._fields['promotion_type'].selection).get(self.promotion_type) } - def format(self, user = None): + def format(self, user = None, qty = 0): ir_attachment = self.env['ir.attachment'] product_price = self.product_id.calculate_website_price() limit_qty = self._res_limit_qty() @@ -126,7 +133,19 @@ class PromotionProgramLine(models.Model): percent_remaining = 0 if limit_qty['all'] > 0: percent_remaining = (limit_qty['all'] - remaining_qty['all']) / limit_qty['all'] * 100 - return { + + qty_can_buy = min(remaining_qty['user'], qty) + if self.limit_qty_transaction > 0: + qty_can_buy = min(qty_can_buy, self.limit_qty_transaction) + + multiplier = 0 + if qty_can_buy > self.minimum_purchase_qty: + multiplier = 1 + + if self.applies_multiply and qty_can_buy > 0: + multiplier = math.floor(qty_can_buy / self.minimum_purchase_qty) + + response = { 'id': self.id, 'name': self.name, 'image': ir_attachment.api_image('promotion.program.line', 'image', self.id), @@ -137,14 +156,16 @@ class PromotionProgramLine(models.Model): 'limit_qty': limit_qty, 'remaining_qty': remaining_qty, 'used_percentage': percent_remaining, - 'price': self.calculate_price(price=product_price) + 'price': self.calculate_price(price=product_price), + 'items': [{ + 'product_id': line.product_id.id, + 'quantity': line.qty * multiplier + } for line in self.line_free_item] } + + return response def res_format(self, user): data = [x.format(user) for x in self] return data - - def res_format_cart(self, user): - data = self.format(user) - return data diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 8317e1fd..2510d9e5 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -78,6 +78,7 @@ class SaleOrder(models.Model): amount_voucher_disc = fields.Float(string='Voucher Discount') source_id = fields.Many2one('utm.source', 'Source', domain="[('id', 'in', [32, 59, 60, 61])]") estimated_arrival_days = fields.Integer('Estimated Arrival Days', default=0) + picking_iu_id = fields.Many2one('stock.picking', 'Picking IU') def _compute_purchase_total(self): for order in self: @@ -447,6 +448,59 @@ class SaleOrder(models.Model): line.discount = line.initial_discount line.initial_discount = False + def apply_promotion_program(self): + userdata = { + 'user_id': self.partner_id.user_id.id, + 'partner_id': self.partner_id.id + } + + iu_items = [] + for line in self.order_line: + promotion = line.program_line_id.format(user=userdata, qty=line.product_uom_qty) + if promotion['type']['value'] == 'merchandise': + iu_items += filter(lambda x: x['quantity'] > 0, promotion['items']) + + if len(iu_items) > 0: + self._create_promotion_program_iu_docs(iu_items) + + def _create_promotion_program_iu_docs(self, items): + 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 + }) + + product_model = self.env['product.product'] + for item in items: + picking.move_ids_without_package.create({ + 'product_id': item['product_id'], + 'name': product_model.browse(item['product_id']).display_name, + 'product_uom_qty': item['quantity'], + '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/website_user_cart.py b/indoteknik_custom/models/website_user_cart.py index b3695ba1..1468e9dc 100644 --- a/indoteknik_custom/models/website_user_cart.py +++ b/indoteknik_custom/models/website_user_cart.py @@ -22,10 +22,6 @@ class WebsiteUserCart(models.Model): record.user_other_carts = others def get_product(self): - user_data = { - 'partner_id': self.user_id.partner_id.id, - 'user_id': self.user_id.id - } product = self.product_id.v2_api_single_response(self.product_id) product['cart_id'] = self.id product['quantity'] = self.qty @@ -33,10 +29,14 @@ class WebsiteUserCart(models.Model): product['selected'] = self.is_selected product['program'] = None product['can_buy'] = True - product_flashsale = self.product_id._get_active_flash_sale() - product['has_flashsale'] = True if len(product_flashsale) > 0 else False + flashsales = self.product_id._get_active_flash_sale() + product['has_flashsale'] = True if len(flashsales) > 0 else False if self.program_line_id: - product['program'] = self.program_line_id.res_format_cart(user=user_data, quantity=self.qty) + userdata = { + 'partner_id': self.user_id.partner_id.id, + 'user_id': self.user_id.id + } + product['program'] = self.program_line_id.format(user=userdata, qty=self.qty) if product['program']: if self.qty < product['program']['minimum_purchase_qty'] or self.qty > product['program']['remaining_qty']['transaction']: diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml index f2cab699..3ea65498 100755 --- a/indoteknik_custom/views/sale_order.xml +++ b/indoteknik_custom/views/sale_order.xml @@ -109,6 +109,7 @@ + -- cgit v1.2.3 From dae4a3bf266ba4c19b1ba1d11c52ed9e19259b7c Mon Sep 17 00:00:00 2001 From: Rafi Zadanly Date: Sat, 23 Sep 2023 11:50:20 +0700 Subject: Update promotion program scheme --- indoteknik_api/controllers/api_v1/cart.py | 21 ++- .../controllers/api_v1/product_variant.py | 4 +- indoteknik_api/controllers/api_v1/sale_order.py | 51 +++--- indoteknik_custom/__manifest__.py | 6 +- indoteknik_custom/models/__init__.py | 5 +- indoteknik_custom/models/promotion/__init__.py | 7 + .../models/promotion/promotion_free_product.py | 29 ++++ .../models/promotion/promotion_product.py | 29 ++++ .../models/promotion/promotion_program.py | 18 +++ .../models/promotion/promotion_program_line.py | 114 ++++++++++++++ indoteknik_custom/models/promotion/sale_order.py | 110 +++++++++++++ .../models/promotion/sale_order_line.py | 7 + .../models/promotion/sale_order_promotion.py | 15 ++ indoteknik_custom/models/promotion_program.py | 19 --- .../models/promotion_program_free_item.py | 10 -- .../models/promotion_program_keyword.py | 8 - indoteknik_custom/models/promotion_program_line.py | 171 --------------------- indoteknik_custom/models/sale_order.py | 52 ------- indoteknik_custom/models/sale_order_line.py | 1 - indoteknik_custom/models/website_user_cart.py | 41 ++--- indoteknik_custom/security/ir.model.access.csv | 5 +- .../views/promotion/promotion_program.xml | 72 +++++++++ .../promotion/promotion_program_free_item.xml | 44 ++++++ .../views/promotion/promotion_program_keyword.xml | 44 ++++++ .../views/promotion/promotion_program_line.xml | 72 +++++++++ indoteknik_custom/views/promotion_program.xml | 73 --------- .../views/promotion_program_free_item.xml | 44 ------ .../views/promotion_program_keyword.xml | 44 ------ indoteknik_custom/views/promotion_program_line.xml | 98 ------------ indoteknik_custom/views/sale_order.xml | 17 +- indoteknik_custom/views/website_user_cart.xml | 12 +- 31 files changed, 653 insertions(+), 590 deletions(-) create mode 100644 indoteknik_custom/models/promotion/__init__.py create mode 100644 indoteknik_custom/models/promotion/promotion_free_product.py create mode 100644 indoteknik_custom/models/promotion/promotion_product.py create mode 100644 indoteknik_custom/models/promotion/promotion_program.py create mode 100644 indoteknik_custom/models/promotion/promotion_program_line.py create mode 100644 indoteknik_custom/models/promotion/sale_order.py create mode 100644 indoteknik_custom/models/promotion/sale_order_line.py create mode 100644 indoteknik_custom/models/promotion/sale_order_promotion.py delete mode 100644 indoteknik_custom/models/promotion_program.py delete mode 100644 indoteknik_custom/models/promotion_program_free_item.py delete mode 100644 indoteknik_custom/models/promotion_program_keyword.py delete mode 100644 indoteknik_custom/models/promotion_program_line.py create mode 100644 indoteknik_custom/views/promotion/promotion_program.xml create mode 100644 indoteknik_custom/views/promotion/promotion_program_free_item.xml create mode 100644 indoteknik_custom/views/promotion/promotion_program_keyword.xml create mode 100644 indoteknik_custom/views/promotion/promotion_program_line.xml delete mode 100644 indoteknik_custom/views/promotion_program.xml delete mode 100644 indoteknik_custom/views/promotion_program_free_item.xml delete mode 100644 indoteknik_custom/views/promotion_program_keyword.xml delete mode 100644 indoteknik_custom/views/promotion_program_line.xml diff --git a/indoteknik_api/controllers/api_v1/cart.py b/indoteknik_api/controllers/api_v1/cart.py index 6faac27f..2243ec0f 100644 --- a/indoteknik_api/controllers/api_v1/cart.py +++ b/indoteknik_api/controllers/api_v1/cart.py @@ -31,7 +31,7 @@ class Cart(controller.Controller): return self.response(carts) @http.route(PREFIX_USER + 'cart/create-or-update', auth='public', methods=['POST', 'OPTIONS'], csrf=False) - @controller.Controller.must_authorized() + @controller.Controller.must_authorized(private=True, private_key='user_id') def create_or_update_cart(self, user_id, **kw): # Convert input values to appropriate types user_id = int(user_id) @@ -54,19 +54,29 @@ class Cart(controller.Controller): user_query = ('user_id', '=', user_id) website_user_cart.search([user_query, ('source', '=', 'buy')]).unlink() - # Prepare query to find existing cart entry for the product - query = [user_query, ('product_id', '=', product_id), ('source', '=', 'add_to_cart')] + # Prepare query to find existing cart entry + query = [user_query, ('source', '=', 'add_to_cart')] + if product_id: + query.append(('product_id', '=', product_id)) + elif program_line_id: + query.append(('program_line_id', '=', program_line_id)) + cart = website_user_cart.search(query, limit=1) - result = {} + data_to_update = { 'qty': qty, 'is_selected': is_selected, - 'program_line_id': program_line_id + 'program_line_id': program_line_id, + 'product_id': product_id } + if program_line_id: + data_to_update['product_id'] = False + if source: data_to_update['source'] = source + result = {} if cart and source in (None, 'add_to_cart'): # Update existing cart entry cart.write(data_to_update) @@ -75,7 +85,6 @@ class Cart(controller.Controller): # Create a new cart entry if it doesn't exist create = website_user_cart.create({ 'user_id': user_id, - 'product_id': product_id, **data_to_update }) result['id'] = create.id diff --git a/indoteknik_api/controllers/api_v1/product_variant.py b/indoteknik_api/controllers/api_v1/product_variant.py index 8de4669e..fce2edf0 100644 --- a/indoteknik_api/controllers/api_v1/product_variant.py +++ b/indoteknik_api/controllers/api_v1/product_variant.py @@ -21,7 +21,7 @@ class ProductVariant(controller.Controller): return self.response(data) - @http.route(prefix + 'product_variant//promotions', auth='public', methods=['GET', 'OPTIONS']) + @http.route(prefix + 'product-variant//promotions', auth='public', methods=['GET', 'OPTIONS']) @controller.Controller.must_authorized() def get_product_variant_promotions(self, product_id): product_id = int(product_id) @@ -29,6 +29,6 @@ class ProductVariant(controller.Controller): program_line = request.env['promotion.program.line'] program_lines = program_line.get_active_promotions(product_id) - program_lines = program_lines.res_format(user=user_data) + program_lines = program_lines.formats(user=user_data) return self.response(program_lines) diff --git a/indoteknik_api/controllers/api_v1/sale_order.py b/indoteknik_api/controllers/api_v1/sale_order.py index 2c6958d8..ef4c2688 100644 --- a/indoteknik_api/controllers/api_v1/sale_order.py +++ b/indoteknik_api/controllers/api_v1/sale_order.py @@ -295,47 +295,44 @@ class SaleOrder(controller.Controller): if params['value']['type'] == 'sale_order': parameters['approval_status'] = 'pengajuan1' sale_order = request.env['sale.order'].create([parameters]) - parameters = [] user_id = params['value']['user_id'] user_cart = request.env['website.user.cart'] source = params['value']['source'] - products = user_cart.get_product_by_user(user_id=user_id, selected=True, source=source) - for product in products: - price_unit = product['price']['price'] - price_discount =product['price']['discount_percentage'] - order_line = { - 'company_id': 1, - 'order_id': sale_order.id, - 'price_unit': price_unit, - 'discount': price_discount, - 'product_id': product['id'], - 'product_uom_qty': product['quantity'] - } - - if product['program']: - order_line.update({ - 'program_line_id': product['program']['id'] + carts = user_cart.get_product_by_user(user_id=user_id, selected=True, source=source) + + order_lines = [] + promotions = [] + for cart in carts: + if cart['cart_type'] == 'product': + order_lines.append({ + 'company_id': 1, + 'order_id': sale_order.id, + 'price_unit': cart['price']['price'], + 'discount': cart['price']['discount_percentage'], + 'product_id': cart['id'], + 'product_uom_qty': cart['quantity'] + }) + elif cart['cart_type'] == 'promotion': + promotions.append({ + 'order_id': sale_order.id, + 'program_line_id': cart['id'], + 'quantity': cart['quantity'] }) - parameters.append(order_line) - # if product['program'] and product['program']['type']['value'] != 'special_price': - # total_qty += sum(x['quantity'] for x in product['program']['items']) - # price_unit = product['subtotal'] / total_qty - # price_discount = 0 - - request.env['sale.order.line'].create(parameters) + request.env['sale.order.line'].create(order_lines) + request.env['sale.order.promotion'].create(promotions) - if any(x['program'] for x in products): + if len(promotions) > 0: sale_order.apply_promotion_program() voucher_code = params['value']['voucher'] voucher = request.env['voucher'].search([('code', '=', voucher_code)]) - if voucher: + if voucher and len(promotions) == 0: sale_order.voucher_id = voucher.id sale_order.apply_voucher() - cart_ids = [x['cart_id'] for x in products] + cart_ids = [x['cart_id'] for x in carts] # user_cart.browse(cart_ids).unlink() return self.response({ 'id': sale_order.id, diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py index 55708acc..10878e32 100755 --- a/indoteknik_custom/__manifest__.py +++ b/indoteknik_custom/__manifest__.py @@ -75,10 +75,8 @@ 'views/procurement_monitoring_detail.xml', 'views/product_product.xml', 'views/brand_vendor.xml', - 'views/promotion_program.xml', - 'views/promotion_program_line.xml', - 'views/promotion_program_free_item.xml', - 'views/promotion_program_keyword.xml', + 'views/promotion/promotion_program.xml', + 'views/promotion/promotion_program_line.xml', 'views/requisition.xml', 'views/landedcost.xml', 'views/product_sla.xml', diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index 77779d22..f51f95af 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -15,10 +15,6 @@ from . import product_pricelist from . import product_public_category from . import product_spec from . import product_template -from . import promotion_program -from . import promotion_program_line -from . import promotion_program_free_item -from . import promotion_program_keyword from . import purchase_order_line from . import purchase_order from . import purchase_outstanding @@ -90,3 +86,4 @@ from . import cost_centre from . import account_account from . import account_move_line from . import stock_scheduler_compute +from . import promotion \ No newline at end of file diff --git a/indoteknik_custom/models/promotion/__init__.py b/indoteknik_custom/models/promotion/__init__.py new file mode 100644 index 00000000..631df8f3 --- /dev/null +++ b/indoteknik_custom/models/promotion/__init__.py @@ -0,0 +1,7 @@ +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 \ 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_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..90220509 --- /dev/null +++ b/indoteknik_custom/models/promotion/promotion_program.py @@ -0,0 +1,18 @@ +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') 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..a16f426d --- /dev/null +++ b/indoteknik_custom/models/promotion/promotion_program_line.py @@ -0,0 +1,114 @@ +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') + 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') + + 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_type(self): + return { + 'value': self.type, + 'label': dict(self._fields['type'].selection).get(self.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(), + 'type': self._res_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 + } + + return response \ 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..abbcb64b --- /dev/null +++ b/indoteknik_custom/models/promotion/sale_order.py @@ -0,0 +1,110 @@ +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['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): + 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({ + 'company_id': 1, + 'order_id': self.id, + 'price_unit': product_price, + 'discount': contrib_disc_unit, + 'product_id': product['id'], + 'product_uom_qty': product['qty'], + 'order_promotion_id': line.id + }) + + self.env['sale.order.line'].create(order_lines) + self._create_promotion_program_iu_docs(iu_products) + + def _merge_promotion_products(self, 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..3527f9e6 --- /dev/null +++ b/indoteknik_custom/models/promotion/sale_order_promotion.py @@ -0,0 +1,15 @@ +from odoo import models, fields + + +class SaleOrderPromotion(models.Model): + _name = 'sale.order.promotion' + + 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 diff --git a/indoteknik_custom/models/promotion_program.py b/indoteknik_custom/models/promotion_program.py deleted file mode 100644 index e60f48e1..00000000 --- a/indoteknik_custom/models/promotion_program.py +++ /dev/null @@ -1,19 +0,0 @@ -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(comodel_name="promotion.program.line", inverse_name="program_id", string="Program Line") - keywords = fields.One2many(comodel_name="promotion.program.keyword", inverse_name="program_id", string="Keywords") diff --git a/indoteknik_custom/models/promotion_program_free_item.py b/indoteknik_custom/models/promotion_program_free_item.py deleted file mode 100644 index 705456dd..00000000 --- a/indoteknik_custom/models/promotion_program_free_item.py +++ /dev/null @@ -1,10 +0,0 @@ -from odoo import fields, models - - -class PromotionProgramFreeItem(models.Model): - _name = "promotion.program.free_item" - _rec_name = "product_id" - - product_id = fields.Many2one(comodel_name="product.product", string="Product Variant") - qty = fields.Integer(string="Qty") - line_id = fields.Many2one(comodel_name="promotion.program.line", string="Program Line") diff --git a/indoteknik_custom/models/promotion_program_keyword.py b/indoteknik_custom/models/promotion_program_keyword.py deleted file mode 100644 index 79b938e2..00000000 --- a/indoteknik_custom/models/promotion_program_keyword.py +++ /dev/null @@ -1,8 +0,0 @@ -from odoo import fields, models - - -class PromotionProgramKeyword(models.Model): - _name = "promotion.program.keyword" - - name = fields.Char(string="Keyword") - program_id = fields.Many2one(comodel_name="promotion.program") diff --git a/indoteknik_custom/models/promotion_program_line.py b/indoteknik_custom/models/promotion_program_line.py deleted file mode 100644 index 71418d6c..00000000 --- a/indoteknik_custom/models/promotion_program_line.py +++ /dev/null @@ -1,171 +0,0 @@ -from odoo import fields, models, api -from datetime import datetime -import math - - -class PromotionProgramLine(models.Model): - _name = "promotion.program.line" - - name = fields.Char(string="Name") - image = fields.Binary(string="Image") - product_id = fields.Many2one(comodel_name="product.product", string="Product Variant") - program_id = fields.Many2one(comodel_name="promotion.program", string="Program") - discount_type = fields.Selection(selection=[ - ("percentage", "Percentage"), - ("fixed_price", "Fixed Price"), - ], string="Discount Type") - discount_amount = fields.Float(string="Discount Amount") - promotion_type = fields.Selection(string="Promotion Type", - selection=[ - ("special_price", "Special Price"), - ("bundling", "Bundling"), - ("discount_loading", "Discount Loading"), - ("merchandise", "Merchandise") - ], - help='- Special Price: Potongan harga barang.\n' - '- Bundling: Menggabungkan beberapa produk menjadi satu paket.\n' - '- Discount Loading: Semakin banyak unit yang dibeli maka semakin murah harga yang dibayar.\n' - '- Merchandise: Pemberian barang gratis.' - ) - minimum_purchase_qty = fields.Integer(string="Minimum Purchase Qty", help="Minimum unit barang yang perlu dibeli untuk dapat menggunakan promosi") - applies_multiply = fields.Boolean(string="Applies Multiply", help="Diterapkan berlipat ganda") - limit_qty = fields.Integer(string="Limit Qty", help="Kuota promosi keseluruhan") - limit_qty_user = fields.Integer(string="Limit Qty / User", help="Limit Qty per User") - limit_qty_transaction = fields.Integer(string="Limit Qty / Transaction", help="Limit Qty per Transaction") - line_free_item = fields.One2many(comodel_name="promotion.program.free_item", inverse_name="line_id", string="Line Free Item") - display_on_homepage = fields.Boolean(string="Display on Homepage") - order_line_ids = fields.One2many('sale.order.line', 'program_line_id') - - @api.onchange('product_id') - def _onchange_product_id(self): - if self.product_id and not self.name: - self.name = self.product_id.display_name - self._discount_loading_auto() - - @api.onchange('promotion_type') - def onchange_promotion_type(self): - self._discount_loading_auto() - - def _discount_loading_auto(self): - program_line = self - product = program_line.product_id - promotion_type = program_line.promotion_type - - if promotion_type != 'discount_loading' or not product: - return - - line = self.browse(self.ids) - line.product_id = self.product_id.id - line.promotion_type = self.promotion_type - - product_added = False - line_free_item = program_line.line_free_item - for line in line_free_item: - if line.product_id.id == product.id: - product_added = True - continue - line.unlink() - if not product_added: - data = {'product_id': product.id, - 'qty': 1, 'line_id': program_line.id} - line_free_item.create(data) - - def calculate_price(self, price): - initial_price = price['price'] - if self.discount_type == 'percentage': - price['discount_percentage'] = self.discount_amount - price['price_discount'] = initial_price - (initial_price * self.discount_amount / 100) - elif self.discount_type == 'fixed_price': - price['price_discount'] = self.discount_amount - price['discount_percentage'] = round((initial_price - self.discount_amount) / initial_price * 100) - return price - - 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_id', '=', product_id) - ]) - - def _get_remaining_qty(self, user): - remaining_qty = { - 'all': self.limit_qty, - 'user': self.limit_qty_user, - 'transaction': self.limit_qty_transaction - } - for order in self.order_line_ids: - parent_order = order.order_id - if parent_order.state != 'cancel': - remaining_qty['all'] -= order.product_uom_qty - if user and parent_order.partner_id.id == user['partner_id']: - remaining_qty['user'] -= order.product_uom_qty - - 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 _get_remaining_time(self): - calculate_time = self.program_id.end_time - datetime.now() - return round(calculate_time.total_seconds()) - - def _res_limit_qty(self): - return { - 'all': self.limit_qty, - 'user': self.limit_qty_user, - 'transaction': self.limit_qty_transaction, - } - - def _res_promotion_type(self): - return { - 'value': self.promotion_type, - 'label': dict(self._fields['promotion_type'].selection).get(self.promotion_type) - } - - def format(self, user = None, qty = 0): - ir_attachment = self.env['ir.attachment'] - product_price = self.product_id.calculate_website_price() - 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 - - qty_can_buy = min(remaining_qty['user'], qty) - if self.limit_qty_transaction > 0: - qty_can_buy = min(qty_can_buy, self.limit_qty_transaction) - - multiplier = 0 - if qty_can_buy > self.minimum_purchase_qty: - multiplier = 1 - - if self.applies_multiply and qty_can_buy > 0: - multiplier = math.floor(qty_can_buy / self.minimum_purchase_qty) - - response = { - 'id': self.id, - 'name': self.name, - 'image': ir_attachment.api_image('promotion.program.line', 'image', self.id), - 'minimum_purchase_qty': self.minimum_purchase_qty, - 'applies_multiply': self.applies_multiply, - 'remaining_time': self._get_remaining_time(), - 'type': self._res_promotion_type(), - 'limit_qty': limit_qty, - 'remaining_qty': remaining_qty, - 'used_percentage': percent_remaining, - 'price': self.calculate_price(price=product_price), - 'items': [{ - 'product_id': line.product_id.id, - 'quantity': line.qty * multiplier - } for line in self.line_free_item] - } - - return response - - def res_format(self, user): - data = [x.format(user) for x in self] - return data - diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 2510d9e5..376b8e40 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -448,58 +448,6 @@ class SaleOrder(models.Model): line.discount = line.initial_discount line.initial_discount = False - def apply_promotion_program(self): - userdata = { - 'user_id': self.partner_id.user_id.id, - 'partner_id': self.partner_id.id - } - - iu_items = [] - for line in self.order_line: - promotion = line.program_line_id.format(user=userdata, qty=line.product_uom_qty) - if promotion['type']['value'] == 'merchandise': - iu_items += filter(lambda x: x['quantity'] > 0, promotion['items']) - - if len(iu_items) > 0: - self._create_promotion_program_iu_docs(iu_items) - - def _create_promotion_program_iu_docs(self, items): - 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 - }) - - product_model = self.env['product.product'] - for item in items: - picking.move_ids_without_package.create({ - 'product_id': item['product_id'], - 'name': product_model.browse(item['product_id']).display_name, - 'product_uom_qty': item['quantity'], - '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/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py index 4ec48b55..69328325 100644 --- a/indoteknik_custom/models/sale_order_line.py +++ b/indoteknik_custom/models/sale_order_line.py @@ -21,7 +21,6 @@ class SaleOrderLine(models.Model): note_procurement = fields.Char(string='Note', help="Harap diisi jika ada keterangan tambahan dari Procurement, agar dapat dimonitoring") vendor_subtotal = fields.Float(string='Vendor Subtotal', compute="_compute_vendor_subtotal") amount_voucher_disc = fields.Float(string='Voucher Discount') - program_line_id = fields.Many2one('promotion.program.line', 'Program Line') def _compute_vendor_subtotal(self): for line in self: diff --git a/indoteknik_custom/models/website_user_cart.py b/indoteknik_custom/models/website_user_cart.py index 1468e9dc..47a695fe 100644 --- a/indoteknik_custom/models/website_user_cart.py +++ b/indoteknik_custom/models/website_user_cart.py @@ -22,28 +22,33 @@ class WebsiteUserCart(models.Model): record.user_other_carts = others def get_product(self): - product = self.product_id.v2_api_single_response(self.product_id) - product['cart_id'] = self.id - product['quantity'] = self.qty - product['subtotal'] = self.qty * product['price']['price_discount'] - product['selected'] = self.is_selected - product['program'] = None - product['can_buy'] = True - flashsales = self.product_id._get_active_flash_sale() - product['has_flashsale'] = True if len(flashsales) > 0 else False - if self.program_line_id: + res = { + 'cart_id': self.id, + 'quantity': self.qty, + 'selected': self.is_selected, + 'can_buy': True + } + + if self.product_id: + res['cart_type'] = 'product' + product = self.product_id.v2_api_single_response(self.product_id) + res.update(product) + flashsales = self.product_id._get_active_flash_sale() + res['has_flashsale'] = True if len(flashsales) > 0 else False + elif self.program_line_id: + res['cart_type'] = 'promotion' userdata = { 'partner_id': self.user_id.partner_id.id, 'user_id': self.user_id.id } - product['program'] = self.program_line_id.format(user=userdata, qty=self.qty) - - if product['program']: - if self.qty < product['program']['minimum_purchase_qty'] or self.qty > product['program']['remaining_qty']['transaction']: - product['can_buy'] = False - product['price'] = product['program']['price'] - - return product + program = self.program_line_id.format(user=userdata, qty=self.qty) + res.update(program) + if program['remaining_qty']['transaction'] < self.qty: + res['can_buy'] = False + + res['subtotal'] = self.qty * res['price']['price_discount'] + + return res def get_products(self): return [x.get_product() for x in self] diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv index 5e77f25d..21bbe093 100755 --- a/indoteknik_custom/security/ir.model.access.csv +++ b/indoteknik_custom/security/ir.model.access.csv @@ -53,8 +53,8 @@ access_rajaongkir_kurir,access.rajaongkir.kurir,model_rajaongkir_kurir,,1,1,1,1 access_brand_vendor,access.brand.vendor,model_brand_vendor,,1,1,1,1 access_promotion_program,access.promotion.program,model_promotion_program,,1,1,1,1 access_promotion_program_line,access.promotion.program.line,model_promotion_program_line,,1,1,1,1 -access_promotion_program_free_item,access.promotion.program.free_item,model_promotion_program_free_item,,1,1,1,1 -access_promotion_program_keyword,access.promotion.program.keyword,model_promotion_program_keyword,,1,1,1,1 +access_promotion_product,access.promotion.product,model_promotion_product,,1,1,1,1 +access_promotion_free_product,access.promotion.free_product,model_promotion_free_product,,1,1,1,1 access_requisition,access.requisition,model_requisition,,1,1,1,1 access_requisition_line,access.requisition.line,model_requisition_line,,1,1,1,1 access_requisition_purchase_match,access.requisition.purchase.match,model_requisition_purchase_match,,1,1,1,1 @@ -74,3 +74,4 @@ access_mrp_production,access.mrp.production,model_mrp_production,,1,1,1,1 access_apache_solr_queue,access.apache.solr.queue,model_apache_solr_queue,,1,1,1,1 access_cost_centre,access.cost.centre,model_cost_centre,,1,1,1,1 access_stock_scheduler_compute,access.stock.scheduler.compute,model_stock_scheduler_compute,,1,1,1,1 +access_sale_order_promotion,access.sale.order.promotion,model_sale_order_promotion,,1,1,1,1 diff --git a/indoteknik_custom/views/promotion/promotion_program.xml b/indoteknik_custom/views/promotion/promotion_program.xml new file mode 100644 index 00000000..99fbd878 --- /dev/null +++ b/indoteknik_custom/views/promotion/promotion_program.xml @@ -0,0 +1,72 @@ + + + Promotion Program Tree + promotion.program + + + + + + + + + + + + Promotion Program Form + promotion.program + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + Promotion Program + ir.actions.act_window + promotion.program + tree,form + + + + + +
\ No newline at end of file diff --git a/indoteknik_custom/views/promotion/promotion_program_free_item.xml b/indoteknik_custom/views/promotion/promotion_program_free_item.xml new file mode 100644 index 00000000..52d421fe --- /dev/null +++ b/indoteknik_custom/views/promotion/promotion_program_free_item.xml @@ -0,0 +1,44 @@ + + + Promotion Program Free Item Tree + promotion.program.free_item + + + + + + + + + + Promotion Program Free Item Form + promotion.program.free_item + +
+ + + + + + + + +
+
+
+ + + Promotion Program Free Item + ir.actions.act_window + promotion.program.free_item + tree,form + + + +
\ No newline at end of file diff --git a/indoteknik_custom/views/promotion/promotion_program_keyword.xml b/indoteknik_custom/views/promotion/promotion_program_keyword.xml new file mode 100644 index 00000000..fd279665 --- /dev/null +++ b/indoteknik_custom/views/promotion/promotion_program_keyword.xml @@ -0,0 +1,44 @@ + + + Promotion Program Keyword Tree + promotion.program.keyword + + + + + + + + + + Promotion Program Keyword Form + promotion.program.keyword + +
+ + + + + + + + +
+
+
+ + + Promotion Program Keyword + ir.actions.act_window + promotion.program.keyword + tree,form + + + +
\ No newline at end of file diff --git a/indoteknik_custom/views/promotion/promotion_program_line.xml b/indoteknik_custom/views/promotion/promotion_program_line.xml new file mode 100644 index 00000000..6fdf0a9a --- /dev/null +++ b/indoteknik_custom/views/promotion/promotion_program_line.xml @@ -0,0 +1,72 @@ + + + Promotion Program Line Tree + promotion.program.line + + + + + + + + + + + + + Promotion Program Line Form + promotion.program.line + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + Promotion Program Line + ir.actions.act_window + promotion.program.line + tree,form + + + +
\ No newline at end of file diff --git a/indoteknik_custom/views/promotion_program.xml b/indoteknik_custom/views/promotion_program.xml deleted file mode 100644 index 5dff80e6..00000000 --- a/indoteknik_custom/views/promotion_program.xml +++ /dev/null @@ -1,73 +0,0 @@ - - - Promotion Program Tree - promotion.program - - - - - - - - - - - - Promotion Program Form - promotion.program - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
- - - Promotion Program - ir.actions.act_window - promotion.program - tree,form - - - - - -
\ No newline at end of file diff --git a/indoteknik_custom/views/promotion_program_free_item.xml b/indoteknik_custom/views/promotion_program_free_item.xml deleted file mode 100644 index 52d421fe..00000000 --- a/indoteknik_custom/views/promotion_program_free_item.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - Promotion Program Free Item Tree - promotion.program.free_item - - - - - - - - - - Promotion Program Free Item Form - promotion.program.free_item - -
- - - - - - - - -
-
-
- - - Promotion Program Free Item - ir.actions.act_window - promotion.program.free_item - tree,form - - - -
\ No newline at end of file diff --git a/indoteknik_custom/views/promotion_program_keyword.xml b/indoteknik_custom/views/promotion_program_keyword.xml deleted file mode 100644 index fd279665..00000000 --- a/indoteknik_custom/views/promotion_program_keyword.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - Promotion Program Keyword Tree - promotion.program.keyword - - - - - - - - - - Promotion Program Keyword Form - promotion.program.keyword - -
- - - - - - - - -
-
-
- - - Promotion Program Keyword - ir.actions.act_window - promotion.program.keyword - tree,form - - - -
\ No newline at end of file diff --git a/indoteknik_custom/views/promotion_program_line.xml b/indoteknik_custom/views/promotion_program_line.xml deleted file mode 100644 index 5963d0dd..00000000 --- a/indoteknik_custom/views/promotion_program_line.xml +++ /dev/null @@ -1,98 +0,0 @@ - - - Promotion Program Line Tree - promotion.program.line - - - - - - - - - - - - - - - Promotion Program Line Form - promotion.program.line - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
- - - Promotion Program Line - ir.actions.act_window - promotion.program.line - tree,form - - - -
\ No newline at end of file diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml index 3ea65498..282e4024 100755 --- a/indoteknik_custom/views/sale_order.xml +++ b/indoteknik_custom/views/sale_order.xml @@ -91,7 +91,6 @@ - @@ -132,6 +131,22 @@
+ + + + + + + +
+ + + + + +
+
+
diff --git a/indoteknik_custom/views/website_user_cart.xml b/indoteknik_custom/views/website_user_cart.xml index f43c9f63..e3630363 100755 --- a/indoteknik_custom/views/website_user_cart.xml +++ b/indoteknik_custom/views/website_user_cart.xml @@ -29,12 +29,12 @@ - - - - - - + + + + + + -- cgit v1.2.3 From 50b5bd7bd984ef108e8bd324440050a222d8262f Mon Sep 17 00:00:00 2001 From: Rafi Zadanly Date: Mon, 25 Sep 2023 14:50:43 +0700 Subject: Fix promotion program feature --- indoteknik_custom/models/promotion/__init__.py | 3 +- .../models/promotion/promotion_keyword.py | 8 +++++ .../models/promotion/promotion_program.py | 1 + .../models/promotion/promotion_program_line.py | 32 ++++++++++++++++---- indoteknik_custom/models/promotion/sale_order.py | 34 +++++++++++++++++----- .../models/promotion/sale_order_promotion.py | 1 + indoteknik_custom/security/ir.model.access.csv | 1 + .../views/promotion/promotion_program.xml | 1 + .../views/promotion/promotion_program_line.xml | 18 +++++++----- indoteknik_custom/views/sale_order.xml | 1 + 10 files changed, 78 insertions(+), 22 deletions(-) create mode 100644 indoteknik_custom/models/promotion/promotion_keyword.py diff --git a/indoteknik_custom/models/promotion/__init__.py b/indoteknik_custom/models/promotion/__init__.py index 631df8f3..1e15d714 100644 --- a/indoteknik_custom/models/promotion/__init__.py +++ b/indoteknik_custom/models/promotion/__init__.py @@ -4,4 +4,5 @@ from . import promotion_product from . import promotion_free_product from . import sale_order_promotion from . import sale_order_line -from . import sale_order \ No newline at end of file +from . import sale_order +from . import promotion_keyword \ 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_program.py b/indoteknik_custom/models/promotion/promotion_program.py index 90220509..29aaa753 100644 --- a/indoteknik_custom/models/promotion/promotion_program.py +++ b/indoteknik_custom/models/promotion/promotion_program.py @@ -16,3 +16,4 @@ class PromotionProgram(models.Model): ("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 index a16f426d..34a0fbb2 100644 --- a/indoteknik_custom/models/promotion/promotion_program_line.py +++ b/indoteknik_custom/models/promotion/promotion_program_line.py @@ -7,7 +7,7 @@ class PromotionProgramLine(models.Model): program_id = fields.Many2one('promotion.program', 'Program') name = fields.Char('Name') - type = fields.Selection([ + promotion_type = fields.Selection([ ("special_price", "Special Price"), ("bundling", "Bundling"), ("discount_loading", "Discount Loading"), @@ -23,6 +23,11 @@ class PromotionProgramLine(models.Model): 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') @@ -62,10 +67,10 @@ class PromotionProgramLine(models.Model): return remaining_qty - def _res_type(self): + def _res_promotion_type(self): return { - 'value': self.type, - 'label': dict(self._fields['type'].selection).get(self.type) + 'value': self.promotion_type, + 'label': dict(self._fields['promotion_type'].selection).get(self.promotion_type) } def _get_remaining_time(self): @@ -97,7 +102,7 @@ class PromotionProgramLine(models.Model): 'name': self.name, 'image': ir_attachment.api_image(self._name, 'image', self.id), 'remaining_time': self._get_remaining_time(), - 'type': self._res_type(), + 'promotion_type': self._res_promotion_type(), 'limit_qty': limit_qty, 'remaining_qty': remaining_qty, 'used_percentage': percent_remaining, @@ -111,4 +116,19 @@ class PromotionProgramLine(models.Model): 'weight': weight } - return response \ No newline at end of file + 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 index abbcb64b..42e972d0 100644 --- a/indoteknik_custom/models/promotion/sale_order.py +++ b/indoteknik_custom/models/promotion/sale_order.py @@ -21,7 +21,7 @@ class SaleOrder(models.Model): free_products = promotion['free_products'] all_products = products.copy() - if promotion['type']['value'] == 'merchandise': + if promotion['promotion_type']['value'] == 'merchandise': iu_products += filter(lambda x: x['qty'] > 0, free_products) else: all_products = self._merge_promotion_products(promotion) @@ -31,6 +31,21 @@ class SaleOrder(models.Model): 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 @@ -46,19 +61,24 @@ class SaleOrder(models.Model): promotion_used_price += product_subtotal order_lines.append({ - 'company_id': 1, - 'order_id': self.id, + **order_line_default, 'price_unit': product_price, - 'discount': contrib_disc_unit, - 'product_id': product['id'], - 'product_uom_qty': product['qty'], - 'order_promotion_id': line.id + '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']: diff --git a/indoteknik_custom/models/promotion/sale_order_promotion.py b/indoteknik_custom/models/promotion/sale_order_promotion.py index 3527f9e6..a3cd8d01 100644 --- a/indoteknik_custom/models/promotion/sale_order_promotion.py +++ b/indoteknik_custom/models/promotion/sale_order_promotion.py @@ -3,6 +3,7 @@ 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') diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv index 21bbe093..da92f81f 100755 --- a/indoteknik_custom/security/ir.model.access.csv +++ b/indoteknik_custom/security/ir.model.access.csv @@ -55,6 +55,7 @@ access_promotion_program,access.promotion.program,model_promotion_program,,1,1,1 access_promotion_program_line,access.promotion.program.line,model_promotion_program_line,,1,1,1,1 access_promotion_product,access.promotion.product,model_promotion_product,,1,1,1,1 access_promotion_free_product,access.promotion.free_product,model_promotion_free_product,,1,1,1,1 +access_promotion_keyword,access.promotion.keyword,model_promotion_keyword,,1,1,1,1 access_requisition,access.requisition,model_requisition,,1,1,1,1 access_requisition_line,access.requisition.line,model_requisition_line,,1,1,1,1 access_requisition_purchase_match,access.requisition.purchase.match,model_requisition_purchase_match,,1,1,1,1 diff --git a/indoteknik_custom/views/promotion/promotion_program.xml b/indoteknik_custom/views/promotion/promotion_program.xml index 99fbd878..f8432d8a 100644 --- a/indoteknik_custom/views/promotion/promotion_program.xml +++ b/indoteknik_custom/views/promotion/promotion_program.xml @@ -22,6 +22,7 @@ + diff --git a/indoteknik_custom/views/promotion/promotion_program_line.xml b/indoteknik_custom/views/promotion/promotion_program_line.xml index 6fdf0a9a..db6d5252 100644 --- a/indoteknik_custom/views/promotion/promotion_program_line.xml +++ b/indoteknik_custom/views/promotion/promotion_program_line.xml @@ -5,7 +5,7 @@ - + @@ -22,30 +22,32 @@ - + - + + + - - + + - + - - + + diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml index 282e4024..04d4b854 100755 --- a/indoteknik_custom/views/sale_order.xml +++ b/indoteknik_custom/views/sale_order.xml @@ -91,6 +91,7 @@ + -- cgit v1.2.3 From d324fdd8ea3b14c966510bde610a96c8f5c3e3c5 Mon Sep 17 00:00:00 2001 From: Rafi Zadanly Date: Tue, 26 Sep 2023 14:56:35 +0700 Subject: Update readonly condition order line on purchase order --- indoteknik_custom/models/purchase_order.py | 5 +++++ indoteknik_custom/views/purchase_order.xml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index daf8d269..92df3119 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -46,6 +46,11 @@ class PurchaseOrder(models.Model): summary_qty_receipt = fields.Float('Summary Qty Receipt', compute='_compute_summary_qty') count_line_product = fields.Float('Total Item', compute='compute_count_line_product') note_description = fields.Char(string='Note', help='bisa diisi sebagai informasi indent barang tertentu atau apapun') + has_active_invoice = fields.Boolean(string='Has Active Invoice', compute='_compute_has_active_invoice') + + def _compute_has_active_invoice(self): + for order in self: + order.has_active_invoice = any(invoice.state != 'cancel' for invoice in order.invoice_ids) def add_product_to_pricelist(self): for line in self.order_line: diff --git a/indoteknik_custom/views/purchase_order.xml b/indoteknik_custom/views/purchase_order.xml index e7a72cd3..0ad76ae5 100755 --- a/indoteknik_custom/views/purchase_order.xml +++ b/indoteknik_custom/views/purchase_order.xml @@ -76,6 +76,11 @@ + + + + + {'readonly': ['|', ('state', 'in', ['done', 'cancel']), ('has_active_invoice', '=', True)]} -- cgit v1.2.3