diff options
| author | HafidBuroiroh <hafidburoiroh09@gmail.com> | 2026-03-10 10:46:23 +0700 |
|---|---|---|
| committer | HafidBuroiroh <hafidburoiroh09@gmail.com> | 2026-03-10 10:46:23 +0700 |
| commit | f58fe20f96995228651a5a1a09c8c17a23e13838 (patch) | |
| tree | dc6700fe931d229523c478ac6f290b3e45ae871c | |
| parent | d91af3da1edea3c6b8074726e5306ea42a7c0a4b (diff) | |
<hafid> final change request
| -rwxr-xr-x | indoteknik_custom/models/product_template.py | 6 | ||||
| -rwxr-xr-x | indoteknik_custom/models/sale_order.py | 18 | ||||
| -rw-r--r-- | indoteknik_custom/models/sourcing_job_order.py | 126 | ||||
| -rwxr-xr-x | indoteknik_custom/views/sale_order.xml | 8 | ||||
| -rw-r--r-- | indoteknik_custom/views/sourcing.xml | 46 |
5 files changed, 149 insertions, 55 deletions
diff --git a/indoteknik_custom/models/product_template.py b/indoteknik_custom/models/product_template.py index 56022d06..ecaf9106 100755 --- a/indoteknik_custom/models/product_template.py +++ b/indoteknik_custom/models/product_template.py @@ -91,7 +91,8 @@ class ProductTemplate(models.Model): group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])]) active_model = self.env.context.get('active_model') - if self.env.user.id not in users_in_group.mapped('id') and active_model == None: + from_sourcing = self.env.context.get('from_sourcing_approval') + if self.env.user.id not in users_in_group.mapped('id') and active_model == None and not from_sourcing: raise UserError('Hanya MD yang bisa membuat Product') result = super(ProductTemplate, self).create(vals) return result @@ -983,7 +984,8 @@ class ProductProduct(models.Model): group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id active_model = self.env.context.get('active_model') users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])]) - if self.env.user.id not in users_in_group.mapped('id') and active_model == None: + from_sourcing = self.env.context.get('from_sourcing_approval') + if self.env.user.id not in users_in_group.mapped('id') and active_model == None and not from_sourcing: raise UserError('Hanya MD yang bisa membuat Product') result = super(ProductProduct, self).create(vals) return result diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 031007ae..567259af 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -404,6 +404,7 @@ class SaleOrder(models.Model): client_order_ref = fields.Char(tracking=True) + sourcing_job_count = fields.Integer(string='Sourcing Count', compute='_compute_sourcing_count') def action_set_shipping_id(self): for rec in self: @@ -3695,6 +3696,17 @@ class SaleOrder(models.Model): 'context': {'default_sale_order_ids': [self.id]}, } + def action_view_related_sjo(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'name': 'Sourcing Job', + 'res_model': 'sourcing.job.order', + 'view_mode': 'tree,form', + 'domain': [('so_id', '=', self.id)], + 'context': {'default_so_id': self.id}, + } + def _compute_refund_ids(self): for order in self: refunds = self.env['refund.sale.order'].search([ @@ -3708,6 +3720,12 @@ class SaleOrder(models.Model): ('sale_order_ids', 'in', order.id) ]) + def _compute_sourcing_count(self): + for order in self: + order.sourcing_job_count = self.env['sourcing.job.order'].search_count([ + ('so_id', '=', order.id) + ]) + @api.depends('invoice_ids') def _compute_advance_payment_move(self): for order in self: diff --git a/indoteknik_custom/models/sourcing_job_order.py b/indoteknik_custom/models/sourcing_job_order.py index 6bb59c62..0e5334a8 100644 --- a/indoteknik_custom/models/sourcing_job_order.py +++ b/indoteknik_custom/models/sourcing_job_order.py @@ -89,11 +89,13 @@ class SourcingJobOrder(models.Model): @api.depends('eta_sales', 'eta_complete', 'create_date', 'state') def _compute_progress_status(self): for rec in self: - if rec.eta_sales: - # Ada tanggal expected - if rec.state == 'taken': - rec.progress_status = '🟡 On Track' - elif rec.state == 'done' and rec.eta_complete: + + if rec.state == 'cancel': + rec.progress_status = '⚫ Cancelled' + continue + + if rec.state == 'done': + if rec.eta_sales and rec.eta_complete: delta = (rec.eta_complete - rec.eta_sales).days if delta < 0: rec.progress_status = f'🟢 Early {abs(delta)} hari' @@ -101,22 +103,15 @@ class SourcingJobOrder(models.Model): rec.progress_status = '🔵 Ontime' else: rec.progress_status = f'🔴 Delay {delta} hari' - elif rec.state == 'cancel': - rec.progress_status = '⚫ Cancelled' - else: - rec.progress_status = '🟡 On Track' - else: - # Tidak ada ETA, hitung durasi - if rec.state == 'done' and rec.eta_complete: - if rec.create_date: - durasi = (rec.eta_complete - rec.create_date.date()).days - rec.progress_status = f'✅ Selesai dalam {durasi} hari' - else: - rec.progress_status = '✅ Selesai' - elif rec.state == 'cancel': - rec.progress_status = '⚫ Cancelled' + elif rec.create_date and rec.eta_complete: + durasi = (rec.eta_complete - rec.create_date.date()).days + rec.progress_status = f'✅ Selesai dalam {durasi} hari' else: - rec.progress_status = '🟡 On Track' + rec.progress_status = '✅ Selesai' + continue + + if rec.state in ['taken', 'partial', 'draft']: + rec.progress_status = '🟡 On Track' @api.depends('line_ids.price', 'line_ids.vendor_id') def _compute_has_price_in_lines(self): @@ -413,6 +408,7 @@ class SourcingJobOrderLine(models.Model): vals['show_salesperson'] = order.so_id.user_id.id rec = super().create(vals) + rec._check_line_limit() return rec def write(self, vals): @@ -427,8 +423,30 @@ class SourcingJobOrderLine(models.Model): res = super().write(vals) if 'state' in vals: self._update_parent_state() + self._check_line_limit() return res + def _check_line_limit(self): + for rec in self: + if not rec.order_id or not rec.order_id.so_id: + continue + + so = rec.order_id.so_id + + so_line_count = len(so.order_line) + + sourcing_lines = self.search([ + ('order_id', '=', rec.order_id.id), + ('state', '!=', 'cancel') + ]) + + if len(sourcing_lines) > so_line_count: + raise UserError( + f"Jumlah Sourcing Line tidak boleh melebihi Sales Order Line.\n\n" + f"Sales Order Line : {so_line_count}\n" + f"Sourcing Line : {len(sourcing_lines)}" + ) + def _update_parent_state(self): for rec in self: order = rec.order_id @@ -617,18 +635,40 @@ class SourcingJobOrderLine(models.Model): if line.state != 'sourcing': raise UserError("⚠️ Hanya line status 'Sourcing' yang bisa minta approval.") - if ( - not line.vendor_id - or not line.product_name_md - or not line.brand_id - or not line.price or line.price <= 0 - or not line.tax_id - or not line.subtotal or line.subtotal <= 0 - or not line.product_type - or not line.product_category - or not line.product_class - ): - raise UserError("❌ Lengkapi data sebelum Ask Approval sales") + missing_fields = [] + + if not line.vendor_id: + missing_fields.append("Vendor") + + if not line.product_name_md: + missing_fields.append("Product Name") + + if not line.brand_id: + missing_fields.append("Manufactures") + + if not line.price or line.price <= 0: + missing_fields.append("Price") + + if not line.tax_id: + missing_fields.append("Tax") + + if not line.subtotal or line.subtotal <= 0: + missing_fields.append("Subtotal") + + if not line.product_type: + missing_fields.append("Product Type") + + if not line.product_category: + missing_fields.append("Product Category") + + if not line.product_class: + missing_fields.append("Product Class") + + if missing_fields: + raise UserError( + "❌ Lengkapi data berikut sebelum Ask Approval Sales:\n- " + + "\n- ".join(missing_fields) + ) line.state = 'sent' @@ -730,7 +770,7 @@ class SourcingJobOrderLine(models.Model): 'consu': 'consu', } - product = ProductProduct.create({ + product = ProductProduct.with_context(from_sourcing_approval=True).create({ 'name': rec.product_name_md, 'default_code': rec.code or False, 'description': rec.descriptions_md or '', @@ -751,6 +791,11 @@ class SourcingJobOrderLine(models.Model): rec.product_id = product.id + self.env.user.notify_success( + message=f"Produk baru '{product.name}' berhasil dibuat dengan SKU {product.default_code}.", + title="Product Created" + ) + jakarta_tz = rec.order_id._get_jakarta_today() purchase_price = PurchasePricelist.search([ @@ -830,9 +875,24 @@ class SourcingJobOrderLine(models.Model): title="Approved" ) + so = self.mapped('so_id')[:1] + if so: + return { + 'type': 'ir.actions.act_window', + 'name': 'Sales Order', + 'res_model': 'sale.order', + 'view_mode': 'form', + 'res_id': so.id, + 'target': 'current', + } + return {'type': 'ir.actions.client', 'tag': 'reload'} def action_multi_approve(self): + so_ids = self.mapped('so_id').ids + + if len(set(so_ids)) > 1: + raise UserError("❌ Multi approve hanya bisa dilakukan jika semua line berasal dari Sales Order yang sama.") self.action_approve_approval() def action_reject_approval(self): diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml index 33cd51e4..80eea779 100755 --- a/indoteknik_custom/views/sale_order.xml +++ b/indoteknik_custom/views/sale_order.xml @@ -83,6 +83,14 @@ attrs="{'invisible': [('refund_count', '=', 0)]}"> <field name="refund_count" widget="statinfo" string="Refund"/> </button> + + <button type="object" + name="action_view_related_sjo" + class="oe_stat_button" + icon="fa-briefcase" + attrs="{'invisible': [('sourcing_job_count', '=', 0)]}"> + <field name="sourcing_job_count" widget="statinfo" string="SJO"/> + </button> </div> <field name="payment_term_id" position="after"> <field name="create_uid" invisible="1"/> diff --git a/indoteknik_custom/views/sourcing.xml b/indoteknik_custom/views/sourcing.xml index ebdbaaf7..006817cf 100644 --- a/indoteknik_custom/views/sourcing.xml +++ b/indoteknik_custom/views/sourcing.xml @@ -420,9 +420,9 @@ <field name="arch" type="xml"> <tree string="Sourcing Job Order" decoration-warning="state=='sourcing'" - decoration-success="state== 'done'" + decoration-success="state== 'approve'" decoration-danger="state=='cancel'" - decoration-info="state in ('sent','approve')"> + decoration-info="state == 'sent'"> <field name="order_id" /> <field name="md_person_ids" widget="many2one_avatar_user"/> <field name="show_salesperson" widget="many2one_avatar_user"/> @@ -508,7 +508,7 @@ <field name="name">sourcing.job.order.line.form</field> <field name="model">sourcing.job.order.line</field> <field name="arch" type="xml"> - <form string="Sourcing Job"> + <form string="Sourcing Job" create="0"> <header> <button name="action_take" string="Take" @@ -577,7 +577,7 @@ <widget name="web_ribbon" title="COMPLETE" bg_color="bg-success" - attrs="{'invisible': [('state', '!=', 'done')]}"/> + attrs="{'invisible': [('state', '!=', 'approve')]}"/> <widget name="web_ribbon" title="CANCEL" @@ -593,20 +593,20 @@ <field name="is_given" invisible="1"/> <field name="is_receiver" invisible="1"/> <field name="md_person_ids" widget="many2one_avatar_user" readonly="1"/> - <field name="brand_id" attrs="{'readonly': [('state', 'in', ['cancel', 'approve'])]}"/> - <field name="product_id" attrs="{'readonly': [('state', 'in', ['cancel', 'approve'])]}"/> - <field name="descriptions_md" attrs="{'readonly': [('state', 'in', ['cancel', 'approve'])]}"/> - <field name="code" attrs="{'readonly': [('state', 'in', ['cancel', 'approve'])]}"/> - <field name="product_name_md" attrs="{'readonly': [('state', 'in', ['cancel', 'approve'])]}"/> - <field name="price" required="1" attrs="{'readonly': [('state', 'in', ['cancel', 'approve'])]}"/> + <field name="brand_id" attrs="{'readonly': [('state', 'in', ['sent', 'cancel', 'approve'])]}"/> + <field name="product_id" attrs="{'readonly': [('state', 'in', ['sent', 'cancel', 'approve'])]}"/> + <field name="descriptions_md" attrs="{'readonly': [('state', 'in', ['sent', 'cancel', 'approve'])]}"/> + <field name="code" attrs="{'readonly': [('state', 'in', ['sent', 'cancel', 'approve'])]}"/> + <field name="product_name_md" attrs="{'readonly': [('state', 'in', ['sent', 'cancel', 'approve'])]}"/> + <field name="price" required="1" attrs="{'readonly': [('state', 'in', ['sent', 'cancel', 'approve'])]}"/> <field name="now_price" force_save="1" readonly="1"/> <field name="last_updated_price" force_save="1"/> - <field name="vendor_id" attrs="{'readonly': [('state', 'in', ['cancel', 'approve'])]}"/> - <field name="tax_id" attrs="{'readonly': [('state', 'in', ['cancel', 'approve'])]}"/> - <field name="subtotal" readonly="1" attrs="{'readonly': [('state', 'in', ['cancel', 'approve'])]}"/> - <field name="product_category" attrs="{'readonly': [('state', 'in', ['cancel', 'approve'])]}"/> - <field name="product_type" attrs="{'readonly': [('state', 'in', ['cancel', 'approve'])]}"/> - <field name="product_class" widget="many2many_tags" attrs="{'readonly': [('state', 'in', ['cancel', 'approve'])]}"/> + <field name="vendor_id" attrs="{'readonly': [('state', 'in', ['sent', 'cancel', 'approve'])]}"/> + <field name="tax_id" attrs="{'readonly': [('state', 'in', ['sent', 'cancel', 'approve'])]}"/> + <field name="subtotal" readonly="1" attrs="{'readonly': [('state', 'in', ['sent', 'cancel', 'approve'])]}"/> + <field name="product_category" attrs="{'readonly': [('state', 'in', ['sent', 'cancel', 'approve'])]}"/> + <field name="product_type" attrs="{'readonly': [('state', 'in', ['sent', 'cancel', 'approve'])]}"/> + <field name="product_class" widget="many2many_tags" attrs="{'readonly': [('state', 'in', ['sent', 'cancel', 'approve'])]}"/> </group> <group> @@ -645,7 +645,7 @@ <notebook> <page string="Cancel Reason" attrs="{'invisible': [('state', 'in', ['approve'])]}"> <group> - <field name="reason"/> + <field name="reason" attrs="{'readonly': [('state', 'in', ['sent', 'cancel', 'approve'])]}"/> </group> </page> </notebook> @@ -700,6 +700,7 @@ <field name="product_name"/> <field name="brand"/> <field name="so_id"/> + <field name="show_salesperson"/> <filter name="filter_sent" string="Sent" @@ -711,6 +712,10 @@ context="{'group_by':'so_id'}"/> </group> + <filter name="filter_my_line" + string="My Line" + domain="[('show_salesperson','=',uid)]"/> + </search> </field> </record> @@ -753,6 +758,7 @@ <field name="binding_model_id" ref="model_sourcing_job_order_line"/> <field name="binding_view_types">list</field> <field name="state">code</field> + <field name="groups_id" eval="[(4, ref('indoteknik_custom.group_role_sales'))]"/> <field name="code"> records.action_multi_approve() </field> @@ -761,16 +767,16 @@ <record id="action_sourcing_line_approval" model="ir.actions.act_window"> <field name="name">Approval Sourcing Line</field> <field name="res_model">sourcing.job.order.line</field> - <field name="view_mode">tree</field> - <field name="view_id" ref="view_sjo_line_approval_tree"/> + <field name="view_mode">tree,form</field> <field name="search_view_id" ref="view_sjo_line_approval_search"/> + <field name="domain">[('state','in',('sent','approve'))]</field> <field name="context"> { 'search_default_filter_sent': 1, + 'search_default_filter_my_line': 1, 'search_default_group_so': 1 } </field> - <field name="groups_id" eval="[(4, ref('indoteknik_custom.group_role_sales'))]"/> </record> <menuitem id="menu_sourcing_line_approval" |
