summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorit-fixcomart <it@fixcomart.co.id>2024-12-04 11:24:24 +0700
committerit-fixcomart <it@fixcomart.co.id>2024-12-04 11:24:24 +0700
commit1094670e6fc35e5150b612b32e1f497b36c022b3 (patch)
tree4482491680db1554c28fb380480ada5282fac8f0
parentc631620706a24426faea6b43c2a0602eebf9a5e4 (diff)
parentdbe24b9cd600c7b5a9d0587f80a782ed93c9a761 (diff)
Merge branch 'production' into iman/telegram
# Conflicts: # indoteknik_custom/models/__init__.py # indoteknik_custom/models/stock_picking.py
-rw-r--r--indoteknik_api/controllers/api_v1/cart.py21
-rw-r--r--indoteknik_api/controllers/api_v1/partner.py6
-rw-r--r--indoteknik_api/controllers/api_v1/product.py16
-rw-r--r--indoteknik_api/controllers/api_v1/sale_order.py10
-rw-r--r--indoteknik_api/controllers/api_v1/stock_picking.py36
-rw-r--r--indoteknik_api/controllers/api_v1/user.py4
-rw-r--r--indoteknik_api/models/product_product.py5
-rw-r--r--indoteknik_api/models/product_template.py6
-rw-r--r--indoteknik_api/models/res_users.py11
-rw-r--r--indoteknik_api/models/sale_order.py10
-rwxr-xr-xindoteknik_custom/__manifest__.py4
-rwxr-xr-xindoteknik_custom/models/__init__.py4
-rw-r--r--indoteknik_custom/models/account_move_due_extension.py7
-rw-r--r--indoteknik_custom/models/approval_date_doc.py1
-rw-r--r--indoteknik_custom/models/approval_retur_picking.py38
-rw-r--r--indoteknik_custom/models/approval_unreserve.py9
-rw-r--r--indoteknik_custom/models/automatic_purchase.py66
-rwxr-xr-xindoteknik_custom/models/crm_lead.py93
-rw-r--r--indoteknik_custom/models/delivery_order.py10
-rw-r--r--indoteknik_custom/models/find_page.py121
-rw-r--r--indoteknik_custom/models/logbook_sj.py6
-rwxr-xr-xindoteknik_custom/models/product_template.py166
-rw-r--r--indoteknik_custom/models/promotion/sale_order.py11
-rwxr-xr-xindoteknik_custom/models/purchase_order.py177
-rwxr-xr-xindoteknik_custom/models/purchase_order_line.py10
-rw-r--r--indoteknik_custom/models/purchasing_job.py90
-rw-r--r--indoteknik_custom/models/report_stock_forecasted.py55
-rw-r--r--indoteknik_custom/models/requisition.py111
-rw-r--r--indoteknik_custom/models/res_partner.py40
-rwxr-xr-xindoteknik_custom/models/sale_order.py151
-rw-r--r--indoteknik_custom/models/sale_order_line.py23
-rw-r--r--indoteknik_custom/models/sales_order_fullfillment.py18
-rw-r--r--indoteknik_custom/models/solr/product_product.py4
-rw-r--r--indoteknik_custom/models/solr/product_template.py16
-rw-r--r--indoteknik_custom/models/stock_picking.py230
-rw-r--r--indoteknik_custom/models/user_company_request.py25
-rw-r--r--indoteknik_custom/models/va_multi_approve.py22
-rw-r--r--indoteknik_custom/models/va_multi_reject.py22
-rw-r--r--indoteknik_custom/models/vendor_approval.py101
-rw-r--r--indoteknik_custom/models/wati.py107
-rw-r--r--indoteknik_custom/models/website_user_cart.py23
-rwxr-xr-xindoteknik_custom/security/ir.model.access.csv9
-rw-r--r--indoteknik_custom/views/account_move_views.xml8
-rw-r--r--indoteknik_custom/views/approval_retur_picking.xml27
-rw-r--r--indoteknik_custom/views/dunning_run.xml1
-rw-r--r--indoteknik_custom/views/find_page.xml70
-rw-r--r--indoteknik_custom/views/form_vendor_approval_multi_approve.xml31
-rw-r--r--indoteknik_custom/views/form_vendor_approval_multi_reject.xml31
-rw-r--r--indoteknik_custom/views/partner_payment_term.xml3
-rw-r--r--indoteknik_custom/views/product_product.xml3
-rwxr-xr-xindoteknik_custom/views/product_template.xml21
-rwxr-xr-xindoteknik_custom/views/purchase_order.xml17
-rw-r--r--indoteknik_custom/views/requisition.xml23
-rw-r--r--indoteknik_custom/views/res_partner.xml11
-rwxr-xr-xindoteknik_custom/views/sale_order.xml71
-rw-r--r--indoteknik_custom/views/stock_picking.xml19
-rw-r--r--indoteknik_custom/views/vendor_approval.xml66
57 files changed, 1863 insertions, 434 deletions
diff --git a/indoteknik_api/controllers/api_v1/cart.py b/indoteknik_api/controllers/api_v1/cart.py
index 2a24b205..7a40b1e2 100644
--- a/indoteknik_api/controllers/api_v1/cart.py
+++ b/indoteknik_api/controllers/api_v1/cart.py
@@ -46,8 +46,25 @@ class Cart(controller.Controller):
def get_cart_count_by_user_id(self, user_id, **kw):
user_id = int(user_id)
query = [('user_id', '=', user_id)]
- carts = request.env['website.user.cart'].search_count(query)
- return self.response(carts)
+ carts = request.env['website.user.cart'].search(query)
+ products_active = []
+ products_inactive = []
+ for cart in carts:
+ if cart.product_id:
+ price = cart.product_id._v2_get_website_price_include_tax()
+ if cart.product_id.active and price > 0:
+ product = cart.with_context(price_for="web").get_products()
+ for product_active in product:
+ products_active.append(product_active)
+ else:
+ product_inactives = cart.with_context(price_for="web").get_products()
+ for inactives in product_inactives:
+ products_inactive.append(inactives)
+ else:
+ program = cart.with_context(price_for="web").get_products()
+ for programs in program:
+ products_active.append(programs)
+ return self.response(len(products_active))
@http.route(PREFIX_USER + 'cart/create-or-update', auth='public', methods=['POST', 'OPTIONS'], csrf=False)
@controller.Controller.must_authorized(private=True, private_key='user_id')
diff --git a/indoteknik_api/controllers/api_v1/partner.py b/indoteknik_api/controllers/api_v1/partner.py
index a7925a02..bbca471b 100644
--- a/indoteknik_api/controllers/api_v1/partner.py
+++ b/indoteknik_api/controllers/api_v1/partner.py
@@ -74,6 +74,9 @@ class Partner(controller.Controller):
'district_id': ['number', 'alias:kecamatan_id'],
'sub_district_id': ['number', 'alias:kelurahan_id', 'exclude_if_null'],
'zip': ['required'],
+ 'longtitude': [],
+ 'latitude': [],
+ 'address_map': [],
'alamat_lengkap_text': []
})
@@ -105,6 +108,9 @@ class Partner(controller.Controller):
'city_id': ['required', 'number', 'alias:kota_id'],
'district_id': ['number', 'alias:kecamatan_id'],
'sub_district_id': ['number', 'alias:kelurahan_id', 'exclude_if_null'],
+ 'longtitude': [],
+ 'latitude': [],
+ 'address_map': [],
'zip': ['required']
})
diff --git a/indoteknik_api/controllers/api_v1/product.py b/indoteknik_api/controllers/api_v1/product.py
index e779e623..32362582 100644
--- a/indoteknik_api/controllers/api_v1/product.py
+++ b/indoteknik_api/controllers/api_v1/product.py
@@ -74,7 +74,6 @@ class Product(controller.Controller):
if qty_available > 0:
qty = qty_available + total_adem + total_excell
- sla_date = '1 Hari'
elif qty_altama > 0 or qty_vendor > 0:
qty = total_adem if qty_altama > 0 else total_excell
sla_date = '2-4 Hari'
@@ -97,6 +96,21 @@ class Product(controller.Controller):
return self.response(data, headers=[('Cache-Control', 'max-age=600, private')])
+ @http.route(prefix + 'product_variant/<id>/qty_available', auth='public', methods=['GET', 'OPTIONS'])
+ @controller.Controller.must_authorized()
+ def get_product_variant_stock_available_by_id(self, **kw):
+ id = int(kw.get('id'))
+ product = request.env['product.product'].search(
+ [('id', '=', id)], limit=1)
+
+ qty_available = product.qty_free_bandengan
+
+ data = {
+ 'qty': qty_available,
+ }
+
+ return self.response(data, headers=[('Cache-Control', 'max-age=600, private')])
+
@http.route(prefix + 'product/template/price/<id>', auth='public', methods=['GET', 'OPTIONS'])
def get_product_template_price_by_id(self, **kw):
if not self.authenticate():
diff --git a/indoteknik_api/controllers/api_v1/sale_order.py b/indoteknik_api/controllers/api_v1/sale_order.py
index e7664c79..8b95ade8 100644
--- a/indoteknik_api/controllers/api_v1/sale_order.py
+++ b/indoteknik_api/controllers/api_v1/sale_order.py
@@ -150,7 +150,8 @@ class SaleOrder(controller.Controller):
def partner_checkout_sale_order_by_id(self, **kw):
params = self.get_request_params(kw, {
'partner_id': ['number'],
- 'id': ['number']
+ 'id': ['number'],
+ 'status': ['boolean']
})
if not params['valid']:
return self.response(code=400, description=params)
@@ -163,6 +164,9 @@ class SaleOrder(controller.Controller):
data = {}
sale_order = request.env['sale.order'].search(domain)
if sale_order:
+ if 'status' in params['value']:
+ sale_order.is_continue_transaction = params['value']['status']
+
if sale_order._requires_approval_margin_leader():
sale_order.approval_status = 'pengajuan2'
elif sale_order._requires_approval_margin_manager():
@@ -446,7 +450,8 @@ class SaleOrder(controller.Controller):
'company_id': 1,
'order_id': sale_order.id,
'product_id': cart['id'],
- 'product_uom_qty': cart['quantity']
+ 'product_uom_qty': cart['quantity'],
+ 'product_available_quantity': cart['available_quantity']
})
order_line.product_id_change()
order_line.onchange_vendor_id()
@@ -463,6 +468,7 @@ class SaleOrder(controller.Controller):
if len(promotions) > 0:
sale_order.apply_promotion_program()
+ sale_order.add_free_product(promotions)
voucher_code = params['value']['voucher']
voucher = request.env['voucher'].search([('code', '=', voucher_code),('apply_type', 'in', ['all', 'brand'])], limit=1)
diff --git a/indoteknik_api/controllers/api_v1/stock_picking.py b/indoteknik_api/controllers/api_v1/stock_picking.py
index 8b941c16..110cde8a 100644
--- a/indoteknik_api/controllers/api_v1/stock_picking.py
+++ b/indoteknik_api/controllers/api_v1/stock_picking.py
@@ -1,6 +1,8 @@
from .. import controller
from odoo import http
from odoo.http import request
+from pytz import timezone
+from datetime import datetime
class StockPicking(controller.Controller):
@@ -30,7 +32,9 @@ class StockPicking(controller.Controller):
pending_domain = [('driver_departure_date', '=', False), ('driver_arrival_date', '=', False)]
shipment_domain = [('driver_departure_date', '!=', False), ('driver_arrival_date', '=', False)]
- completed_domain = [('driver_departure_date', '!=', False), ('driver_arrival_date', '!=', False)]
+ shipment_domain2 = [('driver_departure_date', '!=', False), ('sj_return_date', '=', False)]
+ completed_domain = [('driver_departure_date', '!=', False),'|', ('driver_arrival_date', '!=', False), ('sj_return_date', '!=', False)]
+ completed_domain2 = [('driver_departure_date', '!=', False), ('sj_return_date', '!=', False)]
picking_model = request.env['stock.picking']
domain = [
@@ -72,7 +76,7 @@ class StockPicking(controller.Controller):
'name': picking.sale_id.name,
'client_order_ref': picking.sale_id.client_order_ref or ''
},
- 'delivered': picking.waybill_id.delivered or picking.driver_arrival_date != False,
+ 'delivered': picking.waybill_id.delivered or picking.driver_arrival_date != False or picking.sj_return_date != False,
'status': picking.shipping_status,
'carrier_name': picking.carrier_id.name or '',
'last_manifest': next(iter(manifests), None)
@@ -81,8 +85,8 @@ class StockPicking(controller.Controller):
return self.response({
'summary': {
'pending_count': picking_model.search_count(default_domain + pending_domain),
- 'shipment_count': picking_model.search_count(default_domain + shipment_domain),
- 'completed_count': picking_model.search_count(default_domain + completed_domain)
+ 'shipment_count': picking_model.search_count(default_domain + shipment_domain + shipment_domain2),
+ 'completed_count': picking_model.search_count(default_domain + completed_domain )
},
'picking_total': picking_model.search_count(domain),
'pickings': res_pickings
@@ -110,4 +114,26 @@ class StockPicking(controller.Controller):
if not picking:
return self.response(None)
- return self.response(picking.get_tracking_detail()) \ No newline at end of file
+ return self.response(picking.get_tracking_detail())
+
+ @http.route(prefix + 'stock-picking/<picking_code>/documentation', auth='public', methods=['PUT', 'OPTIONS'], csrf=False)
+ @controller.Controller.must_authorized()
+ def write_partner_stock_picking_documentation(self, **kw):
+ picking_code = int(kw.get('picking_code', 0))
+ sj_document = kw.get('sj_document', False)
+ paket_document = kw.get('paket_document', False)
+
+ params = {'sj_documentation': sj_document,
+ 'paket_documentation': paket_document,
+ 'driver_arrival_date': datetime.utcnow(),
+ }
+
+ picking_data = request.env['stock.picking'].search([('picking_code', '=', picking_code)], limit=1)
+
+ if not picking_data:
+ return self.response(code=404, description='picking not found')
+ picking_data.write(params)
+
+ return self.response({
+ 'name': picking_data.name
+ }) \ No newline at end of file
diff --git a/indoteknik_api/controllers/api_v1/user.py b/indoteknik_api/controllers/api_v1/user.py
index c7bfe91a..3be874fa 100644
--- a/indoteknik_api/controllers/api_v1/user.py
+++ b/indoteknik_api/controllers/api_v1/user.py
@@ -95,7 +95,7 @@ class User(controller.Controller):
user = request.env['res.users'].create(user_data)
user.partner_id.email = email
user.partner_id.customer_type = 'nonpkp'
- user.partner_id.npwp = '0.000.000.0-000.000'
+ user.partner_id.npwp = '00.000.000.0-000.000'
user.partner_id.sppkp = '-'
user.partner_id.nama_wajib_pajak = user.name
user.partner_id.user_id = 3222
@@ -248,7 +248,7 @@ class User(controller.Controller):
if type_acc == 'individu':
user.partner_id.customer_type = 'nonpkp'
- user.partner_id.npwp = '0.000.000.0-000.000'
+ user.partner_id.npwp = '00.000.000.0-000.000'
user.partner_id.sppkp = '-'
user.partner_id.nama_wajib_pajak = name
user.partner_id.user_id = 3222
diff --git a/indoteknik_api/models/product_product.py b/indoteknik_api/models/product_product.py
index 386ddb6a..f8869c7d 100644
--- a/indoteknik_api/models/product_product.py
+++ b/indoteknik_api/models/product_product.py
@@ -61,8 +61,9 @@ class ProductProduct(models.Model):
price_for = self.env.context.get('price_for', 'odoo')
pricelist = pricelist or self.env.context.get('user_pricelist')
default_price_tier = '1_v2'
-
- price_tier = pricelist.get_tier_level()
+ price_tier = []
+ if pricelist:
+ price_tier = pricelist.get_tier_level()
price_tier = price_tier if price_tier else default_price_tier
pricelist = self._get_pricelist_tier(price_tier)
diff --git a/indoteknik_api/models/product_template.py b/indoteknik_api/models/product_template.py
index 75899624..e46e44d3 100644
--- a/indoteknik_api/models/product_template.py
+++ b/indoteknik_api/models/product_template.py
@@ -7,6 +7,11 @@ class ProductTemplate(models.Model):
def api_single_response(self, product_template, with_detail=''):
product_pricelist_default_discount_id = self.env['ir.config_parameter'].get_param('product.pricelist.default_discount_id')
product_pricelist_default_discount_id = int(product_pricelist_default_discount_id)
+ voucher = self.get_voucher_pastihemat(product_template.x_manufacture.id)
+ newVoucherPastiHemat = {"min_purchase": voucher.min_purchase_amount or 0,
+ "discount_type": voucher.discount_type or '',
+ "discount_amount": voucher.discount_amount or 0,
+ "max_discount": voucher.max_discount_amount or 0,}
data = {
'id': product_template.id,
'image': self.env['ir.attachment'].api_image('product.template', 'image_128', product_template.id),
@@ -18,6 +23,7 @@ class ProductTemplate(models.Model):
'weight': product_template.weight,
'manufacture': self.api_manufacture(product_template),
'categories': self.api_categories(product_template),
+ "newVoucherPastiHemat": newVoucherPastiHemat
}
if with_detail != '':
diff --git a/indoteknik_api/models/res_users.py b/indoteknik_api/models/res_users.py
index 534898e1..52a044dc 100644
--- a/indoteknik_api/models/res_users.py
+++ b/indoteknik_api/models/res_users.py
@@ -14,6 +14,9 @@ class ResUsers(models.Model):
'manager': 2,
'director': 3
}
+ partner_tempo = False
+ if main_partner:
+ partner_tempo = main_partner.get_check_tempo_partner()
data = {
'id': res_user.id,
@@ -32,7 +35,8 @@ class ResUsers(models.Model):
'feature': {
'so_approval': main_partner.use_so_approval,
'only_ready_stock': main_partner.use_only_ready_stock
- }
+ },
+ 'partner_tempo': partner_tempo
}
return data
@@ -48,7 +52,7 @@ class ResUsers(models.Model):
'street': user.street or '',
'street2': user.street2 or '',
'city': None,
- 'state_id': None,
+ 'state_id': 0,
'district': None,
'sub_district': None,
'zip': user.zip or '',
@@ -59,6 +63,9 @@ class ResUsers(models.Model):
'rajaongkir_city_id': user.kecamatan_id.rajaongkir_id or 0,
'alamat_wajib_pajak': user.alamat_lengkap_text or None,
'alamat_bisnis': user.street or None,
+ 'longtitude': user.longtitude or None,
+ 'latitude': user.latitude or None,
+ 'address_map': user.address_map or None,
}
if user.state_id:
diff --git a/indoteknik_api/models/sale_order.py b/indoteknik_api/models/sale_order.py
index 725dbb4b..727379c5 100644
--- a/indoteknik_api/models/sale_order.py
+++ b/indoteknik_api/models/sale_order.py
@@ -33,12 +33,14 @@ class SaleOrder(models.Model):
'id': picking.id,
'name': picking.name,
'tracking_number': picking.delivery_tracking_no or '',
- 'delivered': picking.waybill_id.delivered or picking.driver_arrival_date != False,
+ 'delivered': picking.waybill_id.delivered or picking.driver_arrival_date != False or picking.sj_return_date != False,
})
if sale_order.state == 'cancel':
data['status'] = 'cancel'
if sale_order.state in ['draft', 'sent']:
data['status'] = 'draft'
+ if sale_order.is_continue_transaction:
+ data['status'] = 'waiting'
if sale_order.approval_status in ['pengajuan1', 'pengajuan2']:
data['status'] = 'waiting'
if sale_order.state == 'sale':
@@ -84,6 +86,12 @@ class SaleOrder(models.Model):
'subtotal': line.price_subtotal
}
product['quantity'] = line.product_uom_qty
+ product['available_quantity'] = line.product_available_quantity
+ for data_v2 in sale_order.fulfillment_line_v2:
+ product_v2 = self.env['product.product'].api_single_response(data_v2.product_id)
+ if product['id'] == product_v2['id']:
+ product['so_qty'] = data_v2.so_qty
+ product['reserved_stock_qty'] = data_v2.reserved_stock_qty
data_with_detail['products'].append(product)
for invoice in sale_order.invoice_ids:
if invoice.state == 'posted':
diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py
index 234ccebb..61bb4f83 100755
--- a/indoteknik_custom/__manifest__.py
+++ b/indoteknik_custom/__manifest__.py
@@ -145,6 +145,10 @@
'views/vendor_payment_term.xml',
'views/approval_unreserve.xml',
'views/vendor_approval.xml',
+ 'views/find_page.xml',
+ 'views/approval_retur_picking.xml',
+ 'views/form_vendor_approval_multi_approve.xml',
+ 'views/form_vendor_approval_multi_reject.xml',
'report/report.xml',
'report/report_banner_banner.xml',
'report/report_banner_banner2.xml',
diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py
index 13c48673..1e0cedc9 100755
--- a/indoteknik_custom/models/__init__.py
+++ b/indoteknik_custom/models/__init__.py
@@ -131,3 +131,7 @@ from . import approval_unreserve
from . import vendor_approval
from . import partner
from . import website_telegram
+from . import find_page
+from . import approval_retur_picking
+from . import va_multi_approve
+from . import va_multi_reject
diff --git a/indoteknik_custom/models/account_move_due_extension.py b/indoteknik_custom/models/account_move_due_extension.py
index 23f8888c..6fc58cdd 100644
--- a/indoteknik_custom/models/account_move_due_extension.py
+++ b/indoteknik_custom/models/account_move_due_extension.py
@@ -1,6 +1,6 @@
from odoo import models, api, fields
from odoo.exceptions import AccessError, UserError, ValidationError
-from datetime import timedelta, date
+from datetime import timedelta, date, datetime
import logging
_logger = logging.getLogger(__name__)
@@ -31,7 +31,8 @@ class DueExtension(models.Model):
('21', '21 Hari'),
], string='Day Extension', help='Menambah Due Date yang sudah limit dari hari ini', tracking=True)
counter = fields.Integer(string="Counter", compute='_compute_counter')
-
+ approve_by = fields.Many2one('res.users', string="Approve By", readonly=True)
+ date_approve = fields.Datetime(string="Date Approve", readonly=True)
def _compute_counter(self):
for due in self:
due.counter = due.partner_id.counter
@@ -96,6 +97,8 @@ class DueExtension(models.Model):
sales.action_confirm()
self.order_id.due_id = self.id
+ self.approve_by = self.env.user.id
+ self.date_approve = datetime.utcnow()
template = self.env.ref('indoteknik_custom.mail_template_due_extension_approve')
template.send_mail(self.id, force_send=True)
diff --git a/indoteknik_custom/models/approval_date_doc.py b/indoteknik_custom/models/approval_date_doc.py
index 441ada3d..751bae82 100644
--- a/indoteknik_custom/models/approval_date_doc.py
+++ b/indoteknik_custom/models/approval_date_doc.py
@@ -40,6 +40,7 @@ class ApprovalDateDoc(models.Model):
raise UserError("Hanya Accounting Yang Bisa Approve")
self.check_invoice_so_picking
self.picking_id.driver_departure_date = self.driver_departure_date
+ self.picking_id.date_doc_kirim = self.driver_departure_date
self.state = 'done'
self.approve_date = datetime.utcnow()
self.approve_by = self.env.user.id
diff --git a/indoteknik_custom/models/approval_retur_picking.py b/indoteknik_custom/models/approval_retur_picking.py
new file mode 100644
index 00000000..34c449a8
--- /dev/null
+++ b/indoteknik_custom/models/approval_retur_picking.py
@@ -0,0 +1,38 @@
+from odoo import models, fields
+import logging
+
+_logger = logging.getLogger(__name__)
+
+
+class ApprovalReturPicking(models.TransientModel):
+ _name = 'approval.retur.picking'
+ _description = 'Wizard to add Note Return'
+
+ note_return = fields.Text(string="Note Return")
+
+ def action_confirm_note_return(self):
+ picking_ids = self._context.get('picking_ids')
+ picking = self.env['stock.picking'].browse(picking_ids)
+
+ # Update the note_return field
+ picking.update({
+ 'approval_return_status': 'pengajuan1'
+ })
+
+ # Post a highlighted message to lognote
+ # picking.message_post(
+ # body=f"<div style='background-color: #fdf2e9; border: 1px solid #f5c6cb; padding: 10px;'>"
+ # f"<b>Note Return (Pinned):</b><br>{self.note_return}</div>",
+ # subtype_id=self.env.ref("mail.mt_note").id
+ # )
+
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': 'Notification',
+ 'message': 'Status pengajuan telah berubah',
+ 'next': {'type': 'ir.actions.act_window_close'},
+ }
+ }
+
diff --git a/indoteknik_custom/models/approval_unreserve.py b/indoteknik_custom/models/approval_unreserve.py
index 88409c37..d847ea37 100644
--- a/indoteknik_custom/models/approval_unreserve.py
+++ b/indoteknik_custom/models/approval_unreserve.py
@@ -31,12 +31,12 @@ class ApprovalUnreserve(models.Model):
if not self.picking_id:
raise ValidationError("Picking is required")
- stock_move = self.env['stock.move'].search([('picking_id', '=', self.picking_id.id), ('state', '=', 'assigned')])
+ stock_move = self.env['stock.move'].search([('picking_id', '=', self.picking_id.id), ('state', 'in', ['assigned', 'partially_available'])])
if not stock_move:
raise ValidationError("Picking is not found")
- for move in stock_move:
+ for move in stock_move:
self.approval_line.create({
'approval_id': self.id,
'move_id': move.id
@@ -68,13 +68,13 @@ class ApprovalUnreserve(models.Model):
if not move:
raise UserError("Product tidak ada di destination picking")
- qty_unreserve = line.unreserve_qty + move.forecast_availability
+ qty_unreserve = line.unreserve_qty + move.reserved_availability
if move.product_uom_qty < qty_unreserve:
raise UserError("Quantity yang di unreserve melebihi quantity yang ada")
def action_approve(self):
- if self.env.user.id != self.user_id.id:
+ if self.env.user.id != self.user_id.id and not self.env.user.has_group('indoteknik_custom.group_role_it'):
raise UserError("Hanya Sales nya yang bisa approve.")
if self.state != 'waiting_approval':
@@ -86,6 +86,7 @@ class ApprovalUnreserve(models.Model):
})
# Trigger the unreserve function
self._trigger_unreserve()
+ # self.picking_id.check_state_reserve()
def action_reject(self, reason):
if self.env.user.id != self.user_id.id:
diff --git a/indoteknik_custom/models/automatic_purchase.py b/indoteknik_custom/models/automatic_purchase.py
index 548115e6..4e96e6d4 100644
--- a/indoteknik_custom/models/automatic_purchase.py
+++ b/indoteknik_custom/models/automatic_purchase.py
@@ -401,36 +401,40 @@ class AutomaticPurchase(models.Model):
domain = [
('product_id', '=', line.product_id.id)
]
- sale = self.env['v.sales.outstanding'].search(domain, order='sale_order_create_date desc', limit=1)
-
- existing_match = self.env['automatic.purchase.sales.match'].search([
- ('automatic_purchase_id', '=', self.id),
- ('sale_id', '=', sale.sale_id.id),
- ('product_id', '=', sale.product_id.id),
- ])
+ sales = self.env['v.sales.outstanding'].search(domain, order='sale_order_create_date desc')
- price_so = self.env['sale.order.line'].search([
- ('id', '=', sale.sale_line_id.id),
- ])
-
- if existing_match:
- continue
+ for sale in sales:
+ existing_match = self.env['automatic.purchase.sales.match'].search([
+ ('automatic_purchase_id', '=', self.id),
+ ('sale_id', '=', sale.sale_id.id),
+ ('sale_line_id', '=', sale.sale_line_id.id),
+ ('product_id', '=', sale.product_id.id),
+ ])
- self.env['automatic.purchase.sales.match'].create([{
- 'automatic_purchase_id': self.id,
- 'sale_id': sale.sale_id.id,
- 'sale_line_id': sale.sale_line_id.id,
- 'picking_id': sale.picking_id.id,
- 'move_id': sale.move_id.id,
- 'partner_id': sale.partner_id.id,
- 'partner_invoice_id': sale.partner_invoice_id.id,
- 'salesperson_id': sale.salesperson_id.id,
- 'product_id': sale.product_id.id,
- 'qty_so': sale.outgoing,
- 'qty_po': line.qty_purchase,
- 'purchase_price': price_so.purchase_price,
- 'purchase_tax_id': price_so.purchase_tax_id.id if price_so.purchase_tax_id.id else None,
- }])
+ # price_so = self.env['sale.order.line'].search([
+ # ('id', '=', sale.sale_line_id.id),
+ # ])
+
+ if existing_match:
+ continue
+
+ self.env['automatic.purchase.sales.match'].create([{
+ 'automatic_purchase_id': self.id,
+ 'sale_id': sale.sale_id.id,
+ 'sale_line_id': sale.sale_line_id.id,
+ 'picking_id': sale.picking_id.id,
+ 'move_id': sale.move_id.id,
+ 'partner_id': sale.partner_id.id,
+ 'partner_invoice_id': sale.partner_invoice_id.id,
+ 'salesperson_id': sale.salesperson_id.id,
+ 'product_id': sale.product_id.id,
+ 'qty_so': sale.outgoing,
+ 'qty_po': line.qty_purchase,
+ 'purchase_price': sale.sale_line_id.purchase_price,
+ 'purchase_tax_id': sale.sale_line_id.purchase_tax_id.id if sale.sale_line_id.purchase_tax_id.id else None,
+ # 'purchase_price': price_so.purchase_price,
+ # 'purchase_tax_id': price_so.purchase_tax_id.id if price_so.purchase_tax_id.id else None,
+ }])
def _create_sync_purchasing_job(self, jobs):
date = datetime.utcnow()
@@ -462,9 +466,9 @@ class AutomaticPurchase(models.Model):
count = 0
for point in orderpoints:
# _logger.info('test %s' % point.product_id.name)
- if point.product_id.virtual_available > point.product_min_qty:
+ if point.product_id.qty_available_bandengan > point.product_min_qty:
continue
- qty_purchase = point.product_max_qty - point.product_id.virtual_available
+ qty_purchase = point.product_max_qty - point.product_id.qty_available_bandengan
po_line = self.env['purchase.order.line'].search([('product_id', '=', point.product_id.id), ('order_id.state', '=', 'done')], order='id desc', limit=1)
if self.vendor_id:
@@ -491,7 +495,7 @@ class AutomaticPurchase(models.Model):
'qty_purchase': qty_purchase,
'qty_min': point.product_min_qty,
'qty_max': point.product_max_qty,
- 'qty_available': point.product_id.virtual_available,
+ 'qty_available': point.product_id.qty_available_bandengan,
# 'partner_id': po_line.order_id.partner_id.id,
# 'last_price': po_line.price_unit,
'partner_id': vendor_id,
diff --git a/indoteknik_custom/models/crm_lead.py b/indoteknik_custom/models/crm_lead.py
index 9ffd607c..078d9810 100755
--- a/indoteknik_custom/models/crm_lead.py
+++ b/indoteknik_custom/models/crm_lead.py
@@ -2,6 +2,7 @@ from odoo import fields, models, api
import logging
import random
from odoo.exceptions import AccessError, UserError, ValidationError
+from datetime import datetime, timedelta
_logger = logging.getLogger(__name__)
@@ -30,6 +31,8 @@ class CrmLead(models.Model):
if rec.email_from == 'api.noreply@altama.co.id' and rec.name.startswith('INDOTEKNIK|ODOO|'):
rec.user_id = 20 # User ID: Nabila Rahmawati
+ if not rec.user_id:
+ rec.user_id = 2 # User ID: Sales
return rec
@api.onchange('user_id')
@@ -97,4 +100,92 @@ class CrmLead(models.Model):
lead.user_id = salesperson_id
-
+ def _update_pipeline(self, delta=48, limit=100):
+ # Get the current time
+ current_time = datetime.now()
+
+ # Calculate the time 24 hours ago
+ time_48_hours_ago = current_time - timedelta(hours=delta)
+
+ # Define the allowed states
+ allowed_states = ['sale', 'done']
+
+ # Search for sale orders with date_order greater than 24 hours ago and opportunity_id is null
+ orders = self.env['sale.order'].search([
+ ('write_date', '>=', time_48_hours_ago),
+ ('opportunity_id', '!=', False),
+ ('state', 'in', allowed_states)
+ ], limit=limit)
+ for order in orders:
+ order.opportunity_id.stage_id = 4
+ _logger.info('finish order stage pipeline %s' % order.id)
+
+ def _cancel_pipeline(self, delta=48, limit=100):
+ # Get the current time
+ current_time = datetime.now()
+
+ # Calculate the time 24 hours ago
+ time_48_hours_ago = current_time - timedelta(hours=delta)
+
+ # Define the allowed states
+ allowed_states = ['cancel']
+
+ # Search for sale orders with date_order greater than 24 hours ago and opportunity_id is null
+ orders = self.env['sale.order'].search([
+ ('write_date', '>=', time_48_hours_ago),
+ ('opportunity_id', '!=', False),
+ ('state', 'in', allowed_states)
+ ], limit=limit)
+ for order in orders:
+ order.opportunity_id.stage_id = 7
+ _logger.info('cancel order stage pipeline %s' % order.id)
+
+ def _convert_to_pipeline(self, delta=48, limit=100):
+ # Get the current time
+ current_time = datetime.now()
+
+ # Calculate the time 24 hours ago
+ time_48_hours_ago = current_time - timedelta(hours=delta)
+
+ # Define the allowed states
+ allowed_states = ['draft', 'done', 'sale', 'sent', 'cancel']
+
+ # Search for sale orders with date_order greater than 24 hours ago and opportunity_id is null
+ orders = self.env['sale.order'].search([
+ ('write_date', '>=', time_48_hours_ago),
+ ('opportunity_id', '=', False),
+ ('state', 'in', allowed_states)
+ ], limit=limit)
+ # stage
+ # 1 potensi baru, 2 proses quotation, 3 proses lain visit, 4 proses berhasil, 5 proses negosiasi, 7 tidak terpakai / gagal
+ for order in orders:
+ # stage_id = 2
+ if order.state == 'sale' or order.state == 'done':
+ stage_id = 4
+ elif order.state == 'sent':
+ stage_id = 5
+ elif order.state == 'cancel':
+ stage_id = 7
+ else:
+ stage_id = 2
+ crm_lead = self.env['crm.lead'].create([{
+ 'email_normalized': order.email,
+ 'name': order.name,
+ 'user_id': order.user_id.id,
+ 'company_id': 1,
+ 'type': 'opportunity',
+ 'priority': 0,
+ 'team_id': order.team_id.id,
+ 'stage_id': stage_id,
+ 'expected_revenue': order.amount_untaxed,
+ 'partner_id': order.partner_id.parent_id.id or order.partner_id.id,
+ 'contact_name': order.partner_id.name,
+ 'partner_name': order.partner_id.parent_id.name or order.partner_id.name,
+ 'phone': order.partner_id.mobile,
+ 'street': order.partner_id.street,
+ 'street2': order.partner_id.street2,
+ 'zip': order.partner_id.zip,
+ 'order_id': order.id
+ }])
+ order.opportunity_id = crm_lead.id
+ _logger.info('convert order to opportunity %s' % crm_lead.id)
diff --git a/indoteknik_custom/models/delivery_order.py b/indoteknik_custom/models/delivery_order.py
index be5fd2e0..3473197b 100644
--- a/indoteknik_custom/models/delivery_order.py
+++ b/indoteknik_custom/models/delivery_order.py
@@ -33,11 +33,13 @@ class DeliveryOrder(models.TransientModel):
picking.driver_id = self.env.uid
picking.delivery_tracking_no = line_tracking_no
+ if picking.driver_departure_date:
+ picking.sj_return_date = datetime.utcnow()
+
delivery_type = self.env['delivery.order.line'].get_delivery_type(picking.driver_departure_date, picking.driver_arrival_date)
if delivery_type == 'departure':
picking.driver_departure_date = current_time
elif delivery_type == 'arrival':
- picking.driver_arrival_date = current_time
sale_order = False
if picking.origin:
@@ -103,14 +105,16 @@ class DeliveryOrderLine(models.TransientModel):
delivery_type = self.get_delivery_type(picking.driver_departure_date, picking.driver_arrival_date)
if delivery_type != 'departure':
- self.departure_date = picking.driver_departure_date.astimezone(timezone('Asia/Jakarta')).strftime('%Y-%m-%d %H:%M:%S')
+ if picking.driver_departure_date:
+ self.departure_date = picking.driver_departure_date.astimezone(timezone('Asia/Jakarta')).strftime('%Y-%m-%d %H:%M:%S')
if delivery_type == 'departure':
self.departure_date = current_time
elif delivery_type == 'arrival':
self.arrival_date = current_time
else:
- self.arrival_date = picking.driver_arrival_date.astimezone(timezone('Asia/Jakarta')).strftime('%Y-%m-%d %H:%M:%S')
+ if picking.driver_arrival_date:
+ self.arrival_date = picking.driver_arrival_date.astimezone(timezone('Asia/Jakarta')).strftime('%Y-%m-%d %H:%M:%S')
else:
raise UserError('Nomor DO tidak ditemukan')
diff --git a/indoteknik_custom/models/find_page.py b/indoteknik_custom/models/find_page.py
new file mode 100644
index 00000000..467e30d1
--- /dev/null
+++ b/indoteknik_custom/models/find_page.py
@@ -0,0 +1,121 @@
+from odoo import fields, models, api, tools, _
+import logging
+import re
+import pysolr
+
+_logger = logging.getLogger(__name__)
+_cat_brand_solr = pysolr.Solr('http://10.148.0.5:8983/solr/url_category_brand/', always_commit=True, timeout=30)
+# _cat_brand_solr_dev = pysolr.Solr('http://127.0.0.1:8983/solr/url_category_brand/', always_commit=True, timeout=30)
+
+
+class BrandProductCategory(models.Model):
+ _name = 'v.brand.product.category'
+ _auto = False
+ _rec_name = 'brand_id'
+ brand_id = fields.Many2one('x_manufactures', string='Brand')
+ category_id = fields.Many2one('product.public.category', string='Category')
+
+ def init(self):
+ tools.drop_view_if_exists(self.env.cr, self._table)
+ self.env.cr.execute("""
+ CREATE OR REPLACE VIEW %s
+ AS select row_number() over(order by pt.x_manufacture) as id,
+ pt.x_manufacture as brand_id,
+ ppcptr.product_public_category_id as category_id
+ from product_template pt
+ join product_public_category_product_template_rel ppcptr on ppcptr.product_template_id = pt.id
+ join x_manufactures xm on xm.id = pt.x_manufacture
+ group by x_manufacture, ppcptr.product_public_category_id
+ """ % self._table)
+
+
+class FindPage(models.Model):
+ _name = 'web.find.page'
+ _inherit = ['mail.thread']
+ _rec_name = 'url'
+
+ brand_id = fields.Many2one('x_manufactures', string='Brand')
+ category_id = fields.Many2one('product.public.category', string='Category', help='Bisa semua level Category')
+ url = fields.Char(string='Url')
+
+ def _sync_to_solr(self, limit=10000):
+ urls = self.env['web.find.page'].search([])
+ documents = []
+ catch = {}
+ for url in urls:
+ try:
+ document = {
+ 'id': url.id,
+ 'category_id_i': url.category_id.id,
+ 'brand_id_i': url.brand_id.id,
+ 'url_s': url.url
+ }
+ documents.append(document)
+ catch = document
+ except Exception as e:
+ _logger.error('Failed to add document to Solr URL Category Brand: %s', e)
+ _logger.error('Document Data: %s', catch)
+ _cat_brand_solr.add(documents)
+ return True
+
+ def _get_category_hierarchy(self, category):
+ categories = []
+ current_category = category
+ while current_category:
+ categories.insert(0, current_category)
+ current_category = current_category.parent_id
+ return categories
+
+ def _generate_url(self):
+ categories = self.env['v.brand.product.category'].search([])
+
+ list_url = []
+ for category in categories:
+ category_hierarchy = self._get_category_hierarchy(category.category_id)
+
+ for level, cat in enumerate(reversed(category_hierarchy), start=1):
+ list_url.append(self._generate_mod_url(cat, category.brand_id))
+ # print(f"Level {level}: {cat.name} {category.brand_id.x_name}")
+
+ unique_list = []
+ for item in list_url:
+ if item not in unique_list:
+ unique_list.append(item)
+ count = 0
+ for item in unique_list:
+ self._create_find_page(item['url'], item['category_id'], item['brand_id'])
+ count += 1
+ print(f"Total categories processed: {count}")
+
+ def _create_find_page(self, url, category_id, brand_id):
+ param = {
+ 'url': url,
+ 'category_id': category_id,
+ 'brand_id': brand_id,
+ }
+ find_page = self.env['web.find.page'].create(param)
+ _logger.info('Created Web Find Page %s' % find_page.id)
+
+ def _generate_mod_url(self, category, brand):
+ # generate_url = 'https://indoteknik.com/shop/find/category-brand' example
+ cleaned_category = re.sub(r'[^\w\s]', '', category.name)
+ cleaned_brand = re.sub(r'[^\w\s]', '', brand.x_name)
+ cleaned_combined = cleaned_category+' '+cleaned_brand
+ cleaned_combined = cleaned_combined.replace(' ', '-')
+ cleaned_combined = cleaned_combined.replace('--', '-')
+ url = 'https://indoteknik.com/shop/find/'+cleaned_combined
+ url = url.lower()
+ result = {
+ 'url': url,
+ 'category_id': category.id,
+ 'brand_id': brand.id
+ }
+ # print(url)
+ # param = {
+ # 'brand_id': brand.id,
+ # 'category_id': category.id,
+ # 'url':''
+ # }
+ # self.env['web.find.page'].create()
+ # print(1)
+ return result
diff --git a/indoteknik_custom/models/logbook_sj.py b/indoteknik_custom/models/logbook_sj.py
index f84619ad..9f349882 100644
--- a/indoteknik_custom/models/logbook_sj.py
+++ b/indoteknik_custom/models/logbook_sj.py
@@ -101,14 +101,16 @@ class LogbookSJLine(models.TransientModel):
delivery_type = self.get_delivery_type(picking.driver_departure_date, picking.driver_arrival_date)
if delivery_type != 'departure':
- self.departure_date = picking.driver_departure_date.astimezone(timezone('Asia/Jakarta')).strftime('%Y-%m-%d %H:%M:%S')
+ if picking.driver_departure_date:
+ self.departure_date = picking.driver_departure_date.astimezone(timezone('Asia/Jakarta')).strftime('%Y-%m-%d %H:%M:%S')
if delivery_type == 'departure':
self.departure_date = current_time
elif delivery_type == 'arrival':
self.arrival_date = current_time
else:
- self.arrival_date = picking.driver_arrival_date.astimezone(timezone('Asia/Jakarta')).strftime('%Y-%m-%d %H:%M:%S')
+ if picking.driver_arrival_date:
+ self.arrival_date = picking.driver_arrival_date.astimezone(timezone('Asia/Jakarta')).strftime('%Y-%m-%d %H:%M:%S')
else:
raise UserError('Nomor DO tidak ditemukan')
diff --git a/indoteknik_custom/models/product_template.py b/indoteknik_custom/models/product_template.py
index 031d1b5b..4d186568 100755
--- a/indoteknik_custom/models/product_template.py
+++ b/indoteknik_custom/models/product_template.py
@@ -1,10 +1,11 @@
-from odoo import fields, models, api
+from odoo import fields, models, api, tools, _
from datetime import datetime, timedelta, date
from odoo.exceptions import UserError
import logging
import requests
import json
import re
+from bs4 import BeautifulSoup
_logger = logging.getLogger(__name__)
@@ -60,6 +61,7 @@ class ProductTemplate(models.Model):
sni = fields.Boolean(string='SNI')
tkdn = fields.Boolean(string='TKDN')
short_spesification = fields.Char(string='Short Spesification')
+ merchandise_ok = fields.Boolean(string='Product Promotion')
@api.constrains('name', 'internal_reference', 'x_manufacture')
def required_public_categ_ids(self):
@@ -363,6 +365,7 @@ class ProductProduct(models.Model):
specification = fields.Char(string='Specification')
material = fields.Char(string='Material')
qty_onhand_bandengan = fields.Float(string='Onhand BU', compute='_get_qty_onhand_bandengan')
+ clean_website_description = fields.Char(string='Clean Website Description', compute='_get_clean_website_description')
qty_incoming_bandengan = fields.Float(string='Incoming BU', compute='_get_qty_incoming_bandengan')
qty_outgoing_bandengan = fields.Float(string='Outgoing BU', compute='_get_qty_outgoing_bandengan')
qty_available_bandengan = fields.Float(string='Available BU', compute='_get_qty_available_bandengan')
@@ -372,6 +375,15 @@ class ProductProduct(models.Model):
is_edited = fields.Boolean(string='Is Edited')
qty_sold = fields.Float(string='Sold Quantity', compute='_get_qty_sold')
short_spesification = fields.Char(string='Short Spesification')
+ max_qty_reorder = fields.Float(string='Max Qty Reorder', compute='_get_max_qty_reordering_rule')
+ qty_rpo = fields.Float(string='Qty RPO', compute='_get_qty_rpo')
+ plafon_qty = fields.Float(string='Max Plafon', compute='_get_plafon_qty_product')
+ merchandise_ok = fields.Boolean(string='Product Promotion')
+
+ def _get_clean_website_description(self):
+ for rec in self:
+ cleaned_desc = BeautifulSoup(self.website_description or '', "html.parser").get_text()
+ rec.clean_website_description = cleaned_desc
@api.constrains('name', 'internal_reference', 'x_manufacture')
def required_public_categ_ids(self):
@@ -382,22 +394,46 @@ class ProductProduct(models.Model):
@api.constrains('active')
def archive_product(self):
for product in self:
+ if self.env.context.get('skip_unpublished_constraint'):
+ continue # Mencegah looping saat dipanggil dari metode lain
+
product_template = product.product_tmpl_id
variants = product_template.product_variant_ids
- if product_template.active and product.active:
- if not product.active and len(variants) == 1:
- product_template.with_context(skip_active_constraint=True).active = False
- product_template.unpublished = True
- elif not product.active and len(variants) > 1:
+ if len(variants) == 1:
+ # Jika hanya ada satu varian, atur status `unpublished` berdasarkan `active`
+ product_template.with_context(skip_unpublished_constraint=True).unpublished = not product.active
+ product.with_context(skip_unpublished_constraint=True).unpublished = not product.active
+ else:
+ if product.active:
+ product.with_context(skip_unpublished_constraint=True).unpublished = False
+ product_template.with_context(skip_unpublished_constraint=True).unpublished = any(variant.active for variant in variants)
+ else:
+ product.with_context(skip_unpublished_constraint=True).unpublished = True
all_inactive = all(not variant.active for variant in variants)
- if all_inactive:
- product_template.with_context(skip_active_constraint=True).active = False
- product_template.unpublished = True
- else:
- continue
- if any(variant.active for variant in variants):
- product_template.unpublished = False
+ product_template.with_context(skip_unpublished_constraint=True).unpublished = all_inactive
+
+ @api.constrains('unpublished')
+ def archive_product_unpublished(self):
+ for product in self:
+ if self.env.context.get('skip_active_constraint'):
+ continue # Mencegah looping saat dipanggil dari metode lain
+
+ product_template = product.product_tmpl_id
+ variants = product_template.product_variant_ids
+
+ if len(variants) == 1:
+ # Jika hanya ada satu varian, atur status `unpublished` pada template, tetapi biarkan `active` tetap True
+ product_template.with_context(skip_active_constraint=True).unpublished = product.unpublished
+ else:
+ if not product.unpublished:
+ # Jika `unpublished` adalah False, pastikan `active` tetap True
+ product.with_context(skip_active_constraint=True).active = True
+ product_template.with_context(skip_active_constraint=True).active = any(not variant.unpublished for variant in variants)
+ else:
+ # Jika `unpublished` adalah True, atur template hanya jika semua varian di-unpublished
+ all_unpublished = all(variant.unpublished for variant in variants)
+ product_template.with_context(skip_active_constraint=True).active = not all_unpublished
def update_internal_reference_variants(self, limit=100):
variants = self.env['product.product'].search([
@@ -448,23 +484,38 @@ class ProductProduct(models.Model):
def _get_qty_incoming_bandengan(self):
for product in self:
- qty_incoming = self.env['stock.move'].search([
- ('product_id', '=', product.id),
- ('location_dest_id', 'in', [57, 83]),
- ('state', 'not in', ['done', 'cancel'])
- ])
- qty = sum(qty_incoming.mapped('product_uom_qty'))
+ qty = self.env['v.move.outstanding'].read_group(
+ domain=[
+ ('product_id', '=', product.id),
+ ('location_dest_id', 'in', [57, 83]),
+ ],
+ fields=['qty_need'],
+ groupby=[]
+ )[0].get('qty_need', 0.0)
+ product.qty_incoming_bandengan = qty
+
+ def _get_qty_incoming_bandengan_with_exclude(self):
+ for product in self:
+ qty = self.env['v.move.outstanding'].read_group(
+ domain=[
+ ('product_id', '=', product.id),
+ ('location_dest_id', 'in', [57, 83]),
+ ],
+ fields=['qty_need'],
+ groupby=[]
+ )[0].get('qty_need', 0.0)
product.qty_incoming_bandengan = qty
def _get_qty_outgoing_bandengan(self):
for product in self:
- qty_incoming = self.env['stock.move'].search([
- ('product_id', '=', product.id),
- ('location_dest_id', '=', 5),
- ('location_id', 'in', [57, 83]),
- ('state', 'not in', ['done', 'cancel'])
- ])
- qty = sum(qty_incoming.mapped('product_uom_qty'))
+ qty = self.env['v.move.outstanding'].read_group(
+ domain=[
+ ('product_id', '=', product.id),
+ ('location_id', 'in', [57, 83]),
+ ],
+ fields=['qty_need'],
+ groupby=[]
+ )[0].get('qty_need', 0.0)
product.qty_outgoing_bandengan = qty
def _get_qty_onhand_bandengan(self):
@@ -479,13 +530,41 @@ class ProductProduct(models.Model):
def _get_qty_available_bandengan(self):
for product in self:
qty_available = product.qty_incoming_bandengan + product.qty_onhand_bandengan - product.qty_outgoing_bandengan
- product.qty_available_bandengan = qty_available
+ product.qty_available_bandengan = qty_available or 0
def _get_qty_free_bandengan(self):
for product in self:
qty_free = product.qty_onhand_bandengan - product.qty_outgoing_bandengan
product.qty_free_bandengan = qty_free
-
+
+ def _get_max_qty_reordering_rule(self):
+ for product in self:
+ reordering = self.env['stock.warehouse.orderpoint'].search([
+ ('product_id', '=', product.id)
+ ], limit=1)
+ if not reordering:
+ product.max_qty_reorder = 0
+ else:
+ product.max_qty_reorder = reordering.product_max_qty
+
+ def _get_qty_rpo(self):
+ for product in self:
+ rpo = self.env['v.requisition.match.po'].search([
+ ('product_id', '=', product.id)
+ ], limit=1)
+ if not rpo:
+ product.qty_rpo = 0
+ else:
+ product.qty_rpo = rpo.qty_rpo
+
+ def _get_plafon_qty_product(self):
+ for product in self:
+ qty_available = product.qty_available_bandengan
+ max_qty = product.max_qty_reorder
+ qty_rpo = product.qty_rpo
+ product.plafon_qty = max_qty - qty_available + qty_rpo
+
+
# def write(self, vals):
# if 'solr_flag' not in vals:
# for variant in self:
@@ -526,3 +605,34 @@ class ProductProduct(models.Model):
('end_date', '>=', current_time)
], limit=1)
return pricelist
+
+
+class OutstandingMove(models.Model):
+ _name = 'v.move.outstanding'
+ _auto = False
+ _rec_name = 'id'
+
+ id = fields.Integer(string='ID')
+ product_id = fields.Many2one('product.product', string='Product')
+ reference = fields.Char(string='Reference', help='Nomor Dokumen terkait')
+ qty_need = fields.Float(string='Qty Need', help='Qty yang akan outgoing / incoming')
+ location_id = fields.Many2one('stock.location', string='Location', help='Lokasi asal')
+ location_dest_id = fields.Many2one('stock.location', string='Location To', help='Lokasi tujuan')
+
+ def init(self):
+ # where clause 'state in' follow the origin of outgoing and incoming odoo
+ tools.drop_view_if_exists(self.env.cr, self._table)
+ self.env.cr.execute("""
+ CREATE OR REPLACE VIEW %s AS
+ select sm.id, sm.reference, sm.product_id,
+ sm.product_uom_qty as qty_need,
+ sm.location_id, sm.location_dest_id
+ from stock_move sm
+ where 1=1
+ and sm.state in(
+ 'waiting',
+ 'confirmed',
+ 'assigned',
+ 'partially_available'
+ )
+ """ % self._table)
diff --git a/indoteknik_custom/models/promotion/sale_order.py b/indoteknik_custom/models/promotion/sale_order.py
index cb9a6f92..1c31d060 100644
--- a/indoteknik_custom/models/promotion/sale_order.py
+++ b/indoteknik_custom/models/promotion/sale_order.py
@@ -6,6 +6,17 @@ class SaleOrder(models.Model):
order_promotion_ids = fields.One2many('sale.order.promotion', 'order_id', 'Promotions')
+ def add_free_product(self, promotions):
+ for promotion in promotions:
+ program_line = self.env['promotion.program.line'].browse(promotion['program_line_id'])
+ for free_product in program_line.free_product_ids:
+ if free_product.product_id.merchandise_ok:
+ self.env['sale.order.line'].create({
+ 'order_id': self.id,
+ 'name': f"Free Product {free_product.product_id.display_name} Quantity ({free_product.qty})",
+ 'display_type': 'line_note'
+ })
+
def apply_promotion_program(self):
userdata = {
'user_id': self.partner_id.user_id.id,
diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py
index 08408506..9388ae4c 100755
--- a/indoteknik_custom/models/purchase_order.py
+++ b/indoteknik_custom/models/purchase_order.py
@@ -68,8 +68,12 @@ class PurchaseOrder(models.Model):
], string='Printed?', copy=False, tracking=True)
date_done_picking = fields.Datetime(string='Date Done Picking', compute='get_date_done')
bills_dp_id = fields.Many2one('account.move', string='Bills DP')
+ bills_pelunasan_id = fields.Many2one('account.move', string='Bills Pelunasan')
grand_total = fields.Monetary(string='Grand Total', help='Amount total + amount delivery', compute='_compute_grand_total')
total_margin_match = fields.Float(string='Total Margin Match', compute='_compute_total_margin_match')
+ approve_by = fields.Many2one('res.users', string='Approve By')
+ exclude_incoming = fields.Boolean(string='Exclude Incoming', default=False,
+ help='Centang jika tidak mau masuk perhitungan Incoming Qty')
def _compute_total_margin_match(self):
for purchase in self:
@@ -88,6 +92,89 @@ class PurchaseOrder(models.Model):
else:
order.grand_total = order.amount_total
+ def create_bill_pelunasan(self):
+ if not self.env.user.is_accounting:
+ raise UserError('Hanya Accounting yang bisa bikin bill dp')
+
+ # Check for existing vendor bills with the same reference and partner
+ existing_bill = self.env['account.move'].search([
+ ('ref', '=', self.name),
+ ('partner_id', '=', self.partner_id.id),
+ ('move_type', '=', 'in_invoice'),
+ ('state', 'not in', ['cancel', 'posted'])
+ ], limit=1)
+
+ if existing_bill:
+ raise UserError(_('Duplicated vendor reference detected. You probably encoded twice the same vendor bill/credit note: %s') % existing_bill.name)
+
+ current_date = datetime.utcnow()
+ data_bills = {
+ 'partner_id': self.partner_id.id,
+ 'partner_shipping_id': self.partner_id.id,
+ 'ref': self.name,
+ 'invoice_date': current_date,
+ 'date': current_date,
+ 'move_type': 'in_invoice'
+ }
+
+ bills = self.env['account.move'].create([data_bills])
+
+ product_dp = self.env['product.product'].browse(229625)
+
+ data_line_bills = []
+
+ move_line = self.env['account.move.line'].search([
+ ('move_id', '=', self.bills_dp_id.id),
+ ('product_id', '=', product_dp.id),
+ ])
+
+ bills.message_post(
+ body=f"<div>"
+ f"<b>DP :</b><br>{move_line.price_unit}</div>",
+ subtype_id=self.env.ref("mail.mt_note").id
+ )
+
+ data_line_bills.append({
+ 'move_id': bills.id,
+ 'product_id': product_dp.id, # product down payment
+ 'name': '[IT.121456] Down Payment', # product down payment
+ 'account_id': 401, # Uang Muka persediaan barang dagang
+ # 'price_unit': move_line.price_unit,
+ 'quantity': -1,
+ 'product_uom_id': 1,
+ 'tax_ids': [(5, 0, 0)] + [(4, tax.id) for tax in product_dp.taxes_id],
+ })
+
+ for line in self.order_line:
+ if line.product_id:
+ data_line_bills.append({
+ 'move_id': bills.id,
+ 'product_id': line.product_id.id,
+ 'name': self.name + ": " + line.product_id.display_name,
+ 'account_id': 439, # Uang Muka persediaan barang dagang
+ 'quantity': line.product_qty,
+ # 'price_unit': line.price_subtotal,
+ 'product_uom_id': line.product_uom.id,
+ 'tax_ids': [(5, 0, 0)] + [(4, tax.id) for tax in line.taxes_id],
+ 'purchase_line_id': line.id,
+ 'purchase_order_id': line[0].order_id.id,
+ })
+
+ bills_line = self.env['account.move.line'].create(data_line_bills)
+
+ self.bills_pelunasan_id = bills.id
+
+ return {
+ 'name': _('Account Move'),
+ 'view_mode': 'tree,form',
+ 'res_model': 'account.move',
+ 'target': 'current',
+ 'type': 'ir.actions.act_window',
+ 'domain': [('id', '=', bills.id)]
+ }
+
+
+
def create_bill_dp(self):
if not self.env.user.is_accounting:
raise UserError('Hanya Accounting yang bisa bikin bill dp')
@@ -498,20 +585,39 @@ class PurchaseOrder(models.Model):
picking.scheduled_date = self.date_planned
picking.date_deadline = self.date_planned
+ def _check_qty_plafon_product(self):
+ for line in self.order_line:
+ if not line.product_id:
+ continue
+ # test = line.product_uom_qty
+ # test2 = line.product_id.plafon_qty
+ # test3 = test2 + line.product_uom_qty
+ if line.product_uom_qty > line.product_id.plafon_qty + line.product_uom_qty and not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
+ raise UserError('Product '+line.product_id.name+' melebihi plafon, harus Approval MD')
+
+ def check_different_vendor_so_po(self):
+ vendor_po = self.partner_id.id
+ for line in self.order_line:
+ if not line.so_line_id:
+ continue
+ if line.so_line_id.vendor_id.id != vendor_po and not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
+ raise UserError("Produk "+line.product_id.name+" memiliki vendor berbeda dengan SO (Vendor PO: "+str(self.partner_id.name)+", Vendor SO: "+str(line.so_line_id.vendor_id.name)+")")
+
def button_confirm(self):
res = super(PurchaseOrder, self).button_confirm()
current_time = datetime.now()
self.check_ppn_mix()
+ self.check_different_vendor_so_po()
# self.check_data_vendor()
if self.amount_untaxed >= 50000000 and not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
raise UserError("Hanya Merchandiser yang bisa approve")
- if self.total_percent_margin < self.total_so_percent_margin and not self.env.user.is_purchasing_manager and not self.env.user.is_leader:
- raise UserError("Beda Margin dengan Sales, harus approval Manager")
+ if self.total_percent_margin < self.total_so_percent_margin and not self.env.user.has_group('indoteknik_custom.group_role_merchandiser') and not self.env.user.is_leader:
+ raise UserError("Beda Margin dengan Sales, harus approval Merchandise")
if not self.from_apo:
- if not self.sale_order_id and not self.env.user.is_purchasing_manager and not self.env.user.is_leader:
- raise UserError("Tidak ada link dengan SO, harus approval Manager")
+ if not self.matches_so and not self.env.user.has_group('indoteknik_custom.group_role_merchandiser') and not self.env.user.is_leader:
+ raise UserError("Tidak ada link dengan SO, harus approval Merchandise")
send_email = False
self.add_product_to_pricelist()
@@ -533,7 +639,7 @@ class PurchaseOrder(models.Model):
self._send_mail()
if self.revisi_po:
- delta_time = current_time - timedelta(days=2)
+ delta_time = current_time - timedelta(days=1)
delta_time = delta_time.strftime('%Y-%m-%d %H:%M:%S')
self.date_approve = delta_time
@@ -541,6 +647,7 @@ class PurchaseOrder(models.Model):
self.po_status = 'menunggu'
self.calculate_line_no()
+ self.approve_by = self.env.user.id
# override date planned added with two days
leadtime = self.partner_id.leadtime
@@ -549,7 +656,8 @@ class PurchaseOrder(models.Model):
self.date_planned = delta_time
self.date_deadline_ref_date_planned()
self.unlink_purchasing_job_state()
-
+
+ self._check_qty_plafon_product()
return res
@@ -637,14 +745,63 @@ class PurchaseOrder(models.Model):
template.send_mail(self.id, force_send=True)
def po_approve(self):
- if self.amount_untaxed >= 50000000 and not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
- raise UserError("Hanya Merchandiser yang bisa approve")
- if self.env.user.is_leader or self.env.user.is_purchasing_manager:
+ greater_than_plafon, message = self._get_msg_plafon_qty()
+ different_vendor_message = self.check_different_vendor_so() # Panggil fungsi check_different_vendor_so
+
+ if self.env.user.is_leader or self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
raise UserError("Bisa langsung Confirm")
- elif self.total_percent_margin == self.total_so_percent_margin and self.sale_order_id:
+ elif self.total_percent_margin == self.total_so_percent_margin and self.matches_so and not greater_than_plafon and not different_vendor_message:
raise UserError("Bisa langsung Confirm")
else:
+ reason = ''
self.approval_status = 'pengajuan1'
+ if self.amount_untaxed >= 50000000:
+ reason = 'above 50jt, '
+ if self.total_percent_margin < self.total_so_percent_margin:
+ reason += 'diff margin, '
+ if not self.from_apo and not self.matches_so:
+ reason += 'not link with pj and reorder, '
+ if not self.matches_so:
+ reason += 'not link with so, '
+ # Check Plafon Qty and Get Message every Line Product
+ if greater_than_plafon:
+ reason += message
+ # Check for Different Vendor Message
+ if different_vendor_message:
+ reason += different_vendor_message
+
+ # Post a highlighted message to lognote
+ self.message_post(
+ body=f"<div style='background-color: #fdf2e9; border: 1px solid #f5c6cb; padding: 10px;'>"
+ f"<b>Note (Pinned):</b><br>{reason}</div>",
+ subtype_id=self.env.ref("mail.mt_note").id
+ )
+
+
+ def check_different_vendor_so(self):
+ vendor_po = self.partner_id.id
+ message = ''
+ for line in self.order_line:
+ if not line.so_line_id:
+ continue
+ if line.so_line_id.vendor_id.id != vendor_po:
+ product_code = line.product_id.display_name or 'Unknown'
+ message += (f"Produk {product_code} memiliki vendor berbeda dengan SO "
+ f"(Vendor PO: {self.partner_id.name}, Vendor SO: {line.so_line_id.vendor_id.name}), ")
+ return message if message else None
+
+ def _get_msg_plafon_qty(self):
+ message = ''
+ greater_than_plafon = False
+ for line in self.order_line:
+ if not line.product_id:
+ continue
+ if line.product_uom_qty > line.product_id.plafon_qty:
+ message = (message + '\n'+line.product_id.default_code + ' melebihi plafon ('
+ + str(line.product_id.plafon_qty) + ') vs Qty PO ('+str(line.product_uom_qty)+')'
+ + ', ')
+ greater_than_plafon = True
+ return greater_than_plafon, message
def re_calculate(self):
if self.from_apo:
diff --git a/indoteknik_custom/models/purchase_order_line.py b/indoteknik_custom/models/purchase_order_line.py
index 7af84b48..9e7d7e81 100755
--- a/indoteknik_custom/models/purchase_order_line.py
+++ b/indoteknik_custom/models/purchase_order_line.py
@@ -3,6 +3,7 @@ from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
import logging
from dateutil.relativedelta import relativedelta
+from bs4 import BeautifulSoup
from datetime import datetime
_logger = logging.getLogger(__name__)
@@ -37,11 +38,18 @@ class PurchaseOrderLine(models.Model):
is_ltc = fields.Boolean(string='Sudah di LTC', default=False, help='centang ini jika barang sudah di LTC')
note = fields.Char(string='Note')
sale_automatic_id = fields.Many2one('sale.order', string='SO')
-
+ image_small = fields.Binary("Product Image", related="product_id.image_1920")
+ clean_website_description_product = fields.Char(string='Clean Website Description Product', compute='_get_clean_website_description_product')
qty_reserved = fields.Float(string='Qty Reserved', compute='_compute_qty_reserved')
delete_line = fields.Boolean(string='Delete', default=False, help='centang ini jika anda ingin menghapus line ini')
is_edit_product_qty = fields.Boolean(string='Is Edit Product Qty', compute='_compute_is_edit_product_qty')
+ def _get_clean_website_description_product(self):
+ for line in self:
+ description = line.product_id.website_description
+ description_clean = BeautifulSoup(description or '', "html.parser").get_text()
+ line.clean_website_description_product = description_clean
+
def _prepare_stock_move_vals(self, picking, price_unit, product_uom_qty, product_uom):
self.ensure_one()
product = self.product_id.with_context(lang=self.order_id.dest_address_id.lang or self.env.user.lang)
diff --git a/indoteknik_custom/models/purchasing_job.py b/indoteknik_custom/models/purchasing_job.py
index 6e4f239d..4efb0cd4 100644
--- a/indoteknik_custom/models/purchasing_job.py
+++ b/indoteknik_custom/models/purchasing_job.py
@@ -43,73 +43,39 @@ class PurchasingJob(models.Model):
def init(self):
tools.drop_view_if_exists(self.env.cr, self._table)
self.env.cr.execute("""
- CREATE OR REPLACE VIEW %s AS (
- WITH latest_purchase_orders AS (
- SELECT
- pol.product_id,
- po.user_id,
- ROW_NUMBER() OVER (PARTITION BY po.partner_id ORDER BY po.create_date DESC) AS order_rank
- FROM purchase_order po
- RIGHT JOIN purchase_order_line pol ON pol.order_id = po.id
- LEFT JOIN res_partner rp ON rp.id = po.partner_id
- ),
- random_user_ids AS (
- SELECT DISTINCT
- CASE
- WHEN vendor_id = 5571 THEN 27
- WHEN vendor_id = 9688 THEN 397
- WHEN vendor_id = 35475 THEN 397
- WHEN vendor_id = 29712 THEN 397
- ELSE (CASE WHEN random() < 0.5 THEN 397 ELSE 1036 END)
- END AS user_id,
- vendor_id
- FROM (
- SELECT
- sol.vendor_id
- FROM v_sales_outstanding vso
- LEFT JOIN sale_order_line sol ON sol.id = vso.sale_line_id
- ) AS sub
- WHERE sub.vendor_id IS NOT NULL
- )
- SELECT
- pmp.product_id AS id,
- pmp.product_id,
- sub.vendor_id,
- pmp.brand,
- pmp.item_code,
- pmp.product,
- MAX(pmp.onhand) AS onhand,
- MAX(pmp.incoming) AS incoming,
- MAX(pmp.outgoing) AS outgoing,
- pmp.action,
- MAX(pjs.status_apo) AS status_apo,
- MAX(pjs.note) AS note,
- ru.user_id AS purchase_representative_id
- FROM v_procurement_monitoring_by_product pmp
+ CREATE OR REPLACE VIEW %s
+ AS SELECT pmp.product_id AS id,
+ pmp.product_id,
+ sub.vendor_id,
+ pmp.brand,
+ pmp.item_code,
+ pmp.product,
+ max(pmp.onhand) AS onhand,
+ max(pmp.incoming) AS incoming,
+ max(pmp.outgoing) AS outgoing,
+ pmp.action,
+ max(pjs.status_apo::text) AS status_apo,
+ max(pjs.note::text) AS note,
+ CASE
+ WHEN pmp.brand IN ('Tekiro', 'RYU', 'Rexco') THEN 27
+ WHEN sub.vendor_id = 9688 THEN 397
+ WHEN sub.vendor_id = 35475 THEN 397
+ WHEN sub.vendor_id = 29712 THEN 397
+ ELSE 1036
+ END AS purchase_representative_id
+ FROM v_procurement_monitoring_by_product pmp
LEFT JOIN purchasing_job_state pjs ON pjs.purchasing_job_id = pmp.product_id
- LEFT JOIN (
- SELECT
- vso.product_id,
- sol.vendor_id
+ LEFT JOIN (
+ SELECT vso.product_id,
+ max(sol.vendor_id) as vendor_id
FROM v_sales_outstanding vso
LEFT JOIN sale_order_line sol ON sol.id = vso.sale_line_id
- ) AS sub ON sub.product_id = pmp.product_id
- LEFT JOIN latest_purchase_orders po ON po.product_id = pmp.product_id
- LEFT JOIN random_user_ids ru ON ru.vendor_id = sub.vendor_id OR (ru.vendor_id IS NULL AND sub.vendor_id != 9688)
- WHERE pmp.action = 'kurang'
- AND sub.vendor_id IS NOT NULL
- GROUP BY
- pmp.product_id,
- pmp.brand,
- pmp.item_code,
- pmp.product,
- pmp.action,
- sub.vendor_id,
- ru.user_id
- )
+ group by vso.product_id
+ ) sub ON sub.product_id = pmp.product_id
+ WHERE pmp.action = 'kurang'::text AND sub.vendor_id IS NOT NULL
+ GROUP BY pmp.product_id, pmp.brand, pmp.item_code, pmp.product, pmp.action, sub.vendor_id;
""" % self._table)
-
def open_form_multi_generate_request_po(self):
action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_purchasing_job_multi_update')
action['context'] = {
diff --git a/indoteknik_custom/models/report_stock_forecasted.py b/indoteknik_custom/models/report_stock_forecasted.py
index 5f9427f8..c9d54a15 100644
--- a/indoteknik_custom/models/report_stock_forecasted.py
+++ b/indoteknik_custom/models/report_stock_forecasted.py
@@ -6,34 +6,33 @@ class ReplenishmentReport(models.AbstractModel):
@api.model
def _get_report_lines(self, product_template_ids, product_variant_ids, wh_location_ids):
lines = super(ReplenishmentReport, self)._get_report_lines(product_template_ids, product_variant_ids, wh_location_ids)
- result_dict = {}
-
- for line in lines:
- document_out = line.get('document_out')
-
- if document_out and "SO/" in document_out.name:
- order_id = document_out.id
- if document_out == False:
- continue
- product_id = line.get('product', {}).get('id')
- query = [('product_id', '=', product_id)]
-
- if order_id:
- result = self._calculate_result(line)
- quantity = line.get('quantity', 0)
- result_dict.setdefault(order_id, []).append((result, quantity))
-
- for order_id, results in result_dict.items():
- sales_order = self.env['sale.order'].browse(order_id)
-
- for result, quantity in results:
- self.env['sales.order.fullfillment'].create({
- 'sales_order_id': sales_order.id,
- 'product_id': product_id,
- 'reserved_from': result,
- 'qty_fullfillment': quantity,
- })
-
+ # result_dict = {}
+ #
+ # for line in lines:
+ # document_out = line.get('document_out')
+ #
+ # if document_out and "SO/" in document_out.name:
+ # order_id = document_out.id
+ # if document_out == False:
+ # continue
+ # product_id = line.get('product', {}).get('id')
+ # query = [('product_id', '=', product_id)]
+ #
+ # if order_id:
+ # result = self._calculate_result(line)
+ # quantity = line.get('quantity', 0)
+ # result_dict.setdefault(order_id, []).append((result, quantity))
+ #
+ # for order_id, results in result_dict.items():
+ # sales_order = self.env['sale.order'].browse(order_id)
+ #
+ # for result, quantity in results:
+ # self.env['sales.order.fullfillment'].create({
+ # 'sales_order_id': sales_order.id,
+ # 'product_id': product_id,
+ # 'reserved_from': result,
+ # 'qty_fullfillment': quantity,
+ # })
return lines
def _calculate_result(self, line):
diff --git a/indoteknik_custom/models/requisition.py b/indoteknik_custom/models/requisition.py
index 2b148c96..32a9f94f 100644
--- a/indoteknik_custom/models/requisition.py
+++ b/indoteknik_custom/models/requisition.py
@@ -1,4 +1,4 @@
-from odoo import models, fields, api, _
+from odoo import models, fields, api, tools, _
from odoo.exceptions import UserError
from datetime import datetime
import math
@@ -7,6 +7,33 @@ import logging
_logger = logging.getLogger(__name__)
+class RequisitionMatchPO(models.Model):
+ _name = 'v.requisition.match.po'
+ _auto = False
+ _rec_name = 'product_id'
+
+ id = fields.Integer(string='ID')
+ product_id = fields.Many2one('product.product', string='Product')
+ qty_rpo = fields.Float(string='Qty RPO', help='Qty RPO yang sudah di PO namun SO masih Draft')
+
+ def init(self):
+ tools.drop_view_if_exists(self.env.cr, self._table)
+ self.env.cr.execute("""
+ create or replace view %s as
+ select rl.product_id as id, rl.product_id, sum(rl.qty_purchase) as qty_rpo
+ from requisition_line rl
+ join requisition r on r.id = rl.requisition_id
+ join requisition_purchase_match rpm on rpm.requisition_id = r.id
+ join purchase_order po on po.id = rpm.order_id
+ join sale_order so on so.id = r.sale_order_id
+ where 1=1
+ and r.date_doc >= '2024-11-11'
+ and po.state in ('done', 'purchase')
+ and so.state in ('draft', 'sent')
+ group by rl.product_id
+ """ % self._table)
+
+
class Requisition(models.Model):
_name = 'requisition'
_order = 'id desc'
@@ -20,21 +47,61 @@ class Requisition(models.Model):
notification = fields.Char(string='Notification')
is_po = fields.Boolean(string='Is PO')
requisition_match = fields.One2many('requisition.purchase.match', 'requisition_id', string='Matches', auto_join=True)
- sale_order_id = fields.Many2one('sale.order', string='SO', help='harus diisi nomor SO yang ingin digenerate',
- domain="[('state', '=', 'sale')]")
+ sale_order_id = fields.Many2one('sale.order', string='SO', help='harus diisi nomor SO yang ingin digenerate')
+ sales_approve = fields.Boolean(string='Sales Approve', tracking=3, copy=False)
+ merchandise_approve = fields.Boolean(string='Merchandise Approve', tracking=3, copy=False)
+
+ def generate_requisition_from_so(self):
+ state = ['done', 'sale']
+ if not self.sale_order_id:
+ raise UserError('Sale Order Wajib Diisi dan Harus Draft')
+ if self.sale_order_id.state in state:
+ raise UserError('SO sudah Confirm, akan berakibat double Purchase melalui PJ')
+ if not self.sale_order_id.order_line:
+ raise UserError('Line SO masih kosong, harus diisi dulu')
+ for order_line in self.sale_order_id.order_line:
+ param = {
+ 'requisition_id': self.id,
+ 'product_id': order_line.product_id.id,
+ 'partner_id': order_line.vendor_id.id,
+ 'qty_purchase': order_line.product_uom_qty,
+ 'price_unit': order_line.purchase_price,
+ 'taxes_id': order_line.purchase_tax_id.id,
+ 'subtotal': order_line.purchase_price * order_line.product_uom_qty,
+ 'brand_id': order_line.product_id.x_manufacture.id
+ }
+ self.env['requisition.line'].create([param])
@api.model
def create(self, vals):
vals['number'] = self.env['ir.sequence'].next_by_code('requisition') or '0'
result = super(Requisition, self).create(vals)
return result
-
+
+ def button_approve(self):
+ state = ['done', 'sale']
+ if self.sale_order_id.state in state:
+ raise UserError('SO sudah Confirm, akan berakibat double Purchase melalui PJ')
+ if self.env.user.id not in [377, 19]:
+ raise UserError('Hanya Vita dan Darren Yang Bisa Approve')
+ if self.env.user.id == 377:
+ self.sales_approve = True
+ elif self.env.user.id == 19:
+ if not self.sales_approve:
+ raise UserError('Vita Belum Approve')
+ self.merchandise_approve = True
def create_po_from_requisition(self):
+ if not self.sales_approve:
+ raise UserError('Harus Di Approve oleh Vita')
+ if not self.merchandise_approve:
+ raise UserError('Harus Di Approve oleh Darren')
if not self.requisition_lines:
raise UserError('Tidak ada Lines, belum bisa create PO')
if self.is_po:
raise UserError('Sudah pernah di create PO')
+ if not self.sale_order_id:
+ raise UserError('Tidak ada link dengan Sales Order, tidak bisa dihitung sebagai Plafon Qty di PO')
vendor_ids = self.env['requisition.line'].read_group([
('requisition_id', '=', self.id),
@@ -101,13 +168,20 @@ class Requisition(models.Model):
'product_id': product.id,
'product_qty': line.qty_purchase,
'product_uom_qty': line.qty_purchase,
- 'name': product.name,
+ 'name': product.display_name,
'price_unit': line.price_unit,
'taxes_id': tax,
}
new_po_line = self.env['purchase.order.line'].create([param_line])
line.current_po_id = new_po.id
line.current_po_line_id = new_po_line.id
+
+ self.env['requisition.purchase.match'].create([{
+ 'requisition_id': self.id,
+ 'order_id': new_po.id
+ }])
+ self.is_po = True
+
return po_ids
# def create_po_from_requisition(self):
@@ -214,6 +288,7 @@ class RequisitionLine(models.Model):
last_price = fields.Float(string='Last Price')
last_order_id = fields.Many2one('purchase.order', string='Last Order')
last_orderline_id = fields.Many2one('purchase.order.line', string='Last Order Line')
+ taxes_id = fields.Many2one('account.tax', string='Tax')
is_po = fields.Boolean(String='Is PO')
current_po_id = fields.Many2one('purchase.order', string='Current')
current_po_line_id = fields.Many2one('purchase.order.line', string='Current Line')
@@ -221,6 +296,23 @@ class RequisitionLine(models.Model):
qty_available_store = fields.Float(string='Available')
suggest = fields.Char(string='Suggest')
+ def _get_valid_purchase_price(self, purchase_price):
+ price = 0
+ taxes = ''
+ human_last_update = purchase_price.human_last_update or datetime.min
+ system_last_update = purchase_price.system_last_update or datetime.min
+
+ if purchase_price.taxes_product_id.type_tax_use == 'purchase':
+ price = purchase_price.product_price
+ taxes = purchase_price.taxes_product_id.id
+
+ if system_last_update > human_last_update:
+ if purchase_price.taxes_system_id.type_tax_use == 'purchase':
+ price = purchase_price.system_price
+ taxes = purchase_price.taxes_system_id.id
+
+ return price, taxes
+
@api.onchange('price_unit')
def _onchange_price_unit(self):
for line in self:
@@ -230,6 +322,15 @@ class RequisitionLine(models.Model):
def _onchange_product(self):
for line in self:
line.brand_id = line.product_id.product_tmpl_id.x_manufacture.id
+ purchase_pricelist = self.env['purchase.pricelist'].search([
+ ('product_id', '=', line.product_id.id)
+ ],order='count_trx_po desc, count_trx_po_vendor desc', limit=1)
+
+ price, taxes = line._get_valid_purchase_price(purchase_pricelist)
+ line.price_unit = price
+ line.taxes_id = taxes
+ line.partner_id = purchase_pricelist.vendor_id.id
+
class RequisitionPurchaseMatch(models.Model):
_name = 'requisition.purchase.match'
diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py
index 25db16d0..da4a6cb6 100644
--- a/indoteknik_custom/models/res_partner.py
+++ b/indoteknik_custom/models/res_partner.py
@@ -1,7 +1,7 @@
from odoo import models, fields, api
from odoo.exceptions import UserError, ValidationError
from datetime import datetime
-
+from odoo.http import request
class GroupPartner(models.Model):
_name = 'group.partner'
@@ -10,6 +10,16 @@ class GroupPartner(models.Model):
class ResPartner(models.Model):
_inherit = 'res.partner'
+ property_account_payable_id = fields.Many2one('account.account', company_dependent=True,
+ string="Account Payable",
+ domain="[('internal_type', '=', 'payable'), ('deprecated', '=', False), ('company_id', '=', current_company_id)]",
+ help="This account will be used instead of the default one as the payable account for the current partner",
+ default=438)
+ property_account_receivable_id = fields.Many2one('account.account', company_dependent=True,
+ string="Account Receivable",
+ domain="[('internal_type', '=', 'receivable'), ('deprecated', '=', False), ('company_id', '=', current_company_id)]",
+ help="This account will be used instead of the default one as the receivable account for the current partner",
+ default=395)
reference_number = fields.Char(string="Reference Number")
company_type_id = fields.Many2one('res.partner.company_type', string='Company Type')
custom_pricelist_id = fields.Many2one('product.pricelist', string='Price Matrix')
@@ -23,7 +33,7 @@ class ResPartner(models.Model):
counter = fields.Integer(string="Counter", default=0)
leadtime = fields.Integer(string="Leadtime", default=0)
digital_invoice_tax = fields.Boolean(string="Digital Invoice & Faktur Pajak")
- is_potential = fields.Boolean(string='Potential')
+ is_not_potential = fields.Boolean(string='Not Potential')
pakta_integritas = fields.Boolean(string='Pakta Integritas')
use_so_approval = fields.Boolean(string='Use SO Approval')
@@ -48,6 +58,22 @@ class ResPartner(models.Model):
date_payment_terms_sales = fields.Datetime(string='Date Update Payment Terms')
user_payment_terms_purchase = fields.Many2one('res.users', string='Users Update Payment Terms')
date_payment_terms_purchase = fields.Datetime(string='Date Update Payment Terms')
+ longtitude = fields.Char(string='Longtitude')
+ latitude = fields.Char(string='Latitude')
+ address_map = fields.Char(string='Address Map')
+ company_type = fields.Selection(string='Company Type',
+ selection=[('person', 'Individual'), ('company', 'Company')],
+ compute='_compute_company_type', inverse='_write_company_type', tracking=3)
+ warning_stage = fields.Float(string='Warning Amount',
+ help="A warning message will appear once the "
+ "selected customer is crossed warning "
+ "amount. Set its value to 0.00 to"
+ " disable this feature", tracking=3)
+ blocking_stage = fields.Float(string='Blocking Amount',
+ help="Cannot make sales once the selected "
+ "customer is crossed blocking amount."
+ "Set its value to 0.00 to disable "
+ "this feature", tracking=3)
@api.model
def _default_payment_term(self):
@@ -218,6 +244,14 @@ class ResPartner(models.Model):
def _onchange_customer_type(self):
if self.customer_type == 'nonpkp':
self.npwp = '00.000.000.0-000.000'
-
+ def get_check_tempo_partner(self):
+ self.ensure_one()
+
+ partner = self.parent_id or self
+
+ if not partner.property_payment_term_id or 'Tempo' not in partner.property_payment_term_id.name:
+ return False
+ else:
+ return True
diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py
index c1c2c267..7fc6d96a 100755
--- a/indoteknik_custom/models/sale_order.py
+++ b/indoteknik_custom/models/sale_order.py
@@ -11,6 +11,7 @@ _logger = logging.getLogger(__name__)
class SaleOrder(models.Model):
_inherit = "sale.order"
+ fulfillment_line_v2 = fields.One2many('sales.order.fulfillment.v2', 'sale_order_id', string='Fullfillment2')
fullfillment_line = fields.One2many('sales.order.fullfillment', 'sales_order_id', string='Fullfillment')
reject_line = fields.One2many('sales.order.reject', 'sale_order_id', string='Reject Lines')
order_sales_match_line = fields.One2many('sales.order.purchase.match', 'sales_order_id', string='Purchase Match Lines', states={'cancel': [('readonly', True)], 'done': [('readonly', True)]}, copy=True)
@@ -78,7 +79,7 @@ class SaleOrder(models.Model):
payment_link_midtrans = fields.Char(string='Payment Link', help='Url payment yg digenerate oleh midtrans, harap diserahkan ke customer agar dapat dilakukan pembayaran secara mandiri')
payment_qr_code = fields.Binary("Payment QR Code")
due_id = fields.Many2one('due.extension', string="Due Extension", readonly=True, tracking=True)
- vendor_approval_id = fields.Many2one('vendor.approval', string="Vendor Approval", readonly=True, tracking=True)
+ vendor_approval_id = fields.Many2many('vendor.approval', string="Vendor Approval", readonly=True, tracking=True, copy=False)
customer_type = fields.Selection([
('pkp', 'PKP'),
('nonpkp', 'Non PKP')
@@ -98,6 +99,7 @@ class SaleOrder(models.Model):
helper_by_id = fields.Many2one('res.users', 'Helper By')
eta_date = fields.Datetime(string='ETA Date', copy=False, compute='_compute_eta_date')
flash_sale = fields.Boolean(string='Flash Sale', help='Data dari web')
+ is_continue_transaction = fields.Boolean(string='Button Transaction', help='Data dari web')
web_approval = fields.Selection([
('company', 'Company'),
('cust_manager', 'Customer Manager'),
@@ -136,13 +138,27 @@ class SaleOrder(models.Model):
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", tracking=True)
total_weight = fields.Float(string='Total Weight', compute='_compute_total_weight')
+ pareto_status = fields.Selection([
+ ('PR', 'Pareto Repeating'),
+ ('PPR', 'Potensi Pareto Repeating'),
+ ('PNR', 'Pareto Non Repeating'),
+ ('NP', 'Non Pareto')
+ ])
+
+ @api.onchange('payment_status')
+ def _is_continue_transaction(self):
+ if not self.is_continue_transaction:
+ if self.payment_status == 'settlement':
+ self.is_continue_transaction = True
+ else:
+ self.is_continue_transaction = False
def _compute_total_weight(self):
total_weight = 0
missing_weight_products = []
for line in self.order_line:
- if line.weight:
+ if line.weight > 0:
total_weight += line.weight * line.product_uom_qty
self.total_weight = total_weight
@@ -155,7 +171,7 @@ class SaleOrder(models.Model):
missing_weight_products = []
for line in self.order_line:
- if line.weight:
+ if line.weight > 0:
total_weight += line.weight * line.product_uom_qty
line.product_id.weight = line.weight
else:
@@ -180,7 +196,7 @@ class SaleOrder(models.Model):
missing_weight_products = []
for line in self.order_line:
- if line.weight:
+ if line.weight > 0:
total_weight += line.weight * line.product_uom_qty
line.product_id.weight = line.weight
else:
@@ -340,10 +356,10 @@ class SaleOrder(models.Model):
def _compute_fullfillment(self):
for rec in self:
- rec.fullfillment_line.unlink()
-
- for line in rec.order_line:
- line._compute_reserved_from()
+ # rec.fullfillment_line.unlink()
+ #
+ # for line in rec.order_line:
+ # line._compute_reserved_from()
rec.compute_fullfillment = True
@@ -484,8 +500,8 @@ class SaleOrder(models.Model):
raise UserError('Phone Real Delivery Address harus diisi')
if not real_delivery_address.kecamatan_id:
raise UserError('Kecamatan Real Delivery Address harus diisi')
- if not real_delivery_address.kelurahan_id:
- raise UserError('Kelurahan Real Delivery Address harus diisi')
+ # if not real_delivery_address.kelurahan_id:
+ # raise UserError('Kelurahan Real Delivery Address harus diisi')
def generate_payment_link_midtrans_sales_order(self):
# midtrans_url = 'https://app.sandbox.midtrans.com/snap/v1/transactions' # dev - sandbox
@@ -619,20 +635,6 @@ class SaleOrder(models.Model):
# return ['&', ('order_line.invoice_lines.move_id.move_type', 'in', ('out_invoice', 'out_refund')), ('order_line.invoice_lines.move_id', operator, value)]
- def check_data_real_delivery_address(self):
- real_delivery_address = self.real_shipping_id
-
- if not real_delivery_address.state_id:
- raise UserError('State Real Delivery Address harus diisi')
- if not real_delivery_address.zip:
- raise UserError('Zip code Real Delivery Address harus diisi')
- if not real_delivery_address.mobile:
- raise UserError('Mobile Real Delivery Address harus diisi')
- if not real_delivery_address.phone:
- raise UserError('Phone Real Delivery Address harus diisi')
- if not real_delivery_address.kecamatan_id:
- raise UserError('Kecamatan Real Delivery Address harus diisi')
-
@api.onchange('partner_id')
def onchange_partner_contact(self):
parent_id = self.partner_id.parent_id
@@ -642,6 +644,8 @@ class SaleOrder(models.Model):
self.sppkp = parent_id.sppkp
self.customer_type = parent_id.customer_type
self.email = parent_id.email
+ self.pareto_status = parent_id.pareto_status
+ self.user_id = parent_id.user_id
@api.onchange('partner_id')
def onchange_partner_id(self):
@@ -754,14 +758,35 @@ class SaleOrder(models.Model):
raise UserError("Salesperson sudah tidak aktif, mohon diisi yang benar pada data SO dan Contact")
def sale_order_approve(self):
- if self.validate_different_vendor() and not self.vendor_approval and not self.vendor_approval_id:
+ if self.validate_different_vendor() and not self.vendor_approval:
return self._create_notification_action('Notification', 'Terdapat Vendor yang berbeda dengan MD Vendor')
self.check_due()
self._validate_order()
for order in self:
order.order_line.validate_line()
+ order.check_data_real_delivery_address()
+ order._validate_order()
+ order.sale_order_check_approve()
+ main_parent = order.partner_id.get_main_parent()
+ SYSTEM_UID = 25
+ FROM_WEBSITE = order.create_uid.id == SYSTEM_UID
+
+ if FROM_WEBSITE and main_parent.use_so_approval and order.web_approval not in ['cust_procurement','cust_director']:
+ raise UserError("This order not yet approved by customer procurement or director")
+
+ if not order.client_order_ref and order.create_date > datetime(2024, 6, 27):
+ raise UserError("Customer Reference kosong, di isi dengan NO PO jika PO tidak ada mohon ditulis Tanpa PO")
+
+ if not order.commitment_date and order.create_date > datetime(2024, 9, 12):
+ raise UserError("Expected Delivery Date kosong, wajib diisi")
+
+ if not order.real_shipping_id:
+ UserError('Real Delivery Address harus di isi')
+
+ if order.validate_partner_invoice_due():
+ return self._create_notification_action('Notification','Terdapat invoice yang telah melewati batas waktu, mohon perbarui pada dokumen Due Extension')
term_days = 0
for term_line in order.payment_term_id.line_ids:
@@ -866,27 +891,26 @@ class SaleOrder(models.Model):
}).send()
def validate_different_vendor(self):
- different_vendor = self.order_line.filtered(lambda l: l.vendor_id and l.vendor_md_id and l.vendor_id.id != l.vendor_md_id.id)
+ if self.vendor_approval_id.filtered(lambda v: v.state == 'draft'):
+ draft_names = ", ".join(self.vendor_approval_id.filtered(lambda v: v.state == 'draft').mapped('number'))
+ raise UserError(f"SO ini sedang dalam review Vendor Approval: {draft_names}")
- if self.vendor_approval_id and self.vendor_approval_id.state == 'draft':
- raise UserError('SO ini sedang dalam review Vendor Approval')
+ if self.vendor_approval_id and all(v.state != 'draft' for v in self.vendor_approval_id):
+ return False
- if self.vendor_approval_id and self.vendor_approval_id.state == 'cancel':
- raise UserError('Vendor Approval SO ini Di Reject')
+ different_vendor = self.order_line.filtered(
+ lambda l: l.vendor_id and l.vendor_md_id and l.vendor_id.id != l.vendor_md_id.id
+ )
if different_vendor:
- vendor_approval = self.env['vendor.approval'].create({
- 'order_id': self.id,
- 'create_date_so': self.create_date,
- 'partner_id': self.partner_id.id,
- 'state': 'draft',
- })
-
- self.vendor_approval_id = vendor_approval.id
-
+ vendor_approvals = []
for line in different_vendor:
- self.env['vendor.approval.line'].create({
- 'vendor_approval_id': vendor_approval.id,
+ vendor_approval = self.env['vendor.approval'].create({
+ 'order_id': self.id,
+ 'order_line_id': line.id,
+ 'create_date_so': self.create_date,
+ 'partner_id': self.partner_id.id,
+ 'state': 'draft',
'product_id': line.product_id.id,
'product_uom_qty': line.product_uom_qty,
'vendor_id': line.vendor_id.id,
@@ -898,9 +922,15 @@ class SaleOrder(models.Model):
'margin_after': line.item_percent_margin,
'purchase_tax_id': line.purchase_tax_id.id,
'sales_tax_id': line.tax_id[0].id if line.tax_id else False,
- 'percent_margin_difference': (line.price_unit - line.purchase_price_md) / line.purchase_price_md if line.purchase_price_md else False,
+ 'percent_margin_difference': (
+ (line.price_unit - line.purchase_price_md) / line.purchase_price_md
+ if line.purchase_price_md else False
+ ),
})
-
+
+ vendor_approvals.append(vendor_approval.id)
+
+ self.vendor_approval_id = [(4, vid) for vid in vendor_approvals]
return True
else:
return False
@@ -908,7 +938,7 @@ class SaleOrder(models.Model):
def action_confirm(self):
for order in self:
- if self.validate_different_vendor() and not self.vendor_approval and not self.vendor_approval_id:
+ if self.validate_different_vendor() and not self.vendor_approval:
return self._create_notification_action('Notification', 'Terdapat Vendor yang berbeda dengan MD Vendor')
order.check_data_real_delivery_address()
@@ -949,6 +979,16 @@ class SaleOrder(models.Model):
# order.order_line.get_reserved_from()
res = super(SaleOrder, self).action_confirm()
+ for order in self:
+ note = []
+ for line in order.order_line:
+ if line.display_type == 'line_note':
+ note.append(line.name)
+
+ if order.picking_ids:
+ # Sort picking_ids by creation date to get the most recent one
+ latest_picking = order.picking_ids.sorted(key=lambda p: p.create_date, reverse=True)[0]
+ latest_picking.notee = '\n'.join(note)
return res
def action_cancel(self):
@@ -1024,11 +1064,22 @@ class SaleOrder(models.Model):
def _set_sppkp_npwp_contact(self):
partner = self.partner_id.parent_id or self.partner_id
- if not partner.sppkp or not partner.npwp or not partner.email or partner.customer_type:
- partner.customer_type = self.customer_type
- partner.npwp = self.npwp
+ if not partner.sppkp:
partner.sppkp = self.sppkp
+ if not partner.npwp:
+ partner.npwp = self.npwp
+ if not partner.email:
partner.email = self.email
+ if not partner.customer_type:
+ partner.customer_type = self.customer_type
+ if not partner.user_id:
+ partner.user_id = self.user_id.id
+
+ # if not partner.sppkp or not partner.npwp or not partner.email or partner.customer_type:
+ # partner.customer_type = self.customer_type
+ # partner.npwp = self.npwp
+ # partner.sppkp = self.sppkp
+ # partner.email = self.email
def _compute_total_margin(self):
for order in self:
@@ -1298,7 +1349,7 @@ class SaleOrder(models.Model):
def create(self, vals):
# Ensure partner details are updated when a sale order is created
order = super(SaleOrder, self).create(vals)
- order._update_partner_details()
+ # order._update_partner_details()
return order
def write(self, vals):
@@ -1306,8 +1357,8 @@ class SaleOrder(models.Model):
res = super(SaleOrder, self).write(vals)
# Check if the update is coming from a save operation
- if any(field in vals for field in ['sppkp', 'npwp', 'email', 'customer_type']):
- self._update_partner_details()
+ # if any(field in vals for field in ['sppkp', 'npwp', 'email', 'customer_type']):
+ # self._update_partner_details()
return res
diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py
index 59ac1ad4..a31ff569 100644
--- a/indoteknik_custom/models/sale_order_line.py
+++ b/indoteknik_custom/models/sale_order_line.py
@@ -31,11 +31,17 @@ class SaleOrderLine(models.Model):
vendor_subtotal = fields.Float(string='Vendor Subtotal', compute="_compute_vendor_subtotal")
amount_voucher_disc = fields.Float(string='Voucher Discount')
qty_reserved = fields.Float(string='Qty Reserved', compute='_compute_qty_reserved')
+ product_available_quantity = fields.Float(string='Qty pickup by user',)
reserved_from = fields.Char(string='Reserved From', copy=False)
- item_percent_margin_without_deduction = fields.Float('%Margin', compute='_compute_item_margin_without_deduction')
+ item_percent_margin_without_deduction = fields.Float('Margin Without Deduction', compute='_compute_item_margin_without_deduction')
weight = fields.Float(string='Weight')
md_vendor_id = fields.Many2one('res.partner', string='MD Vendor', readonly=True)
margin_md = fields.Float(string='Margin MD')
+ qty_free_bu = fields.Float(string='Free BU', compute='_get_qty_free_bandengan')
+
+ def _get_qty_free_bandengan(self):
+ for line in self:
+ line.qty_free_bu = line.product_id.qty_free_bandengan
@api.constrains('note_procurement')
def note_procurement_to_apo(self):
@@ -252,13 +258,10 @@ class SaleOrderLine(models.Model):
# query, limit=1, order='count_trx_po desc, count_trx_po_vendor desc')
price, taxes, vendor_id = self._get_purchase_price(line.product_id)
line.vendor_id = vendor_id
- line.md_vendor_id = vendor_id
- line.margin_md = line.item_percent_margin
line.tax_id = line.order_id.sales_tax_id
# price, taxes = line._get_valid_purchase_price(purchase_price)
- line.purchase_price_md = price
line.purchase_price = price
- line.purchase_tax_id = taxes
+ line.purchase_tax_id = taxes
attribute_values = line.product_id.product_template_attribute_value_ids.mapped('name')
attribute_values_str = ', '.join(attribute_values) if attribute_values else ''
@@ -270,6 +273,14 @@ class SaleOrderLine(models.Model):
line.name = line_name
line.weight = line.product_id.weight
+ @api.constrains('vendor_id')
+ def _check_vendor_id(self):
+ for line in self:
+ price, taxes, vendor_id = self._get_purchase_price(line.product_id)
+ line.vendor_md_id = vendor_id if vendor_id else None
+ line.margin_md = line.item_percent_margin
+ line.purchase_price_md = price
+
def compute_delivery_amt_line(self):
for line in self:
try:
@@ -366,5 +377,5 @@ class SaleOrderLine(models.Model):
continue
if not line.product_id.product_tmpl_id.sale_ok:
raise UserError('Product %s belum bisa dijual, harap hubungi finance' % line.product_id.display_name)
- if not line.vendor_id or not line.purchase_price:
+ if not line.vendor_id or not line.purchase_price and not line.display_type == 'line_note':
raise UserError(_('Isi Vendor dan Harga Beli sebelum Request Approval')) \ No newline at end of file
diff --git a/indoteknik_custom/models/sales_order_fullfillment.py b/indoteknik_custom/models/sales_order_fullfillment.py
index ab416e8d..05a0641c 100644
--- a/indoteknik_custom/models/sales_order_fullfillment.py
+++ b/indoteknik_custom/models/sales_order_fullfillment.py
@@ -6,6 +6,24 @@ import logging
_logger = logging.getLogger(__name__)
+class SalesOrderFullfillmentV2(models.Model):
+ _name = 'sales.order.fulfillment.v2'
+
+ sale_order_id = fields.Many2one('sale.order', string='Sale Order')
+ sale_order_line_id = fields.Many2one('sale.order.line', string='Sale Order Line')
+ picking_id = fields.Many2one('stock.picking', string='Picking')
+ move_id = fields.Many2one('stock.move', string='Move')
+ move_line_id = fields.Many2one('stock.move.line', string='Move Line')
+ product_id = fields.Many2one('product.product', string='Product')
+ so_qty = fields.Float(string='SO Qty')
+ reserved_stock_qty = fields.Float(string='Reserved Stock Qty', help='Sudah ter-reserved oleh sistem')
+ delivered_qty = fields.Float(string='Delivered Qty', help='Yang sudah terkirim ke Customer')
+ po_ids = fields.Many2many('purchase.order', string='Purchase Order', help='PO yang dibuat, bisa lebih dari satu')
+ po_qty = fields.Float(string='PO Qty', help='Totalan dari semua PO Outstanding')
+ received_qty = fields.Float(string='Received Qty', help='Totalan dari barang yang diterima dari PO tsb')
+ purchaser = fields.Char(string='Purchaser')
+
+
class SalesOrderFullfillment(models.Model):
_name = 'sales.order.fullfillment'
diff --git a/indoteknik_custom/models/solr/product_product.py b/indoteknik_custom/models/solr/product_product.py
index df20cc4b..667511b2 100644
--- a/indoteknik_custom/models/solr/product_product.py
+++ b/indoteknik_custom/models/solr/product_product.py
@@ -52,10 +52,9 @@ class ProductProduct(models.Model):
stock_quant = self.env['stock.quant'].search([
('product_id', 'in', variant.product_variant_ids.ids),
('location_id', 'in', target_locations),
- ('quantity', '>', 0)
])
- is_in_bu = bool(stock_quant)
+ is_in_bu = True if variant.qty_free_bandengan > 0 else False
document = solr_model.get_doc('variants', variant.id)
@@ -68,6 +67,7 @@ class ProductProduct(models.Model):
'product_id_i': variant.id,
'template_id_i': variant.product_tmpl_id.id,
'image_s': ir_attachment.api_image('product.template', 'image_512', variant.product_tmpl_id.id),
+ 'image_mobile_s': ir_attachment.api_image('product.template', 'image_256', variant.product_tmpl_id.id),
'stock_total_f': variant.qty_stock_vendor,
'weight_f': variant.weight,
'manufacture_id_i': variant.product_tmpl_id.x_manufacture.id or 0,
diff --git a/indoteknik_custom/models/solr/product_template.py b/indoteknik_custom/models/solr/product_template.py
index 70246ba3..87e8370f 100644
--- a/indoteknik_custom/models/solr/product_template.py
+++ b/indoteknik_custom/models/solr/product_template.py
@@ -75,10 +75,9 @@ class ProductTemplate(models.Model):
stock_quant = self.env['stock.quant'].search([
('product_id', 'in', template.product_variant_ids.ids),
('location_id', 'in', target_locations),
- ('quantity', '>', 0)
])
- is_in_bu = bool(stock_quant)
+ is_in_bu = any(quant.available_quantity > 0 for quant in stock_quant)
cleaned_desc = BeautifulSoup(template.website_description or '', "html.parser").get_text()
website_description = template.website_description if cleaned_desc else ''
@@ -92,6 +91,7 @@ class ProductTemplate(models.Model):
"product_rating_f": template.virtual_rating,
"product_id_i": template.id,
"image_s": self.env['ir.attachment'].api_image('product.template', 'image_512', template.id),
+ 'image_mobile_s': self.env['ir.attachment'].api_image('product.template', 'image_256', template.id),
"variant_total_i": template.product_variant_count,
"stock_total_f": template.qty_stock_vendor,
"weight_f": template.weight,
@@ -159,10 +159,14 @@ class ProductTemplate(models.Model):
traverse_category(category.parent_id.id)
# Start traversal from the initial category
- traverse_category(category_id)
-
- # Reverse the list to get the hierarchy from top level to the current level
- return list(reversed(category_ids))
+ if category_id: # Check if category_id is not an empty value
+ traverse_category(category_id)
+
+ # Reverse the list to get the hierarchy from top level to the current level
+ return list(reversed(category_ids))
+ else:
+ # If category_id is empty, return an empty list
+ return []
def _sync_price_to_solr(self):
solr_model = self.env['apache.solr']
diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py
index 5f2fe99a..2095e957 100644
--- a/indoteknik_custom/models/stock_picking.py
+++ b/indoteknik_custom/models/stock_picking.py
@@ -3,7 +3,7 @@ from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tools.float_utils import float_is_zero
from datetime import datetime
from itertools import groupby
-import pytz, datetime
+import pytz, datetime, requests, json
from odoo.http import request
@@ -26,7 +26,7 @@ class StockPicking(models.Model):
# Delivery Order
driver_departure_date = fields.Datetime(
- string='Driver Departure Date',
+ string='Delivery Departure Date',
copy=False
)
arrival_time = fields.Datetime(
@@ -34,8 +34,7 @@ class StockPicking(models.Model):
copy=False
)
driver_arrival_date = fields.Datetime(
- string='Driver Arrival Date',
- readonly=True,
+ string='Delivery Arrival Date',
copy=False
)
delivery_tracking_no = fields.Char(
@@ -54,6 +53,10 @@ class StockPicking(models.Model):
readonly=True,
copy=False
)
+ sj_documentation = fields.Binary(string="Dokumentasi Surat Jalan", )
+ paket_documentation = fields.Binary(string="Dokumentasi Paket", )
+ sj_return_date = fields.Datetime(string="SJ Return Date", )
+ responsible = fields.Many2one('res.users', string='Responsible', tracking=True)
approval_status = fields.Selection([
('pengajuan1', 'Approval Accounting'),
@@ -74,10 +77,8 @@ class StockPicking(models.Model):
('hold', 'Hold by Sales'),
('not_paid', 'Customer belum bayar'),
('partial', 'Kirim Parsial'),
- ('not_complete', 'Belum Lengkap'),
('indent', 'Indent'),
('self_pickup', 'Barang belum di pickup Customer'),
- ('delivery_route', 'Belum masuk rute pengiriman'),
('expedition_closed', 'Eskpedisi belum buka')
], string='Note Logistic', help='jika field ini diisi maka tidak akan dihitung ke lead time')
waybill_id = fields.One2many(comodel_name='airway.bill', inverse_name='do_id', string='Airway Bill')
@@ -95,16 +96,108 @@ class StockPicking(models.Model):
date_availability = fields.Datetime(string="Date Availability", copy=False, tracking=True)
sale_order = fields.Char(string='Matches SO', copy=False)
printed_sj = fields.Boolean('Printed Surat Jalan', help='flag which is internal use or not')
+ printed_sj_retur = fields.Boolean('Printed Surat Jalan Retur', help='flag which is internal use or not')
+ date_printed_sj_retur = fields.Datetime(string='Status Printed Surat Jalan Retur', copy=False, tracking=True)
invoice_status = fields.Selection([
('upselling', 'Upselling Opportunity'),
('invoiced', 'Fully Invoiced'),
('to invoice', 'To Invoice'),
('no', 'Nothing to Invoice')
], string='Invoice Status', related="sale_id.invoice_status")
-
+ note_return = fields.Text(string="Note Return", help="Catatan untuk kirim barang kembali")
+
+ state_reserve = fields.Selection([
+ ('waiting', 'Waiting For Fullfilment'),
+ ('ready', 'Ready to Ship'),
+ ('done', 'Done'),
+ ('cancel', 'Cancelled'),
+ ], string='Status Reserve', readonly=True, tracking=True, help="The current state of the stock picking.")
+ notee = fields.Text(string="Note")
+
+ def action_send_to_biteship(self):
+ url = "https://api.biteship.com/v1/orders"
+ api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA"
+
+ # Mencari data sale.order.line berdasarkan sale_id
+ products = self.env['sale.order.line'].search([('order_id', '=', self.sale_id.id)])
+
+ # Fungsi untuk membangun items_data dari order lines
+ def build_items_data(lines):
+ return [{
+ "name": line.product_id.name,
+ "description": line.name,
+ "value": line.price_unit,
+ "quantity": line.product_uom_qty,
+ "weight": line.weight
+ } for line in lines]
+
+ # Items untuk pengiriman standard
+ items_data_standard = build_items_data(products)
+
+ # Items untuk pengiriman instant, mengambil product_id dari move_line_ids_without_package
+ items_data_instant = []
+ for move_line in self.move_line_ids_without_package:
+ # Mencari baris di sale.order.line berdasarkan product_id dari move_line
+ order_line = self.env['sale.order.line'].search([
+ ('order_id', '=', self.sale_id.id),
+ ('product_id', '=', move_line.product_id.id)
+ ], limit=1)
+
+ if order_line:
+ items_data_instant.append({
+ "name": order_line.product_id.name,
+ "description": order_line.name,
+ "value": order_line.price_unit,
+ "quantity": move_line.qty_done, # Menggunakan qty_done dari move_line
+ "weight": order_line.weight
+ })
+
+ payload = {
+ "shipper_contact_name": self.carrier_id.pic_name or '',
+ "shipper_contact_phone": self.carrier_id.pic_phone or '',
+ "shipper_organization": self.carrier_id.name,
+ "origin_contact_name": "PT. Indoteknik Dotcom Gemilang",
+ "origin_contact_phone": "081717181922",
+ "origin_address": "Jl. Bandengan Utara Komp A & BRT. Penjaringan, Kec. Penjaringan, Jakarta (BELAKANG INDOMARET) KOTA JAKARTA UTARA PENJARINGAN",
+ "origin_postal_code": 14440,
+ "destination_contact_name": self.real_shipping_id.name,
+ "destination_contact_phone": self.real_shipping_id.phone or self.real_shipping_id.mobile,
+ "destination_address": self.real_shipping_id.street,
+ "destination_postal_code": self.real_shipping_id.zip,
+ "courier_type": "reg",
+ "courier_company": self.carrier_id.name.lower(),
+ "delivery_type": "now",
+ "destination_postal_code": self.real_shipping_id.zip,
+ "items": items_data_standard
+ }
+
+ # Cek jika pengiriman instant atau same_day
+ if "instant" in self.sale_id.delivery_service_type or "same_day" in self.sale_id.delivery_service_type:
+ payload.update({
+ "origin_note": "BELAKANG INDOMARET",
+ "courier_company": self.carrier_id.name.lower(),
+ "courier_type": self.sale_id.delivery_service_type,
+ "delivery_type": "now",
+ "items": items_data_instant # Gunakan items untuk instant
+ })
+
+ headers = {
+ "Authorization": f"Bearer {api_key}",
+ "Content-Type": "application/json"
+ }
+
+ # Kirim request ke Biteship
+ response = requests.post(url, headers=headers, json=payload)
+
+ if response.status_code == 201:
+ return response.json()
+ else:
+ raise UserError(f"Error saat mengirim ke Biteship: {response.content}")
+
@api.constrains('driver_departure_date')
def constrains_driver_departure_date(self):
- self.date_doc_kirim = self.driver_departure_date
+ if not self.date_doc_kirim:
+ self.date_doc_kirim = self.driver_departure_date
@api.constrains('arrival_time')
def constrains_arrival_time(self):
@@ -135,9 +228,44 @@ class StockPicking(models.Model):
res = super(StockPicking, self).do_unreserve()
current_time = datetime.datetime.utcnow()
self.date_unreserve = current_time
-
+ # self.check_state_reserve()
+
return res
+ # def check_state_reserve(self):
+ # do = self.search([
+ # ('state', 'not in', ['cancel', 'draft', 'done']),
+ # ('picking_type_code', '=', 'outgoing')
+ # ])
+
+ # for rec in do:
+ # rec.state_reserve = 'ready'
+ # rec.date_reserved = datetime.datetime.utcnow()
+
+ # for line in rec.move_ids_without_package:
+ # if line.product_uom_qty > line.reserved_availability:
+ # rec.state_reserve = 'waiting'
+ # rec.date_reserved = ''
+ # break
+
+ def check_state_reserve(self):
+ pickings = self.search([
+ ('state', 'not in', ['cancel', 'draft', 'done']),
+ ('picking_type_code', '=', 'outgoing')
+ ])
+
+ for picking in pickings:
+ fullfillments = self.env['sales.order.fullfillment'].search([
+ ('sales_order_id', '=', picking.sale_id.id)
+ ])
+
+ picking.state_reserve = 'ready'
+ picking.date_reserved = picking.date_reserved or datetime.datetime.utcnow()
+
+ if any(rec.reserved_from not in ['Inventory On Hand', 'Reserved from stock', 'Free Stock'] for rec in fullfillments):
+ picking.state_reserve = 'waiting'
+ picking.date_reserved = ''
+
def _create_approval_notification(self, approval_role):
title = 'Warning'
message = f'Butuh approval sales untuk unreserved'
@@ -153,9 +281,9 @@ class StockPicking(models.Model):
def _compute_shipping_status(self):
for rec in self:
status = 'pending'
- if rec.driver_departure_date and not rec.driver_arrival_date:
+ if rec.driver_departure_date and not (rec.sj_return_date or rec.driver_arrival_date):
status = 'shipment'
- elif rec.driver_departure_date and rec.driver_arrival_date:
+ elif rec.driver_departure_date and (rec.sj_return_date or rec.driver_arrival_date):
status = 'completed'
rec.shipping_status = status
@@ -276,6 +404,7 @@ class StockPicking(models.Model):
current_time = datetime.datetime.utcnow()
self.real_shipping_id = self.sale_id.real_shipping_id
self.date_availability = current_time
+ # self.check_state_reserve()
return res
def ask_approval(self):
@@ -311,6 +440,34 @@ class StockPicking(models.Model):
self.approval_receipt_status = 'pengajuan1'
def ask_return_approval(self):
+ for pick in self:
+ if self.env.user.is_accounting:
+ pick.approval_return_status = 'approved'
+ continue
+
+ action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_stock_return_note_wizard')
+
+ if self.picking_type_code == 'outgoing':
+ if self.env.user.id in [3988, 3401, 20] or (
+ self.env.user.has_group('indoteknik_custom.group_role_purchasing') and 'Return of' in self.origin
+ ):
+ action['context'] = {'picking_ids': [x.id for x in self]}
+ return action
+ elif not self.env.user.has_group('indoteknik_custom.group_role_purchasing') and 'Return of' in self.origin:
+ raise UserError('Harus Purchasing yang Ask Return')
+ else:
+ raise UserError('Harus Sales Admin yang Ask Return')
+
+ elif self.picking_type_code == 'incoming':
+ if self.env.user.has_group('indoteknik_custom.group_role_purchasing') or (
+ self.env.user.id in [3988, 3401, 20] and 'Return of' in self.origin
+ ):
+ action['context'] = {'picking_ids': [x.id for x in self]}
+ return action
+ elif not self.env.user.id in [3988, 3401, 20] and 'Return of' in self.origin:
+ raise UserError('Harus Sales Admin yang Ask Return')
+ else:
+ raise UserError('Harus Purchasing yang Ask Return')
telegram_data = {
'tittle': self.name,
'about': 'Permintaan retur ' + self.name,
@@ -347,6 +504,7 @@ class StockPicking(models.Model):
self.approval_return_status = 'approved'
def calculate_line_no(self):
+
for picking in self:
name = picking.group_id.name
for move in picking.move_ids_without_package:
@@ -401,8 +559,31 @@ class StockPicking(models.Model):
):
raise UserError('Quantity reserved lebih besar dari quantity onhand di product')
+ def check_qty_done_stock(self):
+ for line in self.move_line_ids_without_package:
+ def check_qty_per_inventory(self, product, location):
+ quant = self.env['stock.quant'].search([
+ ('product_id', '=', product.id),
+ ('location_id', '=', location.id),
+ ])
+
+ if quant:
+ return quant.quantity
+
+ return 0
+
+ qty_onhand = check_qty_per_inventory(self, line.product_id, line.location_id)
+ if line.qty_done > qty_onhand:
+ raise UserError(f'{line.product_id.display_name} : Quantity Done melebihi Quantity Onhand')
def button_validate(self):
+ if not self.env.user.is_logistic_approver and self.env.context.get('active_model') == 'stock.picking':
+ if self.origin and 'Return of' in self.origin:
+ raise UserError("Button ini hanya untuk Logistik")
+
+ if self.picking_type_code == 'internal':
+ self.check_qty_done_stock()
+
if self._name != 'stock.picking':
return super(StockPicking, self).button_validate()
@@ -424,10 +605,10 @@ class StockPicking(models.Model):
if self.location_dest_id.id == 47 and not self.env.user.is_purchasing_manager:
raise UserError("Transfer ke gudang selisih harus di approve Rafly Hanggara")
- if self.group_id.sale_id:
- if self.group_id.sale_id.payment_link_midtrans:
- if self.group_id.sale_id.payment_status != 'settlement':
- raise UserError('Uang belum masuk (settlement), mohon konfirmasi ke sales atau finance')
+ # if self.group_id.sale_id:
+ # if self.group_id.sale_id.payment_link_midtrans:
+ # if self.group_id.sale_id.payment_status != 'settlement' and self.group_id.sale_id.state == 'draft':
+ # raise UserError('Uang belum masuk (settlement), mohon konfirmasi ke sales atau finance')
if self.is_internal_use:
self.approval_status = 'approved'
@@ -447,12 +628,21 @@ class StockPicking(models.Model):
self.date_reserved = current_time
self.validation_minus_onhand_quantity()
-
+ self.responsible = self.env.user.id
res = super(StockPicking, self).button_validate()
self.calculate_line_no()
self.date_done = datetime.datetime.utcnow()
+ self.state_reserve = 'done'
+ return res
+ def action_cancel(self):
+ if not self.env.user.is_logistic_approver and self.env.context.get('active_model') == 'stock.picking':
+ if self.origin and 'Return of' in self.origin:
+ raise UserError("Button ini hanya untuk Logistik")
+
+ res = super(StockPicking, self).action_cancel()
return res
+
@api.model
def create(self, vals):
self._use_faktur(vals)
@@ -521,13 +711,13 @@ class StockPicking(models.Model):
manifest_datas = []
departure_date = self.driver_departure_date
- arrival_date = self.driver_arrival_date
+ arrival_date = self.sj_return_date if self.sj_return_date else self.driver_arrival_date
status = status_mapping.get(status_key)
if not status:
return manifest_datas
- if arrival_date:
+ if arrival_date or self.sj_return_date:
manifest_datas.append(self.create_manifest_data(status['arrival'], arrival_date))
if departure_date:
manifest_datas.append(self.create_manifest_data(status['departure'], departure_date))
@@ -554,7 +744,7 @@ class StockPicking(models.Model):
}
if not self.waybill_id or len(self.waybill_id.manifest_ids) == 0:
- response['delivered'] = self.driver_arrival_date != False
+ response['delivered'] = self.sj_return_date != False or self.driver_arrival_date != False
return response
response['delivery_order']['receiver_name'] = self.waybill_id.receiver_name
@@ -587,4 +777,4 @@ class StockPicking(models.Model):
formatted_fastest_eta = fastest_eta.strftime(format_time_fastest)
formatted_longest_eta = longest_eta.strftime(format_time)
- return f'{formatted_fastest_eta} - {formatted_longest_eta}'
+ return f'{formatted_fastest_eta} - {formatted_longest_eta}' \ No newline at end of file
diff --git a/indoteknik_custom/models/user_company_request.py b/indoteknik_custom/models/user_company_request.py
index 6d809385..dd9a35c1 100644
--- a/indoteknik_custom/models/user_company_request.py
+++ b/indoteknik_custom/models/user_company_request.py
@@ -23,24 +23,25 @@ class UserCompanyRequest(models.Model):
else:
record.similar_company_ids = [(6, 0, [])]
- def get_similar_companies(self, user_input):
- query = """
- SELECT id
- FROM res_partner
- WHERE levenshtein(name::text, %s) < 3
- ORDER BY levenshtein(name::text, %s) ASC
- """
- self.env.cr.execute(query, (user_input, user_input))
- return [row[0] for row in self.env.cr.fetchall()]
+ # def get_similar_companies(self, user_input):
+ # query = """
+ # SELECT id
+ # FROM res_partner
+ # WHERE levenshtein(name::text, %s) < 3
+ # ORDER BY levenshtein(name::text, %s) ASC
+ # """
+ # self.env.cr.execute(query, (user_input, user_input))
+ # return [row[0] for row in self.env.cr.fetchall()]
def get_similar_companies(self, user_input):
query = """
SELECT id
FROM res_partner
- WHERE name ILIKE %s OR levenshtein(name::text, %s) < 3
+ WHERE (name ILIKE %s OR levenshtein(name::text, %s) < 3)
+ AND active = TRUE AND is_company = TRUE
ORDER BY levenshtein(name::text, %s) ASC
"""
- # Using '%' to match the partial company name
+ # Menggunakan '%' untuk mencocokkan nama perusahaan sebagian
self.env.cr.execute(query, ('%' + user_input + '%', user_input, user_input))
company_ids = [row[0] for row in self.env.cr.fetchall()]
return company_ids
@@ -73,7 +74,7 @@ class UserCompanyRequest(models.Model):
if not self.is_approve and is_approve:
if is_approve == 'approved':
- self.user_id.parent_id = self.user_company_id.id
+ self.user_id.parent_id = vals.get('user_company_id') if vals.get('user_company_id') else self.user_company_id.id
self.user_id.customer_type = self.user_company_id.customer_type
self.user_id.npwp = self.user_company_id.npwp
self.user_id.sppkp = self.user_company_id.sppkp
diff --git a/indoteknik_custom/models/va_multi_approve.py b/indoteknik_custom/models/va_multi_approve.py
new file mode 100644
index 00000000..028358c2
--- /dev/null
+++ b/indoteknik_custom/models/va_multi_approve.py
@@ -0,0 +1,22 @@
+from odoo import models, fields
+import logging
+
+_logger = logging.getLogger(__name__)
+
+
+class VaMultiApprove(models.TransientModel):
+ _name = 'va.multi.approve'
+
+ def save_multi_approve_va(self):
+ va_ids = self._context['va_ids']
+ vendor_approval = self.env['vendor.approval'].browse(va_ids)
+ vendor_approval.action_approve()
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': 'Notification',
+ 'message': 'Berhasil Di Approve',
+ 'next': {'type': 'ir.actions.act_window_close'},
+ }
+ } \ No newline at end of file
diff --git a/indoteknik_custom/models/va_multi_reject.py b/indoteknik_custom/models/va_multi_reject.py
new file mode 100644
index 00000000..c05581bf
--- /dev/null
+++ b/indoteknik_custom/models/va_multi_reject.py
@@ -0,0 +1,22 @@
+from odoo import models, fields
+import logging
+
+_logger = logging.getLogger(__name__)
+
+
+class VaMultiReject(models.TransientModel):
+ _name = 'va.multi.reject'
+
+ def save_multi_reject_va(self):
+ va_ids = self._context['va_ids']
+ vendor_approval = self.env['vendor.approval'].browse(va_ids)
+ vendor_approval.action_reject()
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': 'Notification',
+ 'message': 'Berhasil Di Reject',
+ 'next': {'type': 'ir.actions.act_window_close'},
+ }
+ } \ No newline at end of file
diff --git a/indoteknik_custom/models/vendor_approval.py b/indoteknik_custom/models/vendor_approval.py
index e540b8fc..01d2e6a2 100644
--- a/indoteknik_custom/models/vendor_approval.py
+++ b/indoteknik_custom/models/vendor_approval.py
@@ -13,61 +13,90 @@ class VendorApproval(models.Model):
number = fields.Char(string='Document No', index=True, copy=False, readonly=True, tracking=True)
partner_id = fields.Many2one('res.partner', string="Customer", readonly=True)
+ order_line_id = fields.Many2one('sale.order.line', string="SO Line", readonly=True)
order_id = fields.Many2one('sale.order', string="SO", readonly=True)
- vendor_approval_line = fields.One2many('vendor.approval.line', 'vendor_approval_id', string='Vendor Approval Lines', auto_join=True)
state = fields.Selection([('draft', 'Draft'), ('done', 'Done'), ('cancel', 'Reject')], string='State', tracking=True)
create_date_so = fields.Datetime(string='Create Date SO', readonly=True)
+ product_id = fields.Many2one('product.product', string='Product')
+ product_uom_qty = fields.Float(string='Quantity')
+ vendor_id = fields.Many2one('res.partner', string='Vendor')
+ vendor_md_id = fields.Many2one('res.partner', string='Vendor MD')
+ sales_price = fields.Float(string='Sales Price')
+ margin_before = fields.Float(string='Margin Before')
+ margin_after = fields.Float(string='Margin After')
+ purchase_price = fields.Float(string='Purchase Price')
+ purchase_price_md= fields.Float(string='Purchase Price MD')
+ purchase_tax_id = fields.Many2one('account.tax', string='Purchase Tax', domain=['|', ('active', '=', False), ('active', '=', True)])
+ sales_tax_id = fields.Many2one('account.tax', string='Sales Tax', domain=['|', ('active', '=', False), ('active', '=', True)])
+ percent_margin_difference = fields.Float(string='Percent Margin Difference')
@api.model
def create(self, vals):
vals['number'] = self.env['ir.sequence'].next_by_code('vendor.approval') or '0'
result = super(VendorApproval, self).create(vals)
return result
+
+ def check_state_so(self):
+ for rec in self:
+ if rec.order_id.state != 'draft':
+ raise UserError(f"SO {rec.order_id.name} sudah tidak bisa diubah")
+
def action_approve(self):
- if not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
- raise UserError('Hanya Merchandiser yang bisa approve')
-
- self.state = 'done'
- self.order_id.update({'vendor_approval': True})
- self.order_id.action_confirm()
- message = "Vendor Approval approved by %s" % (self.env.user.name)
- self.order_id.message_post(body=message)
+ for rec in self:
+ self.check_state_so()
+ if not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
+ raise UserError('Hanya Merchandiser yang bisa approve')
+
+ # Set state menjadi 'done'
+ rec.state = 'done'
+ rec.order_id.vendor_approval = True
+ message = f"Vendor Approval approved by {self.env.user.name}"
+ rec.order_id.message_post(body=message)
+
+ # Cek semua vendor.approval dengan order_id yang sama
+ related_approvals = self.env['vendor.approval'].search([('order_id', '=', rec.order_id.id)])
+ if all(approval.state != 'draft' for approval in related_approvals):
+ # Jalankan action_confirm hanya jika semua state bukan draft
+ if not rec.order_id.due_id and rec.order_id.state == 'draft':
+ rec.order_id.action_confirm()
def action_reject(self):
- if not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
- raise UserError('Hanya Merchandiser yang bisa cancel')
-
- self.state = 'cancel'
+ for rec in self:
+ self.check_state_so()
+ if not self.env.user.has_group('indoteknik_custom.group_role_merchandiser'):
+ raise UserError('Hanya Merchandiser yang bisa cancel')
+
+ rec.state = 'cancel'
+
+ rec.order_line_id.vendor_id = rec.vendor_md_id.id
+ rec.order_line_id.purchase_price = rec.purchase_price_md
+
+ message = "Vendor Approval rejected by %s" % (self.env.user.name)
+ self.order_id.message_post(body=message)
- message = "Vendor Approval rejected by %s" % (self.env.user.name)
- self.order_id.message_post(body=message)
+ related_approvals = self.env['vendor.approval'].search([('order_id', '=', rec.order_id.id)])
+ if all(approval.state != 'draft' for approval in related_approvals):
+ # Jalankan action_confirm hanya jika semua state bukan draft
+ if not rec.order_id.due_id and rec.order_id.state == 'draft':
+ rec.order_id.action_confirm()
def unlink(self):
res = super(VendorApproval, self).unlink()
if not self._name == 'vendor.approval':
raise UserError('Vendor Approval tidak bisa didelete')
return res
-
-class VendorApprovalLine(models.Model):
- _name = 'vendor.approval.line'
- _description = 'Vendor Approval Line'
- _order = 'vendor_approval_id, id'
-
- vendor_approval_id = fields.Many2one('vendor.approval', string='Vendor Approval Ref', required=True, ondelete='cascade', index=True, copy=False)
- product_id = fields.Many2one('product.product', string='Product')
- product_uom_qty = fields.Float(string='Quantity')
- vendor_id = fields.Many2one('res.partner', string='Vendor')
- vendor_md_id = fields.Many2one('res.partner', string='Vendor MD')
- sales_price = fields.Float(string='Sales Price')
- margin_before = fields.Float(string='Margin Before')
- margin_after = fields.Float(string='Margin After')
- purchase_price = fields.Float(string='Purchase Price')
- purchase_price_md= fields.Float(string='Purchase Price MD')
- purchase_tax_id = fields.Many2one('account.tax', string='Purchase Tax', domain=['|', ('active', '=', False), ('active', '=', True)])
- sales_tax_id = fields.Many2one('account.tax', string='Sales Tax', domain=['|', ('active', '=', False), ('active', '=', True)])
- percent_margin_difference = fields.Float(string='Percent Margin Difference')
-
-
+ def open_form_multi_approve(self):
+ action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_va_multi_approve')
+ action['context'] = {
+ 'va_ids': [x.id for x in self]
+ }
+ return action
+ def open_form_multi_reject(self):
+ action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_va_multi_reject')
+ action['context'] = {
+ 'va_ids': [x.id for x in self]
+ }
+ return action \ No newline at end of file
diff --git a/indoteknik_custom/models/wati.py b/indoteknik_custom/models/wati.py
index d9fb7247..f3632334 100644
--- a/indoteknik_custom/models/wati.py
+++ b/indoteknik_custom/models/wati.py
@@ -58,41 +58,56 @@ class WatiNotification(models.Model):
return
def _create_wati_history_header(self, ticket_id, sender_name, notification_json, date_wati):
+ # Helper function to remove NUL characters
+ def remove_null_characters(value):
+ if isinstance(value, str):
+ return value.replace('\0', '')
+ return value
+
+ # Sanitize the input data
param_header = {
'ticket_id': ticket_id,
- 'conversation_id': notification_json['conversationId'],
- 'sender_name': sender_name,
- 'wa_id': notification_json['waId'],
- 'text': notification_json['text'],
- 'date_wati': date_wati or '',
+ 'conversation_id': remove_null_characters(notification_json.get('conversationId', '')),
+ 'sender_name': remove_null_characters(sender_name),
+ 'wa_id': remove_null_characters(notification_json.get('waId', '')),
+ 'text': remove_null_characters(notification_json.get('text', '')),
+ 'date_wati': remove_null_characters(date_wati or ''),
}
+
+ # Create the record
new_header = self.env['wati.history'].create([param_header])
return new_header
def _create_wati_history_line(self, new_header, ticket_id, sender_name, notification_json, date_wati):
- text_body = notification_json['text'] or ''
+ # Helper function to remove NUL characters
+ def remove_null_characters(value):
+ if isinstance(value, str):
+ return value.replace('\0', '')
+ return value
+
+ # Sanitize the input data
param_line = {
"wati_history_id": new_header.id,
- "conversation_id": notification_json['conversationId'],
- "data": notification_json['data'] or '',
- "event_type": notification_json['eventType'] or '',
- # "list_reply": notification_json['listReply'] or '',
- # "message_contact": notification_json['messageContact'] or '',
- "operator_email": notification_json['operatorEmail'] or '',
- "operator_name": notification_json['operatorName'] or '',
- "sender_name": sender_name or '',
- # "source_url": notification_json['sourceUrl'] or '',
- "status_string": notification_json['statusString'] or '',
- "text": text_body,
+ "conversation_id": remove_null_characters(notification_json.get('conversationId', '')),
+ "data": remove_null_characters(notification_json.get('data', '')),
+ "event_type": remove_null_characters(notification_json.get('eventType', '')),
+ "operator_email": remove_null_characters(notification_json.get('operatorEmail', '')),
+ "operator_name": remove_null_characters(notification_json.get('operatorName', '')),
+ "sender_name": remove_null_characters(sender_name or ''),
+ "status_string": remove_null_characters(notification_json.get('statusString', '')),
+ "text": remove_null_characters(notification_json.get('text', '')),
"ticket_id": ticket_id,
- "type": notification_json['type'] or '',
- "wa_id": notification_json['waId'] or '',
- "date_wati": date_wati or '',
+ "type": remove_null_characters(notification_json.get('type', '')),
+ "wa_id": remove_null_characters(notification_json.get('waId', '')),
+ "date_wati": remove_null_characters(date_wati or ''),
}
+
+ # Create the record safely without NUL characters
self.env['wati.history.line'].create([param_line])
- self._update_header_after_create_line(new_header, sender_name, date_wati, text_body)
+ self._update_header_after_create_line(new_header, sender_name, date_wati, param_line['text'])
return
+
def _update_header_after_create_line(self, new_header, sender_name, date_wati, text_body):
new_header.last_reply_by = sender_name
new_header.last_reply_date = date_wati
@@ -192,27 +207,6 @@ class WatiHistory(models.Model):
is_get_attribute = fields.Boolean(string='Get Attribute', default=False)
def _get_attribute_wati(self):
- # url = 'https://live-server-2106.wati.io/api/v1/getContacts'
-
- # cookies = {
- # 'affinity': '1701232090.884.1520.321410|ff187ffce9bc0bae13542bb446e41008',
- # }
-
- # headers = {
- # 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI3MGM5ZmJhNy00MWRlLTRkMWEtYjY2NS1hM2Q5ODc2ZjhlZWIiLCJ1bmlxdWVfbmFtZSI6InR5YXNAaW5kb3Rla25pay5jb20iLCJuYW1laWQiOiJ0eWFzQGluZG90ZWtuaWsuY29tIiwiZW1haWwiOiJ0eWFzQGluZG90ZWtuaWsuY29tIiwiYXV0aF90aW1lIjoiMTEvMjkvMjAyMyAwNDoxNzo0NyIsImRiX25hbWUiOiIyMTA2IiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQURNSU5JU1RSQVRPUiIsImV4cCI6MjUzNDAyMzAwODAwLCJpc3MiOiJDbGFyZV9BSSIsImF1ZCI6IkNsYXJlX0FJIn0.--KHv4GCOG2MM3lNW9Nm-0-d8OAVpn5kbcSX4JKqATQ',
- # # 'Cookie': 'affinity=1701232090.884.1520.321410|ff187ffce9bc0bae13542bb446e41008',
- # }
-
- # files = {
- # 'pageSize': (None, '1'),
- # 'pageNumber': (None, '1'),
- # 'name': (None, ''),
- # 'attribute': (None, '[{name: "phone", operator: "contain", value: "6285751430014"}]'),
- # 'createdDate': (None, ''),
- # }
-
- # response = requests.get(url, cookies=cookies, headers=headers, files=files)
- # print(response.json())
domain = [
'&',
('is_get_attribute', '=', False),
@@ -226,29 +220,36 @@ class WatiHistory(models.Model):
for wati_history in wati_histories:
count += 1
_logger.info('[Parse Notification] Process: %s/%s' % (str(count), str(limit)))
+
wati_api = self.env['wati.api']
+
+ # Perbaikan pada params 'attribute' untuk menghindari masalah "type object is not subscriptable"
params = {
- 'pageSize':1,
- 'pageNumber':1,
- 'attribute':[{'name': "phone", 'operator': "contain", 'value': wati_history.wa_id}],
+ 'pageSize': 1,
+ 'pageNumber': 1,
+ 'attribute': json.dumps([{'name': "phone", 'operator': "contain", 'value': wati_history.wa_id}]),
}
+
wati_contacts = wati_api.http_get('/api/v1/getContacts', params)
- if wati_contacts['result'] != 'success':
+
+ if wati_contacts.get('result') != 'success':
return
- json_dump = json.dumps(wati_contacts, indent=4, sort_keys=True)
- contact_list = json.loads(json_dump)['contact_list']
+
+ contact_list = wati_contacts.get('contact_list', [])
+
perusahaan = email = ''
for data in contact_list:
- custom_params = data['customParams']
+ custom_params = data.get('customParams', [])
for custom_param in custom_params:
- name = custom_param['name']
- value = custom_param['value']
+ name = custom_param.get('name')
+ value = custom_param.get('value')
if name == 'perusahaan':
perusahaan = value
elif name == 'email':
email = value
- # end for 2
- # end for 1
+ # End inner loop
+
+ # Update wati_history fields
wati_history.perusahaan = perusahaan
wati_history.email = email
wati_history.is_get_attribute = True
diff --git a/indoteknik_custom/models/website_user_cart.py b/indoteknik_custom/models/website_user_cart.py
index 76b192c5..44393cf1 100644
--- a/indoteknik_custom/models/website_user_cart.py
+++ b/indoteknik_custom/models/website_user_cart.py
@@ -65,9 +65,11 @@ class WebsiteUserCart(models.Model):
if stock_quant:
res['is_in_bu'] = True
res['on_hand_qty'] = sum(stock_quant.mapped('quantity'))
+ res['available_quantity'] = sum(stock_quant.mapped('available_quantity'))
else:
res['is_in_bu'] = False
res['on_hand_qty'] = 0
+ res['available_quantity'] = 0
flashsales = self.product_id._get_active_flash_sale()
res['has_flashsale'] = True if len(flashsales) > 0 else False
@@ -93,22 +95,31 @@ class WebsiteUserCart(models.Model):
def get_product_by_user(self, user_id, selected=False, source=False):
user_id = int(user_id)
-
+
if source == 'buy':
source = ['buy']
else:
source = ['add_to_cart', 'buy']
parameters = [
- ('user_id', '=', user_id),
+ ('user_id', '=', user_id),
('source', 'in', source)
]
+ carts = self.search(parameters)
+
+ for cart in carts:
+ if cart.product_id:
+ price = cart.product_id._v2_get_website_price_include_tax()
+ if not cart.product_id.active or price < 1:
+ cart.is_selected = False
if selected:
parameters.append(('is_selected', '=', True))
- carts = self.search(parameters)
- products = carts.get_products()
+ products_active = self.search(parameters)
+
+ products = products_active.get_products()
+
return products
def get_user_checkout(self, user_id, voucher=False, voucher_shipping=False, source=False):
@@ -173,11 +184,11 @@ class WebsiteUserCart(models.Model):
}
return result
- def action_mail_reminder_to_checkout(self, limit=250):
+ def action_mail_reminder_to_checkout(self, limit=200):
user_ids = self.search([('is_reminder', '=', False)]).mapped('user_id')[:limit]
for user in user_ids:
- latest_cart = self.search([('user_id', '=', user.id), ('is_reminder', '=', False)], order='create_date desc', limit=1)
+ latest_cart = self.search([('user_id', '=', user.id)], order='create_date desc', limit=1)
carts_to_remind = self.search([('user_id', '=', user.id)])
diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv
index 80ae0afa..7a4a1e24 100755
--- a/indoteknik_custom/security/ir.model.access.csv
+++ b/indoteknik_custom/security/ir.model.access.csv
@@ -140,5 +140,12 @@ access_account_tax,access.account.tax,model_account_tax,,1,1,1,1
access_approval_unreserve,access.approval.unreserve,model_approval_unreserve,,1,1,1,1
access_approval_unreserve_line,access.approval.unreserve.line,model_approval_unreserve_line,,1,1,1,1
access_vendor_approval,access.vendor.approval,model_vendor_approval,,1,1,1,1
-access_vendor_approval_line,access.vendor.approval.line,model_vendor_approval_line,,1,1,1,1
access_vit_kota,access.vit.kota,model_vit_kota,,1,1,1,1
+access_v_brand_product_category,access.v.brand.product.category,model_v_brand_product_category,,1,1,1,1
+access_web_find_page,access.web.find.page,model_web_find_page,,1,1,1,1
+access_v_requisition_match_po,access.v.requisition.match.po,model_v_requisition_match_po,,1,1,1,1
+access_approval_retur_picking,access.approval.retur.picking,model_approval_retur_picking,,1,1,1,1
+access_sales_order_fulfillment_v2,access.sales.order.fulfillment.v2,model_sales_order_fulfillment_v2,,1,1,1,1
+access_v_move_outstanding,access.v.move.outstanding,model_v_move_outstanding,,1,1,1,1
+access_va_multi_approve,access.va.multi.approve,model_va_multi_approve,,1,1,1,1
+access_va_multi_reject,access.va.multi.reject,model_va_multi_reject,,1,1,1,1
diff --git a/indoteknik_custom/views/account_move_views.xml b/indoteknik_custom/views/account_move_views.xml
index 4acafb14..da25636e 100644
--- a/indoteknik_custom/views/account_move_views.xml
+++ b/indoteknik_custom/views/account_move_views.xml
@@ -12,6 +12,10 @@
<field name="description"/>
<field name="approval_status"/>
<field name="is_approve"/>
+ <field name="approve_by" optional="hide"/>
+ <field name="date_approve" optional="hide"/>
+ <field name="create_uid" optional="hide"/>
+ <field name="create_date" optional="hide"/>
</tree>
</field>
</record>
@@ -58,12 +62,14 @@
<group>
<field name="partner_id" readonly="1"/>
<field name="day_extension" attrs="{'readonly': [('is_approve', '=', True)]}"/>
+ <field name="order_id" readonly="1"/>
</group>
<group>
<field name="is_approve" readonly="1"/>
- <field name="order_id" readonly="1"/>
<field name="counter" readonly="1"/>
<field name="approval_status" readonly="1"/>
+ <field name="approve_by" readonly="1"/>
+ <field name="date_approve" readonly="1"/>
</group>
</group>
<group>
diff --git a/indoteknik_custom/views/approval_retur_picking.xml b/indoteknik_custom/views/approval_retur_picking.xml
new file mode 100644
index 00000000..5ce28e20
--- /dev/null
+++ b/indoteknik_custom/views/approval_retur_picking.xml
@@ -0,0 +1,27 @@
+<odoo>
+ <!-- Form View for Stock Return Note Wizard -->
+ <record id="view_stock_return_note_form" model="ir.ui.view">
+ <field name="name">approval.retur.picking.form</field>
+ <field name="model">approval.retur.picking</field>
+ <field name="arch" type="xml">
+ <form string="Add Return Note">
+ <group>
+ <span>Ask Approval Retur?</span>
+ </group>
+ <footer>
+ <button name="action_confirm_note_return" string="Confirm" type="object" class="btn-primary"/>
+ <button string="Cancel" class="btn-secondary" special="cancel"/>
+ </footer>
+ </form>
+ </field>
+ </record>
+
+ <!-- Action to Open the Wizard -->
+ <record id="action_stock_return_note_wizard" model="ir.actions.act_window">
+ <field name="name">Add Return Note</field>
+ <field name="res_model">approval.retur.picking</field>
+ <field name="view_mode">form</field>
+ <field name="view_id" ref="view_stock_return_note_form"/>
+ <field name="target">new</field>
+ </record>
+</odoo>
diff --git a/indoteknik_custom/views/dunning_run.xml b/indoteknik_custom/views/dunning_run.xml
index 522be8c9..2117a7bb 100644
--- a/indoteknik_custom/views/dunning_run.xml
+++ b/indoteknik_custom/views/dunning_run.xml
@@ -14,6 +14,7 @@
<field name="date_terima_tukar_faktur"/>
<field name="shipper_faktur_id"/>
<field name="grand_total"/>
+ <field name="create_uid" optional="hide"/>
</tree>
</field>
</record>
diff --git a/indoteknik_custom/views/find_page.xml b/indoteknik_custom/views/find_page.xml
new file mode 100644
index 00000000..c752aa98
--- /dev/null
+++ b/indoteknik_custom/views/find_page.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<odoo>
+ <record id="web_find_page_tree" model="ir.ui.view">
+ <field name="name">web.find.page.tree</field>
+ <field name="model">web.find.page</field>
+ <field name="arch" type="xml">
+ <tree>
+ <field name="category_id"/>
+ <field name="brand_id"/>
+ <field name="url"/>
+ <field name="create_uid"/>
+ <field name="write_uid"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="web_find_page_form" model="ir.ui.view">
+ <field name="name">web.find.page.form</field>
+ <field name="model">web.find.page</field>
+ <field name="arch" type="xml">
+ <form>
+ <sheet string="Web Find Page">
+ <div class="oe_button_box" name="button_box"/>
+ <group>
+ <group>
+ <field name="category_id"/>
+ <field name="brand_id"/>
+ <field name="url"/>
+ </group>
+ <group>
+ <field name="create_uid"/>
+ <field name="write_uid"/>
+ </group>
+ </group>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids" widget="mail_followers"/>
+ <field name="message_ids" widget="mail_thread"/>
+ </div>
+ </form>
+ </field>
+ </record>
+
+ <record id="view_web_find_page_filter" model="ir.ui.view">
+ <field name="name">web.find.page.list.select</field>
+ <field name="model">web.find.page</field>
+ <field name="priority" eval="15"/>
+ <field name="arch" type="xml">
+ <search string="Search Web Find Page">
+ <field name="category_id"/>
+ <field name="brand_id"/>
+ <field name="url"/>
+ </search>
+ </field>
+ </record>
+
+ <record id="web_find_page_action" model="ir.actions.act_window">
+ <field name="name">Web Find Page</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="res_model">web.find.page</field>
+ <field name="search_view_id" ref="view_web_find_page_filter"/>
+ <field name="view_mode">tree,form</field>
+ </record>
+
+ <menuitem id="menu_web_find_page"
+ name="Web Find Page"
+ action="web_find_page_action"
+ parent="website_sale.menu_orders"
+ sequence="8"/>
+</odoo> \ No newline at end of file
diff --git a/indoteknik_custom/views/form_vendor_approval_multi_approve.xml b/indoteknik_custom/views/form_vendor_approval_multi_approve.xml
new file mode 100644
index 00000000..d9be107e
--- /dev/null
+++ b/indoteknik_custom/views/form_vendor_approval_multi_approve.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<odoo>
+ <data>
+ <record id="view_va_multi_approve_form" model="ir.ui.view">
+ <field name="name">VA Multi Approve</field>
+ <field name="model">va.multi.approve</field>
+ <field name="arch" type="xml">
+ <form>
+ <sheet>
+ <group>
+ <span>Apakah Anda Yakin Ingin Approve Vendor Approval?</span>
+ </group>
+ </sheet>
+ <footer>
+ <button name="save_multi_approve_va" string="Update" type="object" default_focus="1" class="oe_highlight"/>
+ <button string="Cancel" class="btn btn-secondary" special="cancel" />
+ </footer>
+ </form>
+ </field>
+ </record>
+
+ <record id="action_va_multi_approve" model="ir.actions.act_window">
+ <field name="name">VA Multi Approve</field>
+ <field name="res_model">va.multi.approve</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="view_mode">form</field>
+ <field name="view_id" ref="view_va_multi_approve_form"/>
+ <field name="target">new</field>
+ </record>
+ </data>
+</odoo> \ No newline at end of file
diff --git a/indoteknik_custom/views/form_vendor_approval_multi_reject.xml b/indoteknik_custom/views/form_vendor_approval_multi_reject.xml
new file mode 100644
index 00000000..666de261
--- /dev/null
+++ b/indoteknik_custom/views/form_vendor_approval_multi_reject.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<odoo>
+ <data>
+ <record id="view_va_multi_reject_form" model="ir.ui.view">
+ <field name="name">VA Multi Reject</field>
+ <field name="model">va.multi.reject</field>
+ <field name="arch" type="xml">
+ <form>
+ <sheet>
+ <group>
+ <span>Apakah Anda Yakin Ingin Reject Vendor Approval?</span>
+ </group>
+ </sheet>
+ <footer>
+ <button name="save_multi_reject_va" string="Update" type="object" default_focus="1" class="oe_highlight"/>
+ <button string="Cancel" class="btn btn-secondary" special="cancel" />
+ </footer>
+ </form>
+ </field>
+ </record>
+
+ <record id="action_va_multi_reject" model="ir.actions.act_window">
+ <field name="name">VA Multi Reject</field>
+ <field name="res_model">va.multi.reject</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="view_mode">form</field>
+ <field name="view_id" ref="view_va_multi_reject_form"/>
+ <field name="target">new</field>
+ </record>
+ </data>
+</odoo> \ No newline at end of file
diff --git a/indoteknik_custom/views/partner_payment_term.xml b/indoteknik_custom/views/partner_payment_term.xml
index 433cac3e..e0f2fe39 100644
--- a/indoteknik_custom/views/partner_payment_term.xml
+++ b/indoteknik_custom/views/partner_payment_term.xml
@@ -24,6 +24,9 @@
<field name="name"/>
<field name="parent_id" readonly="1"/>
<field name="property_payment_term_id"/>
+ <field name="active_limit"/>
+ <field name="warning_stage"/>
+ <field name="blocking_stage"/>
</group>
</group>
</sheet>
diff --git a/indoteknik_custom/views/product_product.xml b/indoteknik_custom/views/product_product.xml
index c06cc5f1..71748e44 100644
--- a/indoteknik_custom/views/product_product.xml
+++ b/indoteknik_custom/views/product_product.xml
@@ -11,11 +11,14 @@
<field name="incoming_qty"/>
</field>
<field name="virtual_available" position="after">
+ <field name="max_qty_reorder" optional="hide"/>
<field name="qty_onhand_bandengan" optional="hide"/>
<field name="qty_incoming_bandengan" optional="hide"/>
<field name="qty_outgoing_bandengan" optional="hide"/>
<field name="qty_available_bandengan" optional="hide"/>
<field name="qty_free_bandengan" optional="hide"/>
+ <field name="qty_rpo" optional="hide"/>
+ <field name="plafon_qty" optional="hide"/>
</field>
</field>
</record>
diff --git a/indoteknik_custom/views/product_template.xml b/indoteknik_custom/views/product_template.xml
index a77b99de..b6599137 100755
--- a/indoteknik_custom/views/product_template.xml
+++ b/indoteknik_custom/views/product_template.xml
@@ -24,6 +24,12 @@
<field name="public_categ_ids" position="attributes">
<attribute name="required">0</attribute>
</field>
+ <div name="options" position="inside">
+ <div>
+ <field name="merchandise_ok"/>
+ <label for="merchandise_ok"/>
+ </div>
+ </div>
<field name="public_categ_ids" position="attributes">
<attribute name="options">{'no_create': True}</attribute>
</field>
@@ -39,12 +45,23 @@
</group>
</page>
</page>
- <field name="supplier_taxes_id" position="after">
+ <field name="supplier_taxes_id" position="after">
<field name="supplier_url" widget="url"/>
</field>
</field>
</record>
-
+
+ <record id="product_normal_form_view_inherit" model="ir.ui.view">
+ <field name="name">Product Template</field>
+ <field name="model">product.product</field>
+ <field name="inherit_id" ref="product.product_normal_form_view"/>
+ <field name="arch" type="xml">
+ <field name="last_update_solr" position="after">
+ <field name="clean_website_description" />
+ </field>
+ </field>
+ </record>
+
<record id="x_manufactures_group_by" model="ir.ui.view">
<field name="name">product.template.search.inherit</field>
<field name="model">product.template</field>
diff --git a/indoteknik_custom/views/purchase_order.xml b/indoteknik_custom/views/purchase_order.xml
index f6e5a1a4..0e6b6792 100755
--- a/indoteknik_custom/views/purchase_order.xml
+++ b/indoteknik_custom/views/purchase_order.xml
@@ -27,7 +27,11 @@
<button name="delete_line" type="object" string="Delete " states="draft"/>
</button>
<button name="button_unlock" position="after">
- <button name="create_bill_dp" string="Create Bill DP" type="object" class="oe_highlight" attrs="{'invisible': [('state', 'not in', ('purchase', 'done'))]}"/>
+ <button name="create_bill_dp" string="Create Bill DP" type="object" class="oe_highlight" attrs="{'invisible': [('state', 'not in', ('purchase', 'done')), ('bills_pelunasan_id', '!=', False)]}"/>
+ </button>
+ <button name="button_unlock" position="after">
+ <button name="create_bill_pelunasan" string="Create Bill Pelunasan" type="object" class="oe_highlight" attrs="{'invisible': [('state', 'not in', ('purchase', 'done')), ('bills_pelunasan_id', '!=', False)]}"/>
+
</button>
<field name="date_order" position="before">
<field name="sale_order_id" attrs="{'readonly': [('state', 'not in', ['draft'])]}"/>
@@ -41,11 +45,12 @@
<field name="incoterm_id" position="after">
<field name="amount_total_without_service"/>
<field name="delivery_amt"/>
+ <field name="approve_by"/>
</field>
<field name="currency_id" position="after">
<field name="summary_qty_po"/>
<field name="count_line_product"/>
- <field name="payment_term_id" readonly="1"/>
+ <field name="payment_term_id"/>
</field>
<field name="amount_total" position="after">
<field name="total_margin"/>
@@ -64,10 +69,14 @@
<field name="qty_onhand" readonly="1" optional="hide"/>
<field name="qty_incoming" readonly="1" optional="hide"/>
<field name="qty_outgoing" readonly="1" optional="hide"/>
+ <field name="clean_website_description_product" readonly="1" optional="hide"/>
<field name="qty_available" readonly="1" optional="hide"/>
<field name="qty_reserved" readonly="1" optional="hide"/>
<field name="suggest" readonly="1" widget="badge" decoration-danger="suggest == 'harus beli'" decoration-success="suggest == 'masih cukup'"/>
- <!-- <field name="suggest" readonly="1"/> -->
+ <!-- <field name="suggest" readonly="1"/> -->
+ </field>
+ <field name="product_id" position="before">
+ <field name="image_small" readonly="1" optional="hide" widget="image" height="50"/>
</field>
<field name="price_unit" position="after">
<field name="price_vendor" attrs="{'readonly': 1}" optional="hide"/>
@@ -95,6 +104,7 @@
<field name="approval_edit_line"/>
<field name="logbook_bill_id"/>
<field name="bills_dp_id" readonly="1"/>
+ <field name="bills_pelunasan_id" readonly="1"/>
</field>
<field name="order_line" position="attributes">
@@ -202,7 +212,6 @@
<field name="code">model.procure_calculation()</field>
<field name="state">code</field>
<field name="priority">75</field>
- <field name="active">True</field>
</record>
</data>
<data>
diff --git a/indoteknik_custom/views/requisition.xml b/indoteknik_custom/views/requisition.xml
index 652d03d0..957113a7 100644
--- a/indoteknik_custom/views/requisition.xml
+++ b/indoteknik_custom/views/requisition.xml
@@ -50,6 +50,13 @@
<field name="model">requisition</field>
<field name="arch" type="xml">
<form>
+ <header>
+ <button name="button_approve"
+ string="Approve"
+ type="object"
+ class="mr-2 oe_highlight"
+ />
+ </header>
<sheet string="Requisition">
<div class="oe_button_box" name="button_box"/>
<group>
@@ -62,12 +69,19 @@
</group>
<group>
<div>
- <button name="create_po_from_requisition"
- string="Create PO"
- type="object"
- class="mr-2 oe_highlight"
+ <button name="generate_requisition_from_so"
+ string="Create Line from SO"
+ type="object"
+ class="mr-2 oe_highlight"
/>
</div>
+ <div>
+ <button name="create_po_from_requisition"
+ string="Create PO"
+ type="object"
+ class="mr-2 oe_highlight"
+ />
+ </div>
</group>
</group>
<notebook>
@@ -78,6 +92,7 @@
<field name="partner_id" required="1" />
<field name="qty_purchase" required="1" />
<field name="price_unit" required="1" />
+ <field name="taxes_id" readonly="1" />
<field name="subtotal" readonly="1" />
<field name="brand_id" />
</tree>
diff --git a/indoteknik_custom/views/res_partner.xml b/indoteknik_custom/views/res_partner.xml
index 6d02a86b..1b164161 100644
--- a/indoteknik_custom/views/res_partner.xml
+++ b/indoteknik_custom/views/res_partner.xml
@@ -6,6 +6,15 @@
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
+ <field name="active_limit" position="attributes">
+ <attribute name="readonly">1</attribute>
+ </field>
+ <field name="warning_stage" position="attributes">
+ <attribute name="readonly">1</attribute>
+ </field>
+ <field name="blocking_stage" position="attributes">
+ <attribute name="readonly">1</attribute>
+ </field>
<field name="npwp" position="after">
<field name="sppkp"/>
<field name="counter"/>
@@ -22,7 +31,7 @@
<field name="industry_id" position="after">
<field name="company_type_id"/>
<field name="group_partner_id"/>
- <field name="is_potential"/>
+ <field name="is_not_potential"/>
<field name="pareto_status"/>
<field name="digital_invoice_tax"/>
</field>
diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml
index 895b242c..703b4d49 100755
--- a/indoteknik_custom/views/sale_order.xml
+++ b/indoteknik_custom/views/sale_order.xml
@@ -73,6 +73,7 @@
<field name="margin_after_delivery_purchase"/>
<field name="percent_margin_after_delivery_purchase"/>
<field name="total_weight"/>
+ <field name="pareto_status"/>
</field>
<field name="analytic_account_id" position="after">
<field name="customer_type" required="1"/>
@@ -81,7 +82,7 @@
<field name="email" required="1"/>
<field name="unreserve_id"/>
<field name="due_id" readonly="1"/>
- <field name="vendor_approval_id" readonly="1"/>
+ <field name="vendor_approval_id" readonly="1" widget="many2many_tags"/>
<field name="source_id" domain="[('id', 'in', [32, 59, 60, 61])]" required="1"/>
<button name="override_allow_create_invoice"
string="Override Create Invoice"
@@ -117,8 +118,47 @@
}
</attribute>
</xpath>
+ <div name="invoice_lines" position="before">
+ <div name="vendor_id" groups="base.group_no_one" attrs="{'invisible': [('display_type', '!=', False)]}">
+ <label for="vendor_id"/>
+ <div name="vendor_id">
+ <field name="vendor_id"
+ attrs="{'readonly': [('parent.approval_status', '=', 'approved')]}"
+ domain="[('parent_id', '=', False)]"
+ options="{'no_create': True}" class="oe_inline" />
+ </div>
+ </div>
+ </div>
+
+ <div name="invoice_lines" position="before">
+ <div name="purchase_price" groups="base.group_no_one" attrs="{'invisible': [('display_type', '!=', False)]}">
+ <label for="purchase_price"/>
+ <field name="purchase_price"/>
+ </div>
+ </div>
+ <div name="invoice_lines" position="before">
+ <div name="purchase_tax_id" groups="base.group_no_one" attrs="{'invisible': [('display_type', '!=', False)]}">
+ <label for="purchase_tax_id"/>
+ <div name="purchase_tax_id">
+ <field name="purchase_tax_id"/>
+ </div>
+ </div>
+ </div>
+ <div name="invoice_lines" position="before">
+ <div name="item_percent_margin" groups="base.group_no_one" attrs="{'invisible': [('display_type', '!=', False)]}">
+ <label for="item_percent_margin"/>
+ <field name="item_percent_margin"/>
+ </div>
+ </div>
+ <div name="invoice_lines" position="before">
+ <div name="price_subtotal" groups="base.group_no_one" attrs="{'invisible': [('display_type', '!=', False)]}">
+ <label for="price_subtotal"/>
+ <field name="price_subtotal"/>
+ </div>
+ </div>
<xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='price_total']" position="after">
- <field name="vendor_id" attrs="{'readonly': [('parent.approval_status', '=', 'approved')]}" domain="[('parent_id', '=', False)]" options="{'no_create':True}"/>
+ <field name="qty_free_bu" optional="hide"/>
+ <field name="vendor_id" attrs="{'readonly': [('parent.approval_status', '=', 'approved')], 'invisible': [('display_type', '!=', False)]}" domain="[('parent_id', '=', False)]" options="{'no_create':True}"/>
<field name="vendor_md_id" optional="hide"/>
<field name="purchase_price" attrs="
{
@@ -140,7 +180,6 @@
<field name="weight" optional="hide"/>
<field name="amount_voucher_disc" string="Voucher" readonly="1" optional="hide"/>
<field name="order_promotion_id" string="Promotion" readonly="1" optional="hide"/>
- <field name="md_vendor_id" string="MD Vendor" readonly="1" optional="hide"/>
</xpath>
<xpath expr="//form/sheet/notebook/page/field[@name='order_line']/tree/field[@name='product_id']" position="before">
<field name="line_no" readonly="1" optional="hide"/>
@@ -224,8 +263,11 @@
<page string="Matches PO" name="page_matches_po" invisible="1">
<field name="order_sales_match_line" readonly="1"/>
</page>
- <page string="Fullfillment" name="page_sale_order_fullfillment">
+ <!-- <page string="Fullfillment" name="page_sale_order_fullfillment">
<field name="fullfillment_line" readonly="1"/>
+ </page> -->
+ <page string="Fulfillment v2" name="page_sale_order_fullfillment2">
+ <field name="fulfillment_line_v2" readonly="1"/>
</page>
<page string="Reject Line" name="page_sale_order_reject_line">
<field name="reject_line" readonly="1"/>
@@ -245,6 +287,7 @@
<field name="client_order_ref"/>
<field name="payment_type" optional="hide"/>
<field name="payment_status" optional="hide"/>
+ <field name="pareto_status" optional="hide"/>
</field>
</field>
</record>
@@ -263,6 +306,7 @@
<field name="date_driver_arrival"/>
<field name="payment_type" optional="hide"/>
<field name="payment_status" optional="hide"/>
+ <field name="pareto_status" optional="hide"/>
</field>
</field>
</record>
@@ -340,6 +384,25 @@
</data>
<data>
+
+ </data>
+ <record id="sales_order_fulfillment_v2_tree" model="ir.ui.view">
+ <field name="name">sales.order.fulfillment.v2.tree</field>
+ <field name="model">sales.order.fulfillment.v2</field>
+ <field name="arch" type="xml">
+ <tree editable="top" create="false">
+ <field name="product_id" readonly="1"/>
+ <field name="so_qty" readonly="1" optional="show"/>
+ <field name="reserved_stock_qty" readonly="1" optional="show"/>
+ <field name="delivered_qty" readonly="1" optional="hide"/>
+ <field name="po_ids" widget="many2many_tags" readonly="1" optional="show"/>
+ <field name="po_qty" readonly="1" optional="show"/>
+ <field name="received_qty" readonly="1" optional="show"/>
+ <field name="purchaser" readonly="1" optional="hide"/>
+ </tree>
+ </field>
+ </record>
+ <data>
<record id="sales_order_fullfillmet_tree" model="ir.ui.view">
<field name="name">sales.order.fullfillment.tree</field>
<field name="model">sales.order.fullfillment</field>
diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml
index 899d29eb..1893fcaf 100644
--- a/indoteknik_custom/views/stock_picking.xml
+++ b/indoteknik_custom/views/stock_picking.xml
@@ -16,6 +16,8 @@
<field name="driver_arrival_date" optional="hide"/>
<field name="note_logistic" optional="hide"/>
<field name="note" optional="hide"/>
+ <field name="date_reserved" optional="hide"/>
+ <field name="state_reserve" optional="hide"/>
</field>
<field name="partner_id" position="after">
<field name="purchase_representative_id"/>
@@ -50,6 +52,10 @@
type="object"
attrs="{'invisible': ['|', ('state', '!=', 'done'), ('name', 'ilike', 'out')]}"
/>
+ <button name="action_send_to_biteship"
+ string="Biteship"
+ type="object"
+ />
</button>
<field name="backorder_id" position="after">
<field name="summary_qty_detail"/>
@@ -91,6 +97,8 @@
<field name="status_printed"/>
<field name="printed_sj"/>
<field name="date_printed_sj"/>
+ <field name="printed_sj_retur"/>
+ <field name="date_printed_sj_retur"/>
<field name="date_printed_list"/>
<field name="is_internal_use"
string="Internal Use"
@@ -121,14 +129,19 @@
<page string="Delivery" name="delivery_order">
<group>
<group>
+ <field name="notee"/>
<field name="note_logistic"/>
+ <field name="responsible" />
+ <field name="carrier_id"/>
+ <field name="picking_code" attrs="{'invisible': [['picking_code', '=', False]]}"/>
+ <field name="picking_code" string="Picking code (akan digenerate ketika sudah di-validate)" attrs="{'invisible': [['picking_code', '!=', False]]}"/>
<field name="driver_departure_date" attrs="{'readonly':[('invoice_status', '=', 'invoiced')]}"/>
<field name="driver_arrival_date"/>
<field name="delivery_tracking_no"/>
<field name="driver_id"/>
- <field name="carrier_id"/>
- <field name="picking_code" attrs="{'invisible': [['picking_code', '=', False]]}"/>
- <field name="picking_code" string="Picking code (akan digenerate ketika sudah di-validate)" attrs="{'invisible': [['picking_code', '!=', False]]}"/>
+ <field name='sj_return_date'/>
+ <field name="sj_documentation" widget="image" />
+ <field name="paket_documentation" widget="image" />
</group>
</group>
</page>
diff --git a/indoteknik_custom/views/vendor_approval.xml b/indoteknik_custom/views/vendor_approval.xml
index 605edfbf..359c67d4 100644
--- a/indoteknik_custom/views/vendor_approval.xml
+++ b/indoteknik_custom/views/vendor_approval.xml
@@ -5,33 +5,24 @@
<field name="name">vendor.approval.tree</field>
<field name="model">vendor.approval</field>
<field name="arch" type="xml">
- <tree default_order="create_date desc" create="0">
+ <tree default_order="create_date desc" create="0" editable="bottom">
<field name="number"/>
<field name="create_date_so"/>
<field name="order_id"/>
<field name="partner_id"/>
- <field name="state"/>
- </tree>
- </field>
- </record>
-
- <record id="vendor_approval_line_tree" model="ir.ui.view">
- <field name="name">vendor.approval.line.tree</field>
- <field name="model">vendor.approval.line</field>
- <field name="arch" type="xml">
- <tree>
+ <field name="state" readonly="1" widget="badge" decoration-danger="state == 'cancel'" decoration-success="state == 'done'"/>
<field name="product_id"/>
- <field name="sales_price"/>
- <field name="product_uom_qty"/>
- <field name="sales_tax_id"/>
- <field name="margin_after"/>
+ <field name="sales_price" optional="hide"/>
+ <field name="product_uom_qty" optional="hide"/>
+ <field name="sales_tax_id" optional="hide"/>
+ <field name="margin_after" optional="hide"/>
<field name="vendor_id"/>
<field name="vendor_md_id"/>
- <field name="purchase_price"/>
- <field name="purchase_price_md"/>
- <field name="margin_before"/>
- <field name="purchase_tax_id"/>
- <field name="percent_margin_difference"/>
+ <field name="purchase_price" optional="hide"/>
+ <field name="purchase_price_md" optional="hide"/>
+ <field name="margin_before" optional="hide"/>
+ <field name="purchase_tax_id" optional="hide"/>
+ <field name="percent_margin_difference" optional="hide"/>
</tree>
</field>
</record>
@@ -60,13 +51,22 @@
<field name="order_id" readonly="1"/>
<field name="state" readonly="1"/>
<field name="create_date_so" readonly="1"/>
+ <field name="product_id"/>
+ <field name="sales_price"/>
+ <field name="product_uom_qty"/>
+ <field name="sales_tax_id"/>
+ </group>
+ <group>
+ <field name="margin_after"/>
+ <field name="vendor_id"/>
+ <field name="vendor_md_id"/>
+ <field name="purchase_price"/>
+ <field name="purchase_price_md"/>
+ <field name="margin_before"/>
+ <field name="purchase_tax_id"/>
+ <field name="percent_margin_difference"/>
</group>
</group>
- <notebook>
- <page string="SO Line">
- <field name="vendor_approval_line" readonly="1"/>
- </page>
- </notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
@@ -77,7 +77,7 @@
</record>
<record id="vendor_approval_view_search" model="ir.ui.view">
- <field name="name">vendor.approval.search.view</field> <!-- Made the name more descriptive -->
+ <field name="name">vendor.approval.search.view</field>
<field name="model">vendor.approval</field>
<field name="arch" type="xml">
<search string="Search Vendor Approval">
@@ -102,5 +102,19 @@
sequence="4"
action="vendor_approval_action"
/>
+ <record id="vendor_approval_multi_approve_ir_actions_server" model="ir.actions.server">
+ <field name="name">Approve</field>
+ <field name="model_id" ref="model_vendor_approval"/>
+ <field name="binding_model_id" ref="model_vendor_approval"/>
+ <field name="state">code</field>
+ <field name="code">action = records.open_form_multi_approve()</field>
+ </record>
+ <record id="vendor_approval_multi_reject_ir_actions_server" model="ir.actions.server">
+ <field name="name">Reject</field>
+ <field name="model_id" ref="model_vendor_approval"/>
+ <field name="binding_model_id" ref="model_vendor_approval"/>
+ <field name="state">code</field>
+ <field name="code">action = records.open_form_multi_reject()</field>
+ </record>
</data>
</odoo>