diff options
| author | Azka Nathan <darizkyfaz@gmail.com> | 2025-07-18 09:25:57 +0700 |
|---|---|---|
| committer | Azka Nathan <darizkyfaz@gmail.com> | 2025-07-18 09:25:57 +0700 |
| commit | 10e171ea5389873fea3fb13c90242b322a37a990 (patch) | |
| tree | 9288e4c9693cb5ea1a3d18e66e0bb723f9be5133 | |
| parent | 66b6b5863a15377c91300079026b11e7f81d60e4 (diff) | |
On change product otomatis terisi di requisition
Create bills by search PO & SKU
Create bills by upload
| -rwxr-xr-x | fixco_custom/__manifest__.py | 1 | ||||
| -rwxr-xr-x | fixco_custom/models/__init__.py | 3 | ||||
| -rw-r--r-- | fixco_custom/models/account_move.py | 38 | ||||
| -rw-r--r-- | fixco_custom/models/account_move_line.py | 2 | ||||
| -rw-r--r-- | fixco_custom/models/automatic_purchase.py | 33 | ||||
| -rw-r--r-- | fixco_custom/models/manage_stock.py | 1 | ||||
| -rw-r--r-- | fixco_custom/models/purchasing_job.py | 81 | ||||
| -rw-r--r-- | fixco_custom/models/upload_bills.py | 195 | ||||
| -rwxr-xr-x | fixco_custom/security/ir.model.access.csv | 4 | ||||
| -rw-r--r-- | fixco_custom/views/account_move.xml | 11 | ||||
| -rw-r--r-- | fixco_custom/views/automatic_purchase.xml | 2 | ||||
| -rw-r--r-- | fixco_custom/views/ir_sequence.xml | 11 | ||||
| -rw-r--r-- | fixco_custom/views/upload_bills.xml | 60 |
13 files changed, 388 insertions, 54 deletions
diff --git a/fixco_custom/__manifest__.py b/fixco_custom/__manifest__.py index 6c6c0df..12e15ca 100755 --- a/fixco_custom/__manifest__.py +++ b/fixco_custom/__manifest__.py @@ -36,6 +36,7 @@ 'views/manage_stock.xml', 'views/automatic_purchase.xml', 'views/purchasing_job.xml', + 'views/upload_bills.xml', ], 'demo': [], 'css': [], diff --git a/fixco_custom/models/__init__.py b/fixco_custom/models/__init__.py index bc53263..f345625 100755 --- a/fixco_custom/models/__init__.py +++ b/fixco_custom/models/__init__.py @@ -22,4 +22,5 @@ from . import account_move_line from . import manage_stock from . import automatic_purchase from . import purchasing_job -from . import stock_move
\ No newline at end of file +from . import stock_move +from . import upload_bills
\ No newline at end of file diff --git a/fixco_custom/models/account_move.py b/fixco_custom/models/account_move.py index 0a417cd..f491bc4 100644 --- a/fixco_custom/models/account_move.py +++ b/fixco_custom/models/account_move.py @@ -31,12 +31,45 @@ class AccountMove(models.Model): states={'draft': [('readonly', False)]}, help="Auto-complete from multiple past bills / purchase orders.", ) + faktur_pajak = fields.Char('Faktur Pajak') + count_payment = fields.Integer('Count Payment', compute='_compute_count_payment') + def _compute_count_payment(self): + for move in self: + accountPayment = self.env['account.payment'] + + payment = accountPayment.search([]).filtered( + lambda p: move.id in p.reconciled_bill_ids.ids + ) + + + move.count_payment = len(payment) + + def action_view_related_payment(self): + self.ensure_one() + + accountPayment = self.env['account.payment'] + + payment = accountPayment.search([]).filtered( + lambda p: self.id in p.reconciled_bill_ids.ids + ) + + payments = payment + + return { + 'name': 'Payments', + 'type': 'ir.actions.act_window', + 'res_model': 'account.payment', + 'view_mode': 'tree,form', + 'target': 'current', + 'domain': [('id', 'in', list(payments.ids))], + } def action_post(self): res = super(AccountMove, self).action_post() for entry in self: - entry.invoice_date = entry.picking_id.date_done + if entry.move_type == 'out_invoice': + entry.invoice_date = entry.picking_id.date_done return res @@ -59,7 +92,7 @@ class AccountMove(models.Model): invoice_vals.pop('ref', None) self.update(invoice_vals) - po_lines = po.order_line - self.line_ids.mapped('purchase_line_id') + po_lines = po.order_line.filtered(lambda l: l.qty_received != l.qty_invoiced and l.qty_invoiced <= l.qty_received) - self.line_ids.mapped('purchase_line_id') new_lines = self.env['account.move.line'] sequence = max(self.line_ids.mapped('sequence')) + 1 if self.line_ids else 10 @@ -73,6 +106,7 @@ class AccountMove(models.Model): new_lines += new_line new_lines._onchange_mark_recompute_taxes() + # Compute invoice_origin origins = set(self.line_ids.mapped('purchase_line_id.order_id.name')) diff --git a/fixco_custom/models/account_move_line.py b/fixco_custom/models/account_move_line.py index cf4d5d7..0db9c33 100644 --- a/fixco_custom/models/account_move_line.py +++ b/fixco_custom/models/account_move_line.py @@ -15,5 +15,5 @@ class AccountMoveLine(models.Model): @api.onchange('quantity') def _onchange_quantity(self): for line in self: - if line.quantity > line.qty_outstanding: + if line and line.quantity > line.qty_outstanding: raise UserError(_("Quantity Tidak Boleh Melebihi Qty Outstanding")) diff --git a/fixco_custom/models/automatic_purchase.py b/fixco_custom/models/automatic_purchase.py index f3f650d..1ef12ca 100644 --- a/fixco_custom/models/automatic_purchase.py +++ b/fixco_custom/models/automatic_purchase.py @@ -77,7 +77,6 @@ class AutomaticPurchase(models.Model): 'partner_id': vendor.id if vendor else False, 'taxes_id': vendor.tax_id.id if vendor else False, 'price': price, - 'subtotal': subtotal, }) self.env['automatic.purchase.line'].create(lines) @@ -248,7 +247,6 @@ class AutomaticPurchase(models.Model): 'partner_id': stock.vendor_id.id, 'taxes_id': stock.vendor_id.tax_id.id, 'price': price, - 'subtotal': subtotal, }) else: _logger.info( @@ -290,14 +288,35 @@ class AutomaticPurchaseLine(models.Model): qty_outgoing = fields.Float(string='Qty Outgoing', compute='compute_qty_outgoing') partner_id = fields.Many2one('res.partner', string='Vendor') price = fields.Float(string='Price') - subtotal = fields.Float(string='Subtotal') - last_order_id = fields.Many2one('purchase.order', string='Last Order') - last_orderline_id = fields.Many2one('purchase.order.line', string='Last Order Line') + subtotal = fields.Float(string='Subtotal', compute='compute_subtotal') 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') taxes_id = fields.Many2one('account.tax', string='Taxes') + def compute_subtotal(self): + for line in self: + line.subtotal = line.qty_purchase * line.price + + @api.onchange('product_id') + def _onchange_product_id(self): + if self.product_id: + manage_stock = self.env['manage.stock'].search([ + ('product_id', '=', self.product_id.id) + ], limit=1) + + if manage_stock: + self.qty_min = manage_stock.min_stock + self.qty_buffer = manage_stock.buffer_stock + self.taxes_id = manage_stock.vendor_id.tax_id.id + self.partner_id = manage_stock.vendor_id.id + + pricelist = self.env['purchase.pricelist'].search([ + ('product_id', '=', self.product_id.id), + ('vendor_id', '=', manage_stock.vendor_id.id) + ], limit=1) + + if pricelist: + self.price = pricelist.price + def compute_qty_available(self): for line in self: if line.product_id: diff --git a/fixco_custom/models/manage_stock.py b/fixco_custom/models/manage_stock.py index e654b4e..29d482f 100644 --- a/fixco_custom/models/manage_stock.py +++ b/fixco_custom/models/manage_stock.py @@ -78,7 +78,6 @@ class ManageStock(models.Model): 'partner_id': stock.vendor_id.id, 'taxes_id': stock.vendor_id.tax_id.id if stock.vendor_id.tax_id else False, 'price': price, - 'subtotal': subtotal, }) self.env['automatic.purchase.line'].create(lines_to_create) diff --git a/fixco_custom/models/purchasing_job.py b/fixco_custom/models/purchasing_job.py index 4f301f9..1af47b7 100644 --- a/fixco_custom/models/purchasing_job.py +++ b/fixco_custom/models/purchasing_job.py @@ -54,7 +54,6 @@ class PurchasingJob(models.Model): 'partner_id': stock.vendor_id.id, 'taxes_id': stock.vendor_id.tax_id.id if stock.vendor_id.tax_id else False, 'price': price, - 'subtotal': subtotal, }) self.env['automatic.purchase.line'].create(lines_to_create) @@ -71,43 +70,49 @@ class PurchasingJob(models.Model): tools.drop_view_if_exists(self.env.cr, self._table) self.env.cr.execute(""" CREATE OR REPLACE VIEW %s AS - 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 - 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 - 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 - ) a - LEFT JOIN LATERAL ( - SELECT vendor_id - FROM purchase_pricelist - WHERE product_id = a.product_id - ORDER BY id ASC - LIMIT 1 - ) pp2 ON true + 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 + 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 + 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 + ) a + 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() + + diff --git a/fixco_custom/models/upload_bills.py b/fixco_custom/models/upload_bills.py new file mode 100644 index 0000000..0c037f3 --- /dev/null +++ b/fixco_custom/models/upload_bills.py @@ -0,0 +1,195 @@ +from odoo import models, fields, api, _ +from datetime import datetime +import base64 +import xlrd +from odoo.exceptions import ValidationError, UserError +import requests +import json +import hmac +from hashlib import sha256 + +class UploadBills(models.Model): + _name = "upload.bills" + _description = "Upload Bills" + _order = "create_date desc" + _rec_name = "number" + + bills_lines = fields.One2many('upload.bills.line', 'upload_bills_id', + string='Lines', copy=False, auto_join=True) + number = fields.Char('Number', copy=False) + date_upload = fields.Datetime('Upload Date', copy=False) + user_id = fields.Many2one('res.users', 'Created By', default=lambda self: self.env.user.id) + excel_file = fields.Binary('Excel File', attachment=True) + filename = fields.Char('File Name') + + @api.model + def create(self, vals): + vals['number'] = self.env['ir.sequence'].next_by_code('upload.bills') or '/' + return super().create(vals) + + def action_import_excel(self): + self.ensure_one() + if not self.excel_file: + raise ValidationError(_("Please upload an Excel file first.")) + + try: + file_content = base64.b64decode(self.excel_file) + workbook = xlrd.open_workbook(file_contents=file_content) + sheet = workbook.sheet_by_index(0) + except Exception: + raise ValidationError(_("Invalid Excel file format.")) + + if sheet.ncols < 3: + raise ValidationError(_("Excel format tidak valid. Minimal ada 3 kolom: No BU IN, Tanggal, dan Faktur Pajak.")) + + rows_data = [] + for row_idx in range(sheet.nrows): + try: + no_bu = str(sheet.cell(row_idx, 0).value).strip() + tanggal_raw = sheet.cell(row_idx, 1).value + faktur_pajak = str(sheet.cell(row_idx, 2).value).strip() + + if isinstance(tanggal_raw, float): + tanggal = datetime(*xlrd.xldate_as_tuple(tanggal_raw, workbook.datemode)).date() + else: + tanggal = datetime.strptime(tanggal_raw, '%Y-%m-%d').date() + + rows_data.append({ + 'row_num': row_idx + 1, + 'no_bu': no_bu, + 'date': tanggal, + 'faktur_pajak': faktur_pajak, + }) + except Exception: + continue + + faktur_list = [row['faktur_pajak'] for row in rows_data] + existing_faktur = set() + no_bu_list = [row['no_bu'] for row in rows_data] + existing_no_bu = set() + + chunk_size = 500 + for i in range(0, len(faktur_list), chunk_size): + chunk_faktur = faktur_list[i:i + chunk_size] + chunk_no_bu = no_bu_list[i:i + chunk_size] + + existing_lines = self.env['upload.bills.line'].search([ + '|', + ('faktur_pajak', 'in', chunk_faktur), + ('no_bu', 'in', chunk_no_bu) + ]) + + existing_faktur.update(existing_lines.mapped('faktur_pajak')) + existing_no_bu.update(existing_lines.mapped('no_bu')) + + duplicate_rows = [] + for row in rows_data: + if row['faktur_pajak'] in existing_faktur or row['no_bu'] in existing_no_bu: + duplicate_rows.append(str(row['row_num'])) + + if duplicate_rows: + raise ValidationError(_("Data duplikat ditemukan di baris: %s\nPeriksa 'No BU IN' atau 'Faktur Pajak' yang sudah pernah diupload.") % ", ".join(duplicate_rows)) + + line_vals_list = [] + for row in rows_data: + line_vals = { + 'no_bu': row['no_bu'], + 'date': row['date'], + 'faktur_pajak': row['faktur_pajak'], + 'upload_bills_id': self.id, + } + line_vals_list.append((0, 0, line_vals)) + + self.bills_lines.unlink() + self.write({'bills_lines': line_vals_list}) + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Success'), + 'message': _('Berhasil mengimpor %s baris dari Excel.') % len(line_vals_list), + 'sticky': False, + 'next': {'type': 'ir.actions.act_window_close'}, + } + } + + + def _show_notification(self, message): + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Success'), + 'message': message, + 'sticky': False, + } + } + + def action_create_bills(self): + all_invoice_ids = [] + + for line in self.bills_lines: + bu_in = self.env['stock.picking'].search([ + ('name', '=', line.no_bu), + ('picking_type_code', '=', 'incoming') + ], limit=1) + + if not bu_in: + raise UserError(f"BU IN '{line.no_bu}' tidak ditemukan.") + + # Panggil pembuatan invoice + created_invoices = bu_in.action_create_invoice_from_mr() + + # Sinkron faktur dan tanggal ke account.move + if isinstance(created_invoices, dict) and 'res_id' in created_invoices: + # Satu invoice + invoice = self.env['account.move'].browse(created_invoices['res_id']) + invoice.write({ + 'faktur_pajak': line.faktur_pajak, + 'invoice_date': line.date + }) + invoice.action_post() + all_invoice_ids.append(invoice.id) + + elif isinstance(created_invoices, dict) and 'domain' in created_invoices: + # Banyak invoice + invoice_ids = created_invoices.get('domain', [])[0][2] or [] + invoices = self.env['account.move'].browse(invoice_ids) + invoices.write({ + 'faktur_pajak': line.faktur_pajak, + 'invoice_date': line.date + }) + invoices.action_post() + all_invoice_ids.extend(invoices.ids) + + else: + raise UserError("Gagal menemukan invoice yang baru dibuat.") + + # Tampilkan invoice(s) ke user + if not all_invoice_ids: + return {'type': 'ir.actions.act_window_close'} + + action = self.env.ref('account.action_move_in_invoice_type').read()[0] + if len(all_invoice_ids) == 1: + action.update({ + 'view_mode': 'tree,form', + 'res_id': all_invoice_ids, + }) + else: + action.update({ + 'domain': [('id', 'in', all_invoice_ids)], + 'view_mode': 'tree,form', + }) + return action + + +class UploadBillsLine(models.Model): + _name = "upload.bills.line" + _description = "Upload Bills Line" + _inherit = ['mail.thread'] + + upload_bills_id = fields.Many2one('upload.bills', string='Upload') + no_bu = fields.Char('No BU IN') + date = fields.Date('Date') + faktur_pajak = fields.Char('Faktur Pajak') diff --git a/fixco_custom/security/ir.model.access.csv b/fixco_custom/security/ir.model.access.csv index 3c4541b..a3f7ac4 100755 --- a/fixco_custom/security/ir.model.access.csv +++ b/fixco_custom/security/ir.model.access.csv @@ -26,4 +26,6 @@ access_product_shipment_line,access.product.shipment.line,model_product_shipment access_manage_stock,access.manage.stock,model_manage_stock,,1,1,1,1 access_automatic_purchase_line,access.automatic.purchase.line,model_automatic_purchase_line,,1,1,1,1 access_automatic_purchase,access.automatic.purchase,model_automatic_purchase,,1,1,1,1 -access_purchasing_job,access.purchasing.job,model_purchasing_job,,1,1,1,1
\ No newline at end of file +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
\ No newline at end of file diff --git a/fixco_custom/views/account_move.xml b/fixco_custom/views/account_move.xml index 1c55831..3615fb7 100644 --- a/fixco_custom/views/account_move.xml +++ b/fixco_custom/views/account_move.xml @@ -7,11 +7,20 @@ <field name="inherit_id" ref="account.view_move_form"/> <field name="arch" type="xml"> + <button name="open_reconcile_view" position="after"> + <button type="object" name="action_view_related_payment" + class="oe_stat_button" + icon="fa-pencil-square-o"> + <field name="count_payment" widget="statinfo" string="Payments"/> + </button> + </button> + <field name="payment_reference" position="after"> <field name="invoice_marketplace" readonly="1" attrs="{'invisible': [('move_type', '!=', 'out_invoice')]}"/> + <field name="faktur_pajak" readonly="1" attrs="{'invisible': [('move_type', '!=', 'in_invoice')]}"/> <field name="transaction_type" readonly="1" attrs="{'invisible': [('move_type', '!=', 'out_invoice')]}"/> </field> - + <field name="partner_id" position="after"> <field name="address" readonly="1" attrs="{'invisible': [('move_type', '!=', 'out_invoice')]}"/> </field> diff --git a/fixco_custom/views/automatic_purchase.xml b/fixco_custom/views/automatic_purchase.xml index d303b24..f985a54 100644 --- a/fixco_custom/views/automatic_purchase.xml +++ b/fixco_custom/views/automatic_purchase.xml @@ -31,8 +31,6 @@ <field name="qty_outgoing" optional="hide"/> <field name="price"/> <field name="subtotal"/> - <field name="last_order_id" readonly="1" optional="hide"/> - <field name="current_po_line_id" readonly="1" optional="hide"/> </tree> </field> </record> diff --git a/fixco_custom/views/ir_sequence.xml b/fixco_custom/views/ir_sequence.xml index 6e0e42a..06e11cb 100644 --- a/fixco_custom/views/ir_sequence.xml +++ b/fixco_custom/views/ir_sequence.xml @@ -55,5 +55,16 @@ <field name="number_increment">1</field> <field name="company_id">4</field> </record> + + <record id="sequence_upload_bills" model="ir.sequence"> + <field name="name">Upload Bills</field> + <field name="code">upload.bills</field> + <field name="active">TRUE</field> + <field name="prefix">UB/%(year)s/</field> + <field name="padding">5</field> + <field name="number_next">1</field> + <field name="number_increment">1</field> + <field name="company_id">4</field> + </record> </data> </odoo>
\ No newline at end of file diff --git a/fixco_custom/views/upload_bills.xml b/fixco_custom/views/upload_bills.xml new file mode 100644 index 0000000..a90546f --- /dev/null +++ b/fixco_custom/views/upload_bills.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> +<odoo> + <data> + <record id="upload_bills_tree" model="ir.ui.view"> + <field name="name">upload.bills.tree</field> + <field name="model">upload.bills</field> + <field name="arch" type="xml"> + <tree default_order="create_date desc"> + <field name="number"/> + <field name="date_upload"/> + <field name="user_id"/> + </tree> + </field> + </record> + + <record id="view_upload_bills_form" model="ir.ui.view"> + <field name="name">upload.bills.form</field> + <field name="model">upload.bills</field> + <field name="arch" type="xml"> + <form string="Upload Bills"> + <header> + <button name="action_import_excel" string="Import Excel" type="object" class="oe_highlight" attrs="{'invisible': [('number', '=', False)]}"/> + <button name="action_create_bills" string="Create Bills" type="object" class="oe_highlight" attrs="{'invisible': [('number', '=', False)]}"/> + <field name="number" widget="field_no_edit" options="{'no_open': True}"/> + <field name="date_upload"/> + <field name="user_id" widget="field_no_edit" options="{'no_open': True}"/> + </header> + <sheet> + <group> + <field name="excel_file" filename="filename"/> + <field name="filename" invisible="1"/> + </group> + <field name="bills_lines"> + <tree editable="bottom"> + <field name="no_bu"/> + <field name="date"/> + <field name="faktur_pajak"/> + </tree> + </field> + </sheet> + </form> + </field> + </record> + + <record id="upload_bills_action" model="ir.actions.act_window"> + <field name="name">Upload Bills</field> + <field name="type">ir.actions.act_window</field> + <field name="res_model">upload.bills</field> + <field name="view_mode">tree,form</field> + </record> + + <menuitem + id="menu_upload_bills" + name="Upload Bills" + parent="sale.menu_sale_report" + sequence="4" + action="upload_bills_action" + /> + </data> +</odoo> |
