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()