diff options
| author | HafidBuroiroh <hafidburoiroh09@gmail.com> | 2026-03-11 20:09:25 +0700 |
|---|---|---|
| committer | HafidBuroiroh <hafidburoiroh09@gmail.com> | 2026-03-11 20:09:25 +0700 |
| commit | 715a60d3dce76a253afd2b119e0e800a230dd24d (patch) | |
| tree | ba8bd1521c10ac78e45a6e6481b6d651a69fc606 /indoteknik_custom/models/sale_order.py | |
| parent | 4a200ee4e0caf44e78273215b12c3655655f4273 (diff) | |
| parent | e6b6691f518a7400babdbd4b95541fb3d07f154d (diff) | |
<hafid> naekin sorcing job
Diffstat (limited to 'indoteknik_custom/models/sale_order.py')
| -rwxr-xr-x | indoteknik_custom/models/sale_order.py | 262 |
1 files changed, 261 insertions, 1 deletions
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 |
