diff options
| author | Azka Nathan <darizkyfaz@gmail.com> | 2025-07-28 11:22:49 +0700 |
|---|---|---|
| committer | Azka Nathan <darizkyfaz@gmail.com> | 2025-07-28 11:22:49 +0700 |
| commit | ece8f2950b6c8fc8b65def8bc70d347fe7157f96 (patch) | |
| tree | fdd85bcac414aa2ee968ee6f640b010f5d70289d | |
| parent | b48199f0ffefe2ccd43799c06f679f1db96c5334 (diff) | |
skema buffer stock, cr purchasing job, api cancel so
| -rwxr-xr-x | fixco_custom/__manifest__.py | 1 | ||||
| -rwxr-xr-x | fixco_custom/models/__init__.py | 3 | ||||
| -rw-r--r-- | fixco_custom/models/automatic_purchase.py | 27 | ||||
| -rwxr-xr-x | fixco_custom/models/detail_order.py | 17 | ||||
| -rw-r--r-- | fixco_custom/models/purchasing_job.py | 169 | ||||
| -rw-r--r-- | fixco_custom/models/reordering_rule.py | 94 | ||||
| -rwxr-xr-x | fixco_custom/security/ir.model.access.csv | 6 | ||||
| -rwxr-xr-x | fixco_custom/views/detail_order.xml | 2 | ||||
| -rw-r--r-- | fixco_custom/views/purchasing_job.xml | 20 | ||||
| -rw-r--r-- | fixco_custom/views/reordering_rule.xml | 75 | ||||
| -rwxr-xr-x | fixco_custom/views/sale_order.xml | 13 |
11 files changed, 348 insertions, 79 deletions
diff --git a/fixco_custom/__manifest__.py b/fixco_custom/__manifest__.py index 0bcdfaa..0624df9 100755 --- a/fixco_custom/__manifest__.py +++ b/fixco_custom/__manifest__.py @@ -39,6 +39,7 @@ 'views/upload_bills.xml', 'views/brands_views.xml', 'views/product_public_category.xml', + 'views/reordering_rule.xml', ], 'demo': [], 'css': [], diff --git a/fixco_custom/models/__init__.py b/fixco_custom/models/__init__.py index e80acd8..297002e 100755 --- a/fixco_custom/models/__init__.py +++ b/fixco_custom/models/__init__.py @@ -25,4 +25,5 @@ from . import purchasing_job from . import stock_move from . import upload_bills from . import brands -from . import product_public_category
\ No newline at end of file +from . import product_public_category +from . import reordering_rule
\ No newline at end of file diff --git a/fixco_custom/models/automatic_purchase.py b/fixco_custom/models/automatic_purchase.py index d2d6830..037d65a 100644 --- a/fixco_custom/models/automatic_purchase.py +++ b/fixco_custom/models/automatic_purchase.py @@ -108,12 +108,19 @@ class AutomaticPurchase(models.Model): action['views'] = [(self.env.ref('purchase.purchase_order_form').id, 'form')] action['res_id'] = self.purchase_order_ids[0].id return action + + def delete_note_pj(self): + self.env['purchasing.job.note'].search([ + ('product_id', 'in', self.automatic_purchase_lines.mapped('product_id').ids) + ]).unlink() def create_purchase_orders(self): self.ensure_one() if not self.automatic_purchase_lines: raise UserError(_("No purchase lines to process!")) + self.delete_note_pj() + if self.is_po: raise UserError(_("Purchase order already created!")) @@ -162,23 +169,8 @@ class AutomaticPurchase(models.Model): self.is_po = True return self.action_view_purchase_orders() - - def _create_purchase_order(self, vendor, sequence, total_chunks, brand_id=None, category_id=None): - order_name = f"{self.number} - {vendor.name}" - - if brand_id: - brand = self.env['brands'].browse(brand_id) - order_name += f" - {brand.name}" - - if category_id: - cat = self.env['product.public.category'].browse(category_id) - order_name += f" - {cat.name}" - - if total_chunks > 1: - order_name += f" ({sequence}/{total_chunks})" - return self.env['purchase.order'].create({ 'partner_id': vendor.id, 'origin': self.number, @@ -187,7 +179,6 @@ class AutomaticPurchase(models.Model): 'currency_id': vendor.property_purchase_currency_id.id or self.env.company.currency_id.id, 'automatic_purchase_id': self.id, 'source': self.apo_type, - 'name': order_name, }) @@ -377,9 +368,9 @@ class AutomaticPurchaseLine(models.Model): for line in self: if line.product_id: stock_move = self.env['stock.move'].search([ - ('product_id', '=', line.product_id.id), + ('product_id', '=', line.product_id.id), ('location_dest_id', '=', 5), ('location_id', '=', 55), ('state', 'in', ['waiting', 'confirmed', 'assigned', 'partially_available']) - ], limit=1) + ]) line.qty_outgoing = sum(move.product_uom_qty for move in stock_move)
\ No newline at end of file diff --git a/fixco_custom/models/detail_order.py b/fixco_custom/models/detail_order.py index 188424a..e8e87aa 100755 --- a/fixco_custom/models/detail_order.py +++ b/fixco_custom/models/detail_order.py @@ -276,13 +276,6 @@ class DetailOrder(models.Model): # First check if a sale order with this reference already exists existing_order = self.env['sale.order'].search([('order_reference', '=', order_id)], limit=1) - if existing_order: - # If order already exists, just update the references - self.sale_id = existing_order.id - self.execute_status = 'already_so' - return # Exit early since we don't need to create anything - - # Handle cancelled orders if order_status == 'CANCELLED': external_order_id = json_data.get('data', [{}])[0].get('externalOrderId') order_id = json_data.get('data', [{}])[0].get('orderId') @@ -302,10 +295,12 @@ class DetailOrder(models.Model): picking.action_cancel() self.sale_id = existing_order.id self.execute_status = 'cancelled_so_picking' + existing_order.action_cancel() else: existing_order.action_cancel() self.sale_id = existing_order.id self.execute_status = 'cancelled_so' + else: # If no existing SO, create one, then cancel data = self.prepare_data_so(json_data) @@ -320,6 +315,14 @@ class DetailOrder(models.Model): sale_order.action_cancel() self.execute_status = 'cancelled_so_created' + + return + + if existing_order: + # If order already exists, just update the references + self.sale_id = existing_order.id + self.execute_status = 'already_so' + return # Exit early since we don't need to create anything if order_status != 'PENDING_PAYMENT': if order_status in ('PARTIALLY_PAID', 'PAID'): diff --git a/fixco_custom/models/purchasing_job.py b/fixco_custom/models/purchasing_job.py index 2c7138a..5d37c8c 100644 --- a/fixco_custom/models/purchasing_job.py +++ b/fixco_custom/models/purchasing_job.py @@ -6,11 +6,10 @@ class PurchasingJob(models.Model): _name = 'purchasing.job' _description = 'Procurement Monitoring by Product' _auto = False - _rec_name = 'product' + _rec_name = 'product_id' id = fields.Integer(string='ID', readonly=True) item_code = fields.Char(string='Item Code') - product = fields.Char(string='Product') onhand = fields.Float(string='On Hand') incoming = fields.Float(string='Incoming') outgoing = fields.Float(string='Outgoing') @@ -18,6 +17,30 @@ class PurchasingJob(models.Model): product_id = fields.Many2one('product.product', string='Product') vendor_id = fields.Many2one('res.partner', string='Vendor') brand_id = fields.Many2one('brands', string='Brand') + note = fields.Text(string='Note', compute='_compute_note') + + def action_open_note_wizard(self): + self.ensure_one() + return { + 'name': 'Add/Edit Note', + 'type': 'ir.actions.act_window', + 'res_model': 'purchasing.job.note.wizard', + 'view_mode': 'form', + 'target': 'new', + 'context': { + 'default_product_id': self.product_id.id, + 'default_note': self.note, + } + } + + + def _compute_note(self): + for rec in self: + note_obj = self.env['purchasing.job.note'].search([ + ('product_id', '=', rec.product_id.id) + ], limit=1) + rec.note = note_obj.note if note_obj else '' + def create_automatic_purchase(self): if not self: @@ -72,54 +95,102 @@ 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 - SELECT * FROM ( - SELECT - row_number() OVER () AS id, - a.item_code, - a.product, - a.onhand, - a.incoming, - a.outgoing, - CASE - WHEN (a.incoming + a.onhand) < a.outgoing THEN 'kurang' - ELSE 'cukup' - END AS action, - a.product_id, - pp2.vendor_id, - b.id AS brand_id - FROM ( - SELECT - COALESCE(pp.default_code, pt.default_code) AS item_code, - pt.name AS product, - get_qty_onhand(pp.id::numeric) AS onhand, - get_qty_incoming(pp.id::numeric) AS incoming, - get_qty_outgoing(pp.id::numeric) AS outgoing, - pp.id AS product_id, - pp.brand_id - FROM stock_move sm - JOIN stock_picking sp ON sp.id = sm.picking_id - JOIN product_product pp ON pp.id = sm.product_id - JOIN product_template pt ON pt.id = pp.product_tmpl_id - WHERE sp.state IN ('draft', 'waiting', 'confirmed', 'assigned') - AND sp.name LIKE '%%OUT%%' - AND sm.location_id = 55 - GROUP BY pp.id, pp.default_code, pt.default_code, pt.name, pp.brand_id - ) a - LEFT JOIN brands b ON b.id = a.brand_id - LEFT JOIN LATERAL ( - SELECT vendor_id - FROM purchase_pricelist - WHERE product_id = a.product_id - ORDER BY id ASC - LIMIT 1 - ) pp2 ON true - ) final - WHERE final.action = 'kurang' + CREATE OR REPLACE VIEW %s + AS SELECT + row_number() OVER ()::integer AS id, + pmp.item_code, + max(pmp.onhand) AS onhand, + max(pmp.incoming) AS incoming, + max(pmp.outgoing) AS outgoing, + pmp.action, + pmp.product_id, + pmp.brand_id, + pmp.vendor_id + FROM v_procurement_monitoring_by_product pmp + WHERE pmp.action = 'kurang'::text + GROUP BY pmp.product_id, pmp.item_code, pmp.action, pmp.brand_id, pmp.vendor_id; """ % self._table) - super(PurchasingJob, self).init() - - + # def init(self): + # tools.drop_view_if_exists(self.env.cr, self._table) + # self.env.cr.execute(""" + # CREATE OR REPLACE VIEW %s AS + # SELECT * FROM ( + # SELECT + # row_number() OVER () AS id, + # a.item_code, + # a.product, + # a.onhand, + # a.incoming, + # a.outgoing, + # CASE + # WHEN (a.incoming + a.onhand) < a.outgoing THEN 'kurang' + # ELSE 'cukup' + # END AS action, + # a.product_id, + # pp2.vendor_id, + # b.id AS brand_id + # FROM ( + # SELECT + # COALESCE(pp.default_code, pt.default_code) AS item_code, + # pt.name AS product, + # get_qty_onhand(pp.id::numeric) AS onhand, + # get_qty_incoming(pp.id::numeric) AS incoming, + # get_qty_outgoing(pp.id::numeric) AS outgoing, + # pp.id AS product_id, + # pp.brand_id + # FROM stock_move sm + # JOIN stock_picking sp ON sp.id = sm.picking_id + # JOIN product_product pp ON pp.id = sm.product_id + # JOIN product_template pt ON pt.id = pp.product_tmpl_id + # WHERE sp.state IN ('draft', 'waiting', 'confirmed', 'assigned') + # AND sp.name LIKE '%%OUT%%' + # AND sm.location_id = 55 + # GROUP BY pp.id, pp.default_code, pt.default_code, pt.name, pp.brand_id + # ) a + # LEFT JOIN brands b ON b.id = a.brand_id + # LEFT JOIN LATERAL ( + # SELECT vendor_id + # FROM purchase_pricelist + # WHERE product_id = a.product_id + # ORDER BY id ASC + # LIMIT 1 + # ) pp2 ON true + # ) final + # WHERE final.action = 'kurang' + # """ % self._table) + + # super(PurchasingJob, self).init() + +class PurchasingJobNote(models.Model): + _name = 'purchasing.job.note' + _description = 'Note for Product in Purchasing Job' + + product_id = fields.Many2one('product.product', string='Product', required=True, ondelete='cascade', index=True) + note = fields.Text(string='Note') + user_id = fields.Many2one('res.users', string='Created by', default=lambda self: self.env.user) + + + +class PurchasingJobNoteWizard(models.TransientModel): + _name = 'purchasing.job.note.wizard' + _description = 'Add/Edit Note for Product' + + product_id = fields.Many2one('product.product', string='Product', required=True) + note = fields.Text(string='Note') + + def action_confirm(self): + self.ensure_one() + existing = self.env['purchasing.job.note'].search([ + ('product_id', '=', self.product_id.id) + ], limit=1) + if existing: + existing.note = self.note + else: + self.env['purchasing.job.note'].create({ + 'product_id': self.product_id.id, + 'note': self.note, + }) + return {'type': 'ir.actions.act_window_close'} diff --git a/fixco_custom/models/reordering_rule.py b/fixco_custom/models/reordering_rule.py new file mode 100644 index 0000000..dad07b0 --- /dev/null +++ b/fixco_custom/models/reordering_rule.py @@ -0,0 +1,94 @@ +from odoo import models, fields, api +from odoo import tools +from odoo.exceptions import UserError + +class ReorderingRule(models.Model): + _name = 'reordering.rule' + _description = 'Buffer Stock' + _auto = False # Karena ini dari SQL view + + product_id = fields.Many2one('product.product', string='Product', readonly=True) + min_stock = fields.Float(string='Min Stock', readonly=True) + buffer_stock = fields.Float(string='Buffer Stock', readonly=True) + vendor_id = fields.Many2one('res.partner', string='Vendor', readonly=True) + qty_onhand = fields.Float(string='Qty Onhand', readonly=True) + qty_incoming = fields.Float(string='Qty Incoming') + + def create_automatic_purchase(self): + if not self: + raise UserError("No stock records selected.") + + automatic_purchase = self.env['automatic.purchase'].create({ + 'apo_type': 'reordering', + }) + + lines_to_create = [] + + for stock in self: + location_id = 55 + + quant_records = self.env['stock.quant'].search([ + ('product_id', '=', stock.product_id.id), + ('location_id', '=', location_id) + ]) + + total_available = quant_records.quantity or 0.0 + + qty_incoming = stock.product_id.incoming_qty or 0.0 + + qty_purchase = stock.buffer_stock - (total_available + qty_incoming) + + qty_purchase = max(qty_purchase, 0.0) + + pricelist = self.env['purchase.pricelist'].search([ + ('product_id', '=', stock.product_id.id), + ('vendor_id', '=', stock.vendor_id.id) + ], limit=1) + + price = pricelist.price if pricelist else 0.0 + subtotal = qty_purchase * price + + lines_to_create.append({ + 'automatic_purchase_id': automatic_purchase.id, + 'product_id': stock.product_id.id, + 'qty_purchase': qty_purchase, + 'qty_min': stock.min_stock, + 'qty_buffer': stock.buffer_stock, + 'partner_id': stock.vendor_id.id, + 'taxes_id': stock.vendor_id.tax_id.id if stock.vendor_id.tax_id else False, + 'price': price, + 'product_public_category_id': stock.product_id.product_public_category_id.id, + 'brand_id': stock.product_id.brand_id.id, + }) + + self.env['automatic.purchase.line'].create(lines_to_create) + + return { + 'type': 'ir.actions.act_window', + 'res_model': 'automatic.purchase', + 'view_mode': 'form', + 'res_id': automatic_purchase.id, + 'target': 'current', + } + + def init(self): + tools.drop_view_if_exists(self.env.cr, self._table) + self.env.cr.execute(f""" + CREATE OR REPLACE VIEW {self._table} AS ( + SELECT + vmsbm.id AS id, + vmsbm.product_id, + vmsbm.min_stock, + vmsbm.buffer_stock, + vmsbm.vendor_id, + COALESCE(SUM(sq.quantity), 0.0) AS qty_onhand, + vmsbm.incoming_qty AS qty_incoming + FROM + view_manage_stock_below_min vmsbm + LEFT JOIN + stock_quant sq ON sq.product_id = vmsbm.product_id AND sq.location_id = 55 + + GROUP BY + vmsbm.id, vmsbm.product_id, vmsbm.min_stock, vmsbm.buffer_stock, vmsbm.vendor_id, vmsbm.incoming_qty + ) + """) diff --git a/fixco_custom/security/ir.model.access.csv b/fixco_custom/security/ir.model.access.csv index 4b22abd..66ecf82 100755 --- a/fixco_custom/security/ir.model.access.csv +++ b/fixco_custom/security/ir.model.access.csv @@ -29,4 +29,8 @@ access_automatic_purchase,access.automatic.purchase,model_automatic_purchase,,1, access_purchasing_job,access.purchasing.job,model_purchasing_job,,1,1,1,1 access_upload_bills,access.upload.bills,model_upload_bills,,1,1,1,1 access_upload_bills_line,access.upload.bills.line,model_upload_bills_line,,1,1,1,1 -access_brands,access.brands,model_brands,,1,1,1,1
\ No newline at end of file +access_brands,access.brands,model_brands,,1,1,1,1 +access_reordering_rule,access.reordering.rule,model_reordering_rule,,1,1,1,1 +access_purchasing_job_note,access.purchasing.job.note,model_purchasing_job_note,,1,1,1,1 +access_purchasing_job_note_wizard,access.purchasing.job.note.wizard,model_purchasing_job_note_wizard,,1,1,1,1 + diff --git a/fixco_custom/views/detail_order.xml b/fixco_custom/views/detail_order.xml index 228f9e4..90e2e6b 100755 --- a/fixco_custom/views/detail_order.xml +++ b/fixco_custom/views/detail_order.xml @@ -27,12 +27,10 @@ <button name="execute_queue" string="Create Detail Order" type="object" - attrs="{'invisible': [('detail_order', '!=', True)]}" /> <button name="execute_queue_detail" string="Create SO" type="object" - attrs="{'invisible': [('sale_id', '!=', True), ('detail_order', '!=', False)]}" /> </header> <sheet> diff --git a/fixco_custom/views/purchasing_job.xml b/fixco_custom/views/purchasing_job.xml index 7a15160..cb5126a 100644 --- a/fixco_custom/views/purchasing_job.xml +++ b/fixco_custom/views/purchasing_job.xml @@ -7,13 +7,15 @@ <field name="arch" type="xml"> <tree string="Procurement Monitoring by Product" create="false" delete="false"> <field name="item_code"/> - <field name="product"/> + <field name="product_id"/> <field name="brand_id"/> <field name="vendor_id"/> <field name="onhand"/> <field name="incoming"/> <field name="outgoing"/> <field name="action"/> + <field name="note" widget="text"/> + <button name="action_open_note_wizard" string="Add Note" type="object" class="btn-primary"/> </tree> </field> </record> @@ -29,6 +31,22 @@ </field> </record> + <record id="view_purchasing_job_note_wizard_form" model="ir.ui.view"> + <field name="name">purchasing.job.note.wizard.form</field> + <field name="model">purchasing.job.note.wizard</field> + <field name="arch" type="xml"> + <form string="Note for Product"> + <group> + <field name="note" placeholder="Masukkan catatan kamu..."/> + </group> + <footer> + <button string="Confirm" type="object" name="action_confirm" class="btn-primary"/> + <button string="Cancel" class="btn-secondary" special="cancel"/> + </footer> + </form> + </field> + </record> + <record id="action_create_automatic_purchase_purchasing_job" model="ir.actions.server"> <field name="name">Create Automatic Purchase</field> <field name="model_id" ref="model_purchasing_job"/> diff --git a/fixco_custom/views/reordering_rule.xml b/fixco_custom/views/reordering_rule.xml new file mode 100644 index 0000000..dbc3e13 --- /dev/null +++ b/fixco_custom/views/reordering_rule.xml @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8"?> +<odoo> + <record id="reordering_rule_tree" model="ir.ui.view"> + <field name="name">reordering.rule.tree</field> + <field name="model">reordering.rule</field> + <field name="arch" type="xml"> + <tree> + <field name="product_id"/> + <field name="min_stock"/> + <field name="buffer_stock"/> + <field name="qty_onhand"/> + <field name="qty_incoming"/> + <field name="vendor_id"/> + </tree> + </field> + </record> + + <record id="reordering_rule_form" model="ir.ui.view"> + <field name="name">reordering.rule.form</field> + <field name="model">reordering.rule</field> + <field name="arch" type="xml"> + <form> + <sheet> + <group> + <group> + <field name="product_id"/> + <field name="min_stock"/> + <field name="buffer_stock"/> + <field name="qty_onhand"/> + <field name="qty_incoming"/> + <field name="vendor_id"/> + </group> + </group> + </sheet> + </form> + </field> + </record> + + <record id="view_reordering_rule_filter" model="ir.ui.view"> + <field name="name">reordering.rule.list.select</field> + <field name="model">reordering.rule</field> + <field name="priority" eval="15"/> + <field name="arch" type="xml"> + <search string="Search"> + <field name="product_id"/> + </search> + </field> + </record> + + <record id="action_create_automatic_purchase_reordering_rule" model="ir.actions.server"> + <field name="name">Create Automatic Purchase</field> + <field name="model_id" ref="model_reordering_rule"/> + <field name="binding_model_id" ref="model_reordering_rule"/> + <field name="state">code</field> + <field name="code"> + action = records.create_automatic_purchase() + </field> + </record> + + <record id="reordering_rule_action" model="ir.actions.act_window"> + <field name="name">Buffer Stock</field> + <field name="type">ir.actions.act_window</field> + <field name="res_model">reordering.rule</field> + <field name="search_view_id" ref="view_reordering_rule_filter"/> + <field name="view_mode">tree,form</field> + </record> + + <menuitem + action="reordering_rule_action" + id="reordering_rule" + parent="stock.menu_stock_warehouse_mgmt" + name="Buffer Stock" + sequence="1" + /> +</odoo> diff --git a/fixco_custom/views/sale_order.xml b/fixco_custom/views/sale_order.xml index eb609e9..137ded1 100755 --- a/fixco_custom/views/sale_order.xml +++ b/fixco_custom/views/sale_order.xml @@ -27,6 +27,19 @@ </data> <data> + <record id="view_sales_order_filter_inherit" model="ir.ui.view"> + <field name="name">sale.order.list.select.inherit.invoice_mp</field> + <field name="model">sale.order</field> + <field name="inherit_id" ref="sale.view_sales_order_filter"/> + <field name="arch" type="xml"> + <xpath expr="//search" position="inside"> + <field name="invoice_mp" string="Invoice Marketplace" filter_domain="[('invoice_mp', 'ilike', self)]"/> + </xpath> + </field> + </record> + </data> + + <data> <record id="sale_order_multi_create_invoices_ir_actions_server" model="ir.actions.server"> <field name="name">Multi Invoices</field> <field name="model_id" ref="sale.model_sale_order"/> |
