summaryrefslogtreecommitdiff
path: root/indoteknik_custom/models
diff options
context:
space:
mode:
Diffstat (limited to 'indoteknik_custom/models')
-rw-r--r--indoteknik_custom/models/automatic_purchase.py230
-rwxr-xr-xindoteknik_custom/models/purchase_order.py281
-rwxr-xr-xindoteknik_custom/models/sale_order.py262
3 files changed, 629 insertions, 144 deletions
diff --git a/indoteknik_custom/models/automatic_purchase.py b/indoteknik_custom/models/automatic_purchase.py
index f7c0d75e..8e817409 100644
--- a/indoteknik_custom/models/automatic_purchase.py
+++ b/indoteknik_custom/models/automatic_purchase.py
@@ -291,7 +291,7 @@ class AutomaticPurchase(models.Model):
line.current_po_id = new_po.id
line.current_po_line_id = new_po_line.id
- self.create_purchase_order_sales_match(new_po)
+ # self.create_purchase_order_sales_match(new_po)
@@ -304,146 +304,146 @@ class AutomaticPurchase(models.Model):
for sales in sales_match:
sales.sale_line_id.purchase_price = apo.last_price
- def create_purchase_order_sales_match(self, purchase_order):
- matches_so_product_ids = [line.product_id.id for line in purchase_order.order_line]
-
- matches_so = self.env['v.sale.notin.matchpo'].search([
- ('automatic_purchase_id', '=', self.id),
- ('sale_line_id.product_id', 'in', matches_so_product_ids),
- ])
-
- sale_ids_set = set()
- sale_ids_name = set()
- retur_cache = {}
- incoming_cache = {}
- for sale_order in matches_so:
- exist = self.env['purchase.order.sales.match'].search([
- ('product_id', '=', sale_order.product_id.id),
- ('sale_line_id', '=', sale_order.sale_line_id.id),
- ('sale_id', '=', sale_order.sale_id.id),
- ('purchase_order_id.state', '!=', 'cancel'),
- ])
+ # def create_purchase_order_sales_match(self, purchase_order):
+ # matches_so_product_ids = [line.product_id.id for line in purchase_order.order_line]
+
+ # matches_so = self.env['v.sale.notin.matchpo'].search([
+ # ('automatic_purchase_id', '=', self.id),
+ # ('sale_line_id.product_id', 'in', matches_so_product_ids),
+ # ])
+
+ # sale_ids_set = set()
+ # sale_ids_name = set()
+ # retur_cache = {}
+ # incoming_cache = {}
+ # for sale_order in matches_so:
+ # exist = self.env['purchase.order.sales.match'].search([
+ # ('product_id', '=', sale_order.product_id.id),
+ # ('sale_line_id', '=', sale_order.sale_line_id.id),
+ # ('sale_id', '=', sale_order.sale_id.id),
+ # ('purchase_order_id.state', '!=', 'cancel'),
+ # ])
- skip_line = False
+ # skip_line = False
- sale_line_id = sale_order.sale_line_id.id
+ # sale_line_id = sale_order.sale_line_id.id
- if sale_line_id not in incoming_cache:
+ # if sale_line_id not in incoming_cache:
- qty_incoming = 0
+ # qty_incoming = 0
- for existing in exist:
- if existing.purchase_order_id.state in ['done', 'purchase']:
+ # for existing in exist:
+ # if existing.purchase_order_id.state in ['done', 'purchase']:
- incoming_moves = self.env['stock.move'].search([
- ('reference', 'ilike', 'BU/INPUT'),
- ('state', 'not in', ['done','cancel']),
- ('product_id', '=', existing.product_id.id),
- ('purchase_line_id', '=', existing.purchase_line_id.id),
- ])
+ # incoming_moves = self.env['stock.move'].search([
+ # ('reference', 'ilike', 'BU/INPUT'),
+ # ('state', 'not in', ['done','cancel']),
+ # ('product_id', '=', existing.product_id.id),
+ # ('purchase_line_id', '=', existing.purchase_line_id.id),
+ # ])
- qty_incoming += sum(incoming_moves.mapped('product_uom_qty'))
+ # qty_incoming += sum(incoming_moves.mapped('product_uom_qty'))
- incoming_cache[sale_line_id] = qty_incoming
+ # incoming_cache[sale_line_id] = qty_incoming
- qty_need = sale_order.sale_line_id.product_uom_qty
+ # qty_need = sale_order.sale_line_id.product_uom_qty
- if incoming_cache[sale_line_id] >= qty_need:
- skip_line = True
+ # if incoming_cache[sale_line_id] >= qty_need:
+ # skip_line = True
- sale_line_id = sale_order.sale_line_id.id
+ # sale_line_id = sale_order.sale_line_id.id
- if sale_line_id not in retur_cache:
+ # if sale_line_id not in retur_cache:
- fully_received = True
+ # fully_received = True
- for existing in exist:
- if existing.purchase_order_id.state in ['done', 'purchase']:
+ # for existing in exist:
+ # if existing.purchase_order_id.state in ['done', 'purchase']:
- if existing.purchase_line_id.qty_received != existing.purchase_line_id.product_qty:
- fully_received = False
- break
+ # if existing.purchase_line_id.qty_received != existing.purchase_line_id.product_qty:
+ # fully_received = False
+ # break
- retur_cache[sale_line_id] = fully_received
+ # retur_cache[sale_line_id] = fully_received
- if retur_cache[sale_line_id] and exist:
- skip_line = True
+ # if retur_cache[sale_line_id] and exist:
+ # skip_line = True
- if skip_line:
- continue
+ # if skip_line:
+ # continue
- stock_move = self.env['stock.move'].search([
- ('reference', 'ilike', 'BU/PICK'),
- ('state', 'in', ['confirmed','waiting','partially_available']),
- ('product_id', '=', sale_order.product_id.id),
- ('sale_line_id', '=', sale_order.sale_line_id.id),
- ])
- if not stock_move:
- continue
- # @stephan skip so line yang sudah pernah ada di purchase order sales match sebelumnya
+ # stock_move = self.env['stock.move'].search([
+ # ('reference', 'ilike', 'BU/PICK'),
+ # ('state', 'in', ['confirmed','waiting','partially_available']),
+ # ('product_id', '=', sale_order.product_id.id),
+ # ('sale_line_id', '=', sale_order.sale_line_id.id),
+ # ])
+ # if not stock_move:
+ # continue
+ # # @stephan skip so line yang sudah pernah ada di purchase order sales match sebelumnya
- salesperson_name = sale_order.sale_id.user_id.name
+ # salesperson_name = sale_order.sale_id.user_id.name
- sale_id_with_salesperson = f"{sale_order.sale_id.name} - {salesperson_name}"
+ # sale_id_with_salesperson = f"{sale_order.sale_id.name} - {salesperson_name}"
- sale_ids_set.add(sale_id_with_salesperson)
- sale_ids_name.add(sale_order.sale_id.name)
-
- margin_item = sale_order.sale_line_id.item_margin / sale_order.qty_so if sale_order.qty_so else 0
- margin_item = margin_item * sale_order.qty_po
-
- matches_so_line = {
- 'purchase_order_id': purchase_order.id,
- 'sale_id': sale_order.sale_id.id,
- 'sale_line_id': sale_order.sale_line_id.id,
- 'picking_id': sale_order.picking_id.id,
- 'move_id': sale_order.move_id.id,
- 'partner_id': sale_order.partner_id.id,
- 'partner_invoice_id': sale_order.partner_invoice_id.id,
- 'salesperson_id': sale_order.salesperson_id.id,
- 'product_id': sale_order.product_id.id,
- 'qty_so': sale_order.qty_so,
- 'qty_po': sale_order.qty_po,
- 'margin_so': sale_order.sale_line_id.item_percent_margin,
- 'margin_item': margin_item
- }
- po_matches_so_line = self.env['purchase.order.sales.match'].create([matches_so_line])
-
- sale_ids_str = ','.join(sale_ids_set)
- sale_ids_str_name = ','.join(sale_ids_name)
-
- purchase_order.sale_order = sale_ids_str_name
- purchase_order.notes = sale_ids_str
+ # sale_ids_set.add(sale_id_with_salesperson)
+ # sale_ids_name.add(sale_order.sale_id.name)
+
+ # margin_item = sale_order.sale_line_id.item_margin / sale_order.qty_so if sale_order.qty_so else 0
+ # margin_item = margin_item * sale_order.qty_po
+
+ # matches_so_line = {
+ # 'purchase_order_id': purchase_order.id,
+ # 'sale_id': sale_order.sale_id.id,
+ # 'sale_line_id': sale_order.sale_line_id.id,
+ # 'picking_id': sale_order.picking_id.id,
+ # 'move_id': sale_order.move_id.id,
+ # 'partner_id': sale_order.partner_id.id,
+ # 'partner_invoice_id': sale_order.partner_invoice_id.id,
+ # 'salesperson_id': sale_order.salesperson_id.id,
+ # 'product_id': sale_order.product_id.id,
+ # 'qty_so': sale_order.qty_so,
+ # 'qty_po': sale_order.qty_po,
+ # 'margin_so': sale_order.sale_line_id.item_percent_margin,
+ # 'margin_item': margin_item
+ # }
+ # po_matches_so_line = self.env['purchase.order.sales.match'].create([matches_so_line])
+
+ # sale_ids_str = ','.join(sale_ids_set)
+ # sale_ids_str_name = ','.join(sale_ids_name)
+
+ # purchase_order.sale_order = sale_ids_str_name
+ # purchase_order.notes = sale_ids_str
- self.create_sales_order_purchase_match(purchase_order)
+ # self.create_sales_order_purchase_match(purchase_order)
- def create_sales_order_purchase_match(self, purchase_order):
- #TODO add matches po to sales order by automatic_purchase.sales.match
- matches_po_product_ids = [line.product_id.id for line in purchase_order.order_line]
-
- sales_match_line = self.env['automatic.purchase.sales.match'].search([
- ('automatic_purchase_id', '=', self.id),
- ('sale_line_id.product_id', 'in', matches_po_product_ids),
- ])
-
- for sales_match in sales_match_line:
- purchase_line = self.env['automatic.purchase.line'].search([
- ('automatic_purchase_id', '=', self.id),
- ('product_id', 'in', [sales_match.product_id.id]),
- ], limit=1)
-
- matches_po_line = {
- 'sales_order_id' : sales_match.sale_id.id,
- 'purchase_order_id' : purchase_line.current_po_id.id,
- 'purchase_line_id' : purchase_line.current_po_line_id.id,
- 'product_id' : sales_match.product_id.id,
- 'qty_so' : sales_match.qty_so,
- 'qty_po' : sales_match.qty_po,
- }
-
- sales_order_purchase_match = self.env['sales.order.purchase.match'].create([matches_po_line])
+ # def create_sales_order_purchase_match(self, purchase_order):
+ # #TODO add matches po to sales order by automatic_purchase.sales.match
+ # matches_po_product_ids = [line.product_id.id for line in purchase_order.order_line]
+
+ # sales_match_line = self.env['automatic.purchase.sales.match'].search([
+ # ('automatic_purchase_id', '=', self.id),
+ # ('sale_line_id.product_id', 'in', matches_po_product_ids),
+ # ])
+
+ # for sales_match in sales_match_line:
+ # purchase_line = self.env['automatic.purchase.line'].search([
+ # ('automatic_purchase_id', '=', self.id),
+ # ('product_id', 'in', [sales_match.product_id.id]),
+ # ], limit=1)
+
+ # matches_po_line = {
+ # 'sales_order_id' : sales_match.sale_id.id,
+ # 'purchase_order_id' : purchase_line.current_po_id.id,
+ # 'purchase_line_id' : purchase_line.current_po_line_id.id,
+ # 'product_id' : sales_match.product_id.id,
+ # 'qty_so' : sales_match.qty_so,
+ # 'qty_po' : sales_match.qty_po,
+ # }
+
+ # sales_order_purchase_match = self.env['sales.order.purchase.match'].create([matches_po_line])
def generate_regular_purchase(self, jobs):
current_time = datetime.utcnow()
diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py
index 244575ae..1c573371 100755
--- a/indoteknik_custom/models/purchase_order.py
+++ b/indoteknik_custom/models/purchase_order.py
@@ -23,6 +23,11 @@ class PurchaseOrder(models.Model):
_inherit = 'purchase.order'
vcm_id = fields.Many2one('tukar.guling.po', string='Doc VCM', readonly=True, compute='_has_vcm', copy=False)
+ forecast_line_ids = fields.One2many(
+ 'purchase.order.forecast.line',
+ 'order_id',
+ string='Forecast Coverage',
+ )
order_sales_match_line = fields.One2many('purchase.order.sales.match', 'purchase_order_id', string='Sales Match Lines', states={'cancel': [('readonly', True)], 'done': [('readonly', True)]}, copy=True)
sale_order_id = fields.Many2one('sale.order', string='Sale Order')
procurement_status = fields.Char(string='Procurement Status', compute='get_procurement_status', readonly=True)
@@ -132,6 +137,196 @@ class PurchaseOrder(models.Model):
soo_price = fields.Float('SOO Price', copy=False)
soo_discount = fields.Float('SOO Discount', copy=False)
soo_tax = fields.Float('SOO Tax', copy=False)
+
+ forecast_raw = fields.Text(string='Forecast Raw', compute='_compute_forecast_raw')
+ forecast_html = fields.Html(string='Forecast Matches SO', compute='_compute_forecast_html')
+
+ @api.model
+ def cron_generate_po_forecast(self):
+
+ # two_months_ago = fields.Datetime.now() - relativedelta(months=1)
+
+ # pos = self.env['purchase.order'].search([
+ # ('state', 'in', ['purchase','done']),
+ # '|',
+ # ('create_date', '>=', two_months_ago),
+ # ('write_date', '>=', two_months_ago),
+ # ])
+
+ pos = self.env['purchase.order'].search([
+ ('state','in',['purchase','done'])
+ ])
+
+ pos = pos.filtered(
+ lambda po: any(
+ m.state not in ['done','cancel'] and 'BU/INPUT' in m.reference
+ for l in po.order_line
+ for m in l.move_ids
+ )
+ )
+
+ itung = len(pos)
+
+ for po in pos:
+ po._generate_forecast_lines()
+
+ # def _generate_forecast_lines(self):
+
+ # report = self.env['report.stock.report_product_product_replenishment']
+ # Forecast = self.env['purchase.order.forecast.line']
+
+ # for po in self:
+
+ # try:
+
+ # Forecast.search([
+ # ('order_id', '=', po.id)
+ # ]).unlink()
+
+ # products = po.order_line.mapped('product_id')
+ # if not products:
+ # continue
+
+ # for product in products:
+
+ # try:
+
+ # data = report._get_report_data(
+ # product_variant_ids=[product.id]
+ # )
+
+ # for l in data.get('lines', []):
+
+ # doc = l.get('document_in')
+
+ # if doc and doc._name == 'purchase.order' and doc.id == po.id:
+
+ # doc_out = l.get('document_out')
+
+ # Forecast.create({
+ # 'order_id': po.id,
+ # 'product_id': product.id,
+ # 'quantity': l['quantity'],
+ # 'receipt_date': l.get('receipt_date'),
+ # 'delivery_date': l.get('delivery_date'),
+ # 'sale_order': doc_out.display_name if doc_out else '',
+ # 'sale_order_id': doc_out.id if doc_out else '',
+ # 'is_late': bool(l.get('is_late')),
+ # 'replenishment_filled': bool(l.get('replenishment_filled')),
+ # })
+
+ # except Exception as line_error:
+
+ # po.message_post(
+ # body=f"⚠️ Forecast line error: {str(line_error)}"
+ # )
+
+ # continue
+
+ # except Exception as po_error:
+
+ # po.message_post(
+ # body=f"❌ Forecast generation failed: {str(po_error)}"
+ # )
+
+ # continue
+
+ @api.depends('forecast_raw')
+ def _compute_forecast_html(self):
+ for po in self:
+ rows = []
+ try:
+ data = json.loads(po.forecast_raw or '[]')
+ except Exception:
+ data = []
+
+ for l in data:
+ badge = '🟢' if l['replenishment_filled'] else '🔴'
+ late = '⚠️' if l['is_late'] else ''
+
+ rows.append(f"""
+ <tr>
+ <td>{l['product']}</td>
+ <td style="text-align:right">{l['quantity']}</td>
+ <td>{l['document_out'] or ''}</td>
+ </tr>
+ """)
+
+ po.forecast_html = f"""
+ <table class="table table-sm table-bordered">
+ <thead>
+ <tr>
+ <th>Product</th>
+ <th>Qty</th>
+ <th>Sale Order</th>
+ </tr>
+ </thead>
+ <tbody>
+ {''.join(rows)}
+ </tbody>
+ </table>
+ """
+
+ @api.depends('order_line.product_id', 'order_line.product_qty')
+ def _compute_forecast_raw(self):
+ import json
+ report = self.env['report.stock.report_product_product_replenishment']
+
+ for po in self:
+ po.forecast_raw = '[]'
+
+ product_ids = po.order_line.mapped('product_id').ids
+ if not product_ids:
+ continue
+
+ data = report._get_report_data(product_variant_ids=product_ids)
+
+ result = []
+ for l in data.get('lines', []):
+ doc = l.get('document_in')
+ if doc and doc._name == 'purchase.order' and doc.id == po.id:
+ doc_out = l.get('document_out')
+ result.append({
+ 'product': l['product']['display_name'],
+ 'quantity': l['quantity'],
+ 'receipt_date': l.get('receipt_date'),
+ 'delivery_date': l.get('delivery_date'),
+ 'document_out': doc_out.display_name if doc_out else '',
+ 'is_late': l.get('is_late'),
+ 'replenishment_filled': l.get('replenishment_filled'),
+ })
+
+ po.forecast_raw = json.dumps(result)
+
+ def _generate_forecast_lines(self):
+ report = self.env['report.stock.report_product_product_replenishment']
+ Forecast = self.env['purchase.order.forecast.line']
+
+ for po in self:
+
+ Forecast.search([('order_id','=',po.id)]).unlink()
+
+ product_ids = po.order_line.mapped('product_id').ids
+ if not product_ids:
+ continue
+
+ data = report._get_report_data(product_variant_ids=product_ids)
+
+ for l in data.get('lines', []):
+ doc = l.get('document_in')
+
+ if doc and doc._name == 'purchase.order' and doc.id == po.id:
+ doc_out = l.get('document_out')
+
+ Forecast.create({
+ 'order_id': po.id,
+ 'product_id': l['product']['id'],
+ 'quantity': l['quantity'],
+ 'sale_order': doc_out.display_name if doc_out else '',
+ 'sale_order_id': doc_out.id if doc_out else '',
+ 'is_late': bool(l.get('is_late')),
+ 'replenishment_filled': bool(l.get('replenishment_filled')),
+ })
def _get_altama_token(self, source='auto'):
ICP = self.env['ir.config_parameter'].sudo()
@@ -1486,7 +1681,7 @@ class PurchaseOrder(models.Model):
# return self.action_view_related_bu()
if self.partner_id.id == 5571 and not self.revisi_po:
self.action_create_order_altama()
-
+
return res
def _remove_product_bom(self):
@@ -1691,31 +1886,43 @@ class PurchaseOrder(models.Model):
return
sum_so_margin = sum_sales_price = sum_margin = 0
- for line in self.order_line:
- sale_order_line = line.so_line_id
- if not sale_order_line:
- sale_order_line = self.env['sale.order.line'].search([
- ('product_id', '=', line.product_id.id),
- ('order_id', '=', line.order_id.sale_order_id.id)
- ], limit=1, order='price_reduce_taxexcl')
-
- sum_so_margin += sale_order_line.item_margin
- sales_price = sale_order_line.price_reduce_taxexcl * sale_order_line.product_uom_qty
+ for line in self.forecast_line_ids:
+ sale_order_line = self.env['sale.order.line'].search([
+ ('product_id', '=', line.product_id.id),
+ ('order_id', '=', line.sale_order_id.id)
+ ], limit=1, order='price_reduce_taxexcl')
+
+ purchase_order_line = self.env['purchase.order.line'].search([
+ ('product_id', '=', line.product_id.id),
+ ('order_id', '=', line.order_id.id)
+ ], limit=1)
+
+ sol_qty = sale_order_line.product_uom_qty
+ unit_margin = sale_order_line.item_margin / sol_qty if sol_qty else 0
+ sum_so_margin += unit_margin * line.quantity
+ sales_price = sale_order_line.price_reduce_taxexcl * line.quantity
if sale_order_line.order_id.shipping_cost_covered == 'indoteknik':
sales_price -= sale_order_line.delivery_amt_line
if sale_order_line.order_id.fee_third_party > 0:
sales_price -= sale_order_line.fee_third_party_line
sum_sales_price += sales_price
- purchase_price = line.price_subtotal
- if line.ending_price > 0:
- if line.taxes_id.id == 22:
- ending_price = line.ending_price / 1.11
- purchase_price = ending_price
+ po_qty = purchase_order_line.product_qty
+
+ if purchase_order_line.ending_price > 0:
+ if purchase_order_line.taxes_id.id == 22:
+ ending_price = purchase_order_line.ending_price / 1.11
else:
- purchase_price = line.ending_price
+ ending_price = purchase_order_line.ending_price
+
+ unit_cost = ending_price / po_qty if po_qty else 0
+ purchase_price = unit_cost * line.quantity
+
+ else:
+ unit_cost = purchase_order_line.price_subtotal / po_qty if po_qty else 0
+ purchase_price = unit_cost * line.quantity
# purchase_price = line.price_subtotal
if line.order_id.delivery_amount > 0:
- purchase_price += line.delivery_amt_line
+ purchase_price += purchase_order_line.delivery_amt_line
if line.order_id.delivery_amt > 0:
purchase_price += line.order_id.delivery_amt
real_item_margin = sales_price - purchase_price
@@ -1751,23 +1958,25 @@ class PurchaseOrder(models.Model):
def compute_total_margin_from_apo(self):
sum_so_margin = sum_sales_price = sum_margin = cashback_amount = 0
- for line in self.order_sales_match_line:
+ for line in self.forecast_line_ids:
po_line = self.env['purchase.order.line'].search([
('product_id', '=', line.product_id.id),
- ('order_id', '=', line.purchase_order_id.id)
+ ('order_id', '=', line.order_id.id)
], limit=1)
- sale_order_line = line.sale_line_id or self.env['sale.order.line'].search([
+ sale_order_line = self.env['sale.order.line'].search([
('product_id', '=', line.product_id.id),
- ('order_id', '=', line.sale_id.id)
+ ('order_id', '=', line.sale_order_id.id)
], limit=1, order='price_reduce_taxexcl')
if sale_order_line and po_line:
- qty_so = line.qty_so or 0
- qty_po = line.qty_po or 0
+ qty_so = line.quantity or 0
+ qty_po = po_line.product_qty or 0
# Hindari division by zero
so_margin = (qty_po / qty_so) * sale_order_line.item_margin if qty_so > 0 else 0
- sum_so_margin += so_margin
+ sol_qty = sale_order_line.product_uom_qty
+ unit_margin = sale_order_line.item_margin / sol_qty if sol_qty else 0
+ sum_so_margin += unit_margin * line.quantity
sales_price = sale_order_line.price_reduce_taxexcl * qty_po
if sale_order_line.order_id.shipping_cost_covered == 'indoteknik':
@@ -1782,10 +1991,10 @@ class PurchaseOrder(models.Model):
purchase_price = po_line.ending_price / 1.11
else:
purchase_price = po_line.ending_price
- if line.purchase_order_id.delivery_amount > 0:
+ if line.order_id.delivery_amount > 0:
purchase_price += (po_line.delivery_amt_line / po_line.product_qty) * qty_po
- if line.purchase_order_id.delivery_amt > 0:
- purchase_price += line.purchase_order_id.delivery_amt
+ if line.order_id.delivery_amt > 0:
+ purchase_price += line.order_id.delivery_amt
if self.partner_id.id == 5571:
cashback_percent = line.product_id.x_manufacture.cashback_percent or 0.0
@@ -1994,4 +2203,20 @@ class ChangeDatePlannedWizard(models.TransientModel):
})
+class PurchaseOrderForecastLine(models.Model):
+ _name = 'purchase.order.forecast.line'
+ _description = 'PO Forecast Coverage'
+
+ order_id = fields.Many2one('purchase.order', ondelete='cascade')
+
+ sale_order_id = fields.Many2one('sale.order', ondelete='cascade')
+ product_id = fields.Many2one('product.product')
+ quantity = fields.Float()
+
+ sale_order = fields.Char()
+
+ receipt_date = fields.Datetime()
+ delivery_date = fields.Datetime()
+ replenishment_filled = fields.Boolean()
+ is_late = fields.Boolean()
diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py
index 567259af..185cee0d 100755
--- a/indoteknik_custom/models/sale_order.py
+++ b/indoteknik_custom/models/sale_order.py
@@ -125,6 +125,7 @@ class SaleOrderLine(models.Model):
class SaleOrder(models.Model):
_inherit = "sale.order"
+ sale_forecast_lines = fields.One2many('sale.forecast.coverage', 'sale_id', string='Sale Forecast Lines')
ccm_id = fields.Many2one('tukar.guling', string='Doc. CCM', readonly=True, compute='_has_ccm', copy=False)
ongkir_ke_xpdc = fields.Float(string='Ongkir ke Ekspedisi', help='Biaya ongkir ekspedisi', copy=False, index=True,
tracking=3)
@@ -405,7 +406,242 @@ class SaleOrder(models.Model):
client_order_ref = fields.Char(tracking=True)
sourcing_job_count = fields.Integer(string='Sourcing Count', compute='_compute_sourcing_count')
+ forecast_raw = fields.Text(
+ string='Forecast Raw',
+ compute='_compute_forecast_raw'
+ )
+
+ forecast_html = fields.Html(
+ string='Forecast Coverage',
+ compute='_compute_forecast_html'
+ )
+
+ def cron_generate_sale_forecast(self):
+
+ Forecast = self.env['sale.forecast.coverage']
+ report = self.env['report.stock.report_product_product_replenishment']
+
+ # ambil SO yang punya move aktif (lebih cepat)
+ moves = self.env['stock.move'].search([
+ ('state', 'not in', ['done', 'cancel']),
+ ('sale_line_id', '!=', False)
+ ])
+
+ sos = moves.mapped('sale_line_id.order_id').filtered(
+ lambda so: so.state in ('sale', 'done')
+ )
+
+ if not sos:
+ return
+
+ # hapus forecast lama sekaligus (1 query)
+ Forecast.search([
+ ('sale_id', 'in', sos.ids)
+ ]).unlink()
+
+ all_rows = []
+
+ for so in sos:
+
+ rows = self._generate_forecast_for_so_fast(so, report)
+
+ if rows:
+ all_rows.extend(rows)
+
+ if all_rows:
+ Forecast.create(all_rows)
+
+
+ def _generate_forecast_for_so_fast(self, so, report):
+
+ result = []
+
+ products = so.order_line.mapped('product_id')
+ if not products:
+ return result
+
+ # mapping SOL
+ sol_map = {l.product_id.id: l for l in so.order_line}
+
+ # cache reserved qty sekali
+ reserved_map = {}
+ for sol in so.order_line:
+ reserved_map[sol.product_id.id] = sum(
+ sol.move_ids.mapped('reserved_availability')
+ )
+
+ for product in products:
+
+ data = report._get_report_data(
+ product_variant_ids=[product.id]
+ )
+
+ for l in data.get('lines', []):
+
+ doc_out = l.get('document_out')
+
+ if not doc_out or doc_out._name != 'sale.order' or doc_out.id != so.id:
+ continue
+
+ sol = sol_map.get(product.id)
+
+ if not sol:
+ continue
+
+ # supply logic
+ doc_in = l.get('document_in')
+
+ if doc_in:
+ supply_name = doc_in.display_name
+ elif l.get('reservation'):
+ supply_name = "Reserved from stock"
+ elif l.get('replenishment_filled'):
+ supply_name = (
+ "Inventory On Hand"
+ if doc_out
+ else "Free Stock"
+ )
+ else:
+ supply_name = "Not Available"
+
+ receipt_date = l.get('receipt_date')
+
+ if receipt_date:
+ try:
+ receipt_date = datetime.strptime(
+ receipt_date,
+ "%d/%m/%Y %H:%M:%S"
+ ).strftime("%Y-%m-%d %H:%M:%S")
+ except Exception:
+ receipt_date = False
+
+ result.append({
+ 'sale_id': so.id,
+ 'sale_line_id': sol.id,
+ 'product_id': sol.product_id.id,
+ 'so_qty': sol.product_uom_qty,
+ 'reserved_qty': reserved_map.get(product.id, 0),
+ 'forecast_qty': l.get('quantity'),
+ 'receipt_date': receipt_date,
+ 'document_in_name': supply_name,
+ 'reservation': bool(l.get('reservation')),
+ 'is_late': bool(l.get('is_late')),
+ 'replenishment_filled': bool(l.get('replenishment_filled')),
+ })
+
+ return result
+
+ @api.depends('order_line.product_id', 'order_line.product_uom_qty')
+ def _compute_forecast_raw(self):
+ report = self.env['report.stock.report_product_product_replenishment']
+
+ for so in self:
+ so.forecast_raw = '[]'
+
+ product_ids = so.order_line.mapped('product_id').ids
+ if not product_ids:
+ continue
+
+ data = report._get_report_data(product_variant_ids=product_ids)
+
+ result = []
+ for l in data.get('lines', []):
+ sol = self.env['sale.order.line'].search([
+ ('order_id', '=', so.id),
+ ('product_id', '=', l['product']['id'])
+ ], limit=1)
+ so_qty = sol.product_uom_qty if sol else 0
+ reserved_qty = sum(
+ self.env['stock.move.line'].search([
+ ('move_id.sale_line_id', '=', sol.id),
+ ('state', '!=', 'cancel')
+ ]).mapped('product_uom_qty')
+ ) if sol else 0
+
+ doc_out = l.get('document_out')
+
+ if doc_out and doc_out._name == 'sale.order' and doc_out.id == so.id:
+ doc_in = l.get('document_in')
+
+ result.append({
+ 'product': l['product']['display_name'],
+ 'so_qty': so_qty,
+ 'reserved_qty': reserved_qty,
+ 'quantity': l.get('quantity'),
+ 'receipt_date': l.get('receipt_date'),
+ 'delivery_date': l.get('delivery_date'),
+ 'document_in_name': doc_in.display_name if doc_in else '',
+ 'document_in_model': doc_in._name if doc_in else '',
+ 'document_in_id': doc_in.id if doc_in else False,
+ 'document_out_exists': bool(doc_out),
+ 'reservation': bool(l.get('reservation')),
+ 'is_late': bool(l.get('is_late')),
+ 'replenishment_filled': bool(l.get('replenishment_filled')),
+ })
+
+ so.forecast_raw = json.dumps(result)
+ @api.depends('forecast_raw')
+ def _compute_forecast_html(self):
+ for so in self:
+ rows = []
+
+ try:
+ data = json.loads(so.forecast_raw or '[]')
+ except Exception:
+ data = []
+
+ for l in data:
+ badge = '🟢' if l['replenishment_filled'] else '🔴'
+ late = '⚠️' if l['is_late'] else ''
+
+ # ==== SUPPLY STATUS LOGIC ====
+ if l['document_in_id']:
+ supply_html = f"""
+ <a href="/web#id={l['document_in_id']}&model={l['document_in_model']}&view_type=form"
+ class="font-weight-bold">
+ {l['document_in_name']}
+ </a>
+ """
+ elif l['reservation']:
+ supply_html = "Reserved from stock"
+ elif l['replenishment_filled']:
+ if l['document_out_exists']:
+ supply_html = "Inventory On Hand"
+ else:
+ supply_html = "Free Stock"
+ else:
+ supply_html = '<span class="text-muted">Not Available</span>'
+
+ rows.append(f"""
+ <tr class="{ 'o_grid_warning' if l['is_late'] else '' }">
+ <td>{l['product']}</td>
+ <td style="text-align:right">{l['quantity']}</td>
+ <td>{supply_html}</td>
+ <td>{l['receipt_date'] or ''}</td>
+ <td style="text-align:right">{l['so_qty']}</td>
+ <td style="text-align:right">{l['reserved_qty']}</td>
+ </tr>
+ """)
+
+ so.forecast_html = f"""
+ <table class="table table-sm table-bordered">
+ <thead>
+ <tr>
+ <th>Product</th>
+ <th>Qty</th>
+ <th>Supplied By</th>
+ <th>Receipt Date</th>
+ <th>SO Qty</th>
+ <th>Reserved</th>
+ </tr>
+ </thead>
+ <tbody>
+ {''.join(rows)}
+ </tbody>
+ </table>
+ """
+
def action_set_shipping_id(self):
for rec in self:
bu_pick = self.env['stock.picking'].search([
@@ -2430,7 +2666,7 @@ class SaleOrder(models.Model):
user = self.env.user
is_sales_admin = user.id in (3401, 20, 3988, 17340)
if is_sales_admin:
- order.approval_status = 'pengajuan1'
+ order.approval_status = 'pengajuan0'
order.message_post(body="Mengajukan approval ke Sales")
return self._create_approval_notification('Sales')
@@ -3771,6 +4007,7 @@ class SaleOrder(models.Model):
'target': 'current',
}
+<<<<<<< HEAD
def action_open_sjo(self):
return {
'name': 'SJO',
@@ -3782,3 +4019,26 @@ class SaleOrder(models.Model):
'default_so_id': self.id,
}
}
+=======
+class SaleForecastCoverage(models.Model):
+ _name = 'sale.forecast.coverage'
+ _description = 'Sale Forecast Coverage'
+
+ sale_id = fields.Many2one('sale.order', index=True)
+ sale_line_id = fields.Many2one('sale.order.line', index=True)
+
+ product_id = fields.Many2one('product.product')
+ so_qty = fields.Float()
+ reserved_qty = fields.Float()
+
+ forecast_qty = fields.Float()
+ receipt_date = fields.Datetime()
+
+ document_in_model = fields.Char()
+ document_in_id = fields.Integer()
+ document_in_name = fields.Char()
+
+ reservation = fields.Boolean()
+ is_late = fields.Boolean()
+ replenishment_filled = fields.Boolean()
+>>>>>>> e6b6691f518a7400babdbd4b95541fb3d07f154d