diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/br_planilha_de_contas.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/br_planilha_de_contas.json index a1dbddc243..45be1e3fe6 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/br_planilha_de_contas.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/br_planilha_de_contas.json @@ -56,7 +56,9 @@ "Constru\u00e7\u00f5es em Andamento de Im\u00f3veis Destinados \u00e0 Venda": {}, "Estoques Destinados \u00e0 Doa\u00e7\u00e3o": {}, "Im\u00f3veis Destinados \u00e0 Venda": {}, - "Insumos (materiais diretos)": {}, + "Insumos (materiais diretos)": { + "account_type": "Stock" + }, "Insumos Agropecu\u00e1rios": {}, "Mercadorias para Revenda": {}, "Outras 11": {}, @@ -146,6 +148,65 @@ "root_type": "Asset" }, "CUSTOS DE PRODU\u00c7\u00c3O": { + "CUSTO DOS PRODUTOS E SERVI\u00c7OS VENDIDOS": { + "CUSTO DOS PRODUTOS VENDIDOS": { + "CUSTO DOS PRODUTOS VENDIDOS PARA AS DEMAIS ATIVIDADES": { + "Custos dos Produtos Vendidos em Geral": { + "account_type": "Cost of Goods Sold" + }, + "Outros Custos 4": {}, + "account_type": "Cost of Goods Sold" + }, + "CUSTO DOS PRODUTOS VENDIDOS PARA ASSIST\u00caNCIA SOCIAL": { + "Custos dos Produtos para Assist\u00eancia Social - Gratuidades": {}, + "Custos dos Produtos para Assist\u00eancia Social - Vendidos": {}, + "Outras": {} + }, + "CUSTO DOS PRODUTOS VENDIDOS PARA EDUCA\u00c7\u00c3O": { + "Custos dos Produtos para Educa\u00e7\u00e3o - Gratuidades": {}, + "Custos dos Produtos para Educa\u00e7\u00e3o - Vendidos": {}, + "Outros Custos 6": {} + }, + "CUSTO DOS PRODUTOS VENDIDOS PARA SA\u00daDE": { + "Custos dos Produtos para Sa\u00fade - Gratuidades": {}, + "Custos dos Produtos para Sa\u00fade \u2013 Vendidos": {}, + "Outros Custos 5": {} + }, + "account_type": "Cost of Goods Sold" + }, + "CUSTO DOS SERVI\u00c7OS PRESTADOS": { + "CUSTO DOS SERVI\u00c7OS PRESTADOS PARA AS DEMAIS ATIVIDADES": { + "Custo dos Servi\u00e7os Prestados em Geral": {}, + "Outros Custos": {} + }, + "CUSTO DOS SERVI\u00c7OS PRESTADOS PARA ASSIST\u00caNCIA SOCIAL": { + "Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias": {}, + "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es 1": {}, + "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas 1": {}, + "Custo dos Servi\u00e7os Prestados a Gratuidade 1": {}, + "Custo dos Servi\u00e7os Prestados a Pacientes Particulares": {}, + "Outros Custos 2": {} + }, + "CUSTO DOS SERVI\u00c7OS PRESTADOS PARA EDUCA\u00c7\u00c3O": { + "Custo dos Servi\u00e7os Prestados a Alunos N\u00e3o Bolsistas": {}, + "Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias (Exceto PROUNI)": {}, + "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es": {}, + "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas": {}, + "Custo dos Servi\u00e7os Prestados a Gratuidade": {}, + "Custo dos Servi\u00e7os Prestados ao PROUNI": {}, + "Outros Custos 1": {} + }, + "CUSTO DOS SERVI\u00c7OS PRESTADOS PARA SA\u00daDE": { + "Custo dos Servi\u00e7os Prestados a Conv\u00eanios SUS": {}, + "Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias 1": {}, + "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es 2": {}, + "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas 2": {}, + "Custo dos Servi\u00e7os Prestados a Gratuidade 2": {}, + "Custo dos Servi\u00e7os Prestados a Pacientes Particulares 1": {}, + "Outros Custos 3": {} + } + } + }, "CUSTO DOS BENS E SERVI\u00c7OS PRODUZIDOS": { "CUSTO DOS PRODUTOS DE FABRICA\u00c7\u00c3O PR\u00d3PRIA PRODUZIDOS": { "Alimenta\u00e7\u00e3o do Trabalhador": {}, @@ -621,7 +682,9 @@ "Receita das Unidades Imobili\u00e1rias Vendidas": {}, "Receita de Exporta\u00e7\u00e3o Direta de Mercadorias e Produtos": {}, "Receita de Exporta\u00e7\u00e3o de Servi\u00e7os": {}, - "Receita de Loca\u00e7\u00e3o de Bens M\u00f3veis e Im\u00f3veis": {}, + "Receita de Loca\u00e7\u00e3o de Bens M\u00f3veis e Im\u00f3veis": { + "account_type": "Income Account" + }, "Receita de Vendas de Mercadorias e Produtos a Comercial Exportadora com Fim Espec\u00edfico de Exporta\u00e7\u00e3o": {} } } @@ -645,65 +708,6 @@ } }, "RESULTADO OPERACIONAL": { - "CUSTO DOS PRODUTOS E SERVI\u00c7OS VENDIDOS": { - "CUSTO DOS PRODUTOS VENDIDOS": { - "CUSTO DOS PRODUTOS VENDIDOS PARA AS DEMAIS ATIVIDADES": { - "Custos dos Produtos Vendidos em Geral": { - "account_type": "Cost of Goods Sold" - }, - "Outros Custos 4": {}, - "account_type": "Cost of Goods Sold" - }, - "CUSTO DOS PRODUTOS VENDIDOS PARA ASSIST\u00caNCIA SOCIAL": { - "Custos dos Produtos para Assist\u00eancia Social - Gratuidades": {}, - "Custos dos Produtos para Assist\u00eancia Social - Vendidos": {}, - "Outras": {} - }, - "CUSTO DOS PRODUTOS VENDIDOS PARA EDUCA\u00c7\u00c3O": { - "Custos dos Produtos para Educa\u00e7\u00e3o - Gratuidades": {}, - "Custos dos Produtos para Educa\u00e7\u00e3o - Vendidos": {}, - "Outros Custos 6": {} - }, - "CUSTO DOS PRODUTOS VENDIDOS PARA SA\u00daDE": { - "Custos dos Produtos para Sa\u00fade - Gratuidades": {}, - "Custos dos Produtos para Sa\u00fade \u2013 Vendidos": {}, - "Outros Custos 5": {} - }, - "account_type": "Cost of Goods Sold" - }, - "CUSTO DOS SERVI\u00c7OS PRESTADOS": { - "CUSTO DOS SERVI\u00c7OS PRESTADOS PARA AS DEMAIS ATIVIDADES": { - "Custo dos Servi\u00e7os Prestados em Geral": {}, - "Outros Custos": {} - }, - "CUSTO DOS SERVI\u00c7OS PRESTADOS PARA ASSIST\u00caNCIA SOCIAL": { - "Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias": {}, - "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es 1": {}, - "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas 1": {}, - "Custo dos Servi\u00e7os Prestados a Gratuidade 1": {}, - "Custo dos Servi\u00e7os Prestados a Pacientes Particulares": {}, - "Outros Custos 2": {} - }, - "CUSTO DOS SERVI\u00c7OS PRESTADOS PARA EDUCA\u00c7\u00c3O": { - "Custo dos Servi\u00e7os Prestados a Alunos N\u00e3o Bolsistas": {}, - "Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias (Exceto PROUNI)": {}, - "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es": {}, - "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas": {}, - "Custo dos Servi\u00e7os Prestados a Gratuidade": {}, - "Custo dos Servi\u00e7os Prestados ao PROUNI": {}, - "Outros Custos 1": {} - }, - "CUSTO DOS SERVI\u00c7OS PRESTADOS PARA SA\u00daDE": { - "Custo dos Servi\u00e7os Prestados a Conv\u00eanios SUS": {}, - "Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias 1": {}, - "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es 2": {}, - "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas 2": {}, - "Custo dos Servi\u00e7os Prestados a Gratuidade 2": {}, - "Custo dos Servi\u00e7os Prestados a Pacientes Particulares 1": {}, - "Outros Custos 3": {} - } - } - }, "DESPESAS OPERACIONAIS": { "DESPESAS OPERACIONAIS 1": { "DESPESAS OPERACIONAIS 2": { diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index fef3b569ed..5e17881b6c 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -94,10 +94,13 @@ class BankTransaction(Document): pe.append(reference) def update_allocated_amount(self): - self.allocated_amount = ( + allocated_amount = ( sum(p.allocated_amount for p in self.payment_entries) if self.payment_entries else 0.0 ) - self.unallocated_amount = abs(flt(self.withdrawal) - flt(self.deposit)) - self.allocated_amount + unallocated_amount = abs(flt(self.withdrawal) - flt(self.deposit)) - allocated_amount + + self.allocated_amount = flt(allocated_amount, self.precision("allocated_amount")) + self.unallocated_amount = flt(unallocated_amount, self.precision("unallocated_amount")) def before_submit(self): self.allocate_payment_entries() diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index c61c3329c6..e3897bf4aa 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -85,7 +85,14 @@ class Dunning(AccountsController): frappe.throw( _( "The currency of invoice {} ({}) is different from the currency of this dunning ({})." - ).format(row.sales_invoice, invoice_currency, self.currency) + ).format( + frappe.get_desk_link( + "Sales Invoice", + row.sales_invoice, + ), + invoice_currency, + self.currency, + ) ) def validate_overdue_payments(self): diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json index ad2889d0a0..b92579eb79 100644 --- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json +++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json @@ -654,7 +654,7 @@ { "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "serial_no", - "fieldtype": "Small Text", + "fieldtype": "Text", "hidden": 1, "in_list_view": 1, "label": "Serial No", @@ -853,7 +853,7 @@ ], "istable": 1, "links": [], - "modified": "2024-02-04 16:36:25.665743", + "modified": "2024-02-25 15:50:17.140269", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice Item", diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py index 55a577b0c5..c24db1d6a0 100644 --- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py +++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py @@ -72,7 +72,7 @@ class POSInvoiceItem(Document): rate_with_margin: DF.Currency sales_order: DF.Link | None serial_and_batch_bundle: DF.Link | None - serial_no: DF.SmallText | None + serial_no: DF.Text | None service_end_date: DF.Date | None service_start_date: DF.Date | None service_stop_date: DF.Date | None diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index e900b81d4b..ef1f6bd8d8 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -14,23 +14,8 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e super.setup(doc); } company() { + super.company(); erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype); - - let me = this; - if (this.frm.doc.company) { - frappe.call({ - method: - "erpnext.accounts.party.get_party_account", - args: { - party_type: 'Customer', - party: this.frm.doc.customer, - company: this.frm.doc.company - }, - callback: (response) => { - if (response) me.frm.set_value("debit_to", response.message); - }, - }); - } } onload() { var me = this; diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 76ec4a4cf7..3352e0d90a 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -270,7 +270,7 @@ class SalesInvoice(SellingController): super(SalesInvoice, self).validate() self.validate_auto_set_posting_time() - if not self.is_pos: + if not (self.is_pos or self.is_debit_note): self.so_dn_required() self.set_tax_withholding() @@ -447,7 +447,11 @@ class SalesInvoice(SellingController): # Updating stock ledger should always be called after updating prevdoc status, # because updating reserved qty in bin depends upon updated delivered qty in SO if self.update_stock == 1: - self.make_bundle_using_old_serial_batch_fields() + for table_name in ["items", "packed_items"]: + if not self.get(table_name): + continue + + self.make_bundle_using_old_serial_batch_fields(table_name) self.update_stock_ledger() # this sequence because outstanding may get -ve @@ -1479,9 +1483,7 @@ class SalesInvoice(SellingController): "credit_in_account_currency": payment_mode.base_amount if self.party_account_currency == self.company_currency else payment_mode.amount, - "against_voucher": self.return_against - if cint(self.is_return) and self.return_against - else self.name, + "against_voucher": self.name, "against_voucher_type": self.doctype, "cost_center": self.cost_center, }, diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 8c3aedebcd..c6a8362264 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1105,6 +1105,44 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(pos.grand_total, 100.0) self.assertEqual(pos.write_off_amount, 10) + def test_ledger_entries_of_return_pos_invoice(self): + make_pos_profile() + + pos = create_sales_invoice(do_not_save=True) + pos.is_pos = 1 + pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) + pos.save().submit() + self.assertEqual(pos.outstanding_amount, 0.0) + self.assertEqual(pos.status, "Paid") + + from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return + + pos_return = make_sales_return(pos.name) + pos_return.save().submit() + pos_return.reload() + pos.reload() + self.assertEqual(pos_return.is_return, 1) + self.assertEqual(pos_return.return_against, pos.name) + self.assertEqual(pos_return.outstanding_amount, 0.0) + self.assertEqual(pos_return.status, "Return") + self.assertEqual(pos.outstanding_amount, 0.0) + self.assertEqual(pos.status, "Credit Note Issued") + + expected = ( + ("Cash - _TC", 0.0, 100.0, pos_return.name, None), + ("Debtors - _TC", 0.0, 100.0, pos_return.name, pos_return.name), + ("Debtors - _TC", 100.0, 0.0, pos_return.name, pos_return.name), + ("Sales - _TC", 100.0, 0.0, pos_return.name, None), + ) + res = frappe.db.get_all( + "GL Entry", + filters={"voucher_no": pos_return.name, "is_cancelled": 0}, + fields=["account", "debit", "credit", "voucher_no", "against_voucher"], + order_by="account, debit, credit", + as_list=1, + ) + self.assertEqual(expected, res) + def test_pos_with_no_gl_entry_for_change_amount(self): frappe.db.set_single_value("Accounts Settings", "post_change_gl_entries", 0) diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index d06c7861da..e7536e9bb9 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -625,7 +625,7 @@ { "depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1", "fieldname": "serial_no", - "fieldtype": "Small Text", + "fieldtype": "Text", "label": "Serial No", "oldfieldname": "serial_no", "oldfieldtype": "Small Text" @@ -926,7 +926,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2024-02-04 11:52:16.106541", + "modified": "2024-02-25 15:56:44.828634", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py index c71d08e7f7..9be1b42aab 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py @@ -75,7 +75,7 @@ class SalesInvoiceItem(Document): sales_invoice_item: DF.Data | None sales_order: DF.Link | None serial_and_batch_bundle: DF.Link | None - serial_no: DF.SmallText | None + serial_no: DF.Text | None service_end_date: DF.Date | None service_start_date: DF.Date | None service_stop_date: DF.Date | None diff --git a/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py b/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py index f404d9981a..57f66dd21d 100644 --- a/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py +++ b/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py @@ -8,6 +8,7 @@ from frappe.utils import today from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.test.accounts_mixin import AccountsTestMixin +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase): @@ -49,6 +50,16 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase): ) return pe + def create_sales_order(self): + so = make_sales_order( + company=self.company, + customer=self.customer, + item=self.item, + rate=100, + transaction_date=today(), + ) + return so + def test_01_unreconcile_invoice(self): si1 = self.create_sales_invoice() si2 = self.create_sales_invoice() @@ -314,3 +325,41 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase): ), 1, ) + + def test_05_unreconcile_order(self): + so = self.create_sales_order() + + pe = self.create_payment_entry() + # Allocation payment against Sales Order + pe.paid_amount = 100 + pe.append( + "references", + {"reference_doctype": so.doctype, "reference_name": so.name, "allocated_amount": 100}, + ) + pe.save().submit() + + # Assert 'Advance Paid' + so.reload() + self.assertEqual(so.advance_paid, 100) + + unreconcile = frappe.get_doc( + { + "doctype": "Unreconcile Payment", + "company": self.company, + "voucher_type": pe.doctype, + "voucher_no": pe.name, + } + ) + unreconcile.add_references() + self.assertEqual(len(unreconcile.allocations), 1) + allocations = [x.reference_name for x in unreconcile.allocations] + self.assertEquals([so.name], allocations) + # unreconcile so + unreconcile.save().submit() + + # Assert 'Advance Paid' + so.reload() + pe.reload() + self.assertEqual(so.advance_paid, 0) + self.assertEqual(len(pe.references), 0) + self.assertEqual(pe.unallocated_amount, 100) diff --git a/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py index 9b56952faa..664622f38d 100644 --- a/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py +++ b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py @@ -82,6 +82,11 @@ class UnreconcilePayment(Document): update_voucher_outstanding( alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party ) + if doc.doctype in frappe.get_hooks("advance_payment_payable_doctypes") + frappe.get_hooks( + "advance_payment_receivable_doctypes" + ): + doc.set_total_advance_paid() + frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index fc9034b2ee..d8ae2a43b5 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -9,7 +9,7 @@ from frappe import _, msgprint, qb, scrub from frappe.contacts.doctype.address.address import get_company_address, get_default_address from frappe.core.doctype.user_permission.user_permission import get_permitted_documents from frappe.model.utils import get_fetch_values -from frappe.query_builder.functions import Abs, Date, Sum +from frappe.query_builder.functions import Abs, Count, Date, Sum from frappe.utils import ( add_days, add_months, @@ -784,34 +784,37 @@ def get_timeline_data(doctype, name): from frappe.desk.form.load import get_communication_data out = {} - fields = "creation, count(*)" after = add_years(None, -1).strftime("%Y-%m-%d") - group_by = "group by Date(creation)" data = get_communication_data( doctype, name, after=after, - group_by="group by creation", - fields="C.creation as creation, count(C.name)", + group_by="group by communication_date", + fields="C.communication_date as communication_date, count(C.name)", as_dict=False, ) # fetch and append data from Activity Log - data += frappe.db.sql( - """select {fields} - from `tabActivity Log` - where (reference_doctype=%(doctype)s and reference_name=%(name)s) - or (timeline_doctype in (%(doctype)s) and timeline_name=%(name)s) - or (reference_doctype in ("Quotation", "Opportunity") and timeline_name=%(name)s) - and status!='Success' and creation > {after} - {group_by} order by creation desc - """.format( - fields=fields, group_by=group_by, after=after - ), - {"doctype": doctype, "name": name}, - as_dict=False, - ) + activity_log = frappe.qb.DocType("Activity Log") + data += ( + frappe.qb.from_(activity_log) + .select(activity_log.communication_date, Count(activity_log.name)) + .where( + ( + ((activity_log.reference_doctype == doctype) & (activity_log.reference_name == name)) + | ((activity_log.timeline_doctype == doctype) & (activity_log.timeline_name == name)) + | ( + (activity_log.reference_doctype.isin(["Quotation", "Opportunity"])) + & (activity_log.timeline_name == name) + ) + ) + & (activity_log.status != "Success") + & (activity_log.creation > after) + ) + .groupby(activity_log.communication_date) + .orderby(activity_log.communication_date, order=frappe.qb.desc) + ).run() timeline_items = dict(data) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index e3fa5e878d..6d77ef5b11 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -83,7 +83,10 @@ class ReceivablePayableReport(object): self.skip_total_row = 1 if self.filters.get("in_party_currency"): - self.skip_total_row = 1 + if self.filters.get("party") and len(self.filters.get("party")) == 1: + self.skip_total_row = 0 + else: + self.skip_total_row = 1 def get_data(self): self.get_ple_entries() diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index e4efefe7f5..7162aef8f2 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -975,7 +975,7 @@ class GrossProfitGenerator(object): & (sle.is_cancelled == 0) ) .orderby(sle.item_code) - .orderby(sle.warehouse, sle.posting_date, sle.posting_time, sle.creation, order=Order.desc) + .orderby(sle.warehouse, sle.posting_datetime, sle.creation, order=Order.desc) .run(as_dict=True) ) diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index 56ae41a5e1..a4f01fa136 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -357,7 +357,13 @@ def get_conditions(filters, additional_conditions=None): and ifnull(`tabSales Invoice Payment`.mode_of_payment, '') = %(mode_of_payment)s)""" if filters.get("warehouse"): - conditions += """and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s""" + if frappe.db.get_value("Warehouse", filters.get("warehouse"), "is_group"): + lft, rgt = frappe.db.get_all( + "Warehouse", filters={"name": filters.get("warehouse")}, fields=["lft", "rgt"], as_list=True + )[0] + conditions += f"and ifnull(`tabSales Invoice Item`.warehouse, '') in (select name from `tabWarehouse` where lft > {lft} and rgt < {rgt}) " + else: + conditions += """and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s""" if filters.get("brand"): conditions += """and ifnull(`tabSales Invoice Item`.brand, '') = %(brand)s""" diff --git a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py index 3f178f4715..eaeaa62d9a 100644 --- a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py +++ b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py @@ -163,7 +163,7 @@ def get_entries(filters): """select voucher_type, voucher_no, party_type, party, posting_date, debit, credit, remarks, against_voucher from `tabGL Entry` - where company=%(company)s and voucher_type in ('Journal Entry', 'Payment Entry') {0} + where company=%(company)s and voucher_type in ('Journal Entry', 'Payment Entry') and is_cancelled = 0 {0} """.format( get_conditions(filters) ), diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py index 0e3acd7b24..b18570b004 100644 --- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py @@ -242,7 +242,7 @@ def get_columns(filters): "width": 120, }, { - "label": _("Tax Amount"), + "label": _("TDS Amount") if filters.get("party_type") == "Supplier" else _("TCS Amount"), "fieldname": "tax_amount", "fieldtype": "Float", "width": 120, diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 64bc39a77b..157cfdd7f7 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -982,46 +982,6 @@ def get_currency_precision(): return precision -def get_stock_rbnb_difference(posting_date, company): - stock_items = frappe.db.sql_list( - """select distinct item_code - from `tabStock Ledger Entry` where company=%s""", - company, - ) - - pr_valuation_amount = frappe.db.sql( - """ - select sum(pr_item.valuation_rate * pr_item.qty * pr_item.conversion_factor) - from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr - where pr.name = pr_item.parent and pr.docstatus=1 and pr.company=%s - and pr.posting_date <= %s and pr_item.item_code in (%s)""" - % ("%s", "%s", ", ".join(["%s"] * len(stock_items))), - tuple([company, posting_date] + stock_items), - )[0][0] - - pi_valuation_amount = frappe.db.sql( - """ - select sum(pi_item.valuation_rate * pi_item.qty * pi_item.conversion_factor) - from `tabPurchase Invoice Item` pi_item, `tabPurchase Invoice` pi - where pi.name = pi_item.parent and pi.docstatus=1 and pi.company=%s - and pi.posting_date <= %s and pi_item.item_code in (%s)""" - % ("%s", "%s", ", ".join(["%s"] * len(stock_items))), - tuple([company, posting_date] + stock_items), - )[0][0] - - # Balance should be - stock_rbnb = flt(pr_valuation_amount, 2) - flt(pi_valuation_amount, 2) - - # Balance as per system - stock_rbnb_account = "Stock Received But Not Billed - " + frappe.get_cached_value( - "Company", company, "abbr" - ) - sys_bal = get_balance_on(stock_rbnb_account, posting_date, in_account_currency=False) - - # Amount should be credited - return flt(stock_rbnb) + flt(sys_bal) - - def get_held_invoices(party_type, party): """ Returns a list of names Purchase Invoices for the given party that are on hold @@ -1428,8 +1388,7 @@ def sort_stock_vouchers_by_posting_date( .select(sle.voucher_type, sle.voucher_no, sle.posting_date, sle.posting_time, sle.creation) .where((sle.is_cancelled == 0) & (sle.voucher_no.isin(voucher_nos))) .groupby(sle.voucher_type, sle.voucher_no) - .orderby(sle.posting_date) - .orderby(sle.posting_time) + .orderby(sle.posting_datetime) .orderby(sle.creation) ).run(as_dict=True) sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles] diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index df4593bb69..191675c2ab 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -561,15 +561,14 @@ def modify_depreciation_schedule_for_asset_repairs(asset, notes): def reverse_depreciation_entry_made_after_disposal(asset, date): for row in asset.get("finance_books"): asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book) - if not asset_depr_schedule_doc: + if not asset_depr_schedule_doc or not asset_depr_schedule_doc.get("depreciation_schedule"): continue for schedule_idx, schedule in enumerate(asset_depr_schedule_doc.get("depreciation_schedule")): - if schedule.schedule_date == date: + if schedule.schedule_date == date and schedule.journal_entry: if not disposal_was_made_on_original_schedule_date( schedule_idx, row, date ) or disposal_happens_in_the_future(date): - reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) reverse_journal_entry.posting_date = nowdate() diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js index 2f0de97939..110f2c4f2a 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js @@ -6,7 +6,7 @@ frappe.provide("erpnext.assets"); erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.stock.StockController { setup() { - this.frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle']; + this.frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle', 'Asset Movement']; this.setup_posting_date_time_check(); } diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index c9ed806fe4..e27a492fa6 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -138,6 +138,7 @@ class AssetCapitalization(StockController): "Repost Item Valuation", "Serial and Batch Bundle", "Asset", + "Asset Movement", ) self.cancel_target_asset() self.update_stock_ledger() @@ -147,7 +148,7 @@ class AssetCapitalization(StockController): def cancel_target_asset(self): if self.entry_type == "Capitalization" and self.target_asset: asset_doc = frappe.get_doc("Asset", self.target_asset) - frappe.db.set_value("Asset", self.target_asset, "capitalized_in", None) + asset_doc.db_set("capitalized_in", None) if asset_doc.docstatus == 1: asset_doc.cancel() diff --git a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json index 8eda441781..f79a84855d 100644 --- a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json +++ b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json @@ -107,7 +107,7 @@ { "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "serial_no", - "fieldtype": "Small Text", + "fieldtype": "Text", "hidden": 1, "label": "Serial No", "print_hide": 1 @@ -178,7 +178,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-02-04 16:41:09.239762", + "modified": "2024-02-25 15:57:35.007501", "modified_by": "Administrator", "module": "Assets", "name": "Asset Capitalization Stock Item", diff --git a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py index d2b075c3e6..0f06cc7442 100644 --- a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py +++ b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py @@ -24,7 +24,7 @@ class AssetCapitalizationStockItem(Document): parentfield: DF.Data parenttype: DF.Data serial_and_batch_bundle: DF.Link | None - serial_no: DF.SmallText | None + serial_no: DF.Text | None stock_qty: DF.Float stock_uom: DF.Link use_serial_batch_fields: DF.Check diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py index 146c03e8c3..77469df895 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -418,14 +418,13 @@ class AssetDepreciationSchedule(Document): ) # Adjust depreciation amount in the last period based on the expected value after useful life - if row.expected_value_after_useful_life and ( - ( - n == cint(final_number_of_depreciations) - 1 - and value_after_depreciation != row.expected_value_after_useful_life + if ( + n == cint(final_number_of_depreciations) - 1 + and flt(value_after_depreciation) != flt(row.expected_value_after_useful_life) + ) or flt(value_after_depreciation) < flt(row.expected_value_after_useful_life): + depreciation_amount += flt(value_after_depreciation) - flt( + row.expected_value_after_useful_life ) - or value_after_depreciation < row.expected_value_after_useful_life - ): - depreciation_amount += value_after_depreciation - row.expected_value_after_useful_life skip_row = True if flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) > 0: @@ -813,15 +812,11 @@ def make_draft_asset_depr_schedules_if_not_present(asset_doc): asset_depr_schedules_names = [] for row in asset_doc.get("finance_books"): - draft_asset_depr_schedule_name = get_asset_depr_schedule_name( - asset_doc.name, "Draft", row.finance_book + asset_depr_schedule = get_asset_depr_schedule_name( + asset_doc.name, ["Draft", "Active"], row.finance_book ) - active_asset_depr_schedule_name = get_asset_depr_schedule_name( - asset_doc.name, "Active", row.finance_book - ) - - if not draft_asset_depr_schedule_name and not active_asset_depr_schedule_name: + if not asset_depr_schedule: name = make_draft_asset_depr_schedule(asset_doc, row) asset_depr_schedules_names.append(name) @@ -997,16 +992,20 @@ def get_asset_depr_schedule_doc(asset_name, status, finance_book=None): def get_asset_depr_schedule_name(asset_name, status, finance_book=None): - finance_book_filter = ["finance_book", "is", "not set"] - if finance_book: + if finance_book is None: + finance_book_filter = ["finance_book", "is", "not set"] + else: finance_book_filter = ["finance_book", "=", finance_book] + if isinstance(status, str): + status = [status] + return frappe.db.get_value( doctype="Asset Depreciation Schedule", filters=[ ["asset", "=", asset_name], finance_book_filter, - ["status", "=", status], + ["status", "in", status], ], ) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index f11db1a672..9896cad8c3 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -216,6 +216,19 @@ class AccountsController(TransactionBase): ) ) + if self.get("is_return") and self.get("return_against") and not self.get("is_pos"): + # if self.get("is_return") and self.get("return_against"): + document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note" + frappe.msgprint( + _( + "{0} will be treated as a standalone {0}. Post creation use {1} tool to reconcile against {2}." + ).format( + document_type, + get_link_to_form("Payment Reconciliation"), + get_link_to_form(self.doctype, self.get("return_against")), + ) + ) + pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid" if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)): self.set_advances() @@ -333,6 +346,12 @@ class AccountsController(TransactionBase): ple = frappe.qb.DocType("Payment Ledger Entry") frappe.qb.from_(ple).delete().where( (ple.voucher_type == self.doctype) & (ple.voucher_no == self.name) + | ( + (ple.against_voucher_type == self.doctype) + & (ple.against_voucher_no == self.name) + & ple.delinked + == 1 + ) ).run() frappe.db.sql( "delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name) @@ -2684,7 +2703,7 @@ def get_advance_journal_entries( if order_list: q = q.where( - (journal_acc.reference_type == order_doctype) & ((journal_acc.reference_type).isin(order_list)) + (journal_acc.reference_type == order_doctype) & ((journal_acc.reference_name).isin(order_list)) ) q = q.orderby(journal_entry.posting_date) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index fb680100b7..91ee53a796 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -217,8 +217,8 @@ class BuyingController(SubcontractingController): lc_voucher_data = frappe.db.sql( """select sum(applicable_charges), cost_center from `tabLanded Cost Item` - where docstatus = 1 and purchase_receipt_item = %s""", - d.name, + where docstatus = 1 and purchase_receipt_item = %s and receipt_document = %s""", + (d.name, self.name), ) d.landed_cost_voucher_amount = lc_voucher_data[0][0] if lc_voucher_data else 0.0 if not d.cost_center and lc_voucher_data and lc_voucher_data[0][1]: @@ -824,7 +824,8 @@ class BuyingController(SubcontractingController): if self.doctype == "Purchase Invoice" and not self.get("update_stock"): return - frappe.db.sql("delete from `tabAsset Movement` where reference_name=%s", self.name) + asset_movement = frappe.db.get_value("Asset Movement", {"reference_name": self.name}, "name") + frappe.delete_doc("Asset Movement", asset_movement, force=1) def validate_schedule_date(self): if not self.get("items"): diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index dc49023149..359d721429 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -28,7 +28,8 @@ class SellingController(StockController): def validate(self): super(SellingController, self).validate() self.validate_items() - self.validate_max_discount() + if not self.get("is_debit_note"): + self.validate_max_discount() self.validate_selling_price() self.set_qty_as_per_stock_uom() self.set_po_nos(for_validate=True) @@ -703,6 +704,9 @@ def set_default_income_account_for_item(obj): def get_serial_and_batch_bundle(child, parent): from erpnext.stock.serial_batch_bundle import SerialBatchCreation + if child.get("use_serial_batch_fields"): + return + if not frappe.db.get_single_value( "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward" ): diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 74c835c745..a67fbdca62 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -7,7 +7,7 @@ from typing import List, Tuple import frappe from frappe import _, bold -from frappe.utils import cint, flt, get_link_to_form, getdate +from frappe.utils import cint, cstr, flt, get_link_to_form, getdate import erpnext from erpnext.accounts.general_ledger import ( @@ -46,6 +46,9 @@ class BatchExpiredError(frappe.ValidationError): class StockController(AccountsController): def validate(self): super(StockController, self).validate() + + if self.docstatus == 0: + self.validate_duplicate_serial_and_batch_bundle() if not self.get("is_return"): self.validate_inspection() self.validate_serialized_batch() @@ -55,6 +58,32 @@ class StockController(AccountsController): self.validate_internal_transfer() self.validate_putaway_capacity() + def validate_duplicate_serial_and_batch_bundle(self): + if sbb_list := [ + item.get("serial_and_batch_bundle") + for item in self.items + if item.get("serial_and_batch_bundle") + ]: + SLE = frappe.qb.DocType("Stock Ledger Entry") + data = ( + frappe.qb.from_(SLE) + .select(SLE.voucher_type, SLE.voucher_no, SLE.serial_and_batch_bundle) + .where( + (SLE.docstatus == 1) + & (SLE.serial_and_batch_bundle.notnull()) + & (SLE.serial_and_batch_bundle.isin(sbb_list)) + ) + .limit(1) + ).run(as_dict=True) + + if data: + data = data[0] + frappe.throw( + _("Serial and Batch Bundle {0} is already used in {1} {2}.").format( + frappe.bold(data.serial_and_batch_bundle), data.voucher_type, data.voucher_no + ) + ) + def make_gl_entries(self, gl_entries=None, from_repost=False): if self.docstatus == 2: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) @@ -129,26 +158,34 @@ class StockController(AccountsController): # remove extra whitespace and store one serial no on each line row.serial_no = clean_serial_no_string(row.serial_no) - def make_bundle_using_old_serial_batch_fields(self): + def make_bundle_using_old_serial_batch_fields(self, table_name=None): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.serial_batch_bundle import SerialBatchCreation + if self.get("_action") == "update_after_submit": + return + # To handle test cases if frappe.flags.in_test and frappe.flags.use_serial_and_batch_fields: return - table_name = "items" + if not table_name: + table_name = "items" + if self.doctype == "Asset Capitalization": table_name = "stock_items" for row in self.get(table_name): + if row.serial_and_batch_bundle and (row.serial_no or row.batch_no): + self.validate_serial_nos_and_batches_with_bundle(row) + if not row.serial_no and not row.batch_no and not row.get("rejected_serial_no"): continue if not row.use_serial_batch_fields and ( row.serial_no or row.batch_no or row.get("rejected_serial_no") ): - frappe.throw(_("Please enable Use Old Serial / Batch Fields to make_bundle")) + row.use_serial_batch_fields = 1 if row.use_serial_batch_fields and ( not row.serial_and_batch_bundle and not row.get("rejected_serial_and_batch_bundle") @@ -156,14 +193,24 @@ class StockController(AccountsController): if self.doctype == "Stock Reconciliation": qty = row.qty type_of_transaction = "Inward" + warehouse = row.warehouse + elif table_name == "packed_items": + qty = row.qty + warehouse = row.warehouse + type_of_transaction = "Outward" + if self.is_return: + type_of_transaction = "Inward" else: - qty = row.stock_qty + qty = row.stock_qty if self.doctype != "Stock Entry" else row.transfer_qty type_of_transaction = get_type_of_transaction(self, row) + warehouse = ( + row.warehouse if self.doctype != "Stock Entry" else row.s_warehouse or row.t_warehouse + ) sn_doc = SerialBatchCreation( { "item_code": row.item_code, - "warehouse": row.warehouse, + "warehouse": warehouse, "posting_date": self.posting_date, "posting_time": self.posting_time, "voucher_type": self.doctype, @@ -186,7 +233,6 @@ class StockController(AccountsController): row.db_set( { "rejected_serial_and_batch_bundle": sn_doc.name, - "rejected_serial_no": "", } ) else: @@ -194,11 +240,44 @@ class StockController(AccountsController): row.db_set( { "serial_and_batch_bundle": sn_doc.name, - "serial_no": "", - "batch_no": "", } ) + def validate_serial_nos_and_batches_with_bundle(self, row): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + + throw_error = False + if row.serial_no: + serial_nos = frappe.get_all( + "Serial and Batch Entry", fields=["serial_no"], filters={"parent": row.serial_and_batch_bundle} + ) + serial_nos = sorted([cstr(d.serial_no) for d in serial_nos]) + parsed_serial_nos = get_serial_nos(row.serial_no) + + if len(serial_nos) != len(parsed_serial_nos): + throw_error = True + elif serial_nos != parsed_serial_nos: + for serial_no in serial_nos: + if serial_no not in parsed_serial_nos: + throw_error = True + break + + elif row.batch_no: + batches = frappe.get_all( + "Serial and Batch Entry", fields=["batch_no"], filters={"parent": row.serial_and_batch_bundle} + ) + batches = sorted([d.batch_no for d in batches]) + + if batches != [row.batch_no]: + throw_error = True + + if throw_error: + frappe.throw( + _( + "At row {0}: Serial and Batch Bundle {1} has already created. Please remove the values from the serial no or batch no fields." + ).format(row.idx, row.serial_and_batch_bundle) + ) + def set_use_serial_batch_fields(self): if frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields"): for row in self.items: @@ -938,6 +1017,9 @@ class StockController(AccountsController): "Stock Reconciliation", ) + if not frappe.get_all("Putaway Rule", limit=1): + return + if self.doctype == "Purchase Invoice" and self.get("update_stock") == 0: valid_doctype = False diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 17a2b07daa..eac35b0d39 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -539,6 +539,10 @@ class SubcontractingController(StockController): def __add_supplied_item(self, item_row, bom_item, qty): bom_item.conversion_factor = item_row.conversion_factor rm_obj = self.append(self.raw_material_table, bom_item) + if rm_obj.get("qty"): + # Qty field not exists + rm_obj.qty = 0.0 + rm_obj.reference_name = item_row.name if self.doctype == self.subcontract_data.order_doctype: diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py index 47762ac4cf..95a7bcb398 100644 --- a/erpnext/controllers/tests/test_subcontracting_controller.py +++ b/erpnext/controllers/tests/test_subcontracting_controller.py @@ -401,7 +401,7 @@ class TestSubcontractingController(FrappeTestCase): { "main_item_code": "Subcontracted Item SA4", "item_code": "Subcontracted SRM Item 3", - "qty": 1.0, + "qty": 3.0, "rate": 100.0, "stock_uom": "Nos", "warehouse": "_Test Warehouse - _TC", @@ -914,12 +914,6 @@ def update_item_details(child_row, details): else child_row.get("consumed_qty") ) - if child_row.serial_no: - details.serial_no.extend(get_serial_nos(child_row.serial_no)) - - if child_row.batch_no: - details.batch_no[child_row.batch_no] += child_row.get("qty") or child_row.get("consumed_qty") - if child_row.serial_and_batch_bundle: doc = frappe.get_doc("Serial and Batch Bundle", child_row.serial_and_batch_bundle) for row in doc.get("entries"): @@ -928,6 +922,12 @@ def update_item_details(child_row, details): if row.batch_no: details.batch_no[row.batch_no] += row.qty * (-1 if doc.type_of_transaction == "Outward" else 1) + else: + if child_row.serial_no: + details.serial_no.extend(get_serial_nos(child_row.serial_no)) + + if child_row.batch_no: + details.batch_no[child_row.batch_no] += child_row.get("qty") or child_row.get("consumed_qty") def make_stock_transfer_entry(**args): diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py index dea3f2dd36..4f7436ff9e 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py @@ -41,7 +41,9 @@ class SalesPipelineAnalytics(object): month_list = self.get_month_list() for month in month_list: - self.columns.append({"fieldname": month, "fieldtype": based_on, "label": month, "width": 200}) + self.columns.append( + {"fieldname": month, "fieldtype": based_on, "label": _(month), "width": 200} + ) elif self.filters.get("range") == "Quarterly": for quarter in range(1, 5): @@ -156,7 +158,7 @@ class SalesPipelineAnalytics(object): for column in self.columns: if column["fieldname"] != "opportunity_owner" and column["fieldname"] != "sales_stage": - labels.append(column["fieldname"]) + labels.append(_(column["fieldname"])) self.chart = {"data": {"labels": labels, "datasets": datasets}, "type": "line"} diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py index dcf122c4c3..e44d484c14 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py @@ -41,11 +41,49 @@ class BlanketOrder(Document): def validate(self): self.validate_dates() self.validate_duplicate_items() + self.set_party_item_code() def validate_dates(self): if getdate(self.from_date) > getdate(self.to_date): frappe.throw(_("From date cannot be greater than To date")) + def set_party_item_code(self): + item_ref = {} + if self.blanket_order_type == "Selling": + item_ref = self.get_customer_items_ref() + else: + item_ref = self.get_supplier_items_ref() + + if not item_ref: + return + + for row in self.items: + row.party_item_code = item_ref.get(row.item_code) + + def get_customer_items_ref(self): + items = [d.item_code for d in self.items] + + return frappe._dict( + frappe.get_all( + "Item Customer Detail", + filters={"parent": ("in", items), "customer_name": self.customer}, + fields=["parent", "ref_code"], + as_list=True, + ) + ) + + def get_supplier_items_ref(self): + items = [d.item_code for d in self.items] + + return frappe._dict( + frappe.get_all( + "Item Supplier", + filters={"parent": ("in", items), "supplier": self.supplier}, + fields=["parent", "supplier_part_no"], + as_list=True, + ) + ) + def validate_duplicate_items(self): item_list = [] for item in self.items: diff --git a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py index e9fc25b5bc..3f3b6f092c 100644 --- a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py @@ -5,6 +5,7 @@ from frappe.tests.utils import FrappeTestCase from frappe.utils import add_months, today from erpnext import get_company_currency +from erpnext.stock.doctype.item.test_item import make_item from .blanket_order import make_order @@ -90,6 +91,30 @@ class TestBlanketOrder(FrappeTestCase): frappe.db.set_single_value("Buying Settings", "blanket_order_allowance", 10) po.submit() + def test_party_item_code(self): + item_doc = make_item("_Test Item 1 for Blanket Order") + item_code = item_doc.name + + customer = "_Test Customer" + supplier = "_Test Supplier" + + if not frappe.db.exists( + "Item Customer Detail", {"customer_name": customer, "parent": item_code} + ): + item_doc.append("customer_items", {"customer_name": customer, "ref_code": "CUST-REF-1"}) + item_doc.save() + + if not frappe.db.exists("Item Supplier", {"supplier": supplier, "parent": item_code}): + item_doc.append("supplier_items", {"supplier": supplier, "supplier_part_no": "SUPP-PART-1"}) + item_doc.save() + + # Blanket Order for Selling + bo = make_blanket_order(blanket_order_type="Selling", customer=customer, item_code=item_code) + self.assertEqual(bo.items[0].party_item_code, "CUST-REF-1") + + bo = make_blanket_order(blanket_order_type="Purchasing", supplier=supplier, item_code=item_code) + self.assertEqual(bo.items[0].party_item_code, "SUPP-PART-1") + def make_blanket_order(**args): args = frappe._dict(args) diff --git a/erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.json b/erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.json index 977ad547f5..aa7831fd6b 100644 --- a/erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.json +++ b/erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2018-05-24 07:20:04.255236", "doctype": "DocType", "editable_grid": 1, @@ -6,6 +7,7 @@ "field_order": [ "item_code", "item_name", + "party_item_code", "column_break_3", "qty", "rate", @@ -62,10 +64,17 @@ "fieldname": "terms_and_conditions", "fieldtype": "Text", "label": "Terms and Conditions" + }, + { + "fieldname": "party_item_code", + "fieldtype": "Data", + "label": "Party Item Code", + "read_only": 1 } ], "istable": 1, - "modified": "2019-11-18 19:37:46.245878", + "links": [], + "modified": "2024-02-14 18:25:26.479672", "modified_by": "Administrator", "module": "Manufacturing", "name": "Blanket Order Item", @@ -74,5 +83,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.py b/erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.py index 068c2e9118..316d294eaf 100644 --- a/erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.py +++ b/erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.py @@ -20,6 +20,7 @@ class BlanketOrderItem(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data + party_item_code: DF.Data | None qty: DF.Float rate: DF.Currency terms_and_conditions: DF.Text | None diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 6f3520618b..27c8493ab5 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1071,8 +1071,7 @@ def get_valuation_rate(data): frappe.qb.from_(sle) .select(sle.valuation_rate) .where((sle.item_code == item_code) & (sle.valuation_rate > 0) & (sle.is_cancelled == 0)) - .orderby(sle.posting_date, order=frappe.qb.desc) - .orderby(sle.posting_time, order=frappe.qb.desc) + .orderby(sle.posting_datetime, order=frappe.qb.desc) .orderby(sle.creation, order=frappe.qb.desc) .limit(1) ).run(as_dict=True) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 079350b63b..35aebb9539 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -239,12 +239,12 @@ class JobCard(Document): for row in self.sub_operations: self.total_completed_qty += row.completed_qty - def get_overlap_for(self, args, check_next_available_slot=False): + def get_overlap_for(self, args): time_logs = [] - time_logs.extend(self.get_time_logs(args, "Job Card Time Log", check_next_available_slot)) + time_logs.extend(self.get_time_logs(args, "Job Card Time Log")) - time_logs.extend(self.get_time_logs(args, "Job Card Scheduled Time", check_next_available_slot)) + time_logs.extend(self.get_time_logs(args, "Job Card Scheduled Time")) if not time_logs: return {} @@ -269,7 +269,7 @@ class JobCard(Document): self.workstation = workstation_time.get("workstation") return workstation_time - return time_logs[-1] + return time_logs[0] def has_overlap(self, production_capacity, time_logs): overlap = False @@ -308,7 +308,7 @@ class JobCard(Document): return True return overlap - def get_time_logs(self, args, doctype, check_next_available_slot=False): + def get_time_logs(self, args, doctype): jc = frappe.qb.DocType("Job Card") jctl = frappe.qb.DocType(doctype) @@ -318,9 +318,6 @@ class JobCard(Document): ((jctl.from_time >= args.from_time) & (jctl.to_time <= args.to_time)), ] - if check_next_available_slot: - time_conditions.append(((jctl.from_time >= args.from_time) & (jctl.to_time >= args.to_time))) - query = ( frappe.qb.from_(jctl) .from_(jc) @@ -395,18 +392,28 @@ class JobCard(Document): def validate_overlap_for_workstation(self, args, row): # get the last record based on the to time from the job card - data = self.get_overlap_for(args, check_next_available_slot=True) + data = self.get_overlap_for(args) + if not self.workstation: workstations = get_workstations(self.workstation_type) if workstations: # Get the first workstation self.workstation = workstations[0] + if not data: + row.planned_start_time = args.from_time + return + if data: if data.get("planned_start_time"): - row.planned_start_time = get_datetime(data.planned_start_time) + args.planned_start_time = get_datetime(data.planned_start_time) else: - row.planned_start_time = get_datetime(data.to_time + get_mins_between_operations()) + args.planned_start_time = get_datetime(data.to_time + get_mins_between_operations()) + + args.from_time = args.planned_start_time + args.to_time = add_to_date(args.planned_start_time, minutes=row.remaining_time_in_mins) + + self.validate_overlap_for_workstation(args, row) def check_workstation_time(self, row): workstation_doc = frappe.get_cached_doc("Workstation", self.workstation) @@ -748,7 +755,7 @@ class JobCard(Document): fields=["total_time_in_mins", "hour_rate"], filters={"is_corrective_job_card": 1, "docstatus": 1, "work_order": self.work_order}, ): - wo.corrective_operation_cost += flt(row.total_time_in_mins) * flt(row.hour_rate) + wo.corrective_operation_cost += flt(row.total_time_in_mins / 60) * flt(row.hour_rate) wo.calculate_operating_cost() wo.flags.ignore_validate_update_after_submit = True diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json index d3ad51f723..63e3fa3e9f 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json @@ -7,6 +7,7 @@ "field_order": [ "raw_materials_consumption_section", "material_consumption", + "get_rm_cost_from_consumption_entry", "column_break_3", "backflush_raw_materials_based_on", "capacity_planning", @@ -202,13 +203,20 @@ "fieldname": "set_op_cost_and_scrape_from_sub_assemblies", "fieldtype": "Check", "label": "Set Operating Cost / Scrape Items From Sub-assemblies" + }, + { + "default": "0", + "depends_on": "eval: doc.material_consumption", + "fieldname": "get_rm_cost_from_consumption_entry", + "fieldtype": "Check", + "label": "Get Raw Materials Cost from Consumption Entry" } ], "icon": "icon-wrench", "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-12-28 16:37:44.874096", + "modified": "2024-02-08 19:00:37.561244", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing Settings", diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py index 463ba9fe4b..9a501115b0 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py @@ -26,6 +26,7 @@ class ManufacturingSettings(Document): default_scrap_warehouse: DF.Link | None default_wip_warehouse: DF.Link | None disable_capacity_planning: DF.Check + get_rm_cost_from_consumption_entry: DF.Check job_card_excess_transfer: DF.Check make_serial_no_batch_from_work_order: DF.Check material_consumption: DF.Check diff --git a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json index d07bf0fa66..06c1b49755 100644 --- a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json +++ b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json @@ -38,7 +38,8 @@ "in_list_view": 1, "label": "Item Code", "options": "Item", - "reqd": 1 + "reqd": 1, + "search_index": 1 }, { "fieldname": "item_name", @@ -53,7 +54,8 @@ "in_standard_filter": 1, "label": "For Warehouse", "options": "Warehouse", - "reqd": 1 + "reqd": 1, + "search_index": 1 }, { "columns": 1, @@ -141,7 +143,8 @@ "fieldname": "from_warehouse", "fieldtype": "Link", "label": "From Warehouse", - "options": "Warehouse" + "options": "Warehouse", + "search_index": 1 }, { "fetch_from": "item_code.safety_stock", @@ -199,7 +202,7 @@ ], "istable": 1, "links": [], - "modified": "2023-09-12 12:09:08.358326", + "modified": "2024-02-11 16:21:11.977018", "modified_by": "Administrator", "module": "Manufacturing", "name": "Material Request Plan Item", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 257b60c486..54c3893928 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -298,7 +298,8 @@ "no_copy": 1, "options": "\nDraft\nSubmitted\nNot Started\nIn Process\nCompleted\nClosed\nCancelled\nMaterial Requested", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "amended_from", @@ -436,7 +437,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-12-26 16:31:13.740777", + "modified": "2024-02-11 15:42:47.642481", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 6e9d1fcfd8..517b2b0f88 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -652,7 +652,10 @@ class ProductionPlan(Document): "project": self.project, } - key = (d.item_code, d.sales_order, d.warehouse) + key = (d.item_code, d.sales_order, d.sales_order_item, d.warehouse) + if self.combine_items: + key = (d.item_code, d.sales_order, d.warehouse) + if not d.sales_order: key = (d.name, d.item_code, d.warehouse) @@ -1504,19 +1507,17 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get("idx"))) if bom_no: - if ( - data.get("include_exploded_items") - and doc.get("sub_assembly_items") - and doc.get("skip_available_sub_assembly_item") - ): - item_details = get_raw_materials_of_sub_assembly_items( - item_details, - company, - bom_no, - include_non_stock_items, - sub_assembly_items, - planned_qty=planned_qty, - ) + if data.get("include_exploded_items") and doc.get("skip_available_sub_assembly_item"): + item_details = {} + if doc.get("sub_assembly_items"): + item_details = get_raw_materials_of_sub_assembly_items( + item_details, + company, + bom_no, + include_non_stock_items, + sub_assembly_items, + planned_qty=planned_qty, + ) elif data.get("include_exploded_items") and include_subcontracted_items: # fetch exploded items from BOM @@ -1768,23 +1769,23 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): return reserved_qty_for_production_plan - reserved_qty_for_production +@frappe.request_cache def get_non_completed_production_plans(): table = frappe.qb.DocType("Production Plan") child = frappe.qb.DocType("Production Plan Item") - query = ( + return ( frappe.qb.from_(table) .inner_join(child) .on(table.name == child.parent) .select(table.name) + .distinct() .where( (table.docstatus == 1) & (table.status.notin(["Completed", "Closed"])) & (child.planned_qty > child.ordered_qty) ) - ).run(as_dict=True) - - return list(set([d.name for d in query])) + ).run(pluck="name") def get_raw_materials_of_sub_assembly_items( diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 1c748a809b..53537f9e1a 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1232,6 +1232,35 @@ class TestProductionPlan(FrappeTestCase): if row.item_code == "SubAssembly2 For SUB Test": self.assertEqual(row.quantity, 10) + def test_sub_assembly_and_their_raw_materials_exists(self): + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + + bom_tree = { + "FG1 For SUB Test": { + "SAB1 For SUB Test": {"CP1 For SUB Test": {}}, + "SAB2 For SUB Test": {}, + } + } + + parent_bom = create_nested_bom(bom_tree, prefix="") + for item in ["SAB1 For SUB Test", "SAB2 For SUB Test"]: + make_stock_entry(item_code=item, qty=10, rate=100, target="_Test Warehouse - _TC") + + plan = create_production_plan( + item_code=parent_bom.item, + planned_qty=10, + ignore_existing_ordered_qty=1, + do_not_submit=1, + skip_available_sub_assembly_item=1, + warehouse="_Test Warehouse - _TC", + ) + + items = get_items_for_material_requests( + plan.as_dict(), warehouses=[{"warehouse": "_Test Warehouse - _TC"}] + ) + + self.assertFalse(items) + def test_transfer_and_purchase_mrp_for_purchase_uom(self): from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index f6e9a07063..c72232ae49 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -1776,6 +1776,159 @@ class TestWorkOrder(FrappeTestCase): "Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies", 0 ) + @change_settings( + "Manufacturing Settings", {"material_consumption": 1, "get_rm_cost_from_consumption_entry": 1} + ) + def test_get_rm_cost_from_consumption_entry(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import ( + make_stock_entry as make_stock_entry_test_record, + ) + + rm = make_item(properties={"is_stock_item": 1}).name + fg = make_item(properties={"is_stock_item": 1}).name + + make_stock_entry_test_record( + purpose="Material Receipt", + item_code=rm, + target="Stores - _TC", + qty=10, + basic_rate=100, + ) + make_stock_entry_test_record( + purpose="Material Receipt", + item_code=rm, + target="Stores - _TC", + qty=10, + basic_rate=200, + ) + + bom = make_bom(item=fg, raw_materials=[rm], rate=150).name + wo = make_wo_order_test_record( + production_item=fg, + bom_no=bom, + qty=10, + ) + + mte = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 10)) + mte.items[0].s_warehouse = "Stores - _TC" + mte.insert().submit() + + mce = frappe.get_doc(make_stock_entry(wo.name, "Material Consumption for Manufacture", 10)) + mce.insert().submit() + + me = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 10)) + me.insert().submit() + + valuation_rate = sum([item.valuation_rate * item.transfer_qty for item in mce.items]) / 10 + self.assertEqual(me.items[0].valuation_rate, valuation_rate) + + def test_capcity_planning_for_workstation(self): + frappe.db.set_single_value( + "Manufacturing Settings", + { + "disable_capacity_planning": 0, + "capacity_planning_for_days": 1, + "mins_between_operations": 10, + }, + ) + + properties = {"is_stock_item": 1, "valuation_rate": 100} + fg_item = make_item("Test FG Item For Capacity Planning", properties).name + + rm_item = make_item("Test RM Item For Capacity Planning", properties).name + + workstation = "Test Workstation For Capacity Planning" + if not frappe.db.exists("Workstation", workstation): + make_workstation(workstation=workstation, production_capacity=1) + + operation = "Test Operation For Capacity Planning" + if not frappe.db.exists("Operation", operation): + make_operation(operation=operation, workstation=workstation) + + bom_doc = make_bom( + item=fg_item, + source_warehouse="Stores - _TC", + raw_materials=[rm_item], + with_operations=1, + do_not_submit=True, + ) + + bom_doc.append( + "operations", + {"operation": operation, "time_in_mins": 1420, "hour_rate": 100, "workstation": workstation}, + ) + bom_doc.submit() + + # 1st Work Order, + # Capacity to run parallel the operation 'Test Operation For Capacity Planning' is 2 + wo_doc = make_wo_order_test_record( + production_item=fg_item, qty=1, planned_start_date="2024-02-25 00:00:00", do_not_submit=1 + ) + + wo_doc.submit() + job_cards = frappe.get_all( + "Job Card", + filters={"work_order": wo_doc.name}, + ) + + self.assertEqual(len(job_cards), 1) + + # 2nd Work Order, + wo_doc = make_wo_order_test_record( + production_item=fg_item, qty=1, planned_start_date="2024-02-25 00:00:00", do_not_submit=1 + ) + + wo_doc.submit() + job_cards = frappe.get_all( + "Job Card", + filters={"work_order": wo_doc.name}, + ) + + self.assertEqual(len(job_cards), 1) + + # 3rd Work Order, capacity is full + wo_doc = make_wo_order_test_record( + production_item=fg_item, qty=1, planned_start_date="2024-02-25 00:00:00", do_not_submit=1 + ) + + self.assertRaises(CapacityError, wo_doc.submit) + + frappe.db.set_single_value( + "Manufacturing Settings", {"disable_capacity_planning": 1, "mins_between_operations": 0} + ) + + +def make_operation(**kwargs): + kwargs = frappe._dict(kwargs) + + operation_doc = frappe.get_doc( + { + "doctype": "Operation", + "name": kwargs.operation, + "workstation": kwargs.workstation, + } + ) + operation_doc.insert() + + return operation_doc + + +def make_workstation(**kwargs): + kwargs = frappe._dict(kwargs) + + workstation_doc = frappe.get_doc( + { + "doctype": "Workstation", + "workstation_name": kwargs.workstation, + "workstation_type": kwargs.workstation_type, + "production_capacity": kwargs.production_capacity or 0, + "hour_rate": kwargs.hour_rate or 100, + } + ) + workstation_doc.insert() + + return workstation_doc + def prepare_boms_for_sub_assembly_test(): if not frappe.db.exists("BOM", {"item": "Test Final SF Item 1"}): diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 1996e19c37..63c74b61c4 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -447,7 +447,8 @@ "no_copy": 1, "options": "Production Plan", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "production_plan_item", @@ -592,7 +593,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2023-08-11 18:35:49.852069", + "modified": "2024-02-11 15:47:13.454422", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 39beb361de..5e22707150 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -242,8 +242,12 @@ class WorkOrder(Document): def calculate_operating_cost(self): self.planned_operating_cost, self.actual_operating_cost = 0.0, 0.0 for d in self.get("operations"): - d.planned_operating_cost = flt(d.hour_rate) * (flt(d.time_in_mins) / 60.0) - d.actual_operating_cost = flt(d.hour_rate) * (flt(d.actual_operation_time) / 60.0) + d.planned_operating_cost = flt( + flt(d.hour_rate) * (flt(d.time_in_mins) / 60.0), d.precision("planned_operating_cost") + ) + d.actual_operating_cost = flt( + flt(d.hour_rate) * (flt(d.actual_operation_time) / 60.0), d.precision("actual_operating_cost") + ) self.planned_operating_cost += flt(d.planned_operating_cost) self.actual_operating_cost += flt(d.actual_operating_cost) @@ -588,7 +592,6 @@ class WorkOrder(Document): def prepare_data_for_job_card(self, row, index, plan_days, enable_capacity_planning): self.set_operation_start_end_time(index, row) - original_start_time = row.planned_start_time job_card_doc = create_job_card( self, row, auto_create=True, enable_capacity_planning=enable_capacity_planning ) @@ -597,11 +600,15 @@ class WorkOrder(Document): row.planned_start_time = job_card_doc.scheduled_time_logs[-1].from_time row.planned_end_time = job_card_doc.scheduled_time_logs[-1].to_time - if date_diff(row.planned_start_time, original_start_time) > plan_days: + if date_diff(row.planned_end_time, self.planned_start_date) > plan_days: frappe.message_log.pop() frappe.throw( - _("Unable to find the time slot in the next {0} days for the operation {1}.").format( - plan_days, row.operation + _( + "Unable to find the time slot in the next {0} days for the operation {1}. Please increase the 'Capacity Planning For (Days)' in the {2}." + ).format( + plan_days, + row.operation, + get_link_to_form("Manufacturing Settings", "Manufacturing Settings"), ), CapacityError, ) diff --git a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json index f354d45381..0f4d693544 100644 --- a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json +++ b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json @@ -36,7 +36,8 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Item Code", - "options": "Item" + "options": "Item", + "search_index": 1 }, { "fieldname": "source_warehouse", @@ -141,7 +142,7 @@ ], "istable": 1, "links": [], - "modified": "2022-09-28 10:50:43.512562", + "modified": "2024-02-11 15:45:32.318374", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Item", diff --git a/erpnext/manufacturing/report/completed_work_orders/completed_work_orders.json b/erpnext/manufacturing/report/completed_work_orders/completed_work_orders.json index be50e93f1b..7925b8a8ab 100644 --- a/erpnext/manufacturing/report/completed_work_orders/completed_work_orders.json +++ b/erpnext/manufacturing/report/completed_work_orders/completed_work_orders.json @@ -1,25 +1,28 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2013-08-12 12:44:27", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2018-02-13 04:58:51.549413", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "Completed Work Orders", - "owner": "Administrator", - "query": "SELECT\n `tabWork Order`.name as \"Work Order:Link/Work Order:200\",\n `tabWork Order`.creation as \"Date:Date:120\",\n `tabWork Order`.production_item as \"Item:Link/Item:150\",\n `tabWork Order`.qty as \"To Produce:Int:100\",\n `tabWork Order`.produced_qty as \"Produced:Int:100\",\n `tabWork Order`.company as \"Company:Link/Company:\"\nFROM\n `tabWork Order`\nWHERE\n `tabWork Order`.docstatus=1\n AND ifnull(`tabWork Order`.produced_qty,0) = `tabWork Order`.qty", - "ref_doctype": "Work Order", - "report_name": "Completed Work Orders", - "report_type": "Query Report", + "add_total_row": 0, + "columns": [], + "creation": "2013-08-12 12:44:27", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 3, + "is_standard": "Yes", + "letterhead": null, + "modified": "2024-02-21 14:35:14.301848", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Completed Work Orders", + "owner": "Administrator", + "prepared_report": 0, + "query": "SELECT\n `tabWork Order`.name as \"Work Order:Link/Work Order:200\",\n `tabWork Order`.creation as \"Date:Date:120\",\n `tabWork Order`.production_item as \"Item:Link/Item:150\",\n `tabWork Order`.qty as \"To Produce:Int:100\",\n `tabWork Order`.produced_qty as \"Produced:Int:100\",\n `tabWork Order`.company as \"Company:Link/Company:\"\nFROM\n `tabWork Order`\nWHERE\n `tabWork Order`.docstatus=1\n AND ifnull(`tabWork Order`.produced_qty,0) >= `tabWork Order`.qty", + "ref_doctype": "Work Order", + "report_name": "Completed Work Orders", + "report_type": "Query Report", "roles": [ { "role": "Manufacturing User" - }, + }, { "role": "Stock User" } diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py index 97f30ef62e..8d3770805e 100644 --- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py +++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py @@ -58,7 +58,7 @@ def get_data(filters): query_filters["creation"] = ("between", [filters.get("from_date"), filters.get("to_date")]) data = frappe.get_all( - "Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc", debug=1 + "Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc" ) res = [] diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 63ba2f8f8b..815b01d752 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -263,6 +263,7 @@ execute:frappe.rename_doc("Report", "TDS Payable Monthly", "Tax Withholding Deta [post_model_sync] execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings') +erpnext.patches.v14_0.update_posting_datetime_and_dropped_indexes #22-02-2024 erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents erpnext.patches.v14_0.delete_shopify_doctypes erpnext.patches.v14_0.delete_healthcare_doctypes @@ -354,7 +355,9 @@ execute:frappe.db.set_default("date_format", frappe.db.get_single_value("System erpnext.patches.v14_0.update_total_asset_cost_field erpnext.patches.v15_0.create_advance_payment_status erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes +erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20 erpnext.patches.v14_0.set_maintain_stock_for_bom_item +erpnext.patches.v15_0.delete_orphaned_asset_movement_item_records \ No newline at end of file diff --git a/erpnext/patches/v14_0/create_accounting_dimensions_in_reconciliation_tool.py b/erpnext/patches/v14_0/create_accounting_dimensions_in_reconciliation_tool.py new file mode 100644 index 0000000000..4466eaace8 --- /dev/null +++ b/erpnext/patches/v14_0/create_accounting_dimensions_in_reconciliation_tool.py @@ -0,0 +1,8 @@ +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + create_accounting_dimensions_for_doctype, +) + + +def execute(): + create_accounting_dimensions_for_doctype(doctype="Payment Reconciliation") + create_accounting_dimensions_for_doctype(doctype="Payment Reconciliation Allocation") diff --git a/erpnext/patches/v14_0/update_posting_datetime_and_dropped_indexes.py b/erpnext/patches/v14_0/update_posting_datetime_and_dropped_indexes.py new file mode 100644 index 0000000000..ca126a40a4 --- /dev/null +++ b/erpnext/patches/v14_0/update_posting_datetime_and_dropped_indexes.py @@ -0,0 +1,19 @@ +import frappe + + +def execute(): + frappe.db.sql( + """ + UPDATE `tabStock Ledger Entry` + SET posting_datetime = DATE_FORMAT(timestamp(posting_date, posting_time), '%Y-%m-%d %H:%i:%s') + """ + ) + + drop_indexes() + + +def drop_indexes(): + if not frappe.db.has_index("tabStock Ledger Entry", "posting_sort_index"): + return + + frappe.db.sql_ddl("ALTER TABLE `tabStock Ledger Entry` DROP INDEX `posting_sort_index`") diff --git a/erpnext/patches/v15_0/delete_orphaned_asset_movement_item_records.py b/erpnext/patches/v15_0/delete_orphaned_asset_movement_item_records.py new file mode 100644 index 0000000000..a1d7dc9b3d --- /dev/null +++ b/erpnext/patches/v15_0/delete_orphaned_asset_movement_item_records.py @@ -0,0 +1,11 @@ +import frappe + + +def execute(): + # nosemgrep + frappe.db.sql( + """ + DELETE FROM `tabAsset Movement Item` + WHERE parent NOT IN (SELECT name FROM `tabAsset Movement`) + """ + ) diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index b9d801ce90..e26d04aa9a 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -83,7 +83,7 @@ class Timesheet(Document): def set_status(self): self.status = {"0": "Draft", "1": "Submitted", "2": "Cancelled"}[str(self.docstatus or 0)] - if self.per_billed == 100: + if flt(self.per_billed, self.precision("per_billed")) >= 100.0: self.status = "Billed" if self.sales_invoice: diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 77ecf75e0c..1d0d47ec3d 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -368,7 +368,8 @@ erpnext.buying = { let update_values = { "serial_and_batch_bundle": r.name, - "qty": qty + "use_serial_batch_fields": 0, + "qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item)) } if (r.warehouse) { @@ -408,7 +409,8 @@ erpnext.buying = { let update_values = { "serial_and_batch_bundle": r.name, - "rejected_qty": qty + "use_serial_batch_fields": 0, + "rejected_qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item)) } if (r.warehouse) { diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index ba53cf86f4..775bdb476a 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -106,7 +106,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe frappe.ui.form.on(this.frm.doctype + " Item", { items_add: function(frm, cdt, cdn) { - debugger var item = frappe.get_doc(cdt, cdn); if (!item.warehouse && frm.doc.set_warehouse) { item.warehouse = frm.doc.set_warehouse; @@ -145,6 +144,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe }); } + if(this.frm.fields_dict['items'].grid.get_field('batch_no')) { + this.frm.set_query('batch_no', 'items', function(doc, cdt, cdn) { + return me.set_query_for_batch(doc, cdt, cdn); + }); + } + if( this.frm.docstatus < 2 && this.frm.fields_dict["payment_terms_template"] @@ -160,7 +165,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } if(this.frm.fields_dict["items"]) { - this["items_remove"] = this.calculate_net_weight; + this["items_remove"] = this.process_item_removal; } if(this.frm.fields_dict["recurring_print_format"]) { @@ -234,7 +239,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } set_fields_onload_for_line_item() { - if (this.frm.is_new && this.frm.doc?.items) { + if (this.frm.is_new() && this.frm.doc?.items) { this.frm.doc.items.forEach(item => { if (item.docstatus === 0 && frappe.meta.has_field(item.doctype, "use_serial_batch_fields") @@ -936,25 +941,35 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe due_date() { // due_date is to be changed, payment terms template and/or payment schedule must // be removed as due_date is automatically changed based on payment terms - if (this.frm.doc.due_date && !this.frm.updating_party_details && !this.frm.doc.is_pos) { - if (this.frm.doc.payment_terms_template || - (this.frm.doc.payment_schedule && this.frm.doc.payment_schedule.length)) { - var message1 = ""; - var message2 = ""; - var final_message = __("Please clear the") + " "; - - if (this.frm.doc.payment_terms_template) { - message1 = __("selected Payment Terms Template"); - final_message = final_message + message1; - } - - if ((this.frm.doc.payment_schedule || []).length) { - message2 = __("Payment Schedule Table"); - if (message1.length !== 0) message2 = " and " + message2; - final_message = final_message + message2; - } - frappe.msgprint(final_message); + if ( + this.frm.doc.due_date && + !this.frm.updating_party_details && + !this.frm.doc.is_pos && + ( + this.frm.doc.payment_terms_template || + this.frm.doc.payment_schedule?.length + ) + ) { + const to_clear = []; + if (this.frm.doc.payment_terms_template) { + to_clear.push("Payment Terms Template"); } + + if (this.frm.doc.payment_schedule?.length) { + to_clear.push("Payment Schedule Table"); + } + + frappe.confirm( + __( + "Do you want to clear the selected {0}?", + [frappe.utils.comma_and(to_clear.map(dt => __(dt)))] + ), + () => { + this.frm.set_value("payment_terms_template", ""); + this.frm.clear_table("payment_schedule"); + this.frm.refresh_field("payment_schedule"); + } + ); } } @@ -1282,6 +1297,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } } + process_item_removal() { + this.frm.trigger("calculate_taxes_and_totals"); + this.frm.trigger("calculate_net_weight"); + } + calculate_net_weight(){ /* Calculate Total Net Weight then further applied shipping rule to calculate shipping charges.*/ var me = this; @@ -1503,31 +1523,33 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } remove_pricing_rule_for_item(item) { - let me = this; - return this.frm.call({ - method: "erpnext.accounts.doctype.pricing_rule.pricing_rule.remove_pricing_rule_for_item", - args: { - pricing_rules: item.pricing_rules, - item_details: { - "doctype": item.doctype, - "name": item.name, - "item_code": item.item_code, - "pricing_rules": item.pricing_rules, - "parenttype": item.parenttype, - "parent": item.parent, - "price_list_rate": item.price_list_rate + if (item.pricing_rules){ + let me = this; + return this.frm.call({ + method: "erpnext.accounts.doctype.pricing_rule.pricing_rule.remove_pricing_rule_for_item", + args: { + pricing_rules: item.pricing_rules, + item_details: { + "doctype": item.doctype, + "name": item.name, + "item_code": item.item_code, + "pricing_rules": item.pricing_rules, + "parenttype": item.parenttype, + "parent": item.parent, + "price_list_rate": item.price_list_rate + }, + item_code: item.item_code, + rate: item.price_list_rate, }, - item_code: item.item_code, - rate: item.price_list_rate, - }, - callback: function(r) { - if (!r.exc && r.message) { - me.remove_pricing_rule(r.message); - me.calculate_taxes_and_totals(); - if(me.frm.doc.apply_discount_on) me.frm.trigger("apply_discount_on"); + callback: function(r) { + if (!r.exc && r.message) { + me.remove_pricing_rule(r.message); + me.calculate_taxes_and_totals(); + if(me.frm.doc.apply_discount_on) me.frm.trigger("apply_discount_on"); + } } - } - }); + }); + } } apply_pricing_rule(item, calculate_taxes_and_totals) { @@ -1633,18 +1655,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe return item_list; } - items_delete() { - this.update_localstorage_scanned_data(); - } - - update_localstorage_scanned_data() { - let doctypes = ["Sales Invoice", "Purchase Invoice", "Delivery Note", "Purchase Receipt"]; - if (this.frm.is_new() && doctypes.includes(this.frm.doc.doctype)) { - const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm}); - barcode_scanner.update_localstorage_scanned_data(); - } - } - _set_values_for_item_list(children) { const items_rule_dict = {}; diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index de46271e47..aee761f5d2 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -40,7 +40,10 @@ $.extend(erpnext, { is_perpetual_inventory_enabled: function(company) { if(company) { - return frappe.get_doc(":Company", company).enable_perpetual_inventory + let company_local = locals[":Company"] && locals[":Company"][company]; + if(company_local) { + return cint(company_local.enable_perpetual_inventory); + } } }, @@ -676,6 +679,7 @@ erpnext.utils.update_child_items = function(opts) { fieldname: frm.doc.doctype == 'Sales Order' ? "delivery_date" : "schedule_date", in_list_view: 1, label: frm.doc.doctype == 'Sales Order' ? __("Delivery Date") : __("Reqd by date"), + default: frm.doc.doctype == 'Sales Order' ? frm.doc.delivery_date : frm.doc.schedule_date, reqd: 1 }) fields.splice(3, 0, { diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js index b8ec77f8e5..a957530ec8 100644 --- a/erpnext/public/js/utils/sales_common.js +++ b/erpnext/public/js/utils/sales_common.js @@ -339,7 +339,8 @@ erpnext.sales_common = { frappe.model.set_value(item.doctype, item.name, { "serial_and_batch_bundle": r.name, - "qty": qty + "use_serial_batch_fields": 0, + "qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item)) }); } } diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 80ade7086c..fccaf88c71 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -71,6 +71,10 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { let warehouse = this.item?.type_of_transaction === "Outward" ? (this.item.warehouse || this.item.s_warehouse) : ""; + if (this.frm.doc.doctype === 'Stock Entry') { + warehouse = this.item.s_warehouse || this.item.t_warehouse; + } + if (!warehouse && this.frm.doc.doctype === 'Stock Reconciliation') { warehouse = this.get_warehouse(); } @@ -367,19 +371,11 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { label: __('Batch No'), in_list_view: 1, get_query: () => { - if (this.item.type_of_transaction !== "Outward") { - return { - filters: { - 'item': this.item.item_code, - } - } - } else { - return { - query : "erpnext.controllers.queries.get_batch_no", - filters: { - 'item_code': this.item.item_code, - 'warehouse': this.get_warehouse() - } + return { + query : "erpnext.controllers.queries.get_batch_no", + filters: { + 'item_code': this.item.item_code, + 'warehouse': this.item.s_warehouse || this.item.t_warehouse, } } }, diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 47153a8e0c..a8ebccd717 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -297,11 +297,35 @@ class TestCustomer(FrappeTestCase): if credit_limit > outstanding_amt: set_credit_limit("_Test Customer", "_Test Company", credit_limit) - # Makes Sales invoice from Sales Order - so.save(ignore_permissions=True) - si = make_sales_invoice(so.name) - si.save(ignore_permissions=True) - self.assertRaises(frappe.ValidationError, make_sales_order) + def test_customer_credit_limit_after_submit(self): + from erpnext.controllers.accounts_controller import update_child_qty_rate + from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order + + outstanding_amt = self.get_customer_outstanding_amount() + credit_limit = get_credit_limit("_Test Customer", "_Test Company") + + if outstanding_amt <= 0.0: + item_qty = int((abs(outstanding_amt) + 200) / 100) + make_sales_order(qty=item_qty) + + if credit_limit <= 0.0: + set_credit_limit("_Test Customer", "_Test Company", outstanding_amt + 100) + + so = make_sales_order(rate=100, qty=1) + # Update qty in submitted Sales Order to trigger Credit Limit validation + fields = ["name", "item_code", "delivery_date", "conversion_factor", "qty", "rate", "uom", "idx"] + modified_item = frappe._dict() + for x in fields: + modified_item[x] = so.items[0].get(x) + modified_item["docname"] = so.items[0].name + modified_item["qty"] = 2 + self.assertRaises( + frappe.ValidationError, + update_child_qty_rate, + so.doctype, + frappe.json.dumps([modified_item]), + so.name, + ) def test_customer_credit_limit_on_change(self): outstanding_amt = self.get_customer_outstanding_amount() diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 9661bac8ad..ac392e7323 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -515,6 +515,9 @@ class SalesOrder(SellingController): def on_update(self): pass + def on_update_after_submit(self): + self.check_credit_limit() + def before_update_after_submit(self): self.validate_po() self.validate_drop_ship() diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py index 7d28f2b90d..f2f1e4cfba 100644 --- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py +++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py @@ -206,42 +206,36 @@ def prepare_data( def get_actual_data(filters, sales_users_or_territory_data, date_field, sales_field): fiscal_year = get_fiscal_year(fiscal_year=filters.get("fiscal_year"), as_dict=1) - dates = [fiscal_year.year_start_date, fiscal_year.year_end_date] - select_field = "`tab{0}`.{1}".format(filters.get("doctype"), sales_field) - child_table = "`tab{0}`".format(filters.get("doctype") + " Item") + parent_doc = frappe.qb.DocType(filters.get("doctype")) + child_doc = frappe.qb.DocType(filters.get("doctype") + " Item") + sales_team = frappe.qb.DocType("Sales Team") + + query = ( + frappe.qb.from_(parent_doc) + .inner_join(child_doc) + .on(child_doc.parent == parent_doc.name) + .inner_join(sales_team) + .on(sales_team.parent == parent_doc.name) + .select( + child_doc.item_group, + (child_doc.stock_qty * sales_team.allocated_percentage / 100).as_("stock_qty"), + (child_doc.base_net_amount * sales_team.allocated_percentage / 100).as_("base_net_amount"), + sales_team.sales_person, + parent_doc[date_field], + ) + .where( + (parent_doc.docstatus == 1) + & (parent_doc[date_field].between(fiscal_year.year_start_date, fiscal_year.year_end_date)) + ) + ) if sales_field == "sales_person": - select_field = "`tabSales Team`.sales_person" - child_table = "`tab{0}`, `tabSales Team`".format(filters.get("doctype") + " Item") - cond = """`tabSales Team`.parent = `tab{0}`.name and - `tabSales Team`.sales_person in ({1}) """.format( - filters.get("doctype"), ",".join(["%s"] * len(sales_users_or_territory_data)) - ) + query = query.where(sales_team.sales_person.isin(sales_users_or_territory_data)) else: - cond = "`tab{0}`.{1} in ({2})".format( - filters.get("doctype"), sales_field, ",".join(["%s"] * len(sales_users_or_territory_data)) - ) + query = query.where(parent_doc[sales_field].isin(sales_users_or_territory_data)) - return frappe.db.sql( - """ SELECT `tab{child_doc}`.item_group, - `tab{child_doc}`.stock_qty, `tab{child_doc}`.base_net_amount, - {select_field}, `tab{parent_doc}`.{date_field} - FROM `tab{parent_doc}`, {child_table} - WHERE - `tab{child_doc}`.parent = `tab{parent_doc}`.name - and `tab{parent_doc}`.docstatus = 1 and {cond} - and `tab{parent_doc}`.{date_field} between %s and %s""".format( - cond=cond, - date_field=date_field, - select_field=select_field, - child_table=child_table, - parent_doc=filters.get("doctype"), - child_doc=filters.get("doctype") + " Item", - ), - tuple(sales_users_or_territory_data + dates), - as_dict=1, - ) + return query.run(as_dict=True) def get_parents_data(filters, partner_doctype): diff --git a/erpnext/selling/report/sales_person_target_variance_based_on_item_group/test_sales_person_target_variance_based_on_item_group.py b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/test_sales_person_target_variance_based_on_item_group.py new file mode 100644 index 0000000000..4ae5d2bee8 --- /dev/null +++ b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/test_sales_person_target_variance_based_on_item_group.py @@ -0,0 +1,84 @@ +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import flt, nowdate + +from erpnext.accounts.utils import get_fiscal_year +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order +from erpnext.selling.report.sales_person_target_variance_based_on_item_group.sales_person_target_variance_based_on_item_group import ( + execute, +) + + +class TestSalesPersonTargetVarianceBasedOnItemGroup(FrappeTestCase): + def setUp(self): + self.fiscal_year = get_fiscal_year(nowdate())[0] + + def tearDown(self): + frappe.db.rollback() + + def test_achieved_target_and_variance(self): + # Create a Target Distribution + distribution = frappe.new_doc("Monthly Distribution") + distribution.distribution_id = "Target Report Distribution" + distribution.fiscal_year = self.fiscal_year + distribution.get_months() + distribution.insert() + + # Create sales people with targets + person_1 = create_sales_person_with_target("Sales Person 1", self.fiscal_year, distribution.name) + person_2 = create_sales_person_with_target("Sales Person 2", self.fiscal_year, distribution.name) + + # Create a Sales Order with 50-50 contribution + so = make_sales_order( + rate=1000, + qty=20, + do_not_submit=True, + ) + so.set( + "sales_team", + [ + { + "sales_person": person_1.name, + "allocated_percentage": 50, + "allocated_amount": 10000, + }, + { + "sales_person": person_2.name, + "allocated_percentage": 50, + "allocated_amount": 10000, + }, + ], + ) + so.submit() + + # Check Achieved Target and Variance + result = execute( + frappe._dict( + { + "fiscal_year": self.fiscal_year, + "doctype": "Sales Order", + "period": "Yearly", + "target_on": "Quantity", + } + ) + )[1] + row = frappe._dict(result[0]) + self.assertSequenceEqual( + [flt(value, 2) for value in (row.total_target, row.total_achieved, row.total_variance)], + [50, 10, -40], + ) + + +def create_sales_person_with_target(sales_person_name, fiscal_year, distribution_id): + sales_person = frappe.new_doc("Sales Person") + sales_person.sales_person_name = sales_person_name + sales_person.append( + "targets", + { + "fiscal_year": fiscal_year, + "target_qty": 50, + "target_amount": 30000, + "distribution_id": distribution_id, + }, + ) + return sales_person.insert() diff --git a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py index 9f3ba0da8b..847488f6fb 100644 --- a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py +++ b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py @@ -36,6 +36,7 @@ def execute(filters=None): d.base_net_amount, d.sales_person, d.allocated_percentage, + (d.stock_qty * d.allocated_percentage / 100), d.contribution_amt, company_currency, ] @@ -103,7 +104,7 @@ def get_columns(filters): "fieldtype": "Link", "width": 140, }, - {"label": _("Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 140}, + {"label": _("SO Total Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 140}, { "label": _("Amount"), "options": "currency", @@ -119,6 +120,12 @@ def get_columns(filters): "width": 140, }, {"label": _("Contribution %"), "fieldname": "contribution", "fieldtype": "Float", "width": 140}, + { + "label": _("Contribution Qty"), + "fieldname": "contribution_qty", + "fieldtype": "Float", + "width": 140, + }, { "label": _("Contribution Amount"), "options": "currency", diff --git a/erpnext/setup/demo.py b/erpnext/setup/demo.py index df2c49b2b6..688d45a5a7 100644 --- a/erpnext/setup/demo.py +++ b/erpnext/setup/demo.py @@ -95,7 +95,17 @@ def create_demo_record(doctype): def make_transactions(company): frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1) - start_date = get_fiscal_year(date=getdate())[1] + from erpnext.accounts.utils import FiscalYearError + + try: + start_date = get_fiscal_year(date=getdate())[1] + except FiscalYearError: + # User might have setup fiscal year for previous or upcoming years + active_fiscal_years = frappe.db.get_all("Fiscal Year", filters={"disabled": 0}, as_list=1) + if active_fiscal_years: + start_date = frappe.db.get_value("Fiscal Year", active_fiscal_years[0][0], "year_start_date") + else: + frappe.throw(_("There are no active Fiscal Years for which Demo Data can be generated.")) for doctype in frappe.get_hooks("demo_transaction_doctypes"): data = read_data_file_using_hooks(doctype) @@ -159,6 +169,7 @@ def convert_order_to_invoices(): if i % 2 != 0: payment = get_payment_entry(invoice.doctype, invoice.name) + payment.posting_date = order.transaction_date payment.reference_no = invoice.name payment.submit() diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 4eacbc1541..a3903a39a9 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -399,7 +399,12 @@ class DeliveryNote(SellingController): elif self.issue_credit_note: self.make_return_invoice() - self.make_bundle_using_old_serial_batch_fields() + for table_name in ["items", "packed_items"]: + if not self.get(table_name): + continue + + self.make_bundle_using_old_serial_batch_fields(table_name) + # Updating stock ledger should always be called after updating prevdoc status, # because updating reserved qty in bin depends upon updated delivered qty in SO self.update_stock_ledger() diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 459e7e7c4f..293ef9f085 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1078,6 +1078,8 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(si2.items[1].qty, 1) def test_delivery_note_bundle_with_batched_item(self): + frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0) + batched_bundle = make_item("_Test Batched bundle", {"is_stock_item": 0}) batched_item = make_item( "_Test Batched Item", @@ -1099,6 +1101,8 @@ class TestDeliveryNote(FrappeTestCase): batch_no = get_batch_from_bundle(dn.packed_items[0].serial_and_batch_bundle) self.assertTrue(batch_no) + frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1) + def test_payment_terms_are_fetched_when_creating_sales_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( create_payment_terms_template, @@ -1551,6 +1555,53 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(so.items[0].rate, rate) self.assertEqual(dn.items[0].rate, so.items[0].rate) + def test_use_serial_batch_fields_for_packed_items(self): + bundle_item = make_item("Test _Packed Product Bundle Item ", {"is_stock_item": 0}) + serial_item = make_item( + "Test _Packed Serial Item ", + {"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SN-TESTSERIAL-.#####"}, + ) + batch_item = make_item( + "Test _Packed Batch Item ", + { + "is_stock_item": 1, + "has_batch_no": 1, + "batch_no_series": "BATCH-TESTSERIAL-.#####", + "create_new_batch": 1, + }, + ) + make_product_bundle(parent=bundle_item.name, items=[serial_item.name, batch_item.name]) + + item_details = {} + for item in [serial_item, batch_item]: + se = make_stock_entry( + item_code=item.name, target="_Test Warehouse - _TC", qty=5, basic_rate=100 + ) + item_details[item.name] = se.items[0].serial_and_batch_bundle + + dn = create_delivery_note(item_code=bundle_item.name, qty=1, do_not_submit=True) + serial_no = "" + for row in dn.packed_items: + row.use_serial_batch_fields = 1 + + if row.item_code == serial_item.name: + serial_and_batch_bundle = item_details[serial_item.name] + row.serial_no = get_serial_nos_from_bundle(serial_and_batch_bundle)[3] + serial_no = row.serial_no + else: + serial_and_batch_bundle = item_details[batch_item.name] + row.batch_no = get_batch_from_bundle(serial_and_batch_bundle) + + dn.submit() + dn.load_from_db() + + for row in dn.packed_items: + self.assertTrue(row.serial_no or row.batch_no) + self.assertTrue(row.serial_and_batch_bundle) + + if row.serial_no: + self.assertEqual(row.serial_no, serial_no) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index feb4583436..949c1096a7 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -1122,6 +1122,7 @@ def validate_cancelled_item(item_code, docstatus=None): frappe.throw(_("Item {0} is cancelled").format(item_code)) +@frappe.request_cache def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): """returns last purchase details in stock uom""" # get last purchase order item details diff --git a/erpnext/stock/doctype/landed_cost_purchase_receipt/landed_cost_purchase_receipt.json b/erpnext/stock/doctype/landed_cost_purchase_receipt/landed_cost_purchase_receipt.json index 9b2b5da9cb..736eb9d4c2 100644 --- a/erpnext/stock/doctype/landed_cost_purchase_receipt/landed_cost_purchase_receipt.json +++ b/erpnext/stock/doctype/landed_cost_purchase_receipt/landed_cost_purchase_receipt.json @@ -1,187 +1,77 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2013-02-22 01:28:02", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, + "actions": [], + "creation": "2013-02-22 01:28:02", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "receipt_document_type", + "receipt_document", + "supplier", + "col_break1", + "posting_date", + "grand_total" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "receipt_document_type", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Receipt Document Type", - "length": 0, - "no_copy": 0, - "options": "\nPurchase Invoice\nPurchase Receipt", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "receipt_document_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Receipt Document Type", + "options": "\nPurchase Invoice\nPurchase Receipt", + "reqd": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "receipt_document", - "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Receipt Document", - "length": 0, - "no_copy": 0, - "oldfieldname": "purchase_receipt_no", - "oldfieldtype": "Link", - "options": "receipt_document_type", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "220px", - "read_only": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "receipt_document", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Receipt Document", + "oldfieldname": "purchase_receipt_no", + "oldfieldtype": "Link", + "options": "receipt_document_type", + "print_width": "220px", + "reqd": 1, "width": "220px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "supplier", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Supplier", - "length": 0, - "no_copy": 0, - "options": "Supplier", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "supplier", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Supplier", + "options": "Supplier", + "read_only": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "col_break1", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "col_break1", + "fieldtype": "Column Break", "width": "50%" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "posting_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Posting Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date", + "read_only": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "grand_total", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Grand Total", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "grand_total", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Grand Total", + "options": "Company:company:default_currency", + "read_only": 1 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2020-09-18 17:26:09.703215", - "modified_by": "Administrator", - "module": "Stock", - "name": "Landed Cost Purchase Receipt", - "owner": "Administrator", - "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "sort_order": "ASC", - "track_seen": 0 + ], + "idx": 1, + "istable": 1, + "links": [], + "modified": "2024-02-26 18:41:06.281750", + "modified_by": "Administrator", + "module": "Stock", + "name": "Landed Cost Purchase Receipt", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "ASC", + "states": [] } \ No newline at end of file diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index dec75066ec..baff54059d 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -65,6 +65,7 @@ class LandedCostVoucher(Document): def validate(self): self.check_mandatory() self.validate_receipt_documents() + self.validate_line_items() init_landed_taxes_and_totals(self) self.set_total_taxes_and_charges() if not self.get("items"): @@ -72,6 +73,26 @@ class LandedCostVoucher(Document): self.set_applicable_charges_on_item() + def validate_line_items(self): + for d in self.get("items"): + if ( + d.docstatus == 0 + and d.purchase_receipt_item + and not frappe.db.exists( + d.receipt_document_type + " Item", + {"name": d.purchase_receipt_item, "parent": d.receipt_document}, + ) + ): + frappe.throw( + _("Row {0}: {2} Item {1} does not exist in {2} {3}").format( + d.idx, + frappe.bold(d.purchase_receipt_item), + d.receipt_document_type, + frappe.bold(d.receipt_document), + ), + title=_("Incorrect Reference Document (Purchase Receipt Item)"), + ) + def check_mandatory(self): if not self.get("purchase_receipts"): frappe.throw(_("Please enter Receipt Document")) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index e80218a017..a913e2845a 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -199,6 +199,7 @@ frappe.ui.form.on('Material Request', { get_item_data: function(frm, item, overwrite_warehouse=false) { if (item && !item.item_code) { return; } + frappe.call({ method: "erpnext.stock.get_item_details.get_item_details", args: { @@ -225,12 +226,22 @@ frappe.ui.form.on('Material Request', { }, callback: function(r) { const d = item; - const qty_fields = ['actual_qty', 'projected_qty', 'min_order_qty']; + const allow_to_change_fields = ['actual_qty', 'projected_qty', 'min_order_qty', 'item_name', 'description', 'stock_uom', 'uom', 'conversion_factor', 'stock_qty']; if(!r.exc) { - $.each(r.message, function(k, v) { - if(!d[k] || in_list(qty_fields, k)) d[k] = v; + $.each(r.message, function(key, value) { + if(!d[key] || allow_to_change_fields.includes(key)) { + d[key] = value; + } }); + + if (d.price_list_rate != r.message.price_list_rate) { + d.rate = 0.0; + d.price_list_rate = r.message.price_list_rate; + frappe.model.set_value(d.doctype, d.name, "rate", d.price_list_rate); + } + + refresh_field("items"); } } }); @@ -242,7 +253,7 @@ frappe.ui.form.on('Material Request', { fields: [ {"fieldname":"bom", "fieldtype":"Link", "label":__("BOM"), options:"BOM", reqd: 1, get_query: function() { - return {filters: { docstatus:1 }}; + return {filters: { docstatus:1, "is_active": 1 }}; }}, {"fieldname":"warehouse", "fieldtype":"Link", "label":__("For Warehouse"), options:"Warehouse", reqd: 1}, @@ -427,12 +438,11 @@ frappe.ui.form.on("Material Request Item", { frm.events.get_item_data(frm, item, false); }, - rate: function(frm, doctype, name) { + rate(frm, doctype, name) { const item = locals[doctype][name]; item.amount = flt(item.qty) * flt(item.rate); frappe.model.set_value(doctype, name, "amount", item.amount); refresh_field("amount", item.name, item.parentfield); - frm.events.get_item_data(frm, item, false); }, item_code: function(frm, doctype, name) { @@ -452,7 +462,12 @@ frappe.ui.form.on("Material Request Item", { set_schedule_date(frm); } } - } + }, + + conversion_factor: function(frm, doctype, name) { + const item = locals[doctype][name]; + frm.events.get_item_data(frm, item, false); + }, }); erpnext.buying.MaterialRequestController = class MaterialRequestController extends erpnext.buying.BuyingController { diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.json b/erpnext/stock/doctype/material_request_item/material_request_item.json index 5dc07c99f6..c705d59bee 100644 --- a/erpnext/stock/doctype/material_request_item/material_request_item.json +++ b/erpnext/stock/doctype/material_request_item/material_request_item.json @@ -35,6 +35,7 @@ "received_qty", "rate_and_amount_section_break", "rate", + "price_list_rate", "col_break3", "amount", "accounting_details_section", @@ -197,12 +198,14 @@ "fieldname": "rate", "fieldtype": "Currency", "label": "Rate", + "options": "Company:company:default_currency", "print_hide": 1 }, { "fieldname": "amount", "fieldtype": "Currency", "label": "Amount", + "options": "Company:company:default_currency", "print_hide": 1, "read_only": 1 }, @@ -473,13 +476,22 @@ "fieldtype": "Link", "label": "WIP Composite Asset", "options": "Asset" + }, + { + "fieldname": "price_list_rate", + "fieldtype": "Currency", + "hidden": 1, + "label": "Price List Rate", + "options": "currency", + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:37:59.599115", + "modified": "2024-02-26 18:30:03.684872", "modified_by": "Administrator", "module": "Stock", "name": "Material Request Item", diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.py b/erpnext/stock/doctype/material_request_item/material_request_item.py index 2bed596292..d23d041f5f 100644 --- a/erpnext/stock/doctype/material_request_item/material_request_item.py +++ b/erpnext/stock/doctype/material_request_item/material_request_item.py @@ -41,6 +41,7 @@ class MaterialRequestItem(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data + price_list_rate: DF.Currency production_plan: DF.Link | None project: DF.Link | None projected_qty: DF.Float diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index c115e33e17..c5fed0dcd8 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -227,6 +227,9 @@ def update_packed_item_stock_data(main_item_row, pi_row, packing_item, item_data bin = get_packed_item_bin_qty(packing_item.item_code, pi_row.warehouse) pi_row.actual_qty = flt(bin.get("actual_qty")) pi_row.projected_qty = flt(bin.get("projected_qty")) + pi_row.use_serial_batch_fields = frappe.db.get_single_value( + "Stock Settings", "use_serial_batch_fields" + ) def update_packed_item_price_data(pi_row, item_data, doc): diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index aa0e125496..3a5daa1bb7 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -77,6 +77,9 @@ frappe.ui.form.on('Pick List', { }, freeze: 1, freeze_message: __("Setting Item Locations..."), + callback(r) { + refresh_field("locations"); + } }); } }, @@ -327,7 +330,8 @@ frappe.ui.form.on('Pick List Item', { let qty = Math.abs(r.total_qty); frappe.model.set_value(item.doctype, item.name, { "serial_and_batch_bundle": r.name, - "qty": qty + "use_serial_batch_fields": 0, + "qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item)) }); } } diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 98ed569af1..8a1f79d4a2 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -73,6 +73,10 @@ class PickList(Document): self.update_status() self.set_item_locations() + if self.get("locations"): + self.validate_sales_order_percentage() + + def validate_sales_order_percentage(self): # set percentage picked in SO for location in self.get("locations"): if ( @@ -348,9 +352,9 @@ class PickList(Document): picked_items_details = self.get_picked_items_details(items) self.item_location_map = frappe._dict() - from_warehouses = None + from_warehouses = [self.parent_warehouse] if self.parent_warehouse else [] if self.parent_warehouse: - from_warehouses = get_descendants_of("Warehouse", self.parent_warehouse) + from_warehouses.extend(get_descendants_of("Warehouse", self.parent_warehouse)) # Create replica before resetting, to handle empty table on update after submit. locations_replica = self.get("locations") diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index c9fe7d2751..1e2d8eb1b1 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -686,9 +686,7 @@ class PurchaseReceipt(BuyingController): ) stock_value_diff = ( - flt(d.base_net_amount) - + flt(d.item_tax_amount / self.conversion_rate) - + flt(d.landed_cost_voucher_amount) + flt(d.base_net_amount) + flt(d.item_tax_amount) + flt(d.landed_cost_voucher_amount) ) elif warehouse_account.get(d.warehouse): stock_value_diff = get_stock_value_difference(self.name, d.name, d.warehouse) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 2d209220de..d542601c3d 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2237,6 +2237,10 @@ class TestPurchaseReceipt(FrappeTestCase): create_stock_reconciliation, ) + frappe.db.set_single_value( + "Stock Settings", "do_not_update_serial_batch_on_creation_of_auto_bundle", 0 + ) + item_code = make_item( "_Test Use Serial Fields Item Serial Item", properties={"has_serial_no": 1, "serial_no_series": "SNU-TSFISI-.#####"}, @@ -2259,7 +2263,7 @@ class TestPurchaseReceipt(FrappeTestCase): ) self.assertEqual(pr.items[0].use_serial_batch_fields, 1) - self.assertFalse(pr.items[0].serial_no) + self.assertTrue(pr.items[0].serial_no) self.assertTrue(pr.items[0].serial_and_batch_bundle) sbb_doc = frappe.get_doc("Serial and Batch Bundle", pr.items[0].serial_and_batch_bundle) @@ -2317,6 +2321,127 @@ class TestPurchaseReceipt(FrappeTestCase): serial_no_status = frappe.db.get_value("Serial No", sn, "status") self.assertTrue(serial_no_status != "Active") + frappe.db.set_single_value( + "Stock Settings", "do_not_update_serial_batch_on_creation_of_auto_bundle", 1 + ) + + def test_sle_qty_after_transaction(self): + item = make_item( + "_Test Item Qty After Transaction", + properties={"is_stock_item": 1, "valuation_method": "FIFO"}, + ).name + + posting_date = today() + posting_time = nowtime() + + # Step 1: Create Purchase Receipt + pr = make_purchase_receipt( + item_code=item, + qty=1, + rate=100, + posting_date=posting_date, + posting_time=posting_time, + do_not_save=1, + ) + + for i in range(9): + pr.append( + "items", + { + "item_code": item, + "qty": 1, + "rate": 100, + "warehouse": pr.items[0].warehouse, + "cost_center": pr.items[0].cost_center, + "expense_account": pr.items[0].expense_account, + "uom": pr.items[0].uom, + "stock_uom": pr.items[0].stock_uom, + "conversion_factor": pr.items[0].conversion_factor, + }, + ) + + self.assertEqual(len(pr.items), 10) + pr.save() + pr.submit() + + data = frappe.get_all( + "Stock Ledger Entry", + fields=["qty_after_transaction", "creation", "posting_datetime"], + filters={"voucher_no": pr.name, "is_cancelled": 0}, + order_by="creation", + ) + + for index, d in enumerate(data): + self.assertEqual(d.qty_after_transaction, 1 + index) + + # Step 2: Create Purchase Receipt + pr = make_purchase_receipt( + item_code=item, + qty=1, + rate=100, + posting_date=posting_date, + posting_time=posting_time, + do_not_save=1, + ) + + for i in range(9): + pr.append( + "items", + { + "item_code": item, + "qty": 1, + "rate": 100, + "warehouse": pr.items[0].warehouse, + "cost_center": pr.items[0].cost_center, + "expense_account": pr.items[0].expense_account, + "uom": pr.items[0].uom, + "stock_uom": pr.items[0].stock_uom, + "conversion_factor": pr.items[0].conversion_factor, + }, + ) + + self.assertEqual(len(pr.items), 10) + pr.save() + pr.submit() + + data = frappe.get_all( + "Stock Ledger Entry", + fields=["qty_after_transaction", "creation", "posting_datetime"], + filters={"voucher_no": pr.name, "is_cancelled": 0}, + order_by="creation", + ) + + for index, d in enumerate(data): + self.assertEqual(d.qty_after_transaction, 11 + index) + + def test_auto_set_batch_based_on_bundle(self): + item_code = make_item( + "_Test Auto Set Batch Based on Bundle", + properties={ + "has_batch_no": 1, + "batch_number_series": "BATCH-BNU-TASBBB-.#####", + "create_new_batch": 1, + }, + ).name + + frappe.db.set_single_value( + "Stock Settings", "do_not_update_serial_batch_on_creation_of_auto_bundle", 0 + ) + + pr = make_purchase_receipt( + item_code=item_code, + qty=5, + rate=100, + ) + + self.assertTrue(pr.items[0].batch_no) + batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle) + self.assertEqual(pr.items[0].batch_no, batch_no) + + frappe.db.set_single_value( + "Stock Settings", "do_not_update_serial_batch_on_creation_of_auto_bundle", 1 + ) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 31fc2cab6a..a383798af5 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -294,9 +294,20 @@ def repost(doc): doc.log_error("Unable to repost item valuation") message = frappe.message_log.pop() if frappe.message_log else "" + if isinstance(message, dict): + message = message.get("message") + if traceback: - message += "
" + "Traceback:
" + traceback - frappe.db.set_value(doc.doctype, doc.name, "error_log", message) + message += "

" + "Traceback:
" + traceback + + frappe.db.set_value( + doc.doctype, + doc.name, + { + "error_log": message, + "status": "Failed", + }, + ) outgoing_email_account = frappe.get_cached_value( "Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name" diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js index 91b743016b..1f7bb4d245 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js @@ -207,13 +207,24 @@ frappe.ui.form.on('Serial and Batch Bundle', { }; }); - frm.set_query('batch_no', 'entries', () => { - return { - filters: { - item: frm.doc.item_code, - disabled: 0, + frm.set_query('batch_no', 'entries', (doc) => { + + if (doc.type_of_transaction ==="Outward") { + return { + query : "erpnext.controllers.queries.get_batch_no", + filters: { + item_code: doc.item_code, + warehouse: doc.warehouse, + } } - }; + } else { + return { + filters: { + item: doc.item_code, + disabled: 0, + } + }; + } }); frm.set_query('warehouse', 'entries', () => { diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index eb4df29db8..b6e4d6f40c 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -257,9 +257,9 @@ class SerialandBatchBundle(Document): if sn_obj.batch_avg_rate.get(d.batch_no): d.incoming_rate = abs(sn_obj.batch_avg_rate.get(d.batch_no)) - available_qty = flt(sn_obj.available_qty.get(d.batch_no)) + available_qty = flt(sn_obj.available_qty.get(d.batch_no), d.precision("qty")) if self.docstatus == 1: - available_qty += flt(d.qty) + available_qty += flt(d.qty, d.precision("qty")) if not allow_negative_stock: self.validate_negative_batch(d.batch_no, available_qty) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index f430943708..b932c1371d 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -4,8 +4,8 @@ import json import frappe -from frappe.tests.utils import FrappeTestCase -from frappe.utils import add_days, add_to_date, flt, nowdate, nowtime, today +from frappe.tests.utils import FrappeTestCase, change_settings +from frappe.utils import flt, nowtime, today from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( @@ -191,6 +191,7 @@ class TestSerialandBatchBundle(FrappeTestCase): doc.flags.ignore_links = True doc.flags.ignore_validate = True doc.submit() + doc.reload() bundle_doc = make_serial_batch_bundle( { @@ -521,6 +522,24 @@ class TestSerialandBatchBundle(FrappeTestCase): make_serial_nos(item_code, serial_nos) self.assertTrue(frappe.db.exists("Serial No", serial_no_id)) + @change_settings("Stock Settings", {"auto_create_serial_and_batch_bundle_for_outward": 1}) + def test_duplicate_serial_and_batch_bundle(self): + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + + item_code = make_item(properties={"is_stock_item": 1, "has_serial_no": 1}).name + + serial_no = f"{item_code}-001" + serial_nos = [{"serial_no": serial_no, "qty": 1}] + make_serial_nos(item_code, serial_nos) + + pr1 = make_purchase_receipt(item=item_code, qty=1, rate=500, serial_no=[serial_no]) + pr2 = make_purchase_receipt(item=item_code, qty=1, rate=500, do_not_save=True) + + pr1.reload() + pr2.items[0].serial_and_batch_bundle = pr1.items[0].serial_and_batch_bundle + + self.assertRaises(frappe.exceptions.ValidationError, pr2.save) + def get_batch_from_bundle(bundle): from erpnext.stock.serial_batch_bundle import get_batch_nos diff --git a/erpnext/stock/doctype/serial_and_batch_entry/serial_and_batch_entry.json b/erpnext/stock/doctype/serial_and_batch_entry/serial_and_batch_entry.json index 5de2c2ee65..844270bc1d 100644 --- a/erpnext/stock/doctype/serial_and_batch_entry/serial_and_batch_entry.json +++ b/erpnext/stock/doctype/serial_and_batch_entry/serial_and_batch_entry.json @@ -68,7 +68,7 @@ { "fieldname": "incoming_rate", "fieldtype": "Float", - "label": "Incoming Rate", + "label": "Valuation Rate", "no_copy": 1, "read_only": 1, "read_only_depends_on": "eval:parent.type_of_transaction == \"Outward\"" @@ -76,6 +76,7 @@ { "fieldname": "outgoing_rate", "fieldtype": "Float", + "hidden": 1, "label": "Outgoing Rate", "no_copy": 1, "read_only": 1 @@ -95,6 +96,7 @@ "default": "0", "fieldname": "is_outward", "fieldtype": "Check", + "hidden": 1, "label": "Is Outward", "read_only": 1 }, @@ -120,7 +122,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-12-10 19:47:48.227772", + "modified": "2024-02-23 12:44:18.054270", "modified_by": "Administrator", "module": "Stock", "name": "Serial and Batch Entry", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 8da3e8fdd0..7f79f04aae 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -543,7 +543,9 @@ frappe.ui.form.on('Stock Entry', { let fields = [ {"fieldname":"bom", "fieldtype":"Link", "label":__("BOM"), - options:"BOM", reqd: 1, get_query: filters()}, + options:"BOM", reqd: 1, get_query: () => { + return {filters: { docstatus:1, "is_active": 1 }}; + }}, {"fieldname":"source_warehouse", "fieldtype":"Link", "label":__("Source Warehouse"), options:"Warehouse"}, {"fieldname":"target_warehouse", "fieldtype":"Link", "label":__("Target Warehouse"), @@ -936,6 +938,7 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle this.toggle_related_fields(this.frm.doc); this.toggle_enable_bom(); this.show_stock_ledger(); + this.set_fields_onload_for_line_item(); erpnext.utils.view_serial_batch_nos(this.frm); if (this.frm.doc.docstatus===1 && erpnext.is_perpetual_inventory_enabled(this.frm.doc.company)) { this.show_general_ledger(); @@ -944,6 +947,35 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle erpnext.utils.add_item(this.frm); } + serial_no(doc, cdt, cdn) { + var item = frappe.get_doc(cdt, cdn); + + if (item?.serial_no) { + // Replace all occurences of comma with line feed + item.serial_no = item.serial_no.replace(/,/g, '\n'); + item.conversion_factor = item.conversion_factor || 1; + + let valid_serial_nos = []; + let serialnos = item.serial_no.split("\n"); + for (var i = 0; i < serialnos.length; i++) { + if (serialnos[i] != "") { + valid_serial_nos.push(serialnos[i]); + } + } + frappe.model.set_value(item.doctype, item.name, + "qty", valid_serial_nos.length / item.conversion_factor); + } + } + + set_fields_onload_for_line_item() { + if (this.frm.is_new() && this.frm.doc?.items + && cint(frappe.user_defaults?.use_serial_batch_fields) === 1) { + this.frm.doc.items.forEach(item => { + frappe.model.set_value(item.doctype, item.name, "use_serial_batch_fields", 1); + }) + } + } + scan_barcode() { frappe.flags.dialog_set = false; const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm}); @@ -1074,6 +1106,10 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle if(!row.s_warehouse) row.s_warehouse = this.frm.doc.from_warehouse; if(!row.t_warehouse) row.t_warehouse = this.frm.doc.to_warehouse; + + if (cint(frappe.user_defaults?.use_serial_batch_fields)) { + frappe.model.set_value(row.doctype, row.name, "use_serial_batch_fields", 1); + } } from_warehouse(doc) { @@ -1144,7 +1180,8 @@ erpnext.stock.select_batch_and_serial_no = (frm, item) => { if (r) { frappe.model.set_value(item.doctype, item.name, { "serial_and_batch_bundle": r.name, - "qty": Math.abs(r.total_qty) + "use_serial_batch_fields": 0, + "qty": Math.abs(r.total_qty) / flt(item.conversion_factor || 1, precision("conversion_factor", item)) }); } } diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 10e3522579..399e698554 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -839,6 +839,7 @@ class StockEntry(StockController): currency=erpnext.get_company_currency(self.company), company=self.company, raise_error_if_no_rate=raise_error_if_no_rate, + batch_no=d.batch_no, serial_and_batch_bundle=d.serial_and_batch_bundle, ) @@ -867,7 +868,7 @@ class StockEntry(StockController): if reset_outgoing_rate: args = self.get_args_for_incoming_rate(d) rate = get_incoming_rate(args, raise_error_if_no_rate) - if rate > 0: + if rate >= 0: d.basic_rate = rate d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount")) @@ -890,6 +891,8 @@ class StockEntry(StockController): "allow_zero_valuation": item.allow_zero_valuation_rate, "serial_and_batch_bundle": item.serial_and_batch_bundle, "voucher_detail_no": item.name, + "batch_no": item.batch_no, + "serial_no": item.serial_no, } ) @@ -904,14 +907,62 @@ class StockEntry(StockController): return flt(outgoing_items_cost / total_fg_qty) def get_basic_rate_for_manufactured_item(self, finished_item_qty, outgoing_items_cost=0) -> float: + settings = frappe.get_single("Manufacturing Settings") scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item]) - # Get raw materials cost from BOM if multiple material consumption entries - if not outgoing_items_cost and frappe.db.get_single_value( - "Manufacturing Settings", "material_consumption", cache=True - ): - bom_items = self.get_bom_raw_materials(finished_item_qty) - outgoing_items_cost = sum([flt(row.qty) * flt(row.rate) for row in bom_items.values()]) + if settings.material_consumption: + if settings.get_rm_cost_from_consumption_entry and self.work_order: + + # Validate only if Material Consumption Entry exists for the Work Order. + if frappe.db.exists( + "Stock Entry", + { + "docstatus": 1, + "work_order": self.work_order, + "purpose": "Material Consumption for Manufacture", + }, + ): + for item in self.items: + if not item.is_finished_item and not item.is_scrap_item: + label = frappe.get_meta(settings.doctype).get_label("get_rm_cost_from_consumption_entry") + frappe.throw( + _( + "Row {0}: As {1} is enabled, raw materials cannot be added to {2} entry. Use {3} entry to consume raw materials." + ).format( + item.idx, + frappe.bold(label), + frappe.bold("Manufacture"), + frappe.bold("Material Consumption for Manufacture"), + ) + ) + + if frappe.db.exists( + "Stock Entry", {"docstatus": 1, "work_order": self.work_order, "purpose": "Manufacture"} + ): + frappe.throw( + _("Only one {0} entry can be created against the Work Order {1}").format( + frappe.bold("Manufacture"), frappe.bold(self.work_order) + ) + ) + + SE = frappe.qb.DocType("Stock Entry") + SE_ITEM = frappe.qb.DocType("Stock Entry Detail") + + outgoing_items_cost = ( + frappe.qb.from_(SE) + .left_join(SE_ITEM) + .on(SE.name == SE_ITEM.parent) + .select(Sum(SE_ITEM.valuation_rate * SE_ITEM.transfer_qty)) + .where( + (SE.docstatus == 1) + & (SE.work_order == self.work_order) + & (SE.purpose == "Material Consumption for Manufacture") + ) + ).run()[0][0] or 0 + + elif not outgoing_items_cost: + bom_items = self.get_bom_raw_materials(finished_item_qty) + outgoing_items_cost = sum([flt(row.qty) * flt(row.rate) for row in bom_items.values()]) return flt((outgoing_items_cost - scrap_items_cost) / finished_item_qty) @@ -982,6 +1033,9 @@ class StockEntry(StockController): already_picked_serial_nos = [] for row in self.items: + if row.use_serial_batch_fields: + continue + if not row.s_warehouse: continue @@ -989,7 +1043,7 @@ class StockEntry(StockController): continue bundle_doc = None - if row.serial_and_batch_bundle and abs(row.qty) != abs( + if row.serial_and_batch_bundle and abs(row.transfer_qty) != abs( frappe.get_cached_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "total_qty") ): bundle_doc = SerialBatchCreation( @@ -999,7 +1053,7 @@ class StockEntry(StockController): "serial_and_batch_bundle": row.serial_and_batch_bundle, "type_of_transaction": "Outward", "ignore_serial_nos": already_picked_serial_nos, - "qty": row.qty * -1, + "qty": row.transfer_qty * -1, } ).update_serial_and_batch_entries() elif not row.serial_and_batch_bundle: @@ -1011,7 +1065,7 @@ class StockEntry(StockController): "posting_time": self.posting_time, "voucher_type": self.doctype, "voucher_detail_no": row.name, - "qty": row.qty * -1, + "qty": row.transfer_qty * -1, "ignore_serial_nos": already_picked_serial_nos, "type_of_transaction": "Outward", "company": self.company, @@ -1848,6 +1902,7 @@ class StockEntry(StockController): return id = create_serial_and_batch_bundle( + self, row, frappe._dict( { @@ -2118,7 +2173,7 @@ class StockEntry(StockController): "to_warehouse": "", "qty": qty, "item_name": item.item_name, - "serial_and_batch_bundle": create_serial_and_batch_bundle(row, item, "Outward"), + "serial_and_batch_bundle": create_serial_and_batch_bundle(self, row, item, "Outward"), "description": item.description, "stock_uom": item.stock_uom, "expense_account": item.expense_account, @@ -2496,6 +2551,7 @@ class StockEntry(StockController): row = frappe._dict({"serial_nos": serial_nos[0 : cint(d.qty)]}) id = create_serial_and_batch_bundle( + self, row, frappe._dict( { @@ -3019,7 +3075,7 @@ def get_stock_entry_data(work_order): return data -def create_serial_and_batch_bundle(row, child, type_of_transaction=None): +def create_serial_and_batch_bundle(parent_doc, row, child, type_of_transaction=None): item_details = frappe.get_cached_value( "Item", child.item_code, ["has_serial_no", "has_batch_no"], as_dict=1 ) @@ -3037,6 +3093,8 @@ def create_serial_and_batch_bundle(row, child, type_of_transaction=None): "item_code": child.item_code, "warehouse": child.warehouse, "type_of_transaction": type_of_transaction, + "posting_date": parent_doc.posting_date, + "posting_time": parent_doc.posting_time, } ) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py index 0f67e47ad9..271cbbc007 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py @@ -92,9 +92,6 @@ def make_stock_entry(**args): else: args.qty = cint(args.qty) - if args.serial_no or args.batch_no: - args.use_serial_batch_fields = True - # purpose if not args.purpose: if args.source and args.target: @@ -136,7 +133,7 @@ def make_stock_entry(**args): serial_number = args.serial_no bundle_id = None - if args.serial_no or args.batch_no or args.batches: + if not args.use_serial_batch_fields and (args.serial_no or args.batch_no or args.batches): batches = frappe._dict({}) if args.batch_no: batches = frappe._dict({args.batch_no: args.qty}) @@ -164,7 +161,11 @@ def make_stock_entry(**args): .name ) - args.serial_no = serial_number + args["serial_no"] = "" + args["batch_no"] = "" + + else: + args.serial_no = serial_number s.append( "items", diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index af91536acc..9d1a3f7e62 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -1596,6 +1596,7 @@ class TestStockEntry(FrappeTestCase): qty=4, to_warehouse="_Test Warehouse - _TC", batch_no=batch.name, + use_serial_batch_fields=1, do_not_save=True, ) @@ -1610,24 +1611,22 @@ class TestStockEntry(FrappeTestCase): item_code = "Test Negative Item - 001" item_doc = create_item(item_code=item_code, is_stock_item=1, valuation_rate=10) - make_stock_entry( + se1 = make_stock_entry( item_code=item_code, posting_date=add_days(today(), -3), posting_time="00:00:00", - purpose="Material Receipt", + target="_Test Warehouse - _TC", qty=10, to_warehouse="_Test Warehouse - _TC", - do_not_save=True, ) - make_stock_entry( + se2 = make_stock_entry( item_code=item_code, posting_date=today(), posting_time="00:00:00", - purpose="Material Receipt", + source="_Test Warehouse - _TC", qty=8, from_warehouse="_Test Warehouse - _TC", - do_not_save=True, ) sr_doc = create_stock_reconciliation( @@ -1754,6 +1753,51 @@ class TestStockEntry(FrappeTestCase): mr.cancel() mr.delete() + def test_use_serial_and_batch_fields(self): + item = make_item( + "Test Use Serial and Batch Item SN Item", + {"has_serial_no": 1, "is_stock_item": 1}, + ) + + serial_nos = [ + "Test Use Serial and Batch Item SN Item - SN 001", + "Test Use Serial and Batch Item SN Item - SN 002", + ] + + se = make_stock_entry( + item_code=item.name, + qty=2, + to_warehouse="_Test Warehouse - _TC", + use_serial_batch_fields=1, + serial_no="\n".join(serial_nos), + ) + + self.assertTrue(se.items[0].use_serial_batch_fields) + self.assertTrue(se.items[0].serial_no) + self.assertTrue(se.items[0].serial_and_batch_bundle) + + for serial_no in serial_nos: + self.assertTrue(frappe.db.exists("Serial No", serial_no)) + self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Active") + + se1 = make_stock_entry( + item_code=item.name, + qty=2, + from_warehouse="_Test Warehouse - _TC", + use_serial_batch_fields=1, + serial_no="\n".join(serial_nos), + ) + + se1.reload() + + self.assertTrue(se1.items[0].use_serial_batch_fields) + self.assertTrue(se1.items[0].serial_no) + self.assertTrue(se1.items[0].serial_and_batch_bundle) + + for serial_no in serial_nos: + self.assertTrue(frappe.db.exists("Serial No", serial_no)) + self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Delivered") + def make_serialized_item(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index c7b3daab82..48fc31ab66 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -294,7 +294,7 @@ { "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "serial_no", - "fieldtype": "Small Text", + "fieldtype": "Text", "label": "Serial No", "no_copy": 1, "oldfieldname": "serial_no", @@ -610,7 +610,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-02-04 16:16:47.606270", + "modified": "2024-02-25 15:58:40.982582", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py index 47c443c519..bd3dda1b98 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py @@ -54,7 +54,7 @@ class StockEntryDetail(Document): sample_quantity: DF.Int sco_rm_detail: DF.Data | None serial_and_batch_bundle: DF.Link | None - serial_no: DF.SmallText | None + serial_no: DF.Text | None set_basic_rate_manually: DF.Check ste_detail: DF.Data | None stock_uom: DF.Link diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json index be379940ca..3a094f1e8f 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json @@ -11,6 +11,7 @@ "warehouse", "posting_date", "posting_time", + "posting_datetime", "is_adjustment_entry", "auto_created_serial_and_batch_bundle", "column_break_6", @@ -100,7 +101,6 @@ "oldfieldtype": "Date", "print_width": "100px", "read_only": 1, - "search_index": 1, "width": "100px" }, { @@ -253,7 +253,6 @@ "options": "Company", "print_width": "150px", "read_only": 1, - "search_index": 1, "width": "150px" }, { @@ -348,6 +347,11 @@ "fieldname": "auto_created_serial_and_batch_bundle", "fieldtype": "Check", "label": "Auto Created Serial and Batch Bundle" + }, + { + "fieldname": "posting_datetime", + "fieldtype": "Datetime", + "label": "Posting Datetime" } ], "hide_toolbar": 1, @@ -356,7 +360,7 @@ "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2023-11-14 16:47:39.791967", + "modified": "2024-02-07 09:18:13.999231", "modified_by": "Administrator", "module": "Stock", "name": "Stock Ledger Entry", diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 04441f0e8b..a3e51ca60d 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -51,6 +51,7 @@ class StockLedgerEntry(Document): item_code: DF.Link | None outgoing_rate: DF.Currency posting_date: DF.Date | None + posting_datetime: DF.Datetime | None posting_time: DF.Time | None project: DF.Link | None qty_after_transaction: DF.Float @@ -92,6 +93,12 @@ class StockLedgerEntry(Document): self.validate_with_last_transaction_posting_time() self.validate_inventory_dimension_negative_stock() + def set_posting_datetime(self): + from erpnext.stock.utils import get_combine_datetime + + self.posting_datetime = get_combine_datetime(self.posting_date, self.posting_time) + self.db_set("posting_datetime", self.posting_datetime) + def validate_inventory_dimension_negative_stock(self): if self.is_cancelled: return @@ -162,6 +169,7 @@ class StockLedgerEntry(Document): return inv_dimension_dict def on_submit(self): + self.set_posting_datetime() self.check_stock_frozen_date() # Added to handle few test cases where serial_and_batch_bundles are not required @@ -332,9 +340,7 @@ class StockLedgerEntry(Document): def on_doctype_update(): - frappe.db.add_index( - "Stock Ledger Entry", fields=["posting_date", "posting_time"], index_name="posting_sort_index" - ) frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"]) frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"]) frappe.db.add_index("Stock Ledger Entry", ["warehouse", "item_code"], "item_warehouse") + frappe.db.add_index("Stock Ledger Entry", ["posting_datetime", "creation"]) diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index c0999532d0..40a2d5a566 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -2,6 +2,7 @@ # See license.txt import json +import time from uuid import uuid4 import frappe @@ -1077,7 +1078,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): frappe.qb.from_(sle) .select("qty_after_transaction") .where((sle.item_code == item) & (sle.warehouse == warehouse) & (sle.is_cancelled == 0)) - .orderby(CombineDatetime(sle.posting_date, sle.posting_time)) + .orderby(sle.posting_datetime) .orderby(sle.creation) ).run(pluck=True) @@ -1154,6 +1155,89 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): except Exception as e: self.fail("Double processing of qty for clashing timestamp.") + def test_previous_sle_with_clashed_timestamp(self): + + item = make_item().name + warehouse = "_Test Warehouse - _TC" + + reciept1 = make_stock_entry( + item_code=item, + to_warehouse=warehouse, + qty=100, + rate=10, + posting_date="2021-01-01", + posting_time="02:00:00", + ) + + time.sleep(3) + + reciept2 = make_stock_entry( + item_code=item, + to_warehouse=warehouse, + qty=5, + posting_date="2021-01-01", + rate=10, + posting_time="02:00:00.1234", + ) + + sle = frappe.get_all( + "Stock Ledger Entry", + filters={"voucher_no": reciept1.name}, + fields=["qty_after_transaction", "actual_qty"], + ) + self.assertEqual(sle[0].qty_after_transaction, 100) + self.assertEqual(sle[0].actual_qty, 100) + + sle = frappe.get_all( + "Stock Ledger Entry", + filters={"voucher_no": reciept2.name}, + fields=["qty_after_transaction", "actual_qty"], + ) + self.assertEqual(sle[0].qty_after_transaction, 105) + self.assertEqual(sle[0].actual_qty, 5) + + def test_backdated_sle_with_same_timestamp(self): + + item = make_item().name + warehouse = "_Test Warehouse - _TC" + + reciept1 = make_stock_entry( + item_code=item, + to_warehouse=warehouse, + qty=5, + posting_date="2021-01-01", + rate=10, + posting_time="02:00:00.1234", + ) + + time.sleep(3) + + # backdated entry with same timestamp but different ms part + reciept2 = make_stock_entry( + item_code=item, + to_warehouse=warehouse, + qty=100, + rate=10, + posting_date="2021-01-01", + posting_time="02:00:00", + ) + + sle = frappe.get_all( + "Stock Ledger Entry", + filters={"voucher_no": reciept1.name}, + fields=["qty_after_transaction", "actual_qty"], + ) + self.assertEqual(sle[0].qty_after_transaction, 5) + self.assertEqual(sle[0].actual_qty, 5) + + sle = frappe.get_all( + "Stock Ledger Entry", + filters={"voucher_no": reciept2.name}, + fields=["qty_after_transaction", "actual_qty"], + ) + self.assertEqual(sle[0].qty_after_transaction, 105) + self.assertEqual(sle[0].actual_qty, 100) + @change_settings("System Settings", {"float_precision": 3, "currency_precision": 2}) def test_transfer_invariants(self): """Extact stock value should be transferred.""" diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index ce08615ed5..06fd5f9b27 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -834,6 +834,7 @@ class StockReconciliation(StockController): if voucher_detail_no != row.name: continue + val_rate = 0.0 current_qty = 0.0 if row.current_serial_and_batch_bundle: current_qty = self.get_current_qty_for_serial_or_batch(row) @@ -843,7 +844,6 @@ class StockReconciliation(StockController): row.warehouse, self.posting_date, self.posting_time, - voucher_no=self.name, ) current_qty = item_dict.get("qty") @@ -885,7 +885,7 @@ class StockReconciliation(StockController): {"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0}, "name", ) - and (not row.current_serial_and_batch_bundle and not row.batch_no) + and (not row.current_serial_and_batch_bundle) ): self.set_current_serial_and_batch_bundle(voucher_detail_no, save=True) row.reload() @@ -906,8 +906,13 @@ class StockReconciliation(StockController): def has_negative_stock_allowed(self): allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) + if allow_negative_stock: + return True - if all(d.serial_and_batch_bundle and flt(d.qty) == flt(d.current_qty) for d in self.items): + if any( + ((d.serial_and_batch_bundle or d.batch_no) and flt(d.qty) == flt(d.current_qty)) + for d in self.items + ): allow_negative_stock = True return allow_negative_stock diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py index 7e03ac3357..26fe8e1787 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -7,7 +7,7 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.query_builder.functions import Sum -from frappe.utils import cint, flt +from frappe.utils import cint, flt, nowdate, nowtime from erpnext.stock.utils import get_or_make_bin, get_stock_balance @@ -866,6 +866,8 @@ def get_ssb_bundle_for_voucher(sre: dict) -> object: bundle = frappe.new_doc("Serial and Batch Bundle") bundle.type_of_transaction = "Outward" bundle.voucher_type = "Delivery Note" + bundle.posting_date = nowdate() + bundle.posting_time = nowtime() for field in ("item_code", "warehouse", "has_serial_no", "has_batch_no"): setattr(bundle, field, sre[field]) diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index c6982831b7..51036adc2c 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -51,6 +51,7 @@ "use_naming_series", "naming_series_prefix", "use_serial_batch_fields", + "do_not_update_serial_batch_on_creation_of_auto_bundle", "stock_planning_tab", "auto_material_request", "auto_indent", @@ -424,9 +425,18 @@ }, { "default": "1", + "description": "On submission of the stock transaction, system will auto create the Serial and Batch Bundle based on the Serial No / Batch fields.", "fieldname": "use_serial_batch_fields", "fieldtype": "Check", "label": "Use Serial / Batch Fields" + }, + { + "default": "1", + "depends_on": "use_serial_batch_fields", + "description": "If enabled, do not update serial / batch values in the stock transactions on creation of auto Serial \n / Batch Bundle. ", + "fieldname": "do_not_update_serial_batch_on_creation_of_auto_bundle", + "fieldtype": "Check", + "label": "Do Not Update Serial / Batch on Creation of Auto Bundle" } ], "icon": "icon-cog", @@ -434,7 +444,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-02-04 12:01:31.931864", + "modified": "2024-02-25 16:32:01.084453", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index c4960aa67a..d975c29ee5 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -39,6 +39,7 @@ class StockSettings(Document): clean_description_html: DF.Check default_warehouse: DF.Link | None disable_serial_no_and_batch_selector: DF.Check + do_not_update_serial_batch_on_creation_of_auto_bundle: DF.Check enable_stock_reservation: DF.Check item_group: DF.Link | None item_naming_by: DF.Literal["Item Code", "Naming Series"] diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py index e4f657ca70..da958a8b0f 100644 --- a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py +++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py @@ -5,7 +5,7 @@ import frappe from frappe import _ from frappe.query_builder import Field -from frappe.query_builder.functions import CombineDatetime, Min +from frappe.query_builder.functions import Min from frappe.utils import add_days, getdate, today import erpnext @@ -75,7 +75,7 @@ def get_data(report_filters): & (sle.company == report_filters.company) & (sle.is_cancelled == 0) ) - .orderby(CombineDatetime(sle.posting_date, sle.posting_time), sle.creation) + .orderby(sle.posting_datetime, sle.creation) ).run(as_dict=True) for d in data: diff --git a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py index 9e75201bd1..dd79e7fcaf 100644 --- a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py +++ b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py @@ -213,13 +213,11 @@ def get_stock_ledger_entries(filters, items): query = ( frappe.qb.from_(sle) - .force_index("posting_sort_index") .left_join(sle2) .on( (sle.item_code == sle2.item_code) & (sle.warehouse == sle2.warehouse) - & (sle.posting_date < sle2.posting_date) - & (sle.posting_time < sle2.posting_time) + & (sle.posting_datetime < sle2.posting_datetime) & (sle.name < sle2.name) ) .select(sle.item_code, sle.warehouse, sle.qty_after_transaction, sle.company) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 269323810b..500affa51e 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional, TypedDict import frappe from frappe import _ from frappe.query_builder import Order -from frappe.query_builder.functions import Coalesce, CombineDatetime +from frappe.query_builder.functions import Coalesce from frappe.utils import add_days, cint, date_diff, flt, getdate from frappe.utils.nestedset import get_descendants_of @@ -300,7 +300,7 @@ class StockBalanceReport(object): item_table.item_name, ) .where((sle.docstatus < 2) & (sle.is_cancelled == 0)) - .orderby(CombineDatetime(sle.posting_date, sle.posting_time)) + .orderby(sle.posting_datetime) .orderby(sle.creation) .orderby(sle.actual_qty) ) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.js b/erpnext/stock/report/stock_ledger/stock_ledger.js index b00b422a67..2ec757b205 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.js +++ b/erpnext/stock/report/stock_ledger/stock_ledger.js @@ -91,6 +91,12 @@ frappe.query_reports["Stock Ledger"] = { "options": "Currency\nFloat", "default": "Currency" }, + { + "fieldname": "segregate_serial_batch_bundle", + "label": __("Segregate Serial / Batch Bundle"), + "fieldtype": "Check", + "default": 0 + } ], "formatter": function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index e59f2fe644..d859f4e4d9 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -2,6 +2,8 @@ # License: GNU General Public License v3. See license.txt +import copy + import frappe from frappe import _ from frappe.query_builder.functions import CombineDatetime @@ -26,6 +28,10 @@ def execute(filters=None): item_details = get_item_details(items, sl_entries, include_uom) opening_row = get_opening_balance(filters, columns, sl_entries) precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) + bundle_details = {} + + if filters.get("segregate_serial_batch_bundle"): + bundle_details = get_serial_batch_bundle_details(sl_entries) data = [] conversion_factors = [] @@ -45,6 +51,9 @@ def execute(filters=None): item_detail = item_details[sle.item_code] sle.update(item_detail) + if bundle_info := bundle_details.get(sle.serial_and_batch_bundle): + data.extend(get_segregated_bundle_entries(sle, bundle_info)) + continue if filters.get("batch_no") or inventory_dimension_filters_applied: actual_qty += flt(sle.actual_qty, precision) @@ -76,6 +85,60 @@ def execute(filters=None): return columns, data +def get_segregated_bundle_entries(sle, bundle_details): + segregated_entries = [] + qty_before_transaction = sle.qty_after_transaction - sle.actual_qty + stock_value_before_transaction = sle.stock_value - sle.stock_value_difference + + for row in bundle_details: + new_sle = copy.deepcopy(sle) + new_sle.update(row) + + new_sle.update( + { + "in_out_rate": flt(new_sle.stock_value_difference / row.qty) if row.qty else 0, + "in_qty": row.qty if row.qty > 0 else 0, + "out_qty": row.qty if row.qty < 0 else 0, + "qty_after_transaction": qty_before_transaction + row.qty, + "stock_value": stock_value_before_transaction + new_sle.stock_value_difference, + "incoming_rate": row.incoming_rate if row.qty > 0 else 0, + } + ) + + qty_before_transaction += row.qty + stock_value_before_transaction += new_sle.stock_value_difference + + new_sle.valuation_rate = ( + stock_value_before_transaction / qty_before_transaction if qty_before_transaction else 0 + ) + + segregated_entries.append(new_sle) + + return segregated_entries + + +def get_serial_batch_bundle_details(sl_entries): + bundle_details = [] + for sle in sl_entries: + if sle.serial_and_batch_bundle: + bundle_details.append(sle.serial_and_batch_bundle) + + if not bundle_details: + return frappe._dict({}) + + _bundle_details = frappe._dict({}) + batch_entries = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": ("in", bundle_details)}, + fields=["parent", "qty", "incoming_rate", "stock_value_difference", "batch_no", "serial_no"], + order_by="parent, idx", + ) + for entry in batch_entries: + _bundle_details.setdefault(entry.parent, []).append(entry) + + return _bundle_details + + def update_available_serial_nos(available_serial_nos, sle): serial_nos = get_serial_nos(sle.serial_no) key = (sle.item_code, sle.warehouse) @@ -256,7 +319,6 @@ def get_columns(filters): "options": "Serial and Batch Bundle", "width": 100, }, - {"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 100}, { "label": _("Project"), "fieldname": "project", @@ -283,7 +345,7 @@ def get_stock_ledger_entries(filters, items): frappe.qb.from_(sle) .select( sle.item_code, - CombineDatetime(sle.posting_date, sle.posting_time).as_("date"), + sle.posting_datetime.as_("date"), sle.warehouse, sle.posting_date, sle.posting_time, @@ -320,15 +382,45 @@ def get_stock_ledger_entries(filters, items): if items: query = query.where(sle.item_code.isin(items)) - for field in ["voucher_no", "batch_no", "project", "company"]: + for field in ["voucher_no", "project", "company"]: if filters.get(field) and field not in inventory_dimension_fields: query = query.where(sle[field] == filters.get(field)) + if filters.get("batch_no"): + bundles = get_serial_and_batch_bundles(filters) + + if bundles: + query = query.where( + (sle.serial_and_batch_bundle.isin(bundles)) | (sle.batch_no == filters.batch_no) + ) + else: + query = query.where(sle.batch_no == filters.batch_no) + query = apply_warehouse_filter(query, sle, filters) return query.run(as_dict=True) +def get_serial_and_batch_bundles(filters): + SBB = frappe.qb.DocType("Serial and Batch Bundle") + SBE = frappe.qb.DocType("Serial and Batch Entry") + + query = ( + frappe.qb.from_(SBE) + .inner_join(SBB) + .on(SBE.parent == SBB.name) + .select(SBE.parent) + .where( + (SBB.docstatus == 1) + & (SBB.has_batch_no == 1) + & (SBB.voucher_no.notnull()) + & (SBE.batch_no == filters.batch_no) + ) + ) + + return query.run(pluck=SBE.parent) + + def get_inventory_dimension_fields(): return [dimension.fieldname for dimension in get_inventory_dimensions()] diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index d8b5b34d44..24dd9d1d20 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -5,7 +5,7 @@ import frappe from frappe import _, bold from frappe.model.naming import make_autoname from frappe.query_builder.functions import CombineDatetime, Sum -from frappe.utils import cint, flt, get_link_to_form, now, nowtime, today +from frappe.utils import cint, cstr, flt, get_link_to_form, now, nowtime, today from erpnext.stock.deprecated_serial_batch import ( DeprecatedBatchNoValuation, @@ -138,9 +138,19 @@ class SerialBatchBundle: self.child_doctype, self.sle.voucher_detail_no, "rejected_serial_and_batch_bundle", sn_doc.name ) else: - frappe.db.set_value( - self.child_doctype, self.sle.voucher_detail_no, "serial_and_batch_bundle", sn_doc.name - ) + values_to_update = { + "serial_and_batch_bundle": sn_doc.name, + } + + if not frappe.db.get_single_value( + "Stock Settings", "do_not_update_serial_batch_on_creation_of_auto_bundle" + ): + if sn_doc.has_serial_no: + values_to_update["serial_no"] = ",".join(cstr(d.serial_no) for d in sn_doc.entries) + elif sn_doc.has_batch_no and len(sn_doc.entries) == 1: + values_to_update["batch_no"] = sn_doc.entries[0].batch_no + + frappe.db.set_value(self.child_doctype, self.sle.voucher_detail_no, values_to_update) @property def child_doctype(self): @@ -905,8 +915,6 @@ class SerialBatchCreation: self.batches = get_available_batches(kwargs) def set_auto_serial_batch_entries_for_inward(self): - print(self.get("serial_nos")) - if (self.get("batches") and self.has_batch_no) or ( self.get("serial_nos") and self.has_serial_no ): diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index e88b1921fa..2ae6c197a1 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -9,7 +9,7 @@ from typing import Optional, Set, Tuple import frappe from frappe import _, scrub from frappe.model.meta import get_field_precision -from frappe.query_builder.functions import CombineDatetime, Sum +from frappe.query_builder.functions import Sum from frappe.utils import ( cint, cstr, @@ -33,6 +33,7 @@ from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry impor get_sre_reserved_serial_nos_details, ) from erpnext.stock.utils import ( + get_combine_datetime, get_incoming_outgoing_rate_for_cancel, get_incoming_rate, get_or_make_bin, @@ -95,6 +96,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher) args = sle_doc.as_dict() + args["posting_datetime"] = get_combine_datetime(args.posting_date, args.posting_time) if sle.get("voucher_type") == "Stock Reconciliation": # preserve previous_qty_after_transaction for qty reposting @@ -616,12 +618,14 @@ class update_entries_after(object): self.process_sle(sle) def get_sle_against_current_voucher(self): - self.args["time_format"] = "%H:%i:%s" + self.args["posting_datetime"] = get_combine_datetime( + self.args.posting_date, self.args.posting_time + ) return frappe.db.sql( """ select - *, timestamp(posting_date, posting_time) as "timestamp" + *, posting_datetime as "timestamp" from `tabStock Ledger Entry` where @@ -629,8 +633,7 @@ class update_entries_after(object): and warehouse = %(warehouse)s and is_cancelled = 0 and ( - posting_date = %(posting_date)s and - time_format(posting_time, %(time_format)s) = time_format(%(posting_time)s, %(time_format)s) + posting_datetime = %(posting_datetime)s ) order by creation ASC @@ -899,7 +902,7 @@ class update_entries_after(object): precision = doc.precision("total_qty") self.wh_data.qty_after_transaction += flt(doc.total_qty, precision) - if self.wh_data.qty_after_transaction: + if flt(self.wh_data.qty_after_transaction, precision): self.wh_data.valuation_rate = flt(self.wh_data.stock_value, precision) / flt( self.wh_data.qty_after_transaction, precision ) @@ -1399,11 +1402,11 @@ class update_entries_after(object): def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False): """get stock ledger entries filtered by specific posting datetime conditions""" - args["time_format"] = "%H:%i:%s" if not args.get("posting_date"): - args["posting_date"] = "1900-01-01" - if not args.get("posting_time"): - args["posting_time"] = "00:00" + args["posting_datetime"] = "1900-01-01 00:00:00" + + if not args.get("posting_datetime"): + args["posting_datetime"] = get_combine_datetime(args["posting_date"], args["posting_time"]) voucher_condition = "" if exclude_current_voucher: @@ -1412,23 +1415,20 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc sle = frappe.db.sql( """ - select *, timestamp(posting_date, posting_time) as "timestamp" + select *, posting_datetime as "timestamp" from `tabStock Ledger Entry` where item_code = %(item_code)s and warehouse = %(warehouse)s and is_cancelled = 0 {voucher_condition} and ( - posting_date < %(posting_date)s or - ( - posting_date = %(posting_date)s and - time_format(posting_time, %(time_format)s) {operator} time_format(%(posting_time)s, %(time_format)s) - ) + posting_datetime {operator} %(posting_datetime)s ) - order by timestamp(posting_date, posting_time) desc, creation desc + order by posting_datetime desc, creation desc limit 1 for update""".format( - operator=operator, voucher_condition=voucher_condition + operator=operator, + voucher_condition=voucher_condition, ), args, as_dict=1, @@ -1469,9 +1469,7 @@ def get_stock_ledger_entries( extra_cond=None, ): """get stock ledger entries filtered by specific posting datetime conditions""" - conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format( - operator - ) + conditions = " and posting_datetime {0} %(posting_datetime)s".format(operator) if previous_sle.get("warehouse"): conditions += " and warehouse = %(warehouse)s" elif previous_sle.get("warehouse_condition"): @@ -1497,9 +1495,11 @@ def get_stock_ledger_entries( ) if not previous_sle.get("posting_date"): - previous_sle["posting_date"] = "1900-01-01" - if not previous_sle.get("posting_time"): - previous_sle["posting_time"] = "00:00" + previous_sle["posting_datetime"] = "1900-01-01 00:00:00" + else: + previous_sle["posting_datetime"] = get_combine_datetime( + previous_sle["posting_date"], previous_sle["posting_time"] + ) if operator in (">", "<=") and previous_sle.get("name"): conditions += " and name!=%(name)s" @@ -1509,12 +1509,12 @@ def get_stock_ledger_entries( return frappe.db.sql( """ - select *, timestamp(posting_date, posting_time) as "timestamp" + select *, posting_datetime as "timestamp" from `tabStock Ledger Entry` where item_code = %%(item_code)s and is_cancelled = 0 %(conditions)s - order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s + order by posting_datetime %(order)s, creation %(order)s %(limit)s %(for_update)s""" % { "conditions": conditions, @@ -1540,7 +1540,7 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None): "posting_date", "posting_time", "voucher_detail_no", - "timestamp(posting_date, posting_time) as timestamp", + "posting_datetime as timestamp", ], as_dict=1, ) @@ -1552,13 +1552,10 @@ def get_batch_incoming_rate( sle = frappe.qb.DocType("Stock Ledger Entry") - timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime( - posting_date, posting_time - ) + timestamp_condition = sle.posting_datetime < get_combine_datetime(posting_date, posting_time) if creation: timestamp_condition |= ( - CombineDatetime(sle.posting_date, sle.posting_time) - == CombineDatetime(posting_date, posting_time) + sle.posting_datetime == get_combine_datetime(posting_date, posting_time) ) & (sle.creation < creation) batch_details = ( @@ -1639,7 +1636,7 @@ def get_valuation_rate( AND valuation_rate >= 0 AND is_cancelled = 0 AND NOT (voucher_no = %s AND voucher_type = %s) - order by posting_date desc, posting_time desc, name desc limit 1""", + order by posting_datetime desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type), ): return flt(last_valuation_rate[0][0]) @@ -1698,7 +1695,7 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): datetime_limit_condition = "" qty_shift = args.actual_qty - args["time_format"] = "%H:%i:%s" + args["posting_datetime"] = get_combine_datetime(args["posting_date"], args["posting_time"]) # find difference/shift in qty caused by stock reconciliation if args.voucher_type == "Stock Reconciliation": @@ -1708,8 +1705,6 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): next_stock_reco_detail = get_next_stock_reco(args) if next_stock_reco_detail: detail = next_stock_reco_detail[0] - - # add condition to update SLEs before this date & time datetime_limit_condition = get_datetime_limit_condition(detail) frappe.db.sql( @@ -1722,13 +1717,9 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): and voucher_no != %(voucher_no)s and is_cancelled = 0 and ( - posting_date > %(posting_date)s or - ( - posting_date = %(posting_date)s and - time_format(posting_time, %(time_format)s) > time_format(%(posting_time)s, %(time_format)s) - ) + posting_datetime > %(posting_datetime)s ) - {datetime_limit_condition} + {datetime_limit_condition} """, args, ) @@ -1785,20 +1776,11 @@ def get_next_stock_reco(kwargs): & (sle.voucher_no != kwargs.get("voucher_no")) & (sle.is_cancelled == 0) & ( - ( - CombineDatetime(sle.posting_date, sle.posting_time) - > CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time")) - ) - | ( - ( - CombineDatetime(sle.posting_date, sle.posting_time) - == CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time")) - ) - & (sle.creation > kwargs.get("creation")) - ) + sle.posting_datetime + >= get_combine_datetime(kwargs.get("posting_date"), kwargs.get("posting_time")) ) ) - .orderby(CombineDatetime(sle.posting_date, sle.posting_time)) + .orderby(sle.posting_datetime) .orderby(sle.creation) .limit(1) ) @@ -1810,11 +1792,13 @@ def get_next_stock_reco(kwargs): def get_datetime_limit_condition(detail): + posting_datetime = get_combine_datetime(detail.posting_date, detail.posting_time) + return f""" and - (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}') + (posting_datetime < '{posting_datetime}' or ( - timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}') + posting_datetime = '{posting_datetime}' and creation < '{detail.creation}' ) )""" @@ -1888,10 +1872,10 @@ def get_future_sle_with_negative_qty(args): item_code = %(item_code)s and warehouse = %(warehouse)s and voucher_no != %(voucher_no)s - and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s) + and posting_datetime >= %(posting_datetime)s and is_cancelled = 0 and qty_after_transaction < 0 - order by timestamp(posting_date, posting_time) asc + order by posting_datetime asc limit 1 """, args, @@ -1904,20 +1888,20 @@ def get_future_sle_with_negative_batch_qty(args): """ with batch_ledger as ( select - posting_date, posting_time, voucher_type, voucher_no, - sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total + posting_date, posting_time, posting_datetime, voucher_type, voucher_no, + sum(actual_qty) over (order by posting_datetime, creation) as cumulative_total from `tabStock Ledger Entry` where item_code = %(item_code)s and warehouse = %(warehouse)s and batch_no=%(batch_no)s and is_cancelled = 0 - order by posting_date, posting_time, creation + order by posting_datetime, creation ) select * from batch_ledger where cumulative_total < 0.0 - and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s) + and posting_datetime >= %(posting_datetime)s limit 1 """, args, @@ -2059,6 +2043,7 @@ def is_internal_transfer(sle): def get_stock_value_difference(item_code, warehouse, posting_date, posting_time, voucher_no=None): table = frappe.qb.DocType("Stock Ledger Entry") + posting_datetime = get_combine_datetime(posting_date, posting_time) query = ( frappe.qb.from_(table) @@ -2067,10 +2052,7 @@ def get_stock_value_difference(item_code, warehouse, posting_date, posting_time, (table.is_cancelled == 0) & (table.item_code == item_code) & (table.warehouse == warehouse) - & ( - (table.posting_date < posting_date) - | ((table.posting_date == posting_date) & (table.posting_time <= posting_time)) - ) + & (table.posting_datetime <= posting_datetime) ) ) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 9eac172aa7..93e2fa46c7 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -8,7 +8,7 @@ from typing import Dict, Optional import frappe from frappe import _ from frappe.query_builder.functions import CombineDatetime, IfNull, Sum -from frappe.utils import cstr, flt, get_link_to_form, nowdate, nowtime +from frappe.utils import cstr, flt, get_link_to_form, get_time, getdate, nowdate, nowtime import erpnext from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( @@ -262,7 +262,7 @@ def get_incoming_rate(args, raise_error_if_no_rate=True): item_code=args.get("item_code"), ) - in_rate = sn_obj.get_incoming_rate() + return sn_obj.get_incoming_rate() elif item_details and item_details.has_batch_no and args.get("serial_and_batch_bundle"): args.actual_qty = args.qty @@ -272,23 +272,33 @@ def get_incoming_rate(args, raise_error_if_no_rate=True): item_code=args.get("item_code"), ) - in_rate = batch_obj.get_incoming_rate() + return batch_obj.get_incoming_rate() elif (args.get("serial_no") or "").strip() and not args.get("serial_and_batch_bundle"): - in_rate = get_avg_purchase_rate(args.get("serial_no")) + args.actual_qty = args.qty + args.serial_nos = get_serial_nos_data(args.get("serial_no")) + + sn_obj = SerialNoValuation( + sle=args, warehouse=args.get("warehouse"), item_code=args.get("item_code") + ) + + return sn_obj.get_incoming_rate() elif ( args.get("batch_no") and frappe.db.get_value("Batch", args.get("batch_no"), "use_batchwise_valuation", cache=True) and not args.get("serial_and_batch_bundle") ): - in_rate = get_batch_incoming_rate( - item_code=args.get("item_code"), + + args.actual_qty = args.qty + args.batch_nos = frappe._dict({args.batch_no: args}) + + batch_obj = BatchNoValuation( + sle=args, warehouse=args.get("warehouse"), - batch_no=args.get("batch_no"), - posting_date=args.get("posting_date"), - posting_time=args.get("posting_time"), + item_code=args.get("item_code"), ) + return batch_obj.get_incoming_rate() else: valuation_method = get_valuation_method(args.get("item_code")) previous_sle = get_previous_sle(args) @@ -647,3 +657,18 @@ def _update_item_info(scan_result: Dict[str, Optional[str]]) -> Dict[str, Option ): scan_result.update(item_info) return scan_result + + +def get_combine_datetime(posting_date, posting_time): + import datetime + + if isinstance(posting_date, str): + posting_date = getdate(posting_date) + + if isinstance(posting_time, str): + posting_time = get_time(posting_time) + + if isinstance(posting_time, datetime.timedelta): + posting_time = (datetime.datetime.min + posting_time).time() + + return datetime.datetime.combine(posting_date, posting_time).replace(microsecond=0) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index 5523c318a5..0450038d80 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -643,10 +643,6 @@ class TestSubcontractingReceipt(FrappeTestCase): ) scr = make_subcontracting_receipt(sco.name) scr.save() - for row in scr.supplied_items: - self.assertNotEqual(row.rate, 300.00) - self.assertFalse(row.serial_and_batch_bundle) - scr.submit() scr.reload() diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index dc1529b26f..e4c4d60eb5 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -113,8 +113,8 @@ class Issue(Document): "reference_name": self.name, } ) - communication.ignore_permissions = True - communication.ignore_mandatory = True + communication.flags.ignore_permissions = True + communication.flags.ignore_mandatory = True communication.save() @frappe.whitelist()