@@ -68,6 +64,7 @@ frappe.ui.form.on('Accounting Dimension Filter', {
frm.clear_table("dimensions");
let row = frm.add_child("dimensions");
row.accounting_dimension = frm.doc.accounting_dimension;
+ frm.fields_dict["dimensions"].grid.update_docfield_property("dimension_value", "label", frm.doc.accounting_dimension);
frm.refresh_field("dimensions");
frm.trigger('setup_filters');
},
diff --git a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js
index febf85ca6c12..99cc0a72fb37 100644
--- a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js
+++ b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.js
@@ -43,20 +43,13 @@ frappe.ui.form.on('Bank Guarantee', {
reference_docname: function(frm) {
if (frm.doc.reference_docname && frm.doc.reference_doctype) {
- let fields_to_fetch = ["grand_total"];
let party_field = frm.doc.reference_doctype == "Sales Order" ? "customer" : "supplier";
- if (frm.doc.reference_doctype == "Sales Order") {
- fields_to_fetch.push("project");
- }
-
- fields_to_fetch.push(party_field);
frappe.call({
- method: "erpnext.accounts.doctype.bank_guarantee.bank_guarantee.get_vouchar_detials",
+ method: "erpnext.accounts.doctype.bank_guarantee.bank_guarantee.get_voucher_details",
args: {
- "column_list": fields_to_fetch,
- "doctype": frm.doc.reference_doctype,
- "docname": frm.doc.reference_docname
+ "bank_guarantee_type": frm.doc.bg_type,
+ "reference_name": frm.doc.reference_docname
},
callback: function(r) {
if (r.message) {
diff --git a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py
index 9144a29c6ef6..02eb599acc82 100644
--- a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py
+++ b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py
@@ -2,11 +2,8 @@
# For license information, please see license.txt
-import json
-
import frappe
from frappe import _
-from frappe.desk.search import sanitize_searchfield
from frappe.model.document import Document
@@ -25,14 +22,18 @@ def on_submit(self):
@frappe.whitelist()
-def get_vouchar_detials(column_list, doctype, docname):
- column_list = json.loads(column_list)
- for col in column_list:
- sanitize_searchfield(col)
- return frappe.db.sql(
- """ select {columns} from `tab{doctype}` where name=%s""".format(
- columns=", ".join(column_list), doctype=doctype
- ),
- docname,
- as_dict=1,
- )[0]
+def get_voucher_details(bank_guarantee_type: str, reference_name: str):
+ if not isinstance(reference_name, str):
+ raise TypeError("reference_name must be a string")
+
+ fields_to_fetch = ["grand_total"]
+
+ if bank_guarantee_type == "Receiving":
+ doctype = "Sales Order"
+ fields_to_fetch.append("customer")
+ fields_to_fetch.append("project")
+ else:
+ doctype = "Purchase Order"
+ fields_to_fetch.append("supplier")
+
+ return frappe.db.get_value(doctype, reference_name, fields_to_fetch, as_dict=True)
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
index 0efe086d94e9..710c0146153d 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
@@ -299,7 +299,7 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
dict(
account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"]
),
- ["credit", "debit"],
+ ["credit_in_account_currency as credit", "debit_in_account_currency as debit"],
as_dict=1,
)
gl_amount, transaction_amount = (
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index e43f18b5c744..69b611669f36 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -138,7 +138,7 @@ def get_paid_amount(payment_entry, currency, bank_account):
)
elif doc.payment_type == "Pay":
paid_amount_field = (
- "paid_amount" if doc.paid_to_account_currency == currency else "base_paid_amount"
+ "paid_amount" if doc.paid_from_account_currency == currency else "base_paid_amount"
)
return frappe.db.get_value(
diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py
index 069ab5ea843c..c3e83ad168f0 100644
--- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py
+++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py
@@ -169,5 +169,6 @@ def auto_create_fiscal_year():
def get_from_and_to_date(fiscal_year):
- fields = ["year_start_date as from_date", "year_end_date as to_date"]
- return frappe.db.get_value("Fiscal Year", fiscal_year, fields, as_dict=1)
+ fields = ["year_start_date", "year_end_date"]
+ cached_results = frappe.get_cached_value("Fiscal Year", fiscal_year, fields, as_dict=1)
+ return dict(from_date=cached_results.year_start_date, to_date=cached_results.year_end_date)
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js
index 5c0d3265680f..2808141f7321 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.js
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js
@@ -173,8 +173,8 @@ frappe.ui.form.on("Journal Entry", {
var update_jv_details = function(doc, r) {
$.each(r, function(i, d) {
var row = frappe.model.add_child(doc, "Journal Entry Account", "accounts");
- row.account = d.account;
- row.balance = d.balance;
+ frappe.model.set_value(row.doctype, row.name, "account", d.account)
+ frappe.model.set_value(row.doctype, row.name, "balance", d.balance)
});
refresh_field("accounts");
}
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index 34b166e83c22..dd17744dffae 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -194,7 +194,9 @@ def apply_tax_withholding(self):
}
)
- tax_withholding_details = get_party_tax_withholding_details(inv, self.tax_withholding_category)
+ tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details(
+ inv, self.tax_withholding_category
+ )
if not tax_withholding_details:
return
diff --git a/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js b/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js
index cf5fbe12afe7..88f1c9069c82 100644
--- a/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js
+++ b/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js
@@ -45,21 +45,6 @@ frappe.ui.form.on("Journal Entry Template", {
frm.trigger("clear_child");
switch(frm.doc.voucher_type){
- case "Opening Entry":
- frm.set_value("is_opening", "Yes");
- frappe.call({
- type:"GET",
- method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_opening_accounts",
- args: {
- "company": frm.doc.company
- },
- callback: function(r) {
- if(r.message) {
- add_accounts(frm.doc, r.message);
- }
- }
- });
- break;
case "Bank Entry":
case "Cash Entry":
frappe.call({
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js
index 7eb5c4234d13..e9612c360279 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js
@@ -20,7 +20,6 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
frm.dashboard.reset();
frm.doc.import_in_progress = true;
}
- if (data.user != frappe.session.user) return;
if (data.count == data.total) {
setTimeout((title) => {
frm.doc.import_in_progress = false;
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
index 0f0ab68dcb84..510b69c96d53 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
@@ -271,10 +271,10 @@ def publish(index, total, doctype):
dict(
title=_("Opening Invoice Creation In Progress"),
message=_("Creating {} out of {} {}").format(index + 1, total, doctype),
- user=frappe.session.user,
count=index + 1,
total=total,
),
+ user=frappe.session.user,
)
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 3a89ce8cd129..6be0920d2a8e 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -1111,7 +1111,7 @@ frappe.ui.form.on('Payment Entry', {
$.each(tax_fields, function(i, fieldname) { tax[fieldname] = 0.0; });
- frm.doc.paid_amount_after_tax = frm.doc.paid_amount;
+ frm.doc.paid_amount_after_tax = frm.doc.base_paid_amount;
});
},
@@ -1202,7 +1202,7 @@ frappe.ui.form.on('Payment Entry', {
}
cumulated_tax_fraction += tax.tax_fraction_for_current_item;
- frm.doc.paid_amount_after_tax = flt(frm.doc.paid_amount/(1+cumulated_tax_fraction))
+ frm.doc.paid_amount_after_tax = flt(frm.doc.base_paid_amount/(1+cumulated_tax_fraction))
});
},
@@ -1234,6 +1234,7 @@ frappe.ui.form.on('Payment Entry', {
frm.doc.total_taxes_and_charges = 0.0;
frm.doc.base_total_taxes_and_charges = 0.0;
+ let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
let actual_tax_dict = {};
// maintain actual tax rate based on idx
@@ -1254,8 +1255,8 @@ frappe.ui.form.on('Payment Entry', {
}
}
- tax.tax_amount = current_tax_amount;
- tax.base_tax_amount = tax.tax_amount * frm.doc.source_exchange_rate;
+ // tax accounts are only in company currency
+ tax.base_tax_amount = current_tax_amount;
current_tax_amount *= (tax.add_deduct_tax == "Deduct") ? -1.0 : 1.0;
if(i==0) {
@@ -1264,9 +1265,29 @@ frappe.ui.form.on('Payment Entry', {
tax.total = flt(frm.doc["taxes"][i-1].total + current_tax_amount, precision("total", tax));
}
- tax.base_total = tax.total * frm.doc.source_exchange_rate;
- frm.doc.total_taxes_and_charges += current_tax_amount;
- frm.doc.base_total_taxes_and_charges += current_tax_amount * frm.doc.source_exchange_rate;
+ // tac accounts are only in company currency
+ tax.base_total = tax.total
+
+ // calculate total taxes and base total taxes
+ if(frm.doc.payment_type == "Pay") {
+ // tax accounts only have company currency
+ if(tax.currency != frm.doc.paid_to_account_currency) {
+ //total_taxes_and_charges has the target currency. so using target conversion rate
+ frm.doc.total_taxes_and_charges += flt(current_tax_amount / frm.doc.target_exchange_rate);
+
+ } else {
+ frm.doc.total_taxes_and_charges += current_tax_amount;
+ }
+ } else if(frm.doc.payment_type == "Receive") {
+ if(tax.currency != frm.doc.paid_from_account_currency) {
+ //total_taxes_and_charges has the target currency. so using source conversion rate
+ frm.doc.total_taxes_and_charges += flt(current_tax_amount / frm.doc.source_exchange_rate);
+ } else {
+ frm.doc.total_taxes_and_charges += current_tax_amount;
+ }
+ }
+
+ frm.doc.base_total_taxes_and_charges += tax.base_tax_amount;
frm.refresh_field('taxes');
frm.refresh_field('total_taxes_and_charges');
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index b1976ebae65d..70a5f9e526c9 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -67,7 +67,6 @@ def validate(self):
self.set_missing_values()
self.validate_payment_type()
self.validate_party_details()
- self.validate_bank_accounts()
self.set_exchange_rate()
self.validate_mandatory()
self.validate_reference_documents()
@@ -250,23 +249,6 @@ def validate_party_details(self):
if not frappe.db.exists(self.party_type, self.party):
frappe.throw(_("Invalid {0}: {1}").format(self.party_type, self.party))
- if self.party_account and self.party_type in ("Customer", "Supplier"):
- self.validate_account_type(
- self.party_account, [erpnext.get_party_account_type(self.party_type)]
- )
-
- def validate_bank_accounts(self):
- if self.payment_type in ("Pay", "Internal Transfer"):
- self.validate_account_type(self.paid_from, ["Bank", "Cash"])
-
- if self.payment_type in ("Receive", "Internal Transfer"):
- self.validate_account_type(self.paid_to, ["Bank", "Cash"])
-
- def validate_account_type(self, account, account_types):
- account_type = frappe.db.get_value("Account", account, "account_type")
- # if account_type not in account_types:
- # frappe.throw(_("Account Type for {0} must be {1}").format(account, comma_or(account_types)))
-
def set_exchange_rate(self, ref_doc=None):
self.set_source_exchange_rate(ref_doc)
self.set_target_exchange_rate(ref_doc)
@@ -944,6 +926,13 @@ def add_tax_gl_entries(self, gl_entries):
)
if not d.included_in_paid_amount:
+ if get_account_currency(payment_account) != self.company_currency:
+ if self.payment_type == "Receive":
+ exchange_rate = self.target_exchange_rate
+ elif self.payment_type in ["Pay", "Internal Transfer"]:
+ exchange_rate = self.source_exchange_rate
+ base_tax_amount = flt((tax_amount / exchange_rate), self.precision("paid_amount"))
+
gl_entries.append(
self.get_gl_dict(
{
@@ -1059,7 +1048,7 @@ def initialize_taxes(self):
for fieldname in tax_fields:
tax.set(fieldname, 0.0)
- self.paid_amount_after_tax = self.paid_amount
+ self.paid_amount_after_tax = self.base_paid_amount
def determine_exclusive_rate(self):
if not any((cint(tax.included_in_paid_amount) for tax in self.get("taxes"))):
@@ -1078,7 +1067,7 @@ def determine_exclusive_rate(self):
cumulated_tax_fraction += tax.tax_fraction_for_current_item
- self.paid_amount_after_tax = flt(self.paid_amount / (1 + cumulated_tax_fraction))
+ self.paid_amount_after_tax = flt(self.base_paid_amount / (1 + cumulated_tax_fraction))
def calculate_taxes(self):
self.total_taxes_and_charges = 0.0
@@ -1101,7 +1090,7 @@ def calculate_taxes(self):
current_tax_amount += actual_tax_dict[tax.idx]
tax.tax_amount = current_tax_amount
- tax.base_tax_amount = tax.tax_amount * self.source_exchange_rate
+ tax.base_tax_amount = current_tax_amount
if tax.add_deduct_tax == "Deduct":
current_tax_amount *= -1.0
@@ -1115,14 +1104,20 @@ def calculate_taxes(self):
self.get("taxes")[i - 1].total + current_tax_amount, self.precision("total", tax)
)
- tax.base_total = tax.total * self.source_exchange_rate
+ tax.base_total = tax.total
if self.payment_type == "Pay":
- self.base_total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
- self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
- else:
- self.base_total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
- self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
+ if tax.currency != self.paid_to_account_currency:
+ self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
+ else:
+ self.total_taxes_and_charges += current_tax_amount
+ elif self.payment_type == "Receive":
+ if tax.currency != self.paid_from_account_currency:
+ self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
+ else:
+ self.total_taxes_and_charges += current_tax_amount
+
+ self.base_total_taxes_and_charges += tax.base_tax_amount
if self.get("taxes"):
self.paid_amount_after_tax = self.get("taxes")[-1].base_total
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index a8211c81f1b2..004c84c0221f 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -4,6 +4,7 @@
import unittest
import frappe
+from frappe import qb
from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import (
@@ -743,6 +744,46 @@ def test_multi_currency_payment_entry_with_taxes(self):
flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2)
)
+ def test_gl_of_multi_currency_payment_with_taxes(self):
+ payment_entry = create_payment_entry(
+ party="_Test Supplier USD", paid_to="_Test Payable USD - _TC", save=True
+ )
+ payment_entry.append(
+ "taxes",
+ {
+ "account_head": "_Test Account Service Tax - _TC",
+ "charge_type": "Actual",
+ "tax_amount": 100,
+ "add_deduct_tax": "Add",
+ "description": "Test",
+ },
+ )
+ payment_entry.target_exchange_rate = 80
+ payment_entry.received_amount = 12.5
+ payment_entry = payment_entry.submit()
+ gle = qb.DocType("GL Entry")
+ gl_entries = (
+ qb.from_(gle)
+ .select(
+ gle.account,
+ gle.debit,
+ gle.credit,
+ gle.debit_in_account_currency,
+ gle.credit_in_account_currency,
+ )
+ .orderby(gle.account)
+ .where(gle.voucher_no == payment_entry.name)
+ .run()
+ )
+
+ expected_gl_entries = (
+ ("_Test Account Service Tax - _TC", 100.0, 0.0, 100.0, 0.0),
+ ("_Test Bank - _TC", 0.0, 1100.0, 0.0, 1100.0),
+ ("_Test Payable USD - _TC", 1000.0, 0.0, 12.5, 0),
+ )
+
+ self.assertEqual(gl_entries, expected_gl_entries)
+
def test_payment_entry_against_onhold_purchase_invoice(self):
pi = make_purchase_invoice()
diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json
index 8961167f0188..3003c68196ee 100644
--- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json
+++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json
@@ -25,7 +25,8 @@
"in_list_view": 1,
"label": "Type",
"options": "DocType",
- "reqd": 1
+ "reqd": 1,
+ "search_index": 1
},
{
"columns": 2,
@@ -35,7 +36,8 @@
"in_list_view": 1,
"label": "Name",
"options": "reference_doctype",
- "reqd": 1
+ "reqd": 1,
+ "search_index": 1
},
{
"fieldname": "due_date",
@@ -104,7 +106,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-09-26 17:06:55.597389",
+ "modified": "2022-12-12 12:31:44.919895",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Reference",
@@ -113,5 +115,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index e5b942fb6ef9..1e75471c8485 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -3,12 +3,18 @@
import frappe
-from frappe import _, msgprint
+from frappe import _, msgprint, qb
from frappe.model.document import Document
+from frappe.query_builder import Criterion
+from frappe.query_builder.functions import Sum
from frappe.utils import flt, getdate, nowdate, today
import erpnext
-from erpnext.accounts.utils import get_outstanding_invoices, reconcile_against_document
+from erpnext.accounts.utils import (
+ get_outstanding_invoices,
+ reconcile_against_document,
+ update_reference_in_payment_entry,
+)
from erpnext.controllers.accounts_controller import get_advance_payment_entries
@@ -43,6 +49,10 @@ def get_nonreconciled_payment_entries(self):
def get_payment_entries(self):
order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order"
condition = self.get_conditions(get_payments=True)
+
+ if self.get("cost_center"):
+ condition += " and cost_center = '{0}' ".format(self.cost_center)
+
payment_entries = get_advance_payment_entries(
self.party_type,
self.party,
@@ -57,6 +67,10 @@ def get_payment_entries(self):
def get_jv_entries(self):
condition = self.get_conditions()
+
+ if self.get("cost_center"):
+ condition += " and t2.cost_center = '{0}' ".format(self.cost_center)
+
dr_or_cr = (
"credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
@@ -108,53 +122,78 @@ def get_jv_entries(self):
return list(journal_entries)
def get_dr_or_cr_notes(self):
- condition = self.get_conditions(get_return_invoices=True)
+ gl = qb.DocType("GL Entry")
+
+ voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
+ doc = qb.DocType(voucher_type)
+
+ # build conditions
+ sub_query_conditions = []
+ conditions = []
+ sub_query_conditions.append(doc.company == self.company)
+
+ if self.get("from_payment_date"):
+ sub_query_conditions.append(doc.posting_date.gte(self.from_payment_date))
+
+ if self.get("to_payment_date"):
+ sub_query_conditions.append(doc.posting_date.lte(self.to_payment_date))
+
+ if self.get("cost_center"):
+ sub_query_conditions.append(doc.cost_center == self.cost_center)
+
dr_or_cr = (
- "credit_in_account_currency"
+ gl["credit_in_account_currency"]
if erpnext.get_party_account_type(self.party_type) == "Receivable"
- else "debit_in_account_currency"
+ else gl["debit_in_account_currency"]
)
reconciled_dr_or_cr = (
- "debit_in_account_currency"
- if dr_or_cr == "credit_in_account_currency"
- else "credit_in_account_currency"
+ gl["debit_in_account_currency"]
+ if dr_or_cr.name == "credit_in_account_currency"
+ else gl["credit_in_account_currency"]
)
- voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
+ having_clause = qb.Field("amount") > 0
+
+ if self.minimum_payment_amount:
+ having_clause = qb.Field("amount") >= self.minimum_payment_amount
+ if self.maximum_payment_amount:
+ having_clause = having_clause & qb.Field("amount") <= self.maximum_payment_amount
+
+ sub_query = (
+ qb.from_(doc)
+ .select(doc.name)
+ .where(Criterion.all(sub_query_conditions))
+ .where(
+ (doc.docstatus == 1)
+ & (doc.is_return == 1)
+ & ((doc.return_against == "") | (doc.return_against.isnull()))
+ )
+ )
- return frappe.db.sql(
- """ SELECT doc.name as reference_name, %(voucher_type)s as reference_type,
- (sum(gl.{dr_or_cr}) - sum(gl.{reconciled_dr_or_cr})) as amount, doc.posting_date,
- account_currency as currency
- FROM `tab{doc}` doc, `tabGL Entry` gl
- WHERE
- (doc.name = gl.against_voucher or doc.name = gl.voucher_no)
- and doc.{party_type_field} = %(party)s
- and doc.is_return = 1 and ifnull(doc.return_against, "") = ""
- and gl.against_voucher_type = %(voucher_type)s
- and doc.docstatus = 1 and gl.party = %(party)s
- and gl.party_type = %(party_type)s and gl.account = %(account)s
- and gl.is_cancelled = 0 {condition}
- GROUP BY doc.name
- Having
- amount > 0
- ORDER BY doc.posting_date
- """.format(
- doc=voucher_type,
- dr_or_cr=dr_or_cr,
- reconciled_dr_or_cr=reconciled_dr_or_cr,
- party_type_field=frappe.scrub(self.party_type),
- condition=condition or "",
- ),
- {
- "party": self.party,
- "party_type": self.party_type,
- "voucher_type": voucher_type,
- "account": self.receivable_payable_account,
- },
- as_dict=1,
+ query = (
+ qb.from_(gl)
+ .select(
+ gl.against_voucher_type.as_("reference_type"),
+ gl.against_voucher.as_("reference_name"),
+ (Sum(dr_or_cr) - Sum(reconciled_dr_or_cr)).as_("amount"),
+ gl.posting_date,
+ gl.account_currency.as_("currency"),
+ )
+ .where(
+ (gl.against_voucher.isin(sub_query))
+ & (gl.against_voucher_type == voucher_type)
+ & (gl.is_cancelled == 0)
+ & (gl.account == self.receivable_payable_account)
+ & (gl.party_type == self.party_type)
+ & (gl.party == self.party)
+ )
+ .where(Criterion.all(conditions))
+ .groupby(gl.against_voucher)
+ .having(having_clause)
)
+ dr_cr_notes = query.run(as_dict=True)
+ return dr_cr_notes
def add_payment_entries(self, non_reconciled_payments):
self.set("payments", [])
@@ -168,6 +207,9 @@ def get_invoice_entries(self):
condition = self.get_conditions(get_invoices=True)
+ if self.get("cost_center"):
+ condition += " and cost_center = '{0}' ".format(self.cost_center)
+
non_reconciled_invoices = get_outstanding_invoices(
self.party_type, self.party, self.receivable_payable_account, condition=condition
)
@@ -190,6 +232,23 @@ def add_invoice_entries(self, non_reconciled_invoices):
inv.currency = entry.get("currency")
inv.outstanding_amount = flt(entry.get("outstanding_amount"))
+ def get_difference_amount(self, allocated_entry):
+ if allocated_entry.get("reference_type") != "Payment Entry":
+ return
+
+ dr_or_cr = (
+ "credit_in_account_currency"
+ if erpnext.get_party_account_type(self.party_type) == "Receivable"
+ else "debit_in_account_currency"
+ )
+
+ row = self.get_payment_details(allocated_entry, dr_or_cr)
+
+ doc = frappe.get_doc(allocated_entry.reference_type, allocated_entry.reference_name)
+ update_reference_in_payment_entry(row, doc, do_not_save=True)
+
+ return doc.difference_amount
+
@frappe.whitelist()
def allocate_entries(self, args):
self.validate_entries()
@@ -205,12 +264,16 @@ def allocate_entries(self, args):
res = self.get_allocated_entry(pay, inv, pay["amount"])
inv["outstanding_amount"] = flt(inv.get("outstanding_amount")) - flt(pay.get("amount"))
pay["amount"] = 0
+
+ res.difference_amount = self.get_difference_amount(res)
+
if pay.get("amount") == 0:
entries.append(res)
break
elif inv.get("outstanding_amount") == 0:
entries.append(res)
continue
+
else:
break
@@ -329,12 +392,9 @@ def validate_allocation(self):
if not invoices_to_reconcile:
frappe.throw(_("No records found in Allocation table"))
- def get_conditions(self, get_invoices=False, get_payments=False, get_return_invoices=False):
+ def get_conditions(self, get_invoices=False, get_payments=False):
condition = " and company = '{0}' ".format(self.company)
- if self.get("cost_center") and (get_invoices or get_payments or get_return_invoices):
- condition = " and cost_center = '{0}' ".format(self.cost_center)
-
if get_invoices:
condition += (
" and posting_date >= {0}".format(frappe.db.escape(self.from_invoice_date))
@@ -360,35 +420,7 @@ def get_conditions(self, get_invoices=False, get_payments=False, get_return_invo
condition += " and {dr_or_cr} <= {amount}".format(
dr_or_cr=dr_or_cr, amount=flt(self.maximum_invoice_amount)
)
-
- elif get_return_invoices:
- condition = " and doc.company = '{0}' ".format(self.company)
- condition += (
- " and doc.posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
- if self.from_payment_date
- else ""
- )
- condition += (
- " and doc.posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
- if self.to_payment_date
- else ""
- )
- dr_or_cr = (
- "debit_in_account_currency"
- if erpnext.get_party_account_type(self.party_type) == "Receivable"
- else "credit_in_account_currency"
- )
-
- if self.minimum_invoice_amount:
- condition += " and gl.{dr_or_cr} >= {amount}".format(
- dr_or_cr=dr_or_cr, amount=flt(self.minimum_payment_amount)
- )
- if self.maximum_invoice_amount:
- condition += " and gl.{dr_or_cr} <= {amount}".format(
- dr_or_cr=dr_or_cr, amount=flt(self.maximum_payment_amount)
- )
-
- else:
+ elif get_payments:
condition += (
" and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
if self.from_payment_date
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json
index 2ee356aaf403..2f3516e135a4 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.json
+++ b/erpnext/accounts/doctype/payment_request/payment_request.json
@@ -186,8 +186,10 @@
{
"fetch_from": "bank_account.bank",
"fieldname": "bank",
- "fieldtype": "Read Only",
- "label": "Bank"
+ "fieldtype": "Link",
+ "label": "Bank",
+ "options": "Bank",
+ "read_only": 1
},
{
"fetch_from": "bank_account.bank_account_no",
@@ -366,10 +368,11 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-09-18 12:24:14.178853",
+ "modified": "2022-09-30 16:19:43.680025",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Request",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
@@ -401,5 +404,6 @@
}
],
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json
index 6ed7a3154e52..dde9980ce533 100644
--- a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json
+++ b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json
@@ -39,6 +39,7 @@
{
"columns": 2,
"fetch_from": "payment_term.description",
+ "fetch_if_empty": 1,
"fieldname": "description",
"fieldtype": "Small Text",
"in_list_view": 1,
@@ -159,7 +160,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-04-28 05:41:35.084233",
+ "modified": "2022-09-16 13:57:06.382859",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Schedule",
@@ -168,5 +169,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
index 1d596c1bfbb9..e6d9fe2b54d0 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
@@ -25,7 +25,7 @@ frappe.ui.form.on('POS Closing Entry', {
frappe.realtime.on('closing_process_complete', async function(data) {
await frm.reload_doc();
- if (frm.doc.status == 'Failed' && frm.doc.error_message && data.user == frappe.session.user) {
+ if (frm.doc.status == 'Failed' && frm.doc.error_message) {
frappe.msgprint({
title: __('POS Closing Failed'),
message: frm.doc.error_message,
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
index b8500270d1ac..6458756957d3 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
@@ -1572,7 +1572,7 @@
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
- "modified": "2022-03-22 13:00:24.166684",
+ "modified": "2022-09-27 13:00:24.166684",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 59c7c843edf7..6f1434d429b4 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -239,14 +239,14 @@ def validate_stock_availablility(self):
frappe.bold(d.warehouse),
frappe.bold(d.qty),
)
- if flt(available_stock) <= 0:
+ if is_stock_item and flt(available_stock) <= 0:
frappe.throw(
_("Row #{}: Item Code: {} is not available under warehouse {}.").format(
d.idx, item_code, warehouse
),
title=_("Item Unavailable"),
)
- elif flt(available_stock) < flt(d.qty):
+ elif is_stock_item and flt(available_stock) < flt(d.qty):
frappe.throw(
_(
"Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}."
@@ -634,11 +634,12 @@ def get_stock_availability(item_code, warehouse):
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
return bin_qty - pos_sales_qty, is_stock_item
else:
- is_stock_item = False
+ is_stock_item = True
if frappe.db.exists("Product Bundle", item_code):
return get_bundle_availability(item_code, warehouse), is_stock_item
else:
- # Is a service item
+ is_stock_item = False
+ # Is a service item or non_stock item
return 0, is_stock_item
@@ -652,7 +653,9 @@ def get_bundle_availability(bundle_item_code, warehouse):
available_qty = item_bin_qty - item_pos_reserved_qty
max_available_bundles = available_qty / item.qty
- if bundle_bin_qty > max_available_bundles:
+ if bundle_bin_qty > max_available_bundles and frappe.get_value(
+ "Item", item.item_code, "is_stock_item"
+ ):
bundle_bin_qty = max_available_bundles
pos_sales_qty = get_pos_reserved_qty(bundle_item_code, warehouse)
@@ -744,3 +747,7 @@ def append_payment(payment_mode):
]:
payment_mode = get_mode_of_payment_info(mode_of_payment, doc.company)
append_payment(payment_mode[0])
+
+
+def on_doctype_update():
+ frappe.db.add_index("POS Invoice", ["return_against"])
diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
index 70f128e0e39a..3132fdd259a0 100644
--- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
@@ -495,6 +495,67 @@ def test_invalid_serial_no_validation(self):
self.assertRaises(frappe.ValidationError, pos.submit)
+ def test_value_error_on_serial_no_validation(self):
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
+
+ se = make_serialized_item(
+ company="_Test Company",
+ target_warehouse="Stores - _TC",
+ cost_center="Main - _TC",
+ expense_account="Cost of Goods Sold - _TC",
+ )
+ serial_nos = se.get("items")[0].serial_no
+
+ # make a pos invoice
+ pos = create_pos_invoice(
+ company="_Test Company",
+ debit_to="Debtors - _TC",
+ account_for_change_amount="Cash - _TC",
+ warehouse="Stores - _TC",
+ income_account="Sales - _TC",
+ expense_account="Cost of Goods Sold - _TC",
+ cost_center="Main - _TC",
+ item=se.get("items")[0].item_code,
+ rate=1000,
+ qty=1,
+ do_not_save=1,
+ )
+ pos.get("items")[0].has_serial_no = 1
+ pos.get("items")[0].serial_no = serial_nos.split("\n")[0]
+ pos.set("payments", [])
+ pos.append(
+ "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1}
+ )
+ pos = pos.save().submit()
+
+ # make a return
+ pos_return = make_sales_return(pos.name)
+ pos_return.paid_amount = pos_return.grand_total
+ pos_return.save()
+ pos_return.submit()
+
+ # set docstatus to 2 for pos to trigger this issue
+ frappe.db.set_value("POS Invoice", pos.name, "docstatus", 2)
+
+ pos2 = create_pos_invoice(
+ company="_Test Company",
+ debit_to="Debtors - _TC",
+ account_for_change_amount="Cash - _TC",
+ warehouse="Stores - _TC",
+ income_account="Sales - _TC",
+ expense_account="Cost of Goods Sold - _TC",
+ cost_center="Main - _TC",
+ item=se.get("items")[0].item_code,
+ rate=1000,
+ qty=1,
+ do_not_save=1,
+ )
+
+ pos2.get("items")[0].has_serial_no = 1
+ pos2.get("items")[0].serial_no = serial_nos.split("\n")[0]
+ # Value error should not be triggered on validation
+ pos2.save()
+
def test_loyalty_points(self):
from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
get_loyalty_program_details_with_points,
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
index f6002163f0f9..fc356f2378d0 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
@@ -432,7 +432,7 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
finally:
frappe.db.commit()
- frappe.publish_realtime("closing_process_complete", {"user": frappe.session.user})
+ frappe.publish_realtime("closing_process_complete", user=frappe.session.user)
def cancel_merge_logs(merge_logs, closing_entry=None):
@@ -459,7 +459,7 @@ def cancel_merge_logs(merge_logs, closing_entry=None):
finally:
frappe.db.commit()
- frappe.publish_realtime("closing_process_complete", {"user": frappe.session.user})
+ frappe.publish_realtime("closing_process_complete", user=frappe.session.user)
def enqueue_job(job, **kwargs):
diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
index 6ea244003c34..a75230a42a87 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
@@ -269,6 +269,18 @@ def get_serial_no_for_item(args):
return item_details
+def update_pricing_rule_uom(pricing_rule, args):
+ child_doc = {"Item Code": "items", "Item Group": "item_groups", "Brand": "brands"}.get(
+ pricing_rule.apply_on
+ )
+
+ apply_on_field = frappe.scrub(pricing_rule.apply_on)
+
+ for row in pricing_rule.get(child_doc):
+ if row.get(apply_on_field) == args.get(apply_on_field):
+ pricing_rule.uom = row.uom
+
+
def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=False):
from erpnext.accounts.doctype.pricing_rule.utils import (
get_applied_pricing_rules,
@@ -325,7 +337,8 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
if isinstance(pricing_rule, string_types):
pricing_rule = frappe.get_cached_doc("Pricing Rule", pricing_rule)
- pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule)
+ update_pricing_rule_uom(pricing_rule, args)
+ pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule) or []
if pricing_rule.get("suggestion"):
continue
@@ -439,12 +452,15 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
if pricing_rule.currency == args.currency:
pricing_rule_rate = pricing_rule.rate
+ # TODO https://github.com/frappe/erpnext/pull/23636 solve this in some other way.
if pricing_rule_rate:
+ is_blank_uom = pricing_rule.get("uom") != args.get("uom")
# Override already set price list rate (from item price)
# if pricing_rule_rate > 0
item_details.update(
{
- "price_list_rate": pricing_rule_rate * args.get("conversion_factor", 1),
+ "price_list_rate": pricing_rule_rate
+ * (args.get("conversion_factor", 1) if is_blank_uom else 1),
}
)
item_details.update({"discount_percentage": 0.0})
diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
index 5701402811ec..4b4bfb181888 100644
--- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
@@ -597,6 +597,255 @@ def test_item_price_with_pricing_rule(self):
frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete()
item.delete()
+ def test_item_price_with_blank_uom_pricing_rule(self):
+ properties = {
+ "item_code": "Item Blank UOM",
+ "stock_uom": "Nos",
+ "sales_uom": "Box",
+ "uoms": [dict(uom="Box", conversion_factor=10)],
+ }
+ item = make_item(properties=properties)
+
+ make_item_price("Item Blank UOM", "_Test Price List", 100)
+
+ pricing_rule_record = {
+ "doctype": "Pricing Rule",
+ "title": "_Test Item Blank UOM Rule",
+ "apply_on": "Item Code",
+ "items": [
+ {
+ "item_code": "Item Blank UOM",
+ }
+ ],
+ "selling": 1,
+ "currency": "INR",
+ "rate_or_discount": "Rate",
+ "rate": 101,
+ "company": "_Test Company",
+ }
+ rule = frappe.get_doc(pricing_rule_record)
+ rule.insert()
+
+ si = create_sales_invoice(
+ do_not_save=True, item_code="Item Blank UOM", uom="Box", conversion_factor=10
+ )
+ si.selling_price_list = "_Test Price List"
+ si.save()
+
+ # If UOM is blank consider it as stock UOM and apply pricing_rule on all UOM.
+ # rate is 101, Selling UOM is Box that have conversion_factor of 10 so 101 * 10 = 1010
+ self.assertEqual(si.items[0].price_list_rate, 1010)
+ self.assertEqual(si.items[0].rate, 1010)
+
+ si.delete()
+
+ si = create_sales_invoice(do_not_save=True, item_code="Item Blank UOM", uom="Nos")
+ si.selling_price_list = "_Test Price List"
+ si.save()
+
+ # UOM is blank so consider it as stock UOM and apply pricing_rule on all UOM.
+ # rate is 101, Selling UOM is Nos that have conversion_factor of 1 so 101 * 1 = 101
+ self.assertEqual(si.items[0].price_list_rate, 101)
+ self.assertEqual(si.items[0].rate, 101)
+
+ si.delete()
+ rule.delete()
+ frappe.get_doc("Item Price", {"item_code": "Item Blank UOM"}).delete()
+
+ item.delete()
+
+ def test_item_price_with_selling_uom_pricing_rule(self):
+ properties = {
+ "item_code": "Item UOM other than Stock",
+ "stock_uom": "Nos",
+ "sales_uom": "Box",
+ "uoms": [dict(uom="Box", conversion_factor=10)],
+ }
+ item = make_item(properties=properties)
+
+ make_item_price("Item UOM other than Stock", "_Test Price List", 100)
+
+ pricing_rule_record = {
+ "doctype": "Pricing Rule",
+ "title": "_Test Item UOM other than Stock Rule",
+ "apply_on": "Item Code",
+ "items": [
+ {
+ "item_code": "Item UOM other than Stock",
+ "uom": "Box",
+ }
+ ],
+ "selling": 1,
+ "currency": "INR",
+ "rate_or_discount": "Rate",
+ "rate": 101,
+ "company": "_Test Company",
+ }
+ rule = frappe.get_doc(pricing_rule_record)
+ rule.insert()
+
+ si = create_sales_invoice(
+ do_not_save=True, item_code="Item UOM other than Stock", uom="Box", conversion_factor=10
+ )
+ si.selling_price_list = "_Test Price List"
+ si.save()
+
+ # UOM is Box so apply pricing_rule only on Box UOM.
+ # Selling UOM is Box and as both UOM are same no need to multiply by conversion_factor.
+ self.assertEqual(si.items[0].price_list_rate, 101)
+ self.assertEqual(si.items[0].rate, 101)
+
+ si.delete()
+
+ si = create_sales_invoice(do_not_save=True, item_code="Item UOM other than Stock", uom="Nos")
+ si.selling_price_list = "_Test Price List"
+ si.save()
+
+ # UOM is Box so pricing_rule won't apply as selling_uom is Nos.
+ # As Pricing Rule is not applied price of 100 will be fetched from Item Price List.
+ self.assertEqual(si.items[0].price_list_rate, 100)
+ self.assertEqual(si.items[0].rate, 100)
+
+ si.delete()
+ rule.delete()
+ frappe.get_doc("Item Price", {"item_code": "Item UOM other than Stock"}).delete()
+
+ item.delete()
+
+ def test_item_group_price_with_blank_uom_pricing_rule(self):
+ group = frappe.get_doc(
+ doctype="Item Group",
+ item_group_name="_Test Pricing Rule Item Group",
+ parent_item_group="All Item Groups",
+ )
+ group.save()
+ properties = {
+ "item_code": "Item with Group Blank UOM",
+ "item_group": "_Test Pricing Rule Item Group",
+ "stock_uom": "Nos",
+ "sales_uom": "Box",
+ "uoms": [dict(uom="Box", conversion_factor=10)],
+ }
+ item = make_item(properties=properties)
+
+ make_item_price("Item with Group Blank UOM", "_Test Price List", 100)
+
+ pricing_rule_record = {
+ "doctype": "Pricing Rule",
+ "title": "_Test Item with Group Blank UOM Rule",
+ "apply_on": "Item Group",
+ "item_groups": [
+ {
+ "item_group": "_Test Pricing Rule Item Group",
+ }
+ ],
+ "selling": 1,
+ "currency": "INR",
+ "rate_or_discount": "Rate",
+ "rate": 101,
+ "company": "_Test Company",
+ }
+ rule = frappe.get_doc(pricing_rule_record)
+ rule.insert()
+
+ si = create_sales_invoice(
+ do_not_save=True, item_code="Item with Group Blank UOM", uom="Box", conversion_factor=10
+ )
+ si.selling_price_list = "_Test Price List"
+ si.save()
+
+ # If UOM is blank consider it as stock UOM and apply pricing_rule on all UOM.
+ # rate is 101, Selling UOM is Box that have conversion_factor of 10 so 101 * 10 = 1010
+ self.assertEqual(si.items[0].price_list_rate, 1010)
+ self.assertEqual(si.items[0].rate, 1010)
+
+ si.delete()
+
+ si = create_sales_invoice(do_not_save=True, item_code="Item with Group Blank UOM", uom="Nos")
+ si.selling_price_list = "_Test Price List"
+ si.save()
+
+ # UOM is blank so consider it as stock UOM and apply pricing_rule on all UOM.
+ # rate is 101, Selling UOM is Nos that have conversion_factor of 1 so 101 * 1 = 101
+ self.assertEqual(si.items[0].price_list_rate, 101)
+ self.assertEqual(si.items[0].rate, 101)
+
+ si.delete()
+ rule.delete()
+ frappe.get_doc("Item Price", {"item_code": "Item with Group Blank UOM"}).delete()
+ item.delete()
+ group.delete()
+
+ def test_item_group_price_with_selling_uom_pricing_rule(self):
+ group = frappe.get_doc(
+ doctype="Item Group",
+ item_group_name="_Test Pricing Rule Item Group UOM",
+ parent_item_group="All Item Groups",
+ )
+ group.save()
+ properties = {
+ "item_code": "Item with Group UOM other than Stock",
+ "item_group": "_Test Pricing Rule Item Group UOM",
+ "stock_uom": "Nos",
+ "sales_uom": "Box",
+ "uoms": [dict(uom="Box", conversion_factor=10)],
+ }
+ item = make_item(properties=properties)
+
+ make_item_price("Item with Group UOM other than Stock", "_Test Price List", 100)
+
+ pricing_rule_record = {
+ "doctype": "Pricing Rule",
+ "title": "_Test Item with Group UOM other than Stock Rule",
+ "apply_on": "Item Group",
+ "item_groups": [
+ {
+ "item_group": "_Test Pricing Rule Item Group UOM",
+ "uom": "Box",
+ }
+ ],
+ "selling": 1,
+ "currency": "INR",
+ "rate_or_discount": "Rate",
+ "rate": 101,
+ "company": "_Test Company",
+ }
+ rule = frappe.get_doc(pricing_rule_record)
+ rule.insert()
+
+ si = create_sales_invoice(
+ do_not_save=True,
+ item_code="Item with Group UOM other than Stock",
+ uom="Box",
+ conversion_factor=10,
+ )
+ si.selling_price_list = "_Test Price List"
+ si.save()
+
+ # UOM is Box so apply pricing_rule only on Box UOM.
+ # Selling UOM is Box and as both UOM are same no need to multiply by conversion_factor.
+ self.assertEqual(si.items[0].price_list_rate, 101)
+ self.assertEqual(si.items[0].rate, 101)
+
+ si.delete()
+
+ si = create_sales_invoice(
+ do_not_save=True, item_code="Item with Group UOM other than Stock", uom="Nos"
+ )
+ si.selling_price_list = "_Test Price List"
+ si.save()
+
+ # UOM is Box so pricing_rule won't apply as selling_uom is Nos.
+ # As Pricing Rule is not applied price of 100 will be fetched from Item Price List.
+ self.assertEqual(si.items[0].price_list_rate, 100)
+ self.assertEqual(si.items[0].rate, 100)
+
+ si.delete()
+ rule.delete()
+ frappe.get_doc("Item Price", {"item_code": "Item with Group UOM other than Stock"}).delete()
+ item.delete()
+ group.delete()
+
def test_pricing_rule_for_different_currency(self):
make_item("Test Sanitizer Item")
diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py
index 70926cfbd72e..984a8bae008d 100644
--- a/erpnext/accounts/doctype/pricing_rule/utils.py
+++ b/erpnext/accounts/doctype/pricing_rule/utils.py
@@ -111,6 +111,12 @@ def _get_pricing_rules(apply_on, args, values):
)
if apply_on_field == "item_code":
+ if args.get("uom", None):
+ item_conditions += (
+ " and ({child_doc}.uom='{item_uom}' or IFNULL({child_doc}.uom, '')='')".format(
+ child_doc=child_doc, item_uom=args.get("uom")
+ )
+ )
if "variant_of" not in args:
args.variant_of = frappe.get_cached_value("Item", args.item_code, "variant_of")
@@ -121,7 +127,12 @@ def _get_pricing_rules(apply_on, args, values):
values["variant_of"] = args.variant_of
elif apply_on_field == "item_group":
item_conditions = _get_tree_conditions(args, "Item Group", child_doc, False)
-
+ if args.get("uom", None):
+ item_conditions += (
+ " and ({child_doc}.uom='{item_uom}' or IFNULL({child_doc}.uom, '')='')".format(
+ child_doc=child_doc, item_uom=args.get("uom")
+ )
+ )
conditions += get_other_conditions(conditions, values, args)
warehouse_conditions = _get_tree_conditions(args, "Warehouse", "`tabPricing Rule`")
if warehouse_conditions:
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
index 82705a9cea46..0da44a464e76 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
@@ -25,7 +25,7 @@
-
+
{{ _("Date") }} |
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
index 29f2e98e7799..7dd5ef36f299 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
@@ -8,7 +8,8 @@ frappe.ui.form.on('Process Statement Of Accounts', {
},
refresh: function(frm){
if(!frm.doc.__islocal) {
- frm.add_custom_button('Send Emails',function(){
+ frm.add_custom_button(__('Send Emails'), function(){
+ if (frm.is_dirty()) frappe.throw(__("Please save before proceeding."))
frappe.call({
method: "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_emails",
args: {
@@ -24,8 +25,9 @@ frappe.ui.form.on('Process Statement Of Accounts', {
}
});
});
- frm.add_custom_button('Download',function(){
- var url = frappe.urllib.get_full_url(
+ frm.add_custom_button(__('Download'), function(){
+ if (frm.is_dirty()) frappe.throw(__("Please save before proceeding."))
+ let url = frappe.urllib.get_full_url(
'/api/method/erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.download_statements?'
+ 'document_name='+encodeURIComponent(frm.doc.name))
$.ajax({
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
index a35374c62425..2f62a0295fbb 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
@@ -27,6 +27,7 @@
"customers",
"preferences",
"orientation",
+ "include_break",
"include_ageing",
"ageing_based_on",
"section_break_14",
@@ -284,10 +285,16 @@
"fieldtype": "Link",
"label": "Terms and Conditions",
"options": "Terms and Conditions"
+ },
+ {
+ "default": "1",
+ "fieldname": "include_break",
+ "fieldtype": "Check",
+ "label": "Page Break After Each SoA"
}
],
"links": [],
- "modified": "2021-09-06 21:00:45.732505",
+ "modified": "2022-10-17 17:47:08.662475",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",
@@ -320,5 +327,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
index 01f716daa211..c6b0c57ce5cd 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
@@ -6,6 +6,7 @@
import frappe
from frappe import _
+from frappe.desk.reportview import get_match_cond
from frappe.model.document import Document
from frappe.utils import add_days, add_months, format_date, getdate, today
from frappe.utils.jinja import validate_template
@@ -128,7 +129,8 @@ def get_report_pdf(doc, consolidated=True):
if not bool(statement_dict):
return False
elif consolidated:
- result = "".join(list(statement_dict.values()))
+ delimiter = '' if doc.include_break else ""
+ result = delimiter.join(list(statement_dict.values()))
return get_pdf(result, {"orientation": doc.orientation})
else:
for customer, statement_html in statement_dict.items():
@@ -240,8 +242,6 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
if int(primary_mandatory):
if primary_email == "":
continue
- elif (billing_email == "") and (primary_email == ""):
- continue
customer_list.append(
{"name": customer.name, "primary_email": primary_email, "billing_email": billing_email}
@@ -273,8 +273,12 @@ def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=Tr
link.link_doctype='Customer'
and link.link_name=%s
and contact.is_billing_contact=1
+ {mcond}
ORDER BY
- contact.creation desc""",
+ contact.creation desc
+ """.format(
+ mcond=get_match_cond("Contact")
+ ),
customer_name,
)
@@ -313,6 +317,8 @@ def send_emails(document_name, from_scheduler=False):
attachments = [{"fname": customer + ".pdf", "fcontent": report_pdf}]
recipients, cc = get_recipients_and_cc(customer, doc)
+ if not recipients:
+ continue
context = get_context(customer, doc)
subject = frappe.render_template(doc.subject, context)
message = frappe.render_template(doc.body, context)
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index aed8cb56d1be..0208975513b0 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -31,7 +31,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
this._super();
// Ignore linked advances
- this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry'];
+ this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice'];
if(!this.frm.doc.__islocal) {
// show credit_to in print format
@@ -569,6 +569,10 @@ frappe.ui.form.on("Purchase Invoice", {
erpnext.queries.setup_queries(frm, "Warehouse", function() {
return erpnext.queries.warehouse(frm.doc);
});
+
+ if (frm.is_new()) {
+ frm.clear_table("tax_withheld_vouchers");
+ }
},
is_subcontracted: function(frm) {
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index 971a2079dda1..99b9f14e5c84 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -84,6 +84,8 @@
"section_break_51",
"taxes_and_charges",
"taxes",
+ "tax_withheld_vouchers_section",
+ "tax_withheld_vouchers",
"sec_tax_breakup",
"other_charges_calculation",
"totals",
@@ -512,7 +514,6 @@
"fieldname": "ignore_pricing_rule",
"fieldtype": "Check",
"label": "Ignore Pricing Rule",
- "no_copy": 1,
"permlevel": 1,
"print_hide": 1
},
@@ -1427,13 +1428,26 @@
"label": "Advance Tax",
"options": "Advance Tax",
"read_only": 1
+ },
+ {
+ "fieldname": "tax_withheld_vouchers_section",
+ "fieldtype": "Section Break",
+ "label": "Tax Withheld Vouchers"
+ },
+ {
+ "fieldname": "tax_withheld_vouchers",
+ "fieldtype": "Table",
+ "label": "Tax Withheld Vouchers",
+ "no_copy": 1,
+ "options": "Tax Withheld Vouchers",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
- "modified": "2021-11-25 13:31:02.716727",
+ "modified": "2022-10-07 14:19:14.214157",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
@@ -1493,7 +1507,8 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"timeline_field": "supplier",
"title_field": "title",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 75508be8d090..9616e93a0205 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -67,6 +67,9 @@ def onload(self):
supplier_tds = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
self.set_onload("supplier_tds", supplier_tds)
+ if self.is_new():
+ self.set("tax_withheld_vouchers", [])
+
def before_save(self):
if not self.on_hold:
self.release_date = ""
@@ -695,6 +698,10 @@ def make_item_gl_entries(self, gl_entries):
)
)
+ credit_amount = item.base_net_amount
+ if self.is_internal_supplier and item.valuation_rate:
+ credit_amount = flt(item.valuation_rate * item.stock_qty)
+
# Intentionally passed negative debit amount to avoid incorrect GL Entry validation
gl_entries.append(
self.get_gl_dict(
@@ -704,7 +711,7 @@ def make_item_gl_entries(self, gl_entries):
"cost_center": item.cost_center,
"project": item.project or self.project,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "debit": -1 * flt(item.base_net_amount, item.precision("base_net_amount")),
+ "debit": -1 * flt(credit_amount, item.precision("base_net_amount")),
},
warehouse_account[item.from_warehouse]["account_currency"],
item=item,
@@ -1364,7 +1371,14 @@ def on_cancel(self):
frappe.db.set(self, "status", "Cancelled")
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
- self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
+
+ self.ignore_linked_doctypes = (
+ "GL Entry",
+ "Stock Ledger Entry",
+ "Repost Item Valuation",
+ "Tax Withheld Vouchers",
+ )
+
self.update_advance_tax_references(cancel=1)
def update_project(self):
@@ -1457,7 +1471,7 @@ def set_tax_withholding(self):
if not self.tax_withholding_category:
return
- tax_withholding_details, advance_taxes = get_party_tax_withholding_details(
+ tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details(
self, self.tax_withholding_category
)
@@ -1486,6 +1500,19 @@ def set_tax_withholding(self):
for d in to_remove:
self.remove(d)
+ ## Add pending vouchers on which tax was withheld
+ self.set("tax_withheld_vouchers", [])
+
+ for voucher_no, voucher_details in voucher_wise_amount.items():
+ self.append(
+ "tax_withheld_vouchers",
+ {
+ "voucher_name": voucher_no,
+ "voucher_type": voucher_details.get("voucher_type"),
+ "taxable_amount": voucher_details.get("amount"),
+ },
+ )
+
# calculate totals again after applying TDS
self.calculate_taxes_and_totals()
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 18b0636becab..60611b0edbbd 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -1549,6 +1549,37 @@ def test_item_less_defaults(self):
pi.save()
self.assertEqual(pi.items[0].conversion_factor, 1000)
+ def test_batch_expiry_for_purchase_invoice(self):
+ from erpnext.controllers.sales_and_purchase_return import make_return_doc
+
+ item = self.make_item(
+ "_Test Batch Item For Return Check",
+ {
+ "is_purchase_item": 1,
+ "is_stock_item": 1,
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ "batch_number_series": "TBIRC.#####",
+ },
+ )
+
+ pi = make_purchase_invoice(
+ qty=1,
+ item_code=item.name,
+ update_stock=True,
+ )
+
+ pi.load_from_db()
+ batch_no = pi.items[0].batch_no
+ self.assertTrue(batch_no)
+
+ frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(nowdate(), -1))
+
+ return_pi = make_return_doc(pi.doctype, pi.name)
+ return_pi.save().submit()
+
+ self.assertTrue(return_pi.docstatus == 1)
+
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql(
diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
index 4b12f9954e8d..c0afa7fe6910 100644
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
@@ -706,6 +706,7 @@
"label": "Valuation Rate",
"no_copy": 1,
"options": "Company:company:default_currency",
+ "precision": "6",
"print_hide": 1,
"read_only": 1
},
@@ -871,7 +872,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-11-15 17:04:07.191013",
+ "modified": "2022-10-12 03:37:29.032732",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index bbbaa67c8e9a..cd3434a7756d 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -651,7 +651,6 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Ignore Pricing Rule",
- "no_copy": 1,
"print_hide": 1
},
{
@@ -2046,7 +2045,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2022-07-11 17:43:56.435382",
+ "modified": "2022-09-16 17:44:22.227332",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 1badb8093411..72e9790700f1 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -2187,6 +2187,9 @@ def update_details(source_doc, target_doc, source_parent):
update_address(
target_doc, "shipping_address", "shipping_address_display", source_doc.customer_address
)
+ update_address(
+ target_doc, "billing_address", "billing_address_display", source_doc.customer_address
+ )
if currency:
target_doc.currency = currency
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 2df095a41a06..79a67b836314 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -7,7 +7,7 @@
import frappe
from frappe.model.dynamic_links import get_dynamic_link_map
from frappe.model.naming import make_autoname
-from frappe.utils import add_days, flt, getdate, nowdate
+from frappe.utils import add_days, flt, getdate, nowdate, today
from six import iteritems
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
@@ -31,10 +31,20 @@
get_qty_after_transaction,
make_stock_entry,
)
-from erpnext.stock.utils import get_incoming_rate
+from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
+ create_stock_reconciliation,
+)
+from erpnext.stock.utils import get_incoming_rate, get_stock_balance
class TestSalesInvoice(unittest.TestCase):
+ def setUp(self):
+ from erpnext.stock.doctype.stock_ledger_entry.test_stock_ledger_entry import create_items
+
+ create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}])
+ create_internal_parties()
+ setup_accounts()
+
def make(self):
w = frappe.copy_doc(test_records[0])
w.is_pos = 0
@@ -906,7 +916,8 @@ def test_pos_returns_with_repayment(self):
pos_return.insert()
pos_return.submit()
- self.assertEqual(pos_return.get("payments")[0].amount, -1000)
+ self.assertEqual(pos_return.get("payments")[0].amount, -500)
+ self.assertEqual(pos_return.get("payments")[1].amount, -500)
def test_pos_change_amount(self):
make_pos_profile(
@@ -1687,7 +1698,7 @@ def test_create_so_with_margin(self):
si.save()
self.assertEqual(si.get("items")[0].rate, flt((price_list_rate * 25) / 100 + price_list_rate))
- def test_outstanding_amount_after_advance_jv_cancelation(self):
+ def test_outstanding_amount_after_advance_jv_cancellation(self):
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
test_records as jv_test_records,
)
@@ -1731,7 +1742,7 @@ def test_outstanding_amount_after_advance_jv_cancelation(self):
flt(si.rounded_total + si.total_advance, si.precision("outstanding_amount")),
)
- def test_outstanding_amount_after_advance_payment_entry_cancelation(self):
+ def test_outstanding_amount_after_advance_payment_entry_cancellation(self):
pe = frappe.get_doc(
{
"doctype": "Payment Entry",
@@ -2369,29 +2380,6 @@ def test_fixed_deferred_revenue(self):
acc_settings.save()
def test_inter_company_transaction(self):
- from erpnext.selling.doctype.customer.test_customer import create_internal_customer
-
- create_internal_customer(
- customer_name="_Test Internal Customer",
- represents_company="_Test Company 1",
- allowed_to_interact_with="Wind Power LLC",
- )
-
- if not frappe.db.exists("Supplier", "_Test Internal Supplier"):
- supplier = frappe.get_doc(
- {
- "supplier_group": "_Test Supplier Group",
- "supplier_name": "_Test Internal Supplier",
- "doctype": "Supplier",
- "is_internal_supplier": 1,
- "represents_company": "Wind Power LLC",
- }
- )
-
- supplier.append("companies", {"company": "_Test Company 1"})
-
- supplier.insert()
-
si = create_sales_invoice(
company="Wind Power LLC",
customer="_Test Internal Customer",
@@ -2451,34 +2439,9 @@ def test_sle_for_target_warehouse(self):
se.cancel()
def test_internal_transfer_gl_entry(self):
- ## Create internal transfer account
- from erpnext.selling.doctype.customer.test_customer import create_internal_customer
-
- account = create_account(
- account_name="Unrealized Profit",
- parent_account="Current Liabilities - TCP1",
- company="_Test Company with perpetual inventory",
- )
-
- frappe.db.set_value(
- "Company", "_Test Company with perpetual inventory", "unrealized_profit_loss_account", account
- )
-
- customer = create_internal_customer(
- "_Test Internal Customer 2",
- "_Test Company with perpetual inventory",
- "_Test Company with perpetual inventory",
- )
-
- create_internal_supplier(
- "_Test Internal Supplier 2",
- "_Test Company with perpetual inventory",
- "_Test Company with perpetual inventory",
- )
-
si = create_sales_invoice(
company="_Test Company with perpetual inventory",
- customer=customer,
+ customer="_Test Internal Customer 2",
debit_to="Debtors - TCP1",
warehouse="Stores - TCP1",
income_account="Sales - TCP1",
@@ -2492,7 +2455,7 @@ def test_internal_transfer_gl_entry(self):
si.update_stock = 1
si.items[0].target_warehouse = "Work In Progress - TCP1"
- # Add stock to stores for succesful stock transfer
+ # Add stock to stores for successful stock transfer
make_stock_entry(
target="Stores - TCP1", company="_Test Company with perpetual inventory", qty=1, basic_rate=100
)
@@ -2830,6 +2793,77 @@ def test_einvoice_without_discounts(self):
self.assertEqual(einvoice["ItemList"][1]["Discount"], 0)
self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 20)
+ def test_internal_transfer_gl_precision_issues(self):
+ # Make a stock queue of an item with two valuations
+
+ # Remove all existing stock for this
+ if get_stock_balance("_Test Internal Transfer Item", "Stores - TCP1", "2022-04-10"):
+ create_stock_reconciliation(
+ item_code="_Test Internal Transfer Item",
+ warehouse="Stores - TCP1",
+ qty=0,
+ rate=0,
+ company="_Test Company with perpetual inventory",
+ expense_account="Stock Adjustment - TCP1"
+ if frappe.get_all("Stock Ledger Entry")
+ else "Temporary Opening - TCP1",
+ posting_date="2020-04-10",
+ posting_time="14:00",
+ )
+
+ make_stock_entry(
+ item_code="_Test Internal Transfer Item",
+ target="Stores - TCP1",
+ qty=9000000,
+ basic_rate=52.0,
+ posting_date="2020-04-10",
+ posting_time="14:00",
+ )
+ make_stock_entry(
+ item_code="_Test Internal Transfer Item",
+ target="Stores - TCP1",
+ qty=60000000,
+ basic_rate=52.349777,
+ posting_date="2020-04-10",
+ posting_time="14:00",
+ )
+
+ # Make an internal transfer Sales Invoice Stock in non stock uom to check
+ # for rounding errors while converting to stock uom
+ si = create_sales_invoice(
+ company="_Test Company with perpetual inventory",
+ customer="_Test Internal Customer 2",
+ item_code="_Test Internal Transfer Item",
+ qty=5000000,
+ uom="Box",
+ debit_to="Debtors - TCP1",
+ warehouse="Stores - TCP1",
+ income_account="Sales - TCP1",
+ expense_account="Cost of Goods Sold - TCP1",
+ cost_center="Main - TCP1",
+ currency="INR",
+ do_not_save=1,
+ )
+
+ # Check GL Entries with precision
+ si.update_stock = 1
+ si.items[0].target_warehouse = "Work In Progress - TCP1"
+ si.items[0].conversion_factor = 10
+ si.save()
+ si.submit()
+
+ # Check if adjustment entry is created
+ self.assertTrue(
+ frappe.db.exists(
+ "GL Entry",
+ {
+ "voucher_type": "Sales Invoice",
+ "voucher_no": si.name,
+ "remarks": "Rounding gain/loss Entry for Stock Transfer",
+ },
+ )
+ )
+
def test_item_tax_net_range(self):
item = create_item("T Shirt")
@@ -3278,7 +3312,7 @@ def test_multi_currency_deferred_revenue_via_journal_entry(self):
[deferred_account, 2022.47, 0.0, "2019-03-15"],
]
- gl_entries = gl_entries = frappe.db.sql(
+ gl_entries = frappe.db.sql(
"""select account, debit, credit, posting_date
from `tabGL Entry`
where voucher_type='Journal Entry' and voucher_detail_no=%s and posting_date <= %s
@@ -3396,6 +3430,37 @@ def test_gain_loss_with_advance_entry(self):
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled
)
+ def test_batch_expiry_for_sales_invoice_return(self):
+ from erpnext.controllers.sales_and_purchase_return import make_return_doc
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ item = make_item(
+ "_Test Batch Item For Return Check",
+ {
+ "is_purchase_item": 1,
+ "is_stock_item": 1,
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ "batch_number_series": "TBIRC.#####",
+ },
+ )
+
+ pr = make_purchase_receipt(qty=1, item_code=item.name)
+
+ batch_no = pr.items[0].batch_no
+ si = create_sales_invoice(qty=1, item_code=item.name, update_stock=1, batch_no=batch_no)
+
+ si.load_from_db()
+ batch_no = si.items[0].batch_no
+ self.assertTrue(batch_no)
+
+ frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1))
+
+ return_si = make_return_doc(si.doctype, si.name)
+ return_si.save().submit()
+
+ self.assertTrue(return_si.docstatus == 1)
+
def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill()
@@ -3652,6 +3717,7 @@ def create_sales_invoice(**args):
"description": args.description or "_Test Item",
"gst_hsn_code": "999800",
"warehouse": args.warehouse or "_Test Warehouse - _TC",
+ "target_warehouse": args.target_warehouse,
"qty": args.qty or 1,
"uom": args.uom or "Nos",
"stock_uom": args.uom or "Nos",
@@ -3664,8 +3730,9 @@ def create_sales_invoice(**args):
"discount_amount": args.discount_amount or 0,
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"serial_no": args.serial_no,
- "conversion_factor": 1,
+ "conversion_factor": args.get("conversion_factor", 1),
"incoming_rate": args.incoming_rate or 0,
+ "batch_no": args.batch_no or None,
},
)
@@ -3777,6 +3844,34 @@ def get_taxes_and_charges():
]
+def create_internal_parties():
+ from erpnext.selling.doctype.customer.test_customer import create_internal_customer
+
+ create_internal_customer(
+ customer_name="_Test Internal Customer",
+ represents_company="_Test Company 1",
+ allowed_to_interact_with="Wind Power LLC",
+ )
+
+ create_internal_customer(
+ customer_name="_Test Internal Customer 2",
+ represents_company="_Test Company with perpetual inventory",
+ allowed_to_interact_with="_Test Company with perpetual inventory",
+ )
+
+ create_internal_supplier(
+ supplier_name="_Test Internal Supplier",
+ represents_company="Wind Power LLC",
+ allowed_to_interact_with="_Test Company 1",
+ )
+
+ create_internal_supplier(
+ supplier_name="_Test Internal Supplier 2",
+ represents_company="_Test Company with perpetual inventory",
+ allowed_to_interact_with="_Test Company with perpetual inventory",
+ )
+
+
def create_internal_supplier(supplier_name, represents_company, allowed_to_interact_with):
if not frappe.db.exists("Supplier", supplier_name):
supplier = frappe.get_doc(
@@ -3799,6 +3894,19 @@ def create_internal_supplier(supplier_name, represents_company, allowed_to_inter
return supplier_name
+def setup_accounts():
+ ## Create internal transfer account
+ account = create_account(
+ account_name="Unrealized Profit",
+ parent_account="Current Liabilities - TCP1",
+ company="_Test Company with perpetual inventory",
+ )
+
+ frappe.db.set_value(
+ "Company", "_Test Company with perpetual inventory", "unrealized_profit_loss_account", account
+ )
+
+
def add_taxes(doc):
doc.append(
"taxes",
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 7bb564b066a5..4fbb4ac1b50f 100644
--- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
+++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
@@ -812,6 +812,8 @@
"fieldtype": "Currency",
"label": "Incoming Rate (Costing)",
"no_copy": 1,
+ "options": "Company:company:default_currency",
+ "precision": "6",
"print_hide": 1
},
{
@@ -841,7 +843,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2022-08-26 12:06:31.205417",
+ "modified": "2022-12-28 16:17:33.484531",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
diff --git a/erpnext/www/book-appointment/__init__.py b/erpnext/accounts/doctype/tax_withheld_vouchers/__init__.py
similarity index 100%
rename from erpnext/www/book-appointment/__init__.py
rename to erpnext/accounts/doctype/tax_withheld_vouchers/__init__.py
diff --git a/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json
new file mode 100644
index 000000000000..a573e7fc763d
--- /dev/null
+++ b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json
@@ -0,0 +1,49 @@
+{
+ "actions": [],
+ "autoname": "autoincrement",
+ "creation": "2022-09-13 16:18:59.404842",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "voucher_type",
+ "voucher_name",
+ "taxable_amount"
+ ],
+ "fields": [
+ {
+ "fieldname": "voucher_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Voucher Type",
+ "options": "DocType"
+ },
+ {
+ "fieldname": "voucher_name",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "label": "Voucher Name",
+ "options": "voucher_type"
+ },
+ {
+ "fieldname": "taxable_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Taxable Amount",
+ "options": "Company:company:default_currency"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2022-12-28 23:40:41.479208",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Tax Withheld Vouchers",
+ "naming_rule": "Random",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.py b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.py
new file mode 100644
index 000000000000..ea54c5403a8b
--- /dev/null
+++ b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class TaxWithheldVouchers(Document):
+ pass
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
index 3db21dc5a418..08ca96afb9f4 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -100,7 +100,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
).format(tax_withholding_category, inv.company, party)
)
- tax_amount, tax_deducted, tax_deducted_on_advances = get_tax_amount(
+ tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount = get_tax_amount(
party_type, parties, inv, tax_details, posting_date, pan_no
)
@@ -110,7 +110,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
if inv.doctype == "Purchase Invoice":
- return tax_row, tax_deducted_on_advances
+ return tax_row, tax_deducted_on_advances, voucher_wise_amount
else:
return tax_row
@@ -208,7 +208,9 @@ def get_lower_deduction_certificate(tax_details, pan_no):
def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=None):
- vouchers = get_invoice_vouchers(parties, tax_details, inv.company, party_type=party_type)
+ vouchers, voucher_wise_amount = get_invoice_vouchers(
+ parties, tax_details, inv.company, party_type=party_type
+ )
advance_vouchers = get_advance_vouchers(
parties,
company=inv.company,
@@ -227,6 +229,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
tax_deducted = get_deducted_tax(taxable_vouchers, tax_details)
tax_amount = 0
+
if party_type == "Supplier":
ldc = get_lower_deduction_certificate(tax_details, pan_no)
if tax_deducted:
@@ -237,6 +240,9 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
)
else:
tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
+
+ # once tds is deducted, not need to add vouchers in the invoice
+ voucher_wise_amount = {}
else:
tax_amount = get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers)
@@ -252,12 +258,13 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
if cint(tax_details.round_off_tax_amount):
tax_amount = round(tax_amount)
- return tax_amount, tax_deducted, tax_deducted_on_advances
+ return tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount
def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
- dr_or_cr = "credit" if party_type == "Supplier" else "debit"
doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice"
+ voucher_wise_amount = {}
+ vouchers = []
filters = {
"company": company,
@@ -272,29 +279,40 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
{"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")}
)
- invoices = frappe.get_all(doctype, filters=filters, pluck="name") or [""]
+ invoices_details = frappe.get_all(doctype, filters=filters, fields=["name", "base_net_total"])
- journal_entries = frappe.db.sql(
+ for d in invoices_details:
+ vouchers.append(d.name)
+ voucher_wise_amount.update({d.name: {"amount": d.base_net_total, "voucher_type": doctype}})
+
+ journal_entries_details = frappe.db.sql(
"""
- SELECT j.name
+ SELECT j.name, ja.credit - ja.debit AS amount
FROM `tabJournal Entry` j, `tabJournal Entry Account` ja
WHERE
- j.docstatus = 1
+ j.name = ja.parent
+ AND j.docstatus = 1
AND j.is_opening = 'No'
AND j.posting_date between %s and %s
- AND ja.{dr_or_cr} > 0
AND ja.party in %s
- """.format(
- dr_or_cr=dr_or_cr
+ AND j.apply_tds = 1
+ AND j.tax_withholding_category = %s
+ """,
+ (
+ tax_details.from_date,
+ tax_details.to_date,
+ tuple(parties),
+ tax_details.get("tax_withholding_category"),
),
- (tax_details.from_date, tax_details.to_date, tuple(parties)),
- as_list=1,
+ as_dict=1,
)
- if journal_entries:
- journal_entries = journal_entries[0]
+ if journal_entries_details:
+ for d in journal_entries_details:
+ vouchers.append(d.name)
+ voucher_wise_amount.update({d.name: {"amount": d.amount, "voucher_type": "Journal Entry"}})
- return invoices + journal_entries
+ return vouchers, voucher_wise_amount
def get_advance_vouchers(
@@ -311,6 +329,9 @@ def get_advance_vouchers(
"party": ["in", parties],
}
+ if party_type == "Customer":
+ filters.update({"against_voucher": ["is", "not set"]})
+
if company:
filters["company"] = company
if from_date and to_date:
@@ -320,23 +341,25 @@ def get_advance_vouchers(
def get_taxes_deducted_on_advances_allocated(inv, tax_details):
- advances = [d.reference_name for d in inv.get("advances")]
tax_info = []
- if advances:
- pe = frappe.qb.DocType("Payment Entry").as_("pe")
- at = frappe.qb.DocType("Advance Taxes and Charges").as_("at")
-
- tax_info = (
- frappe.qb.from_(at)
- .inner_join(pe)
- .on(pe.name == at.parent)
- .select(at.parent, at.name, at.tax_amount, at.allocated_amount)
- .where(pe.tax_withholding_category == tax_details.get("tax_withholding_category"))
- .where(at.parent.isin(advances))
- .where(at.account_head == tax_details.account_head)
- .run(as_dict=True)
- )
+ if inv.get("advances"):
+ advances = [d.reference_name for d in inv.get("advances")]
+
+ if advances:
+ pe = frappe.qb.DocType("Payment Entry").as_("pe")
+ at = frappe.qb.DocType("Advance Taxes and Charges").as_("at")
+
+ tax_info = (
+ frappe.qb.from_(at)
+ .inner_join(pe)
+ .on(pe.name == at.parent)
+ .select(at.parent, at.name, at.tax_amount, at.allocated_amount)
+ .where(pe.tax_withholding_category == tax_details.get("tax_withholding_category"))
+ .where(at.parent.isin(advances))
+ .where(at.account_head == tax_details.account_head)
+ .run(as_dict=True)
+ )
return tax_info
@@ -358,6 +381,9 @@ def get_deducted_tax(taxable_vouchers, tax_details):
def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
tds_amount = 0
+ supp_credit_amt = 0.0
+ supp_jv_credit_amt = 0.0
+
invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1}
field = "sum(net_total)"
@@ -366,30 +392,25 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
invoice_filters.pop("apply_tds", None)
field = "sum(grand_total)"
- supp_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0
+ if vouchers:
+ supp_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0
- supp_jv_credit_amt = (
- frappe.db.get_value(
- "Journal Entry Account",
- {
- "parent": ("in", vouchers),
- "docstatus": 1,
- "party": ("in", parties),
- "reference_type": ("!=", "Purchase Invoice"),
- },
- "sum(credit_in_account_currency)",
- )
- or 0.0
- )
+ supp_jv_credit_amt = (
+ frappe.db.get_value(
+ "Journal Entry Account",
+ {
+ "parent": ("in", vouchers),
+ "docstatus": 1,
+ "party": ("in", parties),
+ "reference_type": ("!=", "Purchase Invoice"),
+ },
+ "sum(credit_in_account_currency)",
+ )
+ ) or 0.0
supp_credit_amt += supp_jv_credit_amt
supp_credit_amt += inv.net_total
- debit_note_amount = get_debit_note_amount(
- parties, tax_details.from_date, tax_details.to_date, inv.company
- )
- supp_credit_amt -= debit_note_amount
-
threshold = tax_details.get("threshold", 0)
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
@@ -401,7 +422,10 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
):
# Get net total again as TDS is calculated on net total
# Grand is used to just check for threshold breach
- net_total = frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(net_total)") or 0.0
+ net_total = 0
+ if vouchers:
+ net_total = frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(net_total)")
+
net_total += inv.net_total
supp_credit_amt = net_total - cumulative_threshold
@@ -422,36 +446,40 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
tcs_amount = 0
+ invoiced_amt = 0
+ advance_amt = 0
# sum of debit entries made from sales invoices
- invoiced_amt = (
- frappe.db.get_value(
- "GL Entry",
- {
- "is_cancelled": 0,
- "party": ["in", parties],
- "company": inv.company,
- "voucher_no": ["in", vouchers],
- },
- "sum(debit)",
+ if vouchers:
+ invoiced_amt = (
+ frappe.db.get_value(
+ "GL Entry",
+ {
+ "is_cancelled": 0,
+ "party": ["in", parties],
+ "company": inv.company,
+ "voucher_no": ["in", vouchers],
+ },
+ "sum(debit)",
+ )
+ or 0.0
)
- or 0.0
- )
# sum of credit entries made from PE / JV with unset 'against voucher'
- advance_amt = (
- frappe.db.get_value(
- "GL Entry",
- {
- "is_cancelled": 0,
- "party": ["in", parties],
- "company": inv.company,
- "voucher_no": ["in", adv_vouchers],
- },
- "sum(credit)",
+ if advance_amt:
+ advance_amt = (
+ frappe.db.get_value(
+ "GL Entry",
+ {
+ "is_cancelled": 0,
+ "party": ["in", parties],
+ "company": inv.company,
+ "voucher_no": ["in", adv_vouchers],
+ },
+ "sum(credit)",
+ )
+ or 0.0
)
- or 0.0
- )
# sum of credit entries made from sales invoice
credit_note_amt = sum(
@@ -506,22 +534,6 @@ def get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net
return tds_amount
-def get_debit_note_amount(suppliers, from_date, to_date, company=None):
-
- filters = {
- "supplier": ["in", suppliers],
- "is_return": 1,
- "docstatus": 1,
- "posting_date": ["between", (from_date, to_date)],
- }
- fields = ["abs(sum(net_total)) as net_total"]
-
- if company:
- filters["company"] = company
-
- return frappe.get_all("Purchase Invoice", filters, fields)[0].get("net_total") or 0.0
-
-
def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details):
if current_amount < (certificate_limit - deducted_amount):
return current_amount * rate / 100
diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
index 3059f8d64b8c..e80fe11ab303 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
@@ -52,7 +52,7 @@ def test_cumulative_threshold_tds(self):
invoices.append(pi)
# delete invoices to avoid clashing
- for d in invoices:
+ for d in reversed(invoices):
d.cancel()
def test_single_threshold_tds(self):
@@ -88,7 +88,7 @@ def test_single_threshold_tds(self):
self.assertEqual(pi.taxes_and_charges_deducted, 1000)
# delete invoices to avoid clashing
- for d in invoices:
+ for d in reversed(invoices):
d.cancel()
def test_tax_withholding_category_checks(self):
@@ -114,7 +114,7 @@ def test_tax_withholding_category_checks(self):
# TDS should be applied only on 1000
self.assertEqual(pi1.taxes[0].tax_amount, 1000)
- for d in invoices:
+ for d in reversed(invoices):
d.cancel()
def test_cumulative_threshold_tcs(self):
@@ -148,8 +148,8 @@ def test_cumulative_threshold_tcs(self):
self.assertEqual(tcs_charged, 500)
invoices.append(si)
- # delete invoices to avoid clashing
- for d in invoices:
+ # cancel invoices to avoid clashing
+ for d in reversed(invoices):
d.cancel()
def test_tds_calculation_on_net_total(self):
@@ -182,8 +182,8 @@ def test_tds_calculation_on_net_total(self):
self.assertEqual(pi1.taxes[0].tax_amount, 4000)
- # delete invoices to avoid clashing
- for d in invoices:
+ # cancel invoices to avoid clashing
+ for d in reversed(invoices):
d.cancel()
def test_multi_category_single_supplier(self):
@@ -207,8 +207,50 @@ def test_multi_category_single_supplier(self):
self.assertEqual(pi1.taxes[0].tax_amount, 250)
- # delete invoices to avoid clashing
- for d in invoices:
+ # cancel invoices to avoid clashing
+ for d in reversed(invoices):
+ d.cancel()
+
+ def test_tax_withholding_category_voucher_display(self):
+ frappe.db.set_value(
+ "Supplier", "Test TDS Supplier6", "tax_withholding_category", "Test Multi Invoice Category"
+ )
+ invoices = []
+
+ pi = create_purchase_invoice(supplier="Test TDS Supplier6", rate=4000, do_not_save=True)
+ pi.apply_tds = 1
+ pi.tax_withholding_category = "Test Multi Invoice Category"
+ pi.save()
+ pi.submit()
+ invoices.append(pi)
+
+ pi1 = create_purchase_invoice(supplier="Test TDS Supplier6", rate=2000, do_not_save=True)
+ pi1.apply_tds = 1
+ pi1.is_return = 1
+ pi1.items[0].qty = -1
+ pi1.tax_withholding_category = "Test Multi Invoice Category"
+ pi1.save()
+ pi1.submit()
+ invoices.append(pi1)
+
+ pi2 = create_purchase_invoice(supplier="Test TDS Supplier6", rate=9000, do_not_save=True)
+ pi2.apply_tds = 1
+ pi2.tax_withholding_category = "Test Multi Invoice Category"
+ pi2.save()
+ pi2.submit()
+ invoices.append(pi2)
+
+ pi2.load_from_db()
+
+ self.assertTrue(pi2.taxes[0].tax_amount, 1100)
+
+ self.assertTrue(pi2.tax_withheld_vouchers[0].voucher_name == pi1.name)
+ self.assertTrue(pi2.tax_withheld_vouchers[0].taxable_amount == pi1.net_total)
+ self.assertTrue(pi2.tax_withheld_vouchers[1].voucher_name == pi.name)
+ self.assertTrue(pi2.tax_withheld_vouchers[1].taxable_amount == pi.net_total)
+
+ # cancel invoices to avoid clashing
+ for d in reversed(invoices):
d.cancel()
@@ -308,6 +350,7 @@ def create_records():
"Test TDS Supplier3",
"Test TDS Supplier4",
"Test TDS Supplier5",
+ "Test TDS Supplier6",
]:
if frappe.db.exists("Supplier", name):
continue
@@ -498,3 +541,22 @@ def create_tax_with_holding_category():
"accounts": [{"company": "_Test Company", "account": "TDS - _TC"}],
}
).insert()
+
+ if not frappe.db.exists("Tax Withholding Category", "Test Multi Invoice Category"):
+ frappe.get_doc(
+ {
+ "doctype": "Tax Withholding Category",
+ "name": "Test Multi Invoice Category",
+ "category_name": "Test Multi Invoice Category",
+ "rates": [
+ {
+ "from_date": fiscal_year[1],
+ "to_date": fiscal_year[2],
+ "tax_withholding_rate": 10,
+ "single_threshold": 5000,
+ "cumulative_threshold": 10000,
+ }
+ ],
+ "accounts": [{"company": "_Test Company", "account": "TDS - _TC"}],
+ }
+ ).insert()
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 7525369d4f6b..bb8f9dc0472c 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -298,20 +298,22 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
gl_map[0].company, gl_map[0].voucher_type, gl_map[0].voucher_no
)
- round_off_account_exists = False
round_off_gle = frappe._dict()
- for d in gl_map:
- if d.account == round_off_account:
- round_off_gle = d
- if d.debit:
- debit_credit_diff -= flt(d.debit)
- else:
- debit_credit_diff += flt(d.credit)
- round_off_account_exists = True
-
- if round_off_account_exists and abs(debit_credit_diff) < (1.0 / (10**precision)):
- gl_map.remove(round_off_gle)
- return
+ round_off_account_exists = False
+
+ if gl_map[0].voucher_type != "Period Closing Voucher":
+ for d in gl_map:
+ if d.account == round_off_account:
+ round_off_gle = d
+ if d.debit:
+ debit_credit_diff -= flt(d.debit) - flt(d.credit)
+ else:
+ debit_credit_diff += flt(d.credit)
+ round_off_account_exists = True
+
+ if round_off_account_exists and abs(debit_credit_diff) < (1.0 / (10**precision)):
+ gl_map.remove(round_off_gle)
+ return
if not round_off_gle:
for k in ["voucher_type", "voucher_no", "company", "posting_date", "remarks"]:
@@ -334,7 +336,6 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
)
update_accounting_dimensions(round_off_gle)
-
if not round_off_account_exists:
gl_map.append(round_off_gle)
diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
index 605ce8383e44..e1b2def2daea 100644
--- a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
+++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
@@ -50,6 +50,10 @@ 1. Transaction Details
{{ einvoice.DocDtls.No }}
+
+
+ {{ einvoice.DocDtls.Dt }}
+
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index c9567f23a344..6293357cca0e 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -5,7 +5,7 @@
from collections import OrderedDict
import frappe
-from frappe import _, scrub
+from frappe import _, qb, scrub
from frappe.utils import cint, cstr, flt, getdate, nowdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@@ -95,6 +95,9 @@ def get_data(self):
# Get return entries
self.get_return_entries()
+ # Get Exchange Rate Revaluations
+ self.get_exchange_rate_revaluations()
+
self.data = []
for gle in self.gl_entries:
self.update_voucher_balance(gle)
@@ -258,7 +261,8 @@ def build_data(self):
row.invoice_grand_total = row.invoiced
if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and (
- abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision
+ (abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision)
+ or (row.voucher_no in self.err_journals)
):
# non-zero oustanding, we must consider this row
@@ -685,10 +689,10 @@ def get_gl_entries(self):
if self.filters.get(scrub(self.party_type)):
select_fields = "debit_in_account_currency as debit, credit_in_account_currency as credit"
+ doc_currency_fields = "debit as debit_in_account_currency, credit as credit_in_account_currency"
else:
select_fields = "debit, credit"
-
- doc_currency_fields = "debit_in_account_currency, credit_in_account_currency"
+ doc_currency_fields = "debit_in_account_currency, credit_in_account_currency"
remarks = ", remarks" if self.filters.get("show_remarks") else ""
@@ -1033,3 +1037,17 @@ def get_chart_data(self):
"data": {"labels": self.ageing_column_labels, "datasets": rows},
"type": "percentage",
}
+
+ def get_exchange_rate_revaluations(self):
+ je = qb.DocType("Journal Entry")
+ results = (
+ qb.from_(je)
+ .select(je.name)
+ .where(
+ (je.company == self.filters.company)
+ & (je.posting_date.lte(self.filters.report_date))
+ & (je.voucher_type == "Exchange Rate Revaluation")
+ )
+ .run()
+ )
+ self.err_journals = [x[0] for x in results] if results else []
diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
index f38890e980c1..09305a20ea1a 100644
--- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
@@ -1,18 +1,51 @@
import unittest
import frappe
-from frappe.utils import add_days, getdate, today
+from frappe.tests.utils import FrappeTestCase, change_settings
+from frappe.utils import add_days, flt, getdate, today
+from erpnext import get_default_cost_center
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute
class TestAccountsReceivable(unittest.TestCase):
- def test_accounts_receivable(self):
+ def setUp(self):
frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'")
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'")
+ frappe.db.sql("delete from `tabJournal Entry` where company='_Test Company 2'")
+ frappe.db.sql("delete from `tabExchange Rate Revaluation` where company='_Test Company 2'")
+
+ self.create_usd_account()
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+ def create_usd_account(self):
+ name = "Debtors USD"
+ exists = frappe.db.get_list(
+ "Account", filters={"company": "_Test Company 2", "account_name": "Debtors USD"}
+ )
+ if exists:
+ self.debtors_usd = exists[0].name
+ else:
+ debtors = frappe.get_doc(
+ "Account",
+ frappe.db.get_list(
+ "Account", filters={"company": "_Test Company 2", "account_name": "Debtors"}
+ )[0].name,
+ )
+
+ debtors_usd = frappe.new_doc("Account")
+ debtors_usd.company = debtors.company
+ debtors_usd.account_name = "Debtors USD"
+ debtors_usd.account_currency = "USD"
+ debtors_usd.parent_account = debtors.parent_account
+ debtors_usd.account_type = debtors.account_type
+ self.debtors_usd = debtors_usd.save().name
+ def test_accounts_receivable(self):
filters = {
"company": "_Test Company 2",
"based_on_payment_terms": 1,
@@ -24,7 +57,7 @@ def test_accounts_receivable(self):
}
# check invoice grand total and invoiced column's value for 3 payment terms
- name = make_sales_invoice()
+ name = make_sales_invoice().name
report = execute(filters)
expected_data = [[100, 30], [100, 50], [100, 20]]
@@ -65,8 +98,74 @@ def test_accounts_receivable(self):
],
)
+ @change_settings(
+ "Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1}
+ )
+ def test_exchange_revaluation_for_party(self):
+ """
+ Exchange Revaluation for party on Receivable/Payable shoule be included
+ """
+
+ company = "_Test Company 2"
+ customer = "_Test Customer 2"
+
+ # Using Exchange Gain/Loss account for unrealized as well.
+ company_doc = frappe.get_doc("Company", company)
+ company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account
+ company_doc.save()
+
+ si = make_sales_invoice(no_payment_schedule=True, do_not_submit=True)
+ si.currency = "USD"
+ si.conversion_rate = 0.90
+ si.debit_to = self.debtors_usd
+ si = si.save().submit()
+
+ # Exchange Revaluation
+ err = frappe.new_doc("Exchange Rate Revaluation")
+ err.company = company
+ err.posting_date = today()
+ accounts = err.get_accounts_data()
+ err.extend("accounts", accounts)
+ err.accounts[0].new_exchange_rate = 0.95
+ row = err.accounts[0]
+ row.new_balance_in_base_currency = flt(
+ row.new_exchange_rate * flt(row.balance_in_account_currency)
+ )
+ row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency)
+ err.set_total_gain_loss()
+ err = err.save().submit()
-def make_sales_invoice():
+ # Submit JV for ERR
+ jv = frappe.get_doc(err.make_jv_entry())
+ jv = jv.save()
+ for x in jv.accounts:
+ x.cost_center = get_default_cost_center(jv.company)
+ jv.submit()
+
+ filters = {
+ "company": company,
+ "report_date": today(),
+ "range1": 30,
+ "range2": 60,
+ "range3": 90,
+ "range4": 120,
+ }
+ report = execute(filters)
+
+ expected_data_for_err = [0, -5, 0, 5]
+ row = [x for x in report[1] if x.voucher_type == jv.doctype and x.voucher_no == jv.name][0]
+ self.assertEqual(
+ expected_data_for_err,
+ [
+ row.invoiced,
+ row.paid,
+ row.credit_note,
+ row.outstanding,
+ ],
+ )
+
+
+def make_sales_invoice(no_payment_schedule=False, do_not_submit=False):
frappe.set_user("Administrator")
si = create_sales_invoice(
@@ -81,22 +180,26 @@ def make_sales_invoice():
do_not_save=1,
)
- si.append(
- "payment_schedule",
- dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
- )
- si.append(
- "payment_schedule",
- dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50),
- )
- si.append(
- "payment_schedule",
- dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20),
- )
+ if not no_payment_schedule:
+ si.append(
+ "payment_schedule",
+ dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
+ )
+ si.append(
+ "payment_schedule",
+ dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50),
+ )
+ si.append(
+ "payment_schedule",
+ dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20),
+ )
+
+ si = si.save()
- si.submit()
+ if not do_not_submit:
+ si = si.submit()
- return si.name
+ return si
def make_payment(docname):
diff --git a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py
index 9d2deea523bd..449ebdcd9240 100644
--- a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py
+++ b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py
@@ -22,8 +22,7 @@ def get_columns():
{
"label": _("Payment Document Type"),
"fieldname": "payment_document_type",
- "fieldtype": "Link",
- "options": "Doctype",
+ "fieldtype": "Data",
"width": 130,
},
{
@@ -33,15 +32,15 @@ def get_columns():
"options": "payment_document_type",
"width": 140,
},
- {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
+ {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120},
{"label": _("Cheque/Reference No"), "fieldname": "cheque_no", "width": 120},
- {"label": _("Clearance Date"), "fieldname": "clearance_date", "fieldtype": "Date", "width": 100},
+ {"label": _("Clearance Date"), "fieldname": "clearance_date", "fieldtype": "Date", "width": 120},
{
"label": _("Against Account"),
"fieldname": "against",
"fieldtype": "Link",
"options": "Account",
- "width": 170,
+ "width": 200,
},
{"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120},
]
diff --git a/erpnext/accounts/report/cash_flow/cash_flow.py b/erpnext/accounts/report/cash_flow/cash_flow.py
index ee924f86a6a1..97637c7e0924 100644
--- a/erpnext/accounts/report/cash_flow/cash_flow.py
+++ b/erpnext/accounts/report/cash_flow/cash_flow.py
@@ -9,6 +9,7 @@
from erpnext.accounts.report.financial_statements import (
get_columns,
+ get_cost_centers_with_children,
get_data,
get_filtered_list_for_consolidated_report,
get_period_list,
@@ -161,10 +162,11 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_
total = 0
for period in period_list:
start_date = get_start_date(period, accumulated_values, company)
+ filters.start_date = start_date
+ filters.end_date = period["to_date"]
+ filters.account_type = account_type
- amount = get_account_type_based_gl_data(
- company, start_date, period["to_date"], account_type, filters
- )
+ amount = get_account_type_based_gl_data(company, filters)
if amount and account_type == "Depreciation":
amount *= -1
@@ -176,7 +178,7 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_
return data
-def get_account_type_based_gl_data(company, start_date, end_date, account_type, filters=None):
+def get_account_type_based_gl_data(company, filters=None):
cond = ""
filters = frappe._dict(filters or {})
@@ -192,17 +194,21 @@ def get_account_type_based_gl_data(company, start_date, end_date, account_type,
frappe.db.escape(cstr(filters.finance_book))
)
+ if filters.get("cost_center"):
+ filters.cost_center = get_cost_centers_with_children(filters.cost_center)
+ cond += " and cost_center in %(cost_center)s"
+
gl_sum = frappe.db.sql_list(
"""
select sum(credit) - sum(debit)
from `tabGL Entry`
- where company=%s and posting_date >= %s and posting_date <= %s
+ where company=%(company)s and posting_date >= %(start_date)s and posting_date <= %(end_date)s
and voucher_type != 'Period Closing Voucher'
- and account in ( SELECT name FROM tabAccount WHERE account_type = %s) {cond}
+ and account in ( SELECT name FROM tabAccount WHERE account_type = %(account_type)s) {cond}
""".format(
cond=cond
),
- (company, start_date, end_date, account_type),
+ filters,
)
return gl_sum[0] if gl_sum and gl_sum[0] else 0
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
index 330e442a808b..560b79243d70 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -268,10 +268,12 @@ def get_cash_flow_data(fiscal_year, companies, filters):
def get_account_type_based_data(account_type, companies, fiscal_year, filters):
data = {}
total = 0
+ filters.account_type = account_type
+ filters.start_date = fiscal_year.year_start_date
+ filters.end_date = fiscal_year.year_end_date
+
for company in companies:
- amount = get_account_type_based_gl_data(
- company, fiscal_year.year_start_date, fiscal_year.year_end_date, account_type, filters
- )
+ amount = get_account_type_based_gl_data(company, filters)
if amount and account_type == "Depreciation":
amount *= -1
@@ -533,12 +535,13 @@ def get_accounts(root_type, companies):
],
filters={"company": company, "root_type": root_type},
):
- if account.account_name not in added_accounts:
+ if account.account_number:
+ account_key = account.account_number + "-" + account.account_name
+ else:
+ account_key = account.account_name
+
+ if account_key not in added_accounts:
accounts.append(account)
- if account.account_number:
- account_key = account.account_number + "-" + account.account_name
- else:
- account_key = account.account_name
added_accounts.append(account_key)
return accounts
diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py
index 3beaa2bfe74a..f5b46bde5865 100644
--- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py
+++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py
@@ -27,6 +27,7 @@ def run(self, args):
)
self.get_gl_entries()
+ self.get_additional_columns()
self.get_return_invoices()
self.get_party_adjustment_amounts()
@@ -34,6 +35,42 @@ def run(self, args):
data = self.get_data()
return columns, data
+ def get_additional_columns(self):
+ """
+ Additional Columns for 'User Permission' based access control
+ """
+ from frappe import qb
+
+ if self.filters.party_type == "Customer":
+ self.territories = frappe._dict({})
+ self.customer_group = frappe._dict({})
+
+ customer = qb.DocType("Customer")
+ result = (
+ frappe.qb.from_(customer)
+ .select(
+ customer.name, customer.territory, customer.customer_group, customer.default_sales_partner
+ )
+ .where((customer.disabled == 0))
+ .run(as_dict=True)
+ )
+
+ for x in result:
+ self.territories[x.name] = x.territory
+ self.customer_group[x.name] = x.customer_group
+ else:
+ self.supplier_group = frappe._dict({})
+ supplier = qb.DocType("Supplier")
+ result = (
+ frappe.qb.from_(supplier)
+ .select(supplier.name, supplier.supplier_group)
+ .where((supplier.disabled == 0))
+ .run(as_dict=True)
+ )
+
+ for x in result:
+ self.supplier_group[x.name] = x.supplier_group
+
def get_columns(self):
columns = [
{
@@ -117,6 +154,35 @@ def get_columns(self):
},
]
+ # Hidden columns for handling 'User Permissions'
+ if self.filters.party_type == "Customer":
+ columns += [
+ {
+ "label": _("Territory"),
+ "fieldname": "territory",
+ "fieldtype": "Link",
+ "options": "Territory",
+ "hidden": 1,
+ },
+ {
+ "label": _("Customer Group"),
+ "fieldname": "customer_group",
+ "fieldtype": "Link",
+ "options": "Customer Group",
+ "hidden": 1,
+ },
+ ]
+ else:
+ columns += [
+ {
+ "label": _("Supplier Group"),
+ "fieldname": "supplier_group",
+ "fieldtype": "Link",
+ "options": "Supplier Group",
+ "hidden": 1,
+ }
+ ]
+
return columns
def get_data(self):
@@ -144,6 +210,12 @@ def get_data(self):
),
)
+ if self.filters.party_type == "Customer":
+ self.party_data[gle.party].update({"territory": self.territories.get(gle.party)})
+ self.party_data[gle.party].update({"customer_group": self.customer_group.get(gle.party)})
+ else:
+ self.party_data[gle.party].update({"supplier_group": self.supplier_group.get(gle.party)})
+
amount = gle.get(invoice_dr_or_cr) - gle.get(reverse_dr_or_cr)
self.party_data[gle.party].closing_balance += amount
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 6a3b38ae7b52..33d46bc901da 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -280,7 +280,7 @@ def get_conditions(filters):
or filters.get("party")
or filters.get("group_by") in ["Group by Account", "Group by Party"]
):
- conditions.append("posting_date >=%(from_date)s")
+ conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')")
diff --git a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py
index 9d566785416a..cd5f36670715 100644
--- a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py
+++ b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py
@@ -155,7 +155,6 @@ def adjust_account(data, period_list, consolidated=False):
for d in data:
for period in period_list:
key = period if consolidated else period.key
- d[key] = totals[d["account"]]
d["total"] = totals[d["account"]]
return data
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py
index 7f6e2b99c89a..6bd3ee5aa6bf 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.py
+++ b/erpnext/accounts/report/gross_profit/gross_profit.py
@@ -3,7 +3,8 @@
import frappe
-from frappe import _, scrub
+from frappe import _, qb, scrub
+from frappe.query_builder import Order
from frappe.utils import cint, flt
from erpnext.controllers.queries import get_match_cond
@@ -367,6 +368,7 @@ def __init__(self, filters=None):
self.average_buying_rate = {}
self.filters = frappe._dict(filters)
self.load_invoice_items()
+ self.get_delivery_notes()
if filters.group_by == "Invoice":
self.group_items_by_invoice()
@@ -535,6 +537,22 @@ def get_buying_amount_from_product_bundle(self, row, product_bundle):
return flt(buying_amount, self.currency_precision)
+ def calculate_buying_amount_from_sle(self, row, my_sle, parenttype, parent, item_row, item_code):
+ for i, sle in enumerate(my_sle):
+ # find the stock valution rate from stock ledger entry
+ if (
+ sle.voucher_type == parenttype
+ and parent == sle.voucher_no
+ and sle.voucher_detail_no == item_row
+ ):
+ previous_stock_value = len(my_sle) > i + 1 and flt(my_sle[i + 1].stock_value) or 0.0
+
+ if previous_stock_value:
+ return abs(previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty))
+ else:
+ return flt(row.qty) * self.get_average_buying_rate(row, item_code)
+ return 0.0
+
def get_buying_amount(self, row, item_code):
# IMP NOTE
# stock_ledger_entries should already be filtered by item_code and warehouse and
@@ -551,19 +569,22 @@ def get_buying_amount(self, row, item_code):
if row.dn_detail:
parenttype, parent = "Delivery Note", row.delivery_note
- for i, sle in enumerate(my_sle):
- # find the stock valution rate from stock ledger entry
- if (
- sle.voucher_type == parenttype
- and parent == sle.voucher_no
- and sle.voucher_detail_no == row.item_row
- ):
- previous_stock_value = len(my_sle) > i + 1 and flt(my_sle[i + 1].stock_value) or 0.0
-
- if previous_stock_value:
- return abs(previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty))
- else:
- return flt(row.qty) * self.get_average_buying_rate(row, item_code)
+ return self.calculate_buying_amount_from_sle(
+ row, my_sle, parenttype, parent, row.item_row, item_code
+ )
+ elif self.delivery_notes.get((row.parent, row.item_code), None):
+ # check if Invoice has delivery notes
+ dn = self.delivery_notes.get((row.parent, row.item_code))
+ parenttype, parent, item_row, warehouse = (
+ "Delivery Note",
+ dn["delivery_note"],
+ dn["item_row"],
+ dn["warehouse"],
+ )
+ my_sle = self.sle.get((item_code, warehouse))
+ return self.calculate_buying_amount_from_sle(
+ row, my_sle, parenttype, parent, item_row, item_code
+ )
else:
return flt(row.qty) * self.get_average_buying_rate(row, item_code)
@@ -667,6 +688,29 @@ def load_invoice_items(self):
as_dict=1,
)
+ def get_delivery_notes(self):
+ self.delivery_notes = frappe._dict({})
+ if self.si_list:
+ invoices = [x.parent for x in self.si_list]
+ dni = qb.DocType("Delivery Note Item")
+ delivery_notes = (
+ qb.from_(dni)
+ .select(
+ dni.against_sales_invoice.as_("sales_invoice"),
+ dni.item_code,
+ dni.warehouse,
+ dni.parent.as_("delivery_note"),
+ dni.name.as_("item_row"),
+ )
+ .where((dni.docstatus == 1) & (dni.against_sales_invoice.isin(invoices)))
+ .groupby(dni.against_sales_invoice, dni.item_code)
+ .orderby(dni.creation, order=Order.desc)
+ .run(as_dict=True)
+ )
+
+ for entry in delivery_notes:
+ self.delivery_notes[(entry.sales_invoice, entry.item_code)] = entry
+
def group_items_by_invoice(self):
"""
Turns list of Sales Invoice Items to a tree of Sales Invoices with their Items as children.
diff --git a/erpnext/accounts/report/gross_profit/test_gross_profit.py b/erpnext/accounts/report/gross_profit/test_gross_profit.py
new file mode 100644
index 000000000000..d9febb74fd41
--- /dev/null
+++ b/erpnext/accounts/report/gross_profit/test_gross_profit.py
@@ -0,0 +1,303 @@
+import frappe
+from frappe import qb
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import add_days, flt, nowdate
+
+from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.accounts.report.gross_profit.gross_profit import execute
+from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
+from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+from erpnext.stock.doctype.item.test_item import create_item
+from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+
+
+class TestGrossProfit(FrappeTestCase):
+ def setUp(self):
+ self.create_company()
+ self.create_item()
+ self.create_bundle()
+ self.create_customer()
+ self.create_sales_invoice()
+ self.clear_old_entries()
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+ def create_company(self):
+ company_name = "_Test Gross Profit"
+ abbr = "_GP"
+ if frappe.db.exists("Company", company_name):
+ company = frappe.get_doc("Company", company_name)
+ else:
+ company = frappe.get_doc(
+ {
+ "doctype": "Company",
+ "company_name": company_name,
+ "country": "India",
+ "default_currency": "INR",
+ "create_chart_of_accounts_based_on": "Standard Template",
+ "chart_of_accounts": "Standard",
+ }
+ )
+ company = company.save()
+
+ self.company = company.name
+ self.cost_center = company.cost_center
+ self.warehouse = "Stores - " + abbr
+ self.finished_warehouse = "Finished Goods - " + abbr
+ self.income_account = "Sales - " + abbr
+ self.expense_account = "Cost of Goods Sold - " + abbr
+ self.debit_to = "Debtors - " + abbr
+ self.creditors = "Creditors - " + abbr
+
+ def create_item(self):
+ item = create_item(
+ item_code="_Test GP Item", is_stock_item=1, company=self.company, warehouse=self.warehouse
+ )
+ self.item = item if isinstance(item, str) else item.item_code
+
+ def create_bundle(self):
+ from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
+
+ item2 = create_item(
+ item_code="_Test GP Item 2", is_stock_item=1, company=self.company, warehouse=self.warehouse
+ )
+ self.item2 = item2 if isinstance(item2, str) else item2.item_code
+
+ # This will be parent item
+ bundle = create_item(
+ item_code="_Test GP bundle", is_stock_item=0, company=self.company, warehouse=self.warehouse
+ )
+ self.bundle = bundle if isinstance(bundle, str) else bundle.item_code
+
+ # Create Product Bundle
+ self.product_bundle = make_product_bundle(parent=self.bundle, items=[self.item, self.item2])
+
+ def create_customer(self):
+ name = "_Test GP Customer"
+ if frappe.db.exists("Customer", name):
+ self.customer = name
+ else:
+ customer = frappe.new_doc("Customer")
+ customer.customer_name = name
+ customer.type = "Individual"
+ customer.save()
+ self.customer = customer.name
+
+ def create_sales_invoice(
+ self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
+ ):
+ """
+ Helper function to populate default values in sales invoice
+ """
+ sinv = create_sales_invoice(
+ qty=qty,
+ rate=rate,
+ company=self.company,
+ customer=self.customer,
+ item_code=self.item,
+ item_name=self.item,
+ cost_center=self.cost_center,
+ warehouse=self.warehouse,
+ debit_to=self.debit_to,
+ parent_cost_center=self.cost_center,
+ update_stock=0,
+ currency="INR",
+ is_pos=0,
+ is_return=0,
+ return_against=None,
+ income_account=self.income_account,
+ expense_account=self.expense_account,
+ do_not_save=do_not_save,
+ do_not_submit=do_not_submit,
+ )
+ return sinv
+
+ def create_delivery_note(
+ self, item=None, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
+ ):
+ """
+ Helper function to populate default values in Delivery Note
+ """
+ dnote = create_delivery_note(
+ company=self.company,
+ customer=self.customer,
+ currency="INR",
+ item=item or self.item,
+ qty=qty,
+ rate=rate,
+ cost_center=self.cost_center,
+ warehouse=self.warehouse,
+ return_against=None,
+ expense_account=self.expense_account,
+ do_not_save=do_not_save,
+ do_not_submit=do_not_submit,
+ )
+ return dnote
+
+ def clear_old_entries(self):
+ doctype_list = [
+ "Sales Invoice",
+ "GL Entry",
+ "Stock Entry",
+ "Stock Ledger Entry",
+ "Delivery Note",
+ ]
+ for doctype in doctype_list:
+ qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
+
+ def test_invoice_without_only_delivery_note(self):
+ """
+ Test buying amount for Invoice without `update_stock` flag set but has Delivery Note
+ """
+ se = make_stock_entry(
+ company=self.company,
+ item_code=self.item,
+ target=self.warehouse,
+ qty=1,
+ basic_rate=100,
+ do_not_submit=True,
+ )
+ item = se.items[0]
+ se.append(
+ "items",
+ {
+ "item_code": item.item_code,
+ "s_warehouse": item.s_warehouse,
+ "t_warehouse": item.t_warehouse,
+ "qty": 1,
+ "basic_rate": 200,
+ "conversion_factor": item.conversion_factor or 1.0,
+ "transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0),
+ "serial_no": item.serial_no,
+ "batch_no": item.batch_no,
+ "cost_center": item.cost_center,
+ "expense_account": item.expense_account,
+ },
+ )
+ se = se.save().submit()
+
+ sinv = create_sales_invoice(
+ qty=1,
+ rate=100,
+ company=self.company,
+ customer=self.customer,
+ item_code=self.item,
+ item_name=self.item,
+ cost_center=self.cost_center,
+ warehouse=self.warehouse,
+ debit_to=self.debit_to,
+ parent_cost_center=self.cost_center,
+ update_stock=0,
+ currency="INR",
+ income_account=self.income_account,
+ expense_account=self.expense_account,
+ )
+
+ filters = frappe._dict(
+ company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
+ )
+
+ columns, data = execute(filters=filters)
+
+ # Without Delivery Note, buying rate should be 150
+ expected_entry_without_dn = {
+ "parent_invoice": sinv.name,
+ "currency": "INR",
+ "sales_invoice": self.item,
+ "customer": self.customer,
+ "posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
+ "item_code": self.item,
+ "item_name": self.item,
+ "warehouse": "Stores - _GP",
+ "qty": 1.0,
+ "avg._selling_rate": 100.0,
+ "valuation_rate": 150.0,
+ "selling_amount": 100.0,
+ "buying_amount": 150.0,
+ "gross_profit": -50.0,
+ "gross_profit_%": -50.0,
+ }
+ gp_entry = [x for x in data if x.parent_invoice == sinv.name]
+ self.assertDictContainsSubset(expected_entry_without_dn, gp_entry[0])
+
+ # make delivery note
+ dn = make_delivery_note(sinv.name)
+ dn.items[0].qty = 1
+ dn = dn.save().submit()
+
+ columns, data = execute(filters=filters)
+
+ # Without Delivery Note, buying rate should be 100
+ expected_entry_with_dn = {
+ "parent_invoice": sinv.name,
+ "currency": "INR",
+ "sales_invoice": self.item,
+ "customer": self.customer,
+ "posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
+ "item_code": self.item,
+ "item_name": self.item,
+ "warehouse": "Stores - _GP",
+ "qty": 1.0,
+ "avg._selling_rate": 100.0,
+ "valuation_rate": 100.0,
+ "selling_amount": 100.0,
+ "buying_amount": 100.0,
+ "gross_profit": 0.0,
+ "gross_profit_%": 0.0,
+ }
+ gp_entry = [x for x in data if x.parent_invoice == sinv.name]
+ self.assertDictContainsSubset(expected_entry_with_dn, gp_entry[0])
+
+ def test_bundled_delivery_note_with_different_warehouses(self):
+ """
+ Test Delivery Note with bundled item. Packed Item from the bundle having different warehouses
+ """
+ se = make_stock_entry(
+ company=self.company,
+ item_code=self.item,
+ target=self.warehouse,
+ qty=1,
+ basic_rate=100,
+ do_not_submit=True,
+ )
+ item = se.items[0]
+ se.append(
+ "items",
+ {
+ "item_code": self.item2,
+ "s_warehouse": "",
+ "t_warehouse": self.finished_warehouse,
+ "qty": 1,
+ "basic_rate": 100,
+ "conversion_factor": item.conversion_factor or 1.0,
+ "transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0),
+ "serial_no": item.serial_no,
+ "batch_no": item.batch_no,
+ "cost_center": item.cost_center,
+ "expense_account": item.expense_account,
+ },
+ )
+ se = se.save().submit()
+
+ # Make a Delivery note with Product bundle
+ # Packed Items will have different warehouses
+ dnote = self.create_delivery_note(item=self.bundle, qty=1, rate=200, do_not_submit=True)
+ dnote.packed_items[1].warehouse = self.finished_warehouse
+ dnote = dnote.submit()
+
+ # make Sales Invoice for above delivery note
+ sinv = make_sales_invoice(dnote.name)
+ sinv = sinv.save().submit()
+
+ filters = frappe._dict(
+ company=self.company,
+ from_date=nowdate(),
+ to_date=nowdate(),
+ group_by="Invoice",
+ sales_invoice=sinv.name,
+ )
+
+ columns, data = execute(filters=filters)
+ self.assertGreater(len(data), 0)
diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
index c04b9c712527..d34c21348c80 100644
--- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
+++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
@@ -53,9 +53,6 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
item_details = get_item_details()
for d in item_list:
- if not d.stock_qty:
- continue
-
item_record = item_details.get(d.item_code)
purchase_receipt = None
@@ -94,7 +91,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
"expense_account": expense_account,
"stock_qty": d.stock_qty,
"stock_uom": d.stock_uom,
- "rate": d.base_net_amount / d.stock_qty,
+ "rate": d.base_net_amount / d.stock_qty if d.stock_qty else d.base_net_amount,
"amount": d.base_net_amount,
}
)
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 ac706666547e..dd9c07361289 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
@@ -19,14 +19,19 @@ def execute(filters=None):
return _execute(filters)
-def _execute(filters=None, additional_table_columns=None, additional_query_columns=None):
+def _execute(
+ filters=None,
+ additional_table_columns=None,
+ additional_query_columns=None,
+ additional_conditions=None,
+):
if not filters:
filters = {}
columns = get_columns(additional_table_columns, filters)
company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
- item_list = get_items(filters, additional_query_columns)
+ item_list = get_items(filters, additional_query_columns, additional_conditions)
if item_list:
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
@@ -97,6 +102,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
row.update({"rate": d.base_net_rate, "amount": d.base_net_amount})
total_tax = 0
+ total_other_charges = 0
for tax in tax_columns:
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
row.update(
@@ -105,10 +111,18 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
frappe.scrub(tax + " Amount"): item_tax.get("tax_amount", 0),
}
)
- total_tax += flt(item_tax.get("tax_amount"))
+ if item_tax.get("is_other_charges"):
+ total_other_charges += flt(item_tax.get("tax_amount"))
+ else:
+ total_tax += flt(item_tax.get("tax_amount"))
row.update(
- {"total_tax": total_tax, "total": d.base_net_amount + total_tax, "currency": company_currency}
+ {
+ "total_tax": total_tax,
+ "total_other_charges": total_other_charges,
+ "total": d.base_net_amount + total_tax,
+ "currency": company_currency,
+ }
)
if filters.get("group_by"):
@@ -319,7 +333,7 @@ def get_columns(additional_table_columns, filters):
return columns
-def get_conditions(filters):
+def get_conditions(filters, additional_conditions=None):
conditions = ""
for opts in (
@@ -332,6 +346,9 @@ def get_conditions(filters):
if filters.get(opts[0]):
conditions += opts[1]
+ if additional_conditions:
+ conditions += additional_conditions
+
if filters.get("mode_of_payment"):
conditions += """ and exists(select name from `tabSales Invoice Payment`
where parent=`tabSales Invoice`.name
@@ -367,8 +384,8 @@ def get_group_by_conditions(filters, doctype):
return "ORDER BY `tab{0}`.{1}".format(doctype, frappe.scrub(filters.get("group_by")))
-def get_items(filters, additional_query_columns):
- conditions = get_conditions(filters)
+def get_items(filters, additional_query_columns, additional_conditions=None):
+ conditions = get_conditions(filters, additional_conditions)
if additional_query_columns:
additional_query_columns = ", " + ", ".join(additional_query_columns)
@@ -477,7 +494,7 @@ def get_tax_accounts(
tax_details = frappe.db.sql(
"""
select
- name, parent, description, item_wise_tax_detail,
+ name, parent, description, item_wise_tax_detail, account_head,
charge_type, {add_deduct_tax}, base_tax_amount_after_discount_amount
from `tab%s`
where
@@ -493,11 +510,22 @@ def get_tax_accounts(
tuple([doctype] + list(invoice_item_row)),
)
+ account_doctype = frappe.qb.DocType("Account")
+
+ query = (
+ frappe.qb.from_(account_doctype)
+ .select(account_doctype.name)
+ .where((account_doctype.account_type == "Tax"))
+ )
+
+ tax_accounts = query.run()
+
for (
name,
parent,
description,
item_wise_tax_detail,
+ account_head,
charge_type,
add_deduct_tax,
tax_amount,
@@ -540,7 +568,11 @@ def get_tax_accounts(
)
itemised_tax.setdefault(d.name, {})[description] = frappe._dict(
- {"tax_rate": tax_rate, "tax_amount": tax_value}
+ {
+ "tax_rate": tax_rate,
+ "tax_amount": tax_value,
+ "is_other_charges": 0 if tuple([account_head]) in tax_accounts else 1,
+ }
)
except ValueError:
@@ -583,6 +615,13 @@ def get_tax_accounts(
"options": "currency",
"width": 100,
},
+ {
+ "label": _("Total Other Charges"),
+ "fieldname": "total_other_charges",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 100,
+ },
{
"label": _("Total"),
"fieldname": "total",
diff --git a/erpnext/accounts/report/purchase_register/purchase_register.py b/erpnext/accounts/report/purchase_register/purchase_register.py
index e8a1e795d927..a05d581207c2 100644
--- a/erpnext/accounts/report/purchase_register/purchase_register.py
+++ b/erpnext/accounts/report/purchase_register/purchase_register.py
@@ -232,12 +232,12 @@ def get_conditions(filters):
conditions += (
common_condition
- + "and ifnull(`tabPurchase Invoice Item`.{0}, '') in %({0})s)".format(dimension.fieldname)
+ + "and ifnull(`tabPurchase Invoice`.{0}, '') in %({0})s)".format(dimension.fieldname)
)
else:
conditions += (
common_condition
- + "and ifnull(`tabPurchase Invoice Item`.{0}, '') in %({0})s)".format(dimension.fieldname)
+ + "and ifnull(`tabPurchase Invoice`.{0}, '') in %({0})s)".format(dimension.fieldname)
)
return conditions
diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py
index 33bd3c749650..b333901d7b38 100644
--- a/erpnext/accounts/report/sales_register/sales_register.py
+++ b/erpnext/accounts/report/sales_register/sales_register.py
@@ -370,7 +370,7 @@ def get_sales_invoice_item_field_condition(field, table="Sales Invoice Item") ->
where parent=`tabSales Invoice`.name
and ifnull(`tab{table}`.{field}, '') = %({field})s)"""
- conditions += get_sales_invoice_item_field_condition("mode_of_payments", "Sales Invoice Payment")
+ conditions += get_sales_invoice_item_field_condition("mode_of_payment", "Sales Invoice Payment")
conditions += get_sales_invoice_item_field_condition("cost_center")
conditions += get_sales_invoice_item_field_condition("warehouse")
conditions += get_sales_invoice_item_field_condition("brand")
@@ -390,12 +390,12 @@ def get_sales_invoice_item_field_condition(field, table="Sales Invoice Item") ->
conditions += (
common_condition
- + "and ifnull(`tabSales Invoice Item`.{0}, '') in %({0})s)".format(dimension.fieldname)
+ + "and ifnull(`tabSales Invoice`.{0}, '') in %({0})s)".format(dimension.fieldname)
)
else:
conditions += (
common_condition
- + "and ifnull(`tabSales Invoice Item`.{0}, '') in %({0})s)".format(dimension.fieldname)
+ + "and ifnull(`tabSales Invoice`.{0}, '') in %({0})s)".format(dimension.fieldname)
)
return conditions
diff --git a/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js b/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js
index f81297760edb..5dc4c3d1c159 100644
--- a/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js
+++ b/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js
@@ -63,24 +63,6 @@ frappe.query_reports["Supplier Ledger Summary"] = {
"fieldtype": "Link",
"options": "Payment Terms Template"
},
- {
- "fieldname":"territory",
- "label": __("Territory"),
- "fieldtype": "Link",
- "options": "Territory"
- },
- {
- "fieldname":"sales_partner",
- "label": __("Sales Partner"),
- "fieldtype": "Link",
- "options": "Sales Partner"
- },
- {
- "fieldname":"sales_person",
- "label": __("Sales Person"),
- "fieldtype": "Link",
- "options": "Sales Person"
- },
{
"fieldname":"tax_id",
"label": __("Tax Id"),
diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py
index 6bd08ad837a1..6d2cd8ed4112 100644
--- a/erpnext/accounts/report/trial_balance/trial_balance.py
+++ b/erpnext/accounts/report/trial_balance/trial_balance.py
@@ -172,6 +172,7 @@ def get_rootwise_opening_balances(filters, report_type):
query_filters = {
"company": filters.company,
"from_date": filters.from_date,
+ "to_date": filters.to_date,
"report_type": report_type,
"year_start_date": filters.year_start_date,
"project": filters.project,
@@ -200,7 +201,7 @@ def get_rootwise_opening_balances(filters, report_type):
where
company=%(company)s
{additional_conditions}
- and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes')
+ and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s))
and account in (select name from `tabAccount` where report_type=%(report_type)s)
and is_cancelled = 0
group by account""".format(
diff --git a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py
index 869f6aaf9429..7ef2d22e3093 100644
--- a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py
+++ b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py
@@ -106,12 +106,17 @@ def get_opening_balances(filters):
where company=%(company)s
and is_cancelled=0
and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != ''
- and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes')
+ and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s))
{account_filter}
group by party""".format(
account_filter=account_filter
),
- {"company": filters.company, "from_date": filters.from_date, "party_type": filters.party_type},
+ {
+ "company": filters.company,
+ "from_date": filters.from_date,
+ "to_date": filters.to_date,
+ "party_type": filters.party_type,
+ },
as_dict=True,
)
diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py
index eed58367739e..d3cd29013f2a 100644
--- a/erpnext/accounts/report/utils.py
+++ b/erpnext/accounts/report/utils.py
@@ -28,7 +28,7 @@ def get_currency(filters):
filters["presentation_currency"] if filters.get("presentation_currency") else company_currency
)
- report_date = filters.get("to_date")
+ report_date = filters.get("to_date") or filters.get("period_end_date")
if not report_date:
fiscal_year_to_date = get_from_and_to_date(filters.get("to_fiscal_year"))["to_date"]
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 47de0eff3591..b0b217684ea4 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -818,6 +818,28 @@ def get_held_invoices(party_type, party):
return held_invoices
+def remove_return_pos_invoices(party_type, party, invoice_list):
+ if invoice_list:
+
+ if party_type == "Customer":
+ sinv = frappe.qb.DocType("Sales Invoice")
+ return_pos = (
+ frappe.qb.from_(sinv)
+ .select(sinv.name)
+ .where((sinv.is_pos == 1) & (sinv.docstatus == 1) & (sinv.is_return == 1))
+ .run()
+ )
+
+ if return_pos:
+ return_pos = [x[0] for x in return_pos]
+ else:
+ return invoice_list
+
+ invoice_list = [x for x in invoice_list if x.voucher_no not in return_pos]
+
+ return invoice_list
+
+
def get_outstanding_invoices(party_type, party, account, condition=None, filters=None):
outstanding_invoices = []
precision = frappe.get_precision("Sales Invoice", "outstanding_amount") or 2
@@ -868,6 +890,8 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters
as_dict=True,
)
+ invoice_list = remove_return_pos_invoices(party_type, party, invoice_list)
+
payment_entries = frappe.db.sql(
"""
select against_voucher_type, against_voucher,
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index 153f5c537a2a..1473b79bea54 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -132,6 +132,10 @@ frappe.ui.form.on('Asset', {
}, __("Manage"));
}
+ if (frm.doc.depr_entry_posting_status === "Failed") {
+ frm.trigger("set_depr_posting_failure_alert");
+ }
+
frm.trigger("setup_chart");
}
@@ -142,6 +146,19 @@ frappe.ui.form.on('Asset', {
}
},
+ set_depr_posting_failure_alert: function (frm) {
+ const alert = `
+
+
+
+ Failed to post depreciation entries
+
+
+ `;
+
+ frm.dashboard.set_headline_alert(alert);
+ },
+
toggle_reference_doc: function(frm) {
if (frm.doc.purchase_receipt && frm.doc.purchase_invoice && frm.doc.docstatus === 1) {
frm.set_df_property('purchase_invoice', 'read_only', 1);
@@ -384,7 +401,11 @@ frappe.ui.form.on('Asset', {
set_values_from_purchase_doc: function(frm, doctype, purchase_doc) {
frm.set_value('company', purchase_doc.company);
- frm.set_value('purchase_date', purchase_doc.posting_date);
+ if (purchase_doc.bill_date) {
+ frm.set_value('purchase_date', purchase_doc.bill_date);
+ } else {
+ frm.set_value('purchase_date', purchase_doc.posting_date);
+ }
const item = purchase_doc.items.find(item => item.item_code === frm.doc.item_code);
if (!item) {
doctype_field = frappe.scrub(doctype)
diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json
index 04e9c32f3793..8132dbd411a6 100644
--- a/erpnext/assets/doctype/asset/asset.json
+++ b/erpnext/assets/doctype/asset/asset.json
@@ -68,6 +68,7 @@
"column_break_51",
"purchase_receipt_amount",
"default_finance_book",
+ "depr_entry_posting_status",
"amended_from"
],
"fields": [
@@ -473,6 +474,16 @@
"fieldname": "section_break_36",
"fieldtype": "Section Break",
"label": "Finance Books"
+ },
+ {
+ "fieldname": "depr_entry_posting_status",
+ "fieldtype": "Select",
+ "hidden": 1,
+ "label": "Depreciation Entry Posting Status",
+ "no_copy": 1,
+ "options": "\nSuccessful\nFailed",
+ "print_hide": 1,
+ "read_only": 1
}
],
"idx": 72,
@@ -487,7 +498,7 @@
{
"group": "Repair",
"link_doctype": "Asset Repair",
- "link_fieldname": "asset_name"
+ "link_fieldname": "asset"
},
{
"group": "Value",
@@ -495,7 +506,7 @@
"link_fieldname": "asset"
}
],
- "modified": "2022-07-20 16:22:44.437579",
+ "modified": "2023-01-17 00:28:37.789345",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 1ce815ae2bd4..59187d36865b 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -819,11 +819,15 @@ def get_pro_rata_amt(self, row, depreciation_amount, from_date, to_date):
def update_maintenance_status():
- assets = frappe.get_all("Asset", filters={"docstatus": 1, "maintenance_required": 1})
+ assets = frappe.get_all(
+ "Asset", filters={"docstatus": 1, "maintenance_required": 1, "disposal_date": ("is", "not set")}
+ )
for asset in assets:
asset = frappe.get_doc("Asset", asset.name)
- if frappe.db.exists("Asset Repair", {"asset_name": asset.name, "repair_status": "Pending"}):
+ if frappe.db.exists(
+ "Asset Repair", {"asset_name": asset.name, "repair_status": "Pending", "docstatus": 0}
+ ):
asset.set_status("Out of Order")
elif frappe.db.exists(
"Asset Maintenance Task", {"parent": asset.name, "next_due_date": today()}
diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py
index 3f7e94599434..f8f581d8ae24 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -4,7 +4,8 @@
import frappe
from frappe import _
-from frappe.utils import cint, flt, getdate, today
+from frappe.utils import cint, flt, get_link_to_form, getdate, today
+from frappe.utils.user import get_users_with_role
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
@@ -20,9 +21,22 @@ def post_depreciation_entries(date=None):
if not date:
date = today()
- for asset in get_depreciable_assets(date):
- make_depreciation_entry(asset, date)
- frappe.db.commit()
+
+ failed_asset_names = []
+
+ for asset_name in get_depreciable_assets(date):
+ try:
+ make_depreciation_entry(asset_name, date)
+ frappe.db.commit()
+ except Exception as e:
+ frappe.db.rollback()
+ failed_asset_names.append(asset_name)
+
+ if failed_asset_names:
+ set_depr_entry_posting_status_for_failed_assets(failed_asset_names)
+ notify_depr_entry_posting_error(failed_asset_names)
+
+ frappe.db.commit()
def get_depreciable_assets(date):
@@ -121,6 +135,8 @@ def make_depreciation_entry(asset_name, date=None):
finance_books.value_after_depreciation -= d.depreciation_amount
finance_books.db_update()
+ frappe.db.set_value("Asset", asset_name, "depr_entry_posting_status", "Successful")
+
asset.set_status()
return asset
@@ -184,6 +200,42 @@ def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation
return credit_account, debit_account
+def set_depr_entry_posting_status_for_failed_assets(failed_asset_names):
+ for asset_name in failed_asset_names:
+ frappe.db.set_value("Asset", asset_name, "depr_entry_posting_status", "Failed")
+
+
+def notify_depr_entry_posting_error(failed_asset_names):
+ recipients = get_users_with_role("Accounts Manager")
+
+ if not recipients:
+ recipients = get_users_with_role("System Manager")
+
+ subject = _("Error while posting depreciation entries")
+
+ asset_links = get_comma_separated_asset_links(failed_asset_names)
+
+ message = (
+ _("Hi,")
+ + " "
+ + _("The following assets have failed to post depreciation entries: {0}").format(asset_links)
+ + "."
+ )
+
+ frappe.sendmail(recipients=recipients, subject=subject, message=message)
+
+
+def get_comma_separated_asset_links(asset_names):
+ asset_links = []
+
+ for asset_name in asset_names:
+ asset_links.append(get_link_to_form("Asset", asset_name))
+
+ asset_links = ", ".join(asset_links)
+
+ return asset_links
+
+
@frappe.whitelist()
def scrap_asset(asset_name):
asset = frappe.get_doc("Asset", asset_name)
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index 13475f34c392..26db6396df36 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -7,7 +7,7 @@
from frappe.utils import add_days, add_months, cstr, flt, get_last_day, getdate, nowdate
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
-from erpnext.assets.doctype.asset.asset import make_sales_invoice
+from erpnext.assets.doctype.asset.asset import make_sales_invoice, update_maintenance_status
from erpnext.assets.doctype.asset.depreciation import (
post_depreciation_entries,
restore_asset,
@@ -238,6 +238,34 @@ def test_gle_made_by_asset_sale(self):
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
+ def test_asset_with_maintenance_required_status_after_sale(self):
+ asset = create_asset(
+ calculate_depreciation=1,
+ available_for_use_date="2020-06-06",
+ purchase_date="2020-01-01",
+ expected_value_after_useful_life=10000,
+ total_number_of_depreciations=3,
+ frequency_of_depreciation=10,
+ maintenance_required=1,
+ depreciation_start_date="2020-12-31",
+ submit=1,
+ )
+
+ post_depreciation_entries(date="2021-01-01")
+
+ si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
+ si.customer = "_Test Customer"
+ si.due_date = nowdate()
+ si.get("items")[0].rate = 25000
+ si.insert()
+ si.submit()
+
+ self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
+
+ update_maintenance_status()
+
+ self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
+
def test_expense_head(self):
pr = make_purchase_receipt(
item_code="Macbook Pro", qty=2, rate=200000.0, location="Test Location"
@@ -1353,11 +1381,13 @@ def create_asset(**args):
"number_of_depreciations_booked": args.number_of_depreciations_booked or 0,
"gross_purchase_amount": args.gross_purchase_amount or 100000,
"purchase_receipt_amount": args.purchase_receipt_amount or 100000,
+ "maintenance_required": args.maintenance_required or 0,
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"available_for_use_date": args.available_for_use_date or "2020-06-06",
"location": args.location or "Test Location",
"asset_owner": args.asset_owner or "Company",
"is_existing_asset": args.is_existing_asset or 1,
+ "depr_entry_posting_status": args.depr_entry_posting_status or "",
}
)
diff --git a/erpnext/assets/doctype/location/location.py b/erpnext/assets/doctype/location/location.py
index 0d87bb2bf4d8..5bff3dd8c99c 100644
--- a/erpnext/assets/doctype/location/location.py
+++ b/erpnext/assets/doctype/location/location.py
@@ -200,11 +200,11 @@ def get_children(doctype, parent=None, location=None, is_root=False):
name as value,
is_group as expandable
from
- `tab{doctype}` comp
+ `tabLocation` comp
where
ifnull(parent_location, "")={parent}
""".format(
- doctype=doctype, parent=frappe.db.escape(parent)
+ parent=frappe.db.escape(parent)
),
as_dict=1,
)
diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
index 6b14dce084e6..dd5dfca8a224 100644
--- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
+++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
@@ -86,6 +86,7 @@ def get_data(filters):
"status",
"department",
"cost_center",
+ "calculate_depreciation",
"purchase_receipt",
"asset_category",
"purchase_date",
@@ -98,11 +99,7 @@ def get_data(filters):
assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields)
for asset in assets_record:
- asset_value = (
- asset.gross_purchase_amount
- - flt(asset.opening_accumulated_depreciation)
- - flt(depreciation_amount_map.get(asset.name))
- )
+ asset_value = get_asset_value(asset, filters.finance_book)
row = {
"asset_id": asset.asset_id,
"asset_name": asset.asset_name,
@@ -125,6 +122,21 @@ def get_data(filters):
return data
+def get_asset_value(asset, finance_book=None):
+ if not asset.calculate_depreciation:
+ return flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation)
+
+ finance_book_filter = ["finance_book", "is", "not set"]
+ if finance_book:
+ finance_book_filter = ["finance_book", "=", finance_book]
+
+ return frappe.db.get_value(
+ doctype="Asset Finance Book",
+ filters=[["parent", "=", asset.asset_id], finance_book_filter],
+ fieldname="value_after_depreciation",
+ )
+
+
def prepare_chart_data(data, filters):
labels_values_map = {}
date_field = frappe.scrub(filters.date_based_on)
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index b6f5ff921918..2559ce76da6e 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -43,6 +43,11 @@ frappe.ui.form.on("Purchase Order", {
erpnext.queries.setup_queries(frm, "Warehouse", function() {
return erpnext.queries.warehouse(frm.doc);
});
+
+ // On cancel and amending a purchase order with advance payment, reset advance paid amount
+ if (frm.is_new()) {
+ frm.set_value("advance_paid", 0)
+ }
},
apply_tds: function(frm) {
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json
index a6e7ba1b27b8..0451798e6fd7 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.json
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.json
@@ -371,7 +371,7 @@
{
"fieldname": "shipping_address",
"fieldtype": "Link",
- "label": "Company Shipping Address",
+ "label": "Shipping Address",
"options": "Address",
"print_hide": 1
},
@@ -440,7 +440,6 @@
"fieldname": "ignore_pricing_rule",
"fieldtype": "Check",
"label": "Ignore Pricing Rule",
- "no_copy": 1,
"permlevel": 1,
"print_hide": 1
},
@@ -1120,7 +1119,8 @@
"fetch_from": "supplier.is_internal_supplier",
"fieldname": "is_internal_supplier",
"fieldtype": "Check",
- "label": "Is Internal Supplier"
+ "label": "Is Internal Supplier",
+ "read_only": 1
},
{
"fetch_from": "supplier.represents_company",
@@ -1178,7 +1178,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2022-04-26 12:16:38.694276",
+ "modified": "2022-12-25 18:08:59.074182",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 250faa7c88b6..3888622d563d 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -18,7 +18,7 @@
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
get_party_tax_withholding_details,
)
-from erpnext.accounts.party import get_party_account_currency
+from erpnext.accounts.party import get_party_account, get_party_account_currency
from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items
from erpnext.controllers.buying_controller import BuyingController
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
@@ -323,6 +323,7 @@ def on_submit(self):
update_linked_doc(self.doctype, self.name, self.inter_company_order_reference)
def on_cancel(self):
+ self.ignore_linked_doctypes = ("GL Entry", "Payment Ledger Entry")
super(PurchaseOrder, self).on_cancel()
if self.is_against_so():
@@ -532,6 +533,7 @@ def postprocess(source, target):
target.set_advances()
target.set_payment_schedule()
+ target.credit_to = get_party_account("Supplier", source.supplier, source.company)
def update_item(obj, target, source_parent):
target.amount = flt(obj.amount) - flt(obj.billed_amt)
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
index 3cc25580013d..ae0fe0d1e8cb 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
@@ -22,6 +22,13 @@ frappe.ui.form.on("Request for Quotation",{
}
};
}
+
+ frm.set_query('warehouse', 'items', () => ({
+ filters: {
+ company: frm.doc.company,
+ is_group: 0
+ }
+ }));
},
onload: function(frm) {
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
index 45a2ad43e7c1..309d321ba814 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
@@ -479,7 +479,7 @@ def get_rfq_containing_supplier(doctype, txt, searchfield, start, page_len, filt
conditions += "and rfq.transaction_date = '{0}'".format(filters.get("transaction_date"))
rfq_data = frappe.db.sql(
- """
+ f"""
select
distinct rfq.name, rfq.transaction_date,
rfq.company
@@ -487,15 +487,18 @@ def get_rfq_containing_supplier(doctype, txt, searchfield, start, page_len, filt
`tabRequest for Quotation` rfq, `tabRequest for Quotation Supplier` rfq_supplier
where
rfq.name = rfq_supplier.parent
- and rfq_supplier.supplier = '{0}'
+ and rfq_supplier.supplier = %(supplier)s
and rfq.docstatus = 1
- and rfq.company = '{1}'
- {2}
+ and rfq.company = %(company)s
+ {conditions}
order by rfq.transaction_date ASC
- limit %(page_len)s offset %(start)s """.format(
- filters.get("supplier"), filters.get("company"), conditions
- ),
- {"page_len": page_len, "start": start},
+ limit %(page_len)s offset %(start)s """,
+ {
+ "page_len": page_len,
+ "start": start,
+ "company": filters.get("company"),
+ "supplier": filters.get("supplier"),
+ },
as_dict=1,
)
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index c4b3c6ec8a74..cd83ad9e5094 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -152,6 +152,7 @@ def validate(self):
self.validate_inter_company_reference()
self.disable_pricing_rule_on_internal_transfer()
+ self.disable_tax_included_prices_for_internal_transfer()
self.set_incoming_rate()
if self.meta.get_field("currency"):
@@ -378,7 +379,7 @@ def validate_inter_company_reference(self):
self.get("inter_company_reference")
or self.get("inter_company_invoice_reference")
or self.get("inter_company_order_reference")
- ):
+ ) and not self.get("is_return"):
msg = _("Internal Sale or Delivery Reference missing.")
msg += _("Please create purchase from internal sale or delivery document itself")
frappe.throw(msg, title=_("Internal Sales Reference Missing"))
@@ -391,6 +392,20 @@ def disable_pricing_rule_on_internal_transfer(self):
alert=1,
)
+ def disable_tax_included_prices_for_internal_transfer(self):
+ if self.is_internal_transfer():
+ tax_updated = False
+ for tax in self.get("taxes"):
+ if tax.get("included_in_print_rate"):
+ tax.included_in_print_rate = 0
+ tax_updated = True
+
+ if tax_updated:
+ frappe.msgprint(
+ _("Disabled tax included prices since this {} is an internal transfer").format(self.doctype),
+ alert=1,
+ )
+
def validate_due_date(self):
if self.get("is_pos"):
return
@@ -554,7 +569,12 @@ def set_missing_item_details(self, for_validate=False):
if bool(uom) != bool(stock_uom): # xor
item.stock_uom = item.uom = uom or stock_uom
- item.conversion_factor = get_uom_conv_factor(item.get("uom"), item.get("stock_uom"))
+ # UOM cannot be zero so substitute as 1
+ item.conversion_factor = (
+ get_uom_conv_factor(item.get("uom"), item.get("stock_uom"))
+ or item.get("conversion_factor")
+ or 1
+ )
if self.doctype == "Purchase Invoice":
self.set_expense_account(for_validate)
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index d0f81c2b270c..ffd021773c91 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -193,16 +193,16 @@ def set_total_in_words(self):
if self.meta.get_field("base_in_words"):
if self.meta.get_field("base_rounded_total") and not self.is_rounded_total_disabled():
- amount = self.base_rounded_total
+ amount = abs(self.base_rounded_total)
else:
- amount = self.base_grand_total
+ amount = abs(self.base_grand_total)
self.base_in_words = money_in_words(amount, self.company_currency)
if self.meta.get_field("in_words"):
if self.meta.get_field("rounded_total") and not self.is_rounded_total_disabled():
- amount = self.rounded_total
+ amount = abs(self.rounded_total)
else:
- amount = self.grand_total
+ amount = abs(self.grand_total)
self.in_words = money_in_words(amount, self.currency)
@@ -303,7 +303,11 @@ def set_incoming_rate(self):
rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
else:
field = "incoming_rate" if self.get("is_internal_supplier") else "rate"
- rate = frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
+ rate = flt(
+ frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
+ * (d.conversion_factor or 1),
+ d.precision("rate"),
+ )
if self.is_internal_transfer():
if rate != d.rate:
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 8a26d9e2c891..57f8a3e1513c 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -26,7 +26,7 @@ def onload(self):
super(SellingController, self).onload()
if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"):
for item in self.get("items"):
- item.update(get_bin_details(item.item_code, item.warehouse))
+ item.update(get_bin_details(item.item_code, item.warehouse, include_child_warehouses=True))
def validate(self):
super(SellingController, self).validate()
@@ -439,11 +439,17 @@ def set_incoming_rate(self):
# For internal transfers use incoming rate as the valuation rate
if self.is_internal_transfer():
if d.doctype == "Packed Item":
- incoming_rate = flt(d.incoming_rate * d.conversion_factor, d.precision("incoming_rate"))
+ incoming_rate = flt(
+ flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor,
+ d.precision("incoming_rate"),
+ )
if d.incoming_rate != incoming_rate:
d.incoming_rate = incoming_rate
else:
- rate = flt(d.incoming_rate * d.conversion_factor, d.precision("rate"))
+ rate = flt(
+ flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor,
+ d.precision("rate"),
+ )
if d.rate != rate:
d.rate = rate
frappe.msgprint(
@@ -572,6 +578,7 @@ def set_customer_address(self):
"customer_address": "address_display",
"shipping_address_name": "shipping_address",
"company_address": "company_address_display",
+ "dispatch_address_name": "dispatch_address",
}
for address_field, address_display_field in address_dict.items():
diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py
index 517e080c972d..62e90ae747da 100644
--- a/erpnext/controllers/status_updater.py
+++ b/erpnext/controllers/status_updater.py
@@ -333,16 +333,21 @@ def limits_crossed_error(self, args, item, qty_or_amount):
)
def warn_about_bypassing_with_role(self, item, qty_or_amount, role):
- action = _("Over Receipt/Delivery") if qty_or_amount == "qty" else _("Overbilling")
-
- msg = _("{} of {} {} ignored for item {} because you have {} role.").format(
- action,
- _(item["target_ref_field"].title()),
- frappe.bold(item["reduce_by"]),
- frappe.bold(item.get("item_code")),
- role,
+ if qty_or_amount == "qty":
+ msg = _("Over Receipt/Delivery of {0} {1} ignored for item {2} because you have {3} role.")
+ else:
+ msg = _("Overbilling of {0} {1} ignored for item {2} because you have {3} role.")
+
+ frappe.msgprint(
+ msg.format(
+ _(item["target_ref_field"].title()),
+ frappe.bold(item["reduce_by"]),
+ frappe.bold(item.get("item_code")),
+ role,
+ ),
+ indicator="orange",
+ alert=True,
)
- frappe.msgprint(msg, indicator="orange", alert=True)
def update_qty(self, update_modified=True):
"""Updates qty or amount at row level
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 2eea0bde8c6d..3a08051b2f38 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -139,13 +139,15 @@ def get_gl_entries(
warehouse_with_no_account = []
precision = self.get_debit_field_precision()
for item_row in voucher_details:
-
sle_list = sle_map.get(item_row.name)
+ sle_rounding_diff = 0.0
if sle_list:
for sle in sle_list:
if warehouse_account.get(sle.warehouse):
# from warehouse account
+ sle_rounding_diff += flt(sle.stock_value_difference)
+
self.check_expense_account(item_row)
# expense account/ target_warehouse / source_warehouse
@@ -188,6 +190,46 @@ def get_gl_entries(
elif sle.warehouse not in warehouse_with_no_account:
warehouse_with_no_account.append(sle.warehouse)
+ if abs(sle_rounding_diff) > (1.0 / (10**precision)) and self.is_internal_transfer():
+ warehouse_asset_account = ""
+ if self.get("is_internal_customer"):
+ warehouse_asset_account = warehouse_account[item_row.get("target_warehouse")]["account"]
+ elif self.get("is_internal_supplier"):
+ warehouse_asset_account = warehouse_account[item_row.get("warehouse")]["account"]
+
+ expense_account = frappe.db.get_value("Company", self.company, "default_expense_account")
+
+ gl_list.append(
+ self.get_gl_dict(
+ {
+ "account": expense_account,
+ "against": warehouse_asset_account,
+ "cost_center": item_row.cost_center,
+ "project": item_row.project or self.get("project"),
+ "remarks": _("Rounding gain/loss Entry for Stock Transfer"),
+ "debit": sle_rounding_diff,
+ "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No",
+ },
+ warehouse_account[sle.warehouse]["account_currency"],
+ item=item_row,
+ )
+ )
+
+ gl_list.append(
+ self.get_gl_dict(
+ {
+ "account": warehouse_asset_account,
+ "against": expense_account,
+ "cost_center": item_row.cost_center,
+ "remarks": _("Rounding gain/loss Entry for Stock Transfer"),
+ "credit": sle_rounding_diff,
+ "project": item_row.get("project") or self.get("project"),
+ "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No",
+ },
+ item=item_row,
+ )
+ )
+
if warehouse_with_no_account:
for wh in warehouse_with_no_account:
if frappe.db.get_value("Warehouse", wh, "company"):
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 66d4cad80374..ed95f8343797 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -890,24 +890,33 @@ def set_item_wise_tax_breakup(self):
self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc)
def set_total_amount_to_default_mop(self, total_amount_to_pay):
- default_mode_of_payment = frappe.db.get_value(
- "POS Payment Method",
- {"parent": self.doc.pos_profile, "default": 1},
- ["mode_of_payment"],
- as_dict=1,
- )
+ total_paid_amount = 0
+ for payment in self.doc.get("payments"):
+ total_paid_amount += (
+ payment.amount if self.doc.party_account_currency == self.doc.currency else payment.base_amount
+ )
+
+ pending_amount = total_amount_to_pay - total_paid_amount
- if default_mode_of_payment:
- self.doc.payments = []
- self.doc.append(
- "payments",
- {
- "mode_of_payment": default_mode_of_payment.mode_of_payment,
- "amount": total_amount_to_pay,
- "default": 1,
- },
+ if pending_amount > 0:
+ default_mode_of_payment = frappe.db.get_value(
+ "POS Payment Method",
+ {"parent": self.doc.pos_profile, "default": 1},
+ ["mode_of_payment"],
+ as_dict=1,
)
+ if default_mode_of_payment:
+ self.doc.payments = []
+ self.doc.append(
+ "payments",
+ {
+ "mode_of_payment": default_mode_of_payment.mode_of_payment,
+ "amount": pending_amount,
+ "default": 1,
+ },
+ )
+
def get_itemised_tax_breakup_html(doc):
if not doc.taxes:
diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json
index 306be7faa74d..c4c26d0c27ed 100644
--- a/erpnext/crm/doctype/appointment/appointment.json
+++ b/erpnext/crm/doctype/appointment/appointment.json
@@ -102,7 +102,7 @@
}
],
"links": [],
- "modified": "2021-06-29 18:27:02.832979",
+ "modified": "2022-12-28 16:35:34.377575",
"modified_by": "Administrator",
"module": "CRM",
"name": "Appointment",
@@ -121,16 +121,6 @@
"share": 1,
"write": 1
},
- {
- "create": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Guest",
- "share": 1
- },
{
"create": 1,
"delete": 1,
diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py
index 69b8c324e009..65360542324c 100644
--- a/erpnext/crm/doctype/appointment/appointment.py
+++ b/erpnext/crm/doctype/appointment/appointment.py
@@ -5,7 +5,9 @@
from collections import Counter
import frappe
+import frappe.share
from frappe import _
+from frappe.desk.form.assign_to import add as add_assignment
from frappe.model.document import Document
from frappe.utils import get_url, getdate
from frappe.utils.verified_command import get_signed_params
@@ -118,21 +120,18 @@ def create_lead_and_link(self):
self.party = lead.name
def auto_assign(self):
- from frappe.desk.form.assign_to import add as add_assignemnt
-
existing_assignee = self.get_assignee_from_latest_opportunity()
if existing_assignee:
# If the latest opportunity is assigned to someone
# Assign the appointment to the same
- add_assignemnt({"doctype": self.doctype, "name": self.name, "assign_to": [existing_assignee]})
+ self.assign_agent(existing_assignee)
return
if self._assign:
return
available_agents = _get_agents_sorted_by_asc_workload(getdate(self.scheduled_time))
for agent in available_agents:
if _check_agent_availability(agent, self.scheduled_time):
- agent = agent[0]
- add_assignemnt({"doctype": self.doctype, "name": self.name, "assign_to": [agent]})
+ self.assign_agent(agent[0])
break
def get_assignee_from_latest_opportunity(self):
@@ -187,9 +186,15 @@ def _get_verify_url(self):
params = {"email": self.customer_email, "appointment": self.name}
return get_url(verify_route + "?" + get_signed_params(params))
+ def assign_agent(self, agent):
+ if not frappe.has_permission(doc=self, user=agent):
+ frappe.share.add(self.doctype, self.name, agent, flags={"ignore_share_permission": True})
+
+ add_assignment({"doctype": self.doctype, "name": self.name, "assign_to": [agent]})
+
def _get_agents_sorted_by_asc_workload(date):
- appointments = frappe.db.get_list("Appointment", fields="*")
+ appointments = frappe.get_all("Appointment", fields="*")
agent_list = _get_agent_list_as_strings()
if not appointments:
return agent_list
@@ -214,7 +219,7 @@ def _get_agent_list_as_strings():
def _check_agent_availability(agent_email, scheduled_time):
- appointemnts_at_scheduled_time = frappe.get_list(
+ appointemnts_at_scheduled_time = frappe.get_all(
"Appointment", filters={"scheduled_time": scheduled_time}
)
for appointment in appointemnts_at_scheduled_time:
diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
index 4b26e4901bd4..ac58e386fe3c 100644
--- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
+++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2019-08-27 10:56:48.309824",
"doctype": "DocType",
"editable_grid": 1,
@@ -101,7 +102,8 @@
}
],
"issingle": 1,
- "modified": "2019-11-26 12:14:17.669366",
+ "links": [],
+ "modified": "2022-12-28 16:41:28.773090",
"modified_by": "Administrator",
"module": "CRM",
"name": "Appointment Booking Settings",
@@ -117,13 +119,6 @@
"share": 1,
"write": 1
},
- {
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "Guest",
- "share": 1
- },
{
"create": 1,
"email": 1,
diff --git a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.js b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.js
index 7b0e4ab47c8c..7ff5174d6484 100644
--- a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.js
+++ b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.js
@@ -11,9 +11,12 @@ frappe.ui.form.on('Course Scheduling Tool', {
},
refresh(frm) {
frm.disable_save();
+ frm.trigger("render_days");
frm.page.set_primary_action(__('Schedule Course'), () => {
- frm.call('schedule_course')
+ frappe.dom.freeze(__("Scheduling..."));
+ frm.call('schedule_course', { days: frm.days.get_checked_options() })
.then(r => {
+ frappe.dom.unfreeze();
if (!r.message) {
frappe.throw(__('There were errors creating Course Schedule'));
}
@@ -40,5 +43,60 @@ frappe.ui.form.on('Course Scheduling Tool', {
}
});
});
+ },
+ render_days: function(frm) {
+ const days_html = $(' ').appendTo(
+ frm.fields_dict.days_html.wrapper
+ );
+
+ if (!frm.days) {
+ frm.days = frappe.ui.form.make_control({
+ parent: days_html,
+ df: {
+ fieldname: "days",
+ fieldtype: "MultiCheck",
+ select_all: true,
+ columns: 4,
+ options: [
+ {
+ label: __("Monday"),
+ value: "Monday",
+ checked: 0,
+ },
+ {
+ label: __("Tuesday"),
+ value: "Tuesday",
+ checked: 0,
+ },
+ {
+ label: __("Wednesday"),
+ value: "Wednesday",
+ checked: 0,
+ },
+ {
+ label: __("Thursday"),
+ value: "Thursday",
+ checked: 0,
+ },
+ {
+ label: __("Friday"),
+ value: "Friday",
+ checked: 0,
+ },
+ {
+ label: __("Saturday"),
+ value: "Saturday",
+ checked: 0,
+ },
+ {
+ label: __("Sunday"),
+ value: "Sunday",
+ checked: 0,
+ },
+ ],
+ },
+ render_input: true,
+ });
+ }
}
});
diff --git a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.json b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.json
index 2926fe8af345..1b89c23d0aa9 100644
--- a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.json
+++ b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.json
@@ -1,661 +1,171 @@
{
- "allow_copy": 1,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2015-09-23 15:37:38.108475",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 0,
- "engine": "InnoDB",
+ "actions": [],
+ "allow_copy": 1,
+ "creation": "2015-09-23 15:37:38.108475",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "engine": "InnoDB",
+ "field_order": [
+ "student_group",
+ "course",
+ "program",
+ "column_break_3",
+ "academic_year",
+ "academic_term",
+ "section_break_6",
+ "instructor",
+ "instructor_name",
+ "column_break_9",
+ "room",
+ "section_break_7",
+ "days_html",
+ "section_break_14",
+ "from_time",
+ "course_start_date",
+ "column_break_15",
+ "to_time",
+ "course_end_date",
+ "reschedule"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "student_group",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Student Group",
- "length": 0,
- "no_copy": 0,
- "options": "Student Group",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "student_group",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Student Group",
+ "options": "Student Group",
+ "reqd": 1
+ },
+ {
+ "fieldname": "course",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Course",
+ "options": "Course",
+ "reqd": 1
+ },
+ {
+ "fieldname": "program",
+ "fieldtype": "Link",
+ "label": "Program",
+ "options": "Program",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "academic_year",
+ "fieldtype": "Link",
+ "label": "Academic Year",
+ "options": "Academic Year",
+ "read_only": 1
+ },
+ {
+ "fieldname": "academic_term",
+ "fieldtype": "Link",
+ "label": "Academic Term",
+ "options": "Academic Term",
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "instructor",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Instructor",
+ "options": "Instructor",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "course",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Course",
- "length": 0,
- "no_copy": 0,
- "options": "Course",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "program",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Program",
- "length": 0,
- "no_copy": 0,
- "options": "Program",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_3",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "academic_year",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Academic Year",
- "length": 0,
- "no_copy": 0,
- "options": "Academic Year",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "academic_term",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Academic Term",
- "length": 0,
- "no_copy": 0,
- "options": "Academic Term",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_6",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "instructor",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Instructor",
- "length": 0,
- "no_copy": 0,
- "options": "Instructor",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fetch_from": "instructor.instructor_name",
- "fieldname": "instructor_name",
- "fieldtype": "Read Only",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Instructor Name",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_9",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "room",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Room",
- "length": 0,
- "no_copy": 0,
- "options": "Room",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_7",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
- "fieldname": "from_time",
- "fieldtype": "Time",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "From Time",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
- "fieldname": "course_start_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Course Start Date",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "day",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Day",
- "length": 0,
- "no_copy": 0,
- "options": "\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "reschedule",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Reschedule",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_15",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "to_time",
- "fieldtype": "Time",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "To TIme",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
- "fieldname": "course_end_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Course End Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "instructor_name",
+ "fieldtype": "Read Only",
+ "label": "Instructor Name",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_9",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "room",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Room",
+ "options": "Room",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_7",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "from_time",
+ "fieldtype": "Time",
+ "label": "From Time",
+ "reqd": 1
+ },
+ {
+ "fieldname": "course_start_date",
+ "fieldtype": "Date",
+ "label": "Course Start Date",
+ "reqd": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "reschedule",
+ "fieldtype": "Check",
+ "label": "Reschedule"
+ },
+ {
+ "fieldname": "column_break_15",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "to_time",
+ "fieldtype": "Time",
+ "label": "To TIme",
+ "reqd": 1
+ },
+ {
+ "fieldname": "course_end_date",
+ "fieldtype": "Date",
+ "label": "Course End Date",
+ "reqd": 1
+ },
+ {
+ "fieldname": "days_html",
+ "fieldtype": "HTML",
+ "label": "Days HTML"
+ },
+ {
+ "fieldname": "section_break_14",
+ "fieldtype": "Section Break"
}
- ],
- "has_web_view": 0,
- "hide_heading": 1,
- "hide_toolbar": 1,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 1,
- "istable": 0,
- "max_attachments": 0,
- "menu_index": 0,
- "modified": "2018-05-16 22:43:29.363798",
- "modified_by": "Administrator",
- "module": "Education",
- "name": "Course Scheduling Tool",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "hide_toolbar": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2022-10-01 17:08:07.180557",
+ "modified_by": "Administrator",
+ "module": "Education",
+ "name": "Course Scheduling Tool",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
- "read": 1,
- "report": 0,
- "role": "Academics User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
+ "create": 1,
+ "read": 1,
+ "role": "Academics User",
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "restrict_to_domain": "Education",
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
+ ],
+ "restrict_to_domain": "Education",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py
index 4db6f981fcaa..5f828dea5b00 100644
--- a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py
+++ b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py
@@ -14,7 +14,7 @@
class CourseSchedulingTool(Document):
@frappe.whitelist()
- def schedule_course(self):
+ def schedule_course(self, days):
"""Creates course schedules as per specified parameters"""
course_schedules = []
@@ -22,7 +22,7 @@ def schedule_course(self):
rescheduled = []
reschedule_errors = []
- self.validate_mandatory()
+ self.validate_mandatory(days)
self.validate_date()
self.instructor_name = frappe.db.get_value("Instructor", self.instructor, "instructor_name")
@@ -34,24 +34,22 @@ def schedule_course(self):
self.course = course
if self.reschedule:
- rescheduled, reschedule_errors = self.delete_course_schedule(rescheduled, reschedule_errors)
+ rescheduled, reschedule_errors = self.delete_course_schedule(
+ rescheduled, reschedule_errors, days
+ )
date = self.course_start_date
while date < self.course_end_date:
- if self.day == calendar.day_name[getdate(date).weekday()]:
+ if calendar.day_name[getdate(date).weekday()] in days:
course_schedule = self.make_course_schedule(date)
try:
- print("pass")
course_schedule.save()
except OverlapError:
- print("fail")
course_schedules_errors.append(date)
else:
course_schedules.append(course_schedule)
- date = add_days(date, 7)
- else:
- date = add_days(date, 1)
+ date = add_days(date, 1)
return dict(
course_schedules=course_schedules,
@@ -60,8 +58,10 @@ def schedule_course(self):
reschedule_errors=reschedule_errors,
)
- def validate_mandatory(self):
+ def validate_mandatory(self, days):
"""Validates all mandatory fields"""
+ if not days:
+ frappe.throw(_("Please select at least one day to schedule the course."))
fields = [
"course",
@@ -71,7 +71,6 @@ def validate_mandatory(self):
"to_time",
"course_start_date",
"course_end_date",
- "day",
]
for d in fields:
if not self.get(d):
@@ -82,9 +81,8 @@ def validate_date(self):
if self.course_start_date > self.course_end_date:
frappe.throw(_("Course Start Date cannot be greater than Course End Date."))
- def delete_course_schedule(self, rescheduled, reschedule_errors):
+ def delete_course_schedule(self, rescheduled, reschedule_errors, days):
"""Delete all course schedule within the Date range and specified filters"""
-
schedules = frappe.get_list(
"Course Schedule",
fields=["name", "schedule_date"],
@@ -98,7 +96,7 @@ def delete_course_schedule(self, rescheduled, reschedule_errors):
for d in schedules:
try:
- if self.day == calendar.day_name[getdate(d.schedule_date).weekday()]:
+ if calendar.day_name[getdate(d.schedule_date).weekday()] in days:
frappe.delete_doc("Course Schedule", d.name)
rescheduled.append(d.name)
except Exception:
@@ -108,7 +106,6 @@ def delete_course_schedule(self, rescheduled, reschedule_errors):
def make_course_schedule(self, date):
"""Makes a new Course Schedule.
:param date: Date on which Course Schedule will be created."""
-
course_schedule = frappe.new_doc("Course Schedule")
course_schedule.student_group = self.student_group
course_schedule.course = self.course
diff --git a/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.py b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.py
index b93c5c4d38c1..da5699776fdf 100644
--- a/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.py
+++ b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.py
@@ -1345,7 +1345,7 @@ def _get_account_name_by_id(self, quickbooks_id):
)[0]["name"]
def _publish(self, *args, **kwargs):
- frappe.publish_realtime("quickbooks_progress_update", *args, **kwargs)
+ frappe.publish_realtime("quickbooks_progress_update", *args, **kwargs, user=self.modified_by)
def _get_unique_account_name(self, quickbooks_name, number=0):
if number:
diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py
index 833bbdfa3f38..7682cd0fe80c 100644
--- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py
+++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py
@@ -302,6 +302,7 @@ def publish(self, title, message, count, total):
frappe.publish_realtime(
"tally_migration_progress_update",
{"title": title, "message": message, "count": count, "total": total},
+ user=self.modified_by,
)
def _import_master_data(self):
diff --git a/erpnext/erpnext_integrations/taxjar_integration.py b/erpnext/erpnext_integrations/taxjar_integration.py
index b8893aa77329..2d9093b6e926 100644
--- a/erpnext/erpnext_integrations/taxjar_integration.py
+++ b/erpnext/erpnext_integrations/taxjar_integration.py
@@ -302,7 +302,7 @@ def check_for_nexus(doc, tax_dict):
item.tax_collectable = flt(0)
item.taxable_amount = flt(0)
- for tax in doc.taxes:
+ for tax in list(doc.taxes):
if tax.account_head == TAX_ACCOUNT_HEAD:
doc.taxes.remove(tax)
return
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 0556d40a4b85..fb56860dae50 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -334,8 +334,6 @@
"Patient": "erpnext.healthcare.web_form.personal_details.personal_details.has_website_permission",
}
-dump_report_map = "erpnext.startup.report_data_map.data_map"
-
before_tests = "erpnext.setup.utils.before_tests"
standard_queries = {
@@ -478,7 +476,6 @@
],
"hourly": [
"erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails",
- "erpnext.accounts.doctype.subscription.subscription.process_all",
"erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_settings.schedule_get_order_details",
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
"erpnext.projects.doctype.project.project.hourly_reminder",
@@ -487,6 +484,7 @@
"erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders",
],
"hourly_long": [
+ "erpnext.accounts.doctype.subscription.subscription.process_all",
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
],
diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py
index 7e51db2978bb..2e734f2aa305 100644
--- a/erpnext/hr/doctype/attendance/attendance.py
+++ b/erpnext/hr/doctype/attendance/attendance.py
@@ -5,7 +5,16 @@
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import cint, cstr, formatdate, get_datetime, getdate, nowdate
+from frappe.utils import (
+ add_days,
+ cint,
+ cstr,
+ formatdate,
+ get_datetime,
+ get_link_to_form,
+ getdate,
+ nowdate,
+)
from erpnext.hr.utils import get_holiday_dates_for_employee, validate_active_employee
@@ -106,8 +115,6 @@ def validate_employee(self):
frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee))
def unlink_attendance_from_checkins(self):
- from frappe.utils import get_link_to_form
-
EmployeeCheckin = frappe.qb.DocType("Employee Checkin")
linked_logs = (
frappe.qb.from_(EmployeeCheckin)
@@ -221,73 +228,39 @@ def mark_bulk_attendance(data):
attendance.submit()
-def get_month_map():
- return frappe._dict(
- {
- "January": 1,
- "February": 2,
- "March": 3,
- "April": 4,
- "May": 5,
- "June": 6,
- "July": 7,
- "August": 8,
- "September": 9,
- "October": 10,
- "November": 11,
- "December": 12,
- }
- )
-
-
@frappe.whitelist()
-def get_unmarked_days(employee, month, exclude_holidays=0):
- import calendar
-
- month_map = get_month_map()
- today = get_datetime()
-
+def get_unmarked_days(employee, from_date, to_date, exclude_holidays=0):
joining_date, relieving_date = frappe.get_cached_value(
"Employee", employee, ["date_of_joining", "relieving_date"]
)
- start_day = 1
- end_day = calendar.monthrange(today.year, month_map[month])[1] + 1
-
- if joining_date and joining_date.month == month_map[month]:
- start_day = joining_date.day
-
- if relieving_date and relieving_date.month == month_map[month]:
- end_day = relieving_date.day + 1
- dates_of_month = [
- "{}-{}-{}".format(today.year, month_map[month], r) for r in range(start_day, end_day)
- ]
- month_start, month_end = dates_of_month[0], dates_of_month[-1]
+ from_date = max(getdate(from_date), joining_date or getdate(from_date))
+ to_date = min(getdate(to_date), relieving_date or getdate(to_date))
records = frappe.get_all(
"Attendance",
fields=["attendance_date", "employee"],
filters=[
- ["attendance_date", ">=", month_start],
- ["attendance_date", "<=", month_end],
+ ["attendance_date", ">=", from_date],
+ ["attendance_date", "<=", to_date],
["employee", "=", employee],
["docstatus", "!=", 2],
],
)
- marked_days = [get_datetime(record.attendance_date) for record in records]
+ marked_days = [getdate(record.attendance_date) for record in records]
+
if cint(exclude_holidays):
- holiday_dates = get_holiday_dates_for_employee(employee, month_start, month_end)
- holidays = [get_datetime(record) for record in holiday_dates]
+ holiday_dates = get_holiday_dates_for_employee(employee, from_date, to_date)
+ holidays = [getdate(record) for record in holiday_dates]
marked_days.extend(holidays)
unmarked_days = []
- for date in dates_of_month:
- date_time = get_datetime(date)
- if today.day <= date_time.day and today.month <= date_time.month:
- break
- if date_time not in marked_days:
- unmarked_days.append(date)
+ while from_date <= to_date:
+ if from_date not in marked_days:
+ unmarked_days.append(from_date)
+
+ from_date = add_days(from_date, 1)
return unmarked_days
diff --git a/erpnext/hr/doctype/attendance/attendance_list.js b/erpnext/hr/doctype/attendance/attendance_list.js
index 3a5c59153964..bf9bc690b1b5 100644
--- a/erpnext/hr/doctype/attendance/attendance_list.js
+++ b/erpnext/hr/doctype/attendance/attendance_list.js
@@ -1,5 +1,6 @@
-frappe.listview_settings['Attendance'] = {
+frappe.listview_settings["Attendance"] = {
add_fields: ["status", "attendance_date"],
+
get_indicator: function (doc) {
if (["Present", "Work From Home"].includes(doc.status)) {
return [__(doc.status), "green", "status,=," + doc.status];
@@ -10,155 +11,185 @@ frappe.listview_settings['Attendance'] = {
}
},
- onload: function(list_view) {
+ onload: function (list_view) {
let me = this;
- const months = moment.months();
- list_view.page.add_inner_button(__("Mark Attendance"), function() {
+
+ list_view.page.add_inner_button(__("Mark Attendance"), function () {
+ let first_day_of_month = moment().startOf('month');
+
+ if (moment().toDate().getDate() === 1) {
+ first_day_of_month = first_day_of_month.subtract(1, "month");
+ }
+
let dialog = new frappe.ui.Dialog({
title: __("Mark Attendance"),
- fields: [{
- fieldname: 'employee',
- label: __('For Employee'),
- fieldtype: 'Link',
- options: 'Employee',
- get_query: () => {
- return {query: "erpnext.controllers.queries.employee_query"};
+ fields: [
+ {
+ fieldname: "employee",
+ label: __("For Employee"),
+ fieldtype: "Link",
+ options: "Employee",
+ get_query: () => {
+ return {
+ query: "erpnext.controllers.queries.employee_query",
+ };
+ },
+ reqd: 1,
+ onchange: () => me.reset_dialog(dialog),
},
- reqd: 1,
- onchange: function() {
- dialog.set_df_property("unmarked_days", "hidden", 1);
- dialog.set_df_property("status", "hidden", 1);
- dialog.set_df_property("exclude_holidays", "hidden", 1);
- dialog.set_df_property("month", "value", '');
- dialog.set_df_property("unmarked_days", "options", []);
- dialog.no_unmarked_days_left = false;
- }
- },
- {
- label: __("For Month"),
- fieldtype: "Select",
- fieldname: "month",
- options: months,
- reqd: 1,
- onchange: function() {
- if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
- dialog.set_df_property("status", "hidden", 0);
- dialog.set_df_property("exclude_holidays", "hidden", 0);
- dialog.set_df_property("unmarked_days", "options", []);
- dialog.no_unmarked_days_left = false;
- me.get_multi_select_options(
- dialog.fields_dict.employee.value,
- dialog.fields_dict.month.value,
- dialog.fields_dict.exclude_holidays.get_value()
- ).then(options => {
- if (options.length > 0) {
- dialog.set_df_property("unmarked_days", "hidden", 0);
- dialog.set_df_property("unmarked_days", "options", options);
- } else {
- dialog.no_unmarked_days_left = true;
- }
- });
- }
- }
- },
- {
- label: __("Status"),
- fieldtype: "Select",
- fieldname: "status",
- options: ["Present", "Absent", "Half Day", "Work From Home"],
- hidden: 1,
- reqd: 1,
-
- },
- {
- label: __("Exclude Holidays"),
- fieldtype: "Check",
- fieldname: "exclude_holidays",
- hidden: 1,
- onchange: function() {
- if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
- dialog.set_df_property("status", "hidden", 0);
- dialog.set_df_property("unmarked_days", "options", []);
- dialog.no_unmarked_days_left = false;
- me.get_multi_select_options(
- dialog.fields_dict.employee.value,
- dialog.fields_dict.month.value,
- dialog.fields_dict.exclude_holidays.get_value()
- ).then(options => {
- if (options.length > 0) {
- dialog.set_df_property("unmarked_days", "hidden", 0);
- dialog.set_df_property("unmarked_days", "options", options);
- } else {
- dialog.no_unmarked_days_left = true;
- }
- });
- }
- }
- },
- {
- label: __("Unmarked Attendance for days"),
- fieldname: "unmarked_days",
- fieldtype: "MultiCheck",
- options: [],
- columns: 2,
- hidden: 1
- }],
+ {
+ fieldtype: "Section Break",
+ fieldname: "time_period_section",
+ hidden: 1,
+ },
+ {
+ label: __("Start"),
+ fieldtype: "Date",
+ fieldname: "from_date",
+ reqd: 1,
+ default: first_day_of_month.toDate(),
+ onchange: () => me.get_unmarked_days(dialog),
+ },
+ {
+ fieldtype: "Column Break",
+ fieldname: "time_period_column",
+ },
+ {
+ label: __("End"),
+ fieldtype: "Date",
+ fieldname: "to_date",
+ reqd: 1,
+ default: moment().subtract(1, 'days').toDate(),
+ onchange: () => me.get_unmarked_days(dialog),
+ },
+ {
+ fieldtype: "Section Break",
+ fieldname: "days_section",
+ hidden: 1,
+ },
+ {
+ label: __("Status"),
+ fieldtype: "Select",
+ fieldname: "status",
+ options: ["Present", "Absent", "Half Day", "Work From Home"],
+ reqd: 1,
+ },
+ {
+ label: __("Exclude Holidays"),
+ fieldtype: "Check",
+ fieldname: "exclude_holidays",
+ onchange: () => me.get_unmarked_days(dialog),
+ },
+ {
+ label: __("Unmarked Attendance for days"),
+ fieldname: "unmarked_days",
+ fieldtype: "MultiCheck",
+ options: [],
+ columns: 2,
+ },
+ ],
primary_action(data) {
if (cur_dialog.no_unmarked_days_left) {
- frappe.msgprint(__("Attendance for the month of {0} , has already been marked for the Employee {1}",
- [dialog.fields_dict.month.value, dialog.fields_dict.employee.value]));
+ frappe.msgprint(
+ __(
+ "Attendance from {0} to {1} has already been marked for the Employee {2}",
+ [data.from_date, data.to_date, data.employee]
+ )
+ );
} else {
- frappe.confirm(__('Mark attendance as {0} for {1} on selected dates?', [data.status, data.month]), () => {
- frappe.call({
- method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance",
- args: {
- data: data
- },
- callback: function (r) {
- if (r.message === 1) {
- frappe.show_alert({
- message: __("Attendance Marked"),
- indicator: 'blue'
- });
- cur_dialog.hide();
- }
- }
- });
- });
+ frappe.confirm(
+ __("Mark attendance as {0} for {1} on selected dates?", [
+ data.status,
+ data.employee,
+ ]),
+ () => {
+ frappe.call({
+ method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance",
+ args: {
+ data: data,
+ },
+ callback: function (r) {
+ if (r.message === 1) {
+ frappe.show_alert({
+ message: __("Attendance Marked"),
+ indicator: "blue",
+ });
+ cur_dialog.hide();
+ }
+ },
+ });
+ }
+ );
}
dialog.hide();
list_view.refresh();
},
- primary_action_label: __('Mark Attendance')
-
+ primary_action_label: __("Mark Attendance"),
});
dialog.show();
});
},
- get_multi_select_options: function(employee, month, exclude_holidays) {
- return new Promise(resolve => {
- frappe.call({
- method: 'erpnext.hr.doctype.attendance.attendance.get_unmarked_days',
- async: false,
- args: {
- employee: employee,
- month: month,
- exclude_holidays: exclude_holidays
- }
- }).then(r => {
- var options = [];
- for (var d in r.message) {
- var momentObj = moment(r.message[d], 'YYYY-MM-DD');
- var date = momentObj.format('DD-MM-YYYY');
- options.push({
- "label": date,
- "value": r.message[d],
- "checked": 1
- });
- }
- resolve(options);
- });
+ reset_dialog: function (dialog) {
+ let fields = dialog.fields_dict;
+
+ dialog.set_df_property(
+ "time_period_section",
+ "hidden",
+ fields.employee.value ? 0 : 1
+ );
+
+ dialog.set_df_property("days_section", "hidden", 1);
+ dialog.set_df_property("unmarked_days", "options", []);
+ dialog.no_unmarked_days_left = false;
+ fields.exclude_holidays.value = false;
+
+ fields.to_date.datepicker.update({
+ maxDate: moment().subtract(1, 'days').toDate()
});
- }
+
+ this.get_unmarked_days(dialog)
+ },
+
+ get_unmarked_days: function (dialog) {
+ let fields = dialog.fields_dict;
+ if (fields.employee.value && fields.from_date.value && fields.to_date.value) {
+ dialog.set_df_property("days_section", "hidden", 0);
+ dialog.set_df_property("status", "hidden", 0);
+ dialog.set_df_property("exclude_holidays", "hidden", 0);
+ dialog.no_unmarked_days_left = false;
+
+ frappe
+ .call({
+ method: "erpnext.hr.doctype.attendance.attendance.get_unmarked_days",
+ async: false,
+ args: {
+ employee: fields.employee.value,
+ from_date: fields.from_date.value,
+ to_date: fields.to_date.value,
+ exclude_holidays: fields.exclude_holidays.value,
+ },
+ })
+ .then((r) => {
+ var options = [];
+
+ for (var d in r.message) {
+ var momentObj = moment(r.message[d], "YYYY-MM-DD");
+ var date = momentObj.format("DD-MM-YYYY");
+ options.push({
+ label: date,
+ value: r.message[d],
+ checked: 1,
+ });
+ }
+
+ dialog.set_df_property(
+ "unmarked_days",
+ "options",
+ options.length > 0 ? options : []
+ );
+ dialog.no_unmarked_days_left = options.length === 0;
+ });
+ }
+ },
};
diff --git a/erpnext/hr/doctype/attendance/test_attendance.py b/erpnext/hr/doctype/attendance/test_attendance.py
index 677a84100d0c..88f4c387bde1 100644
--- a/erpnext/hr/doctype/attendance/test_attendance.py
+++ b/erpnext/hr/doctype/attendance/test_attendance.py
@@ -6,6 +6,7 @@
from frappe.utils import (
add_days,
add_months,
+ get_first_day,
get_last_day,
get_year_ending,
get_year_start,
@@ -13,13 +14,9 @@
nowdate,
)
-from erpnext.hr.doctype.attendance.attendance import (
- get_month_map,
- get_unmarked_days,
- mark_attendance,
-)
+from erpnext.hr.doctype.attendance.attendance import get_unmarked_days, mark_attendance
from erpnext.hr.doctype.employee.test_employee import make_employee
-from erpnext.hr.doctype.leave_application.test_leave_application import get_first_sunday
+from erpnext.hr.tests.test_utils import get_first_sunday
test_records = frappe.get_test_records("Attendance")
@@ -28,7 +25,7 @@ class TestAttendance(FrappeTestCase):
def setUp(self):
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
- from_date = get_year_start(getdate())
+ from_date = get_year_start(add_months(getdate(), -1))
to_date = get_year_ending(getdate())
self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date)
@@ -55,9 +52,10 @@ def test_unmarked_days(self):
frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
mark_attendance(employee, attendance_date, "Present")
- month_name = get_month_name(attendance_date)
- unmarked_days = get_unmarked_days(employee, month_name)
+ unmarked_days = get_unmarked_days(
+ employee, get_first_day(attendance_date), get_last_day(attendance_date)
+ )
unmarked_days = [getdate(date) for date in unmarked_days]
# attendance already marked for the day
@@ -81,9 +79,10 @@ def test_unmarked_days_excluding_holidays(self):
frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
mark_attendance(employee, attendance_date, "Present")
- month_name = get_month_name(attendance_date)
- unmarked_days = get_unmarked_days(employee, month_name, exclude_holidays=True)
+ unmarked_days = unmarked_days = get_unmarked_days(
+ employee, get_first_day(attendance_date), get_last_day(attendance_date), exclude_holidays=True
+ )
unmarked_days = [getdate(date) for date in unmarked_days]
# attendance already marked for the day
@@ -110,9 +109,10 @@ def test_unmarked_days_as_per_joining_and_relieving_dates(self):
attendance_date = add_days(date, 2)
mark_attendance(employee, attendance_date, "Present")
- month_name = get_month_name(attendance_date)
- unmarked_days = get_unmarked_days(employee, month_name)
+ unmarked_days = get_unmarked_days(
+ employee, get_first_day(attendance_date), get_last_day(attendance_date)
+ )
unmarked_days = [getdate(date) for date in unmarked_days]
# attendance already marked for the day
@@ -124,10 +124,3 @@ def test_unmarked_days_as_per_joining_and_relieving_dates(self):
def tearDown(self):
frappe.db.rollback()
-
-
-def get_month_name(date):
- month_number = date.month
- for month, number in get_month_map().items():
- if number == month_number:
- return month
diff --git a/erpnext/hr/doctype/department/department.py b/erpnext/hr/doctype/department/department.py
index a9806c529f62..d4834add02b1 100644
--- a/erpnext/hr/doctype/department/department.py
+++ b/erpnext/hr/doctype/department/department.py
@@ -70,11 +70,11 @@ def get_children(doctype, parent=None, company=None, is_root=False):
select
name as value,
is_group as expandable
- from `tab{doctype}`
+ from `tabDepartment`
where
{condition}
order by name""".format(
- doctype=doctype, condition=condition
+ condition=condition
),
var_dict,
as_dict=1,
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index 99e001ab27ab..f36d0f2da868 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -33,6 +33,7 @@
create_assignment_for_multiple_employees,
)
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
+from erpnext.hr.tests.test_utils import get_first_sunday
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
make_holiday_list,
make_leave_application,
@@ -1105,23 +1106,6 @@ def allocate_leaves(employee, leave_period, leave_type, new_leaves_allocated, el
allocate_leave.submit()
-def get_first_sunday(holiday_list, for_date=None):
- date = for_date or getdate()
- month_start_date = get_first_day(date)
- month_end_date = get_last_day(date)
- first_sunday = frappe.db.sql(
- """
- select holiday_date from `tabHoliday`
- where parent = %s
- and holiday_date between %s and %s
- order by holiday_date
- """,
- (holiday_list, month_start_date, month_end_date),
- )[0][0]
-
- return first_sunday
-
-
def make_policy_assignment(employee, leave_type, leave_period):
frappe.delete_doc_if_exists("Leave Type", leave_type, force=1)
frappe.get_doc(
diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
index 1f7ade23f4c8..fccf722939ec 100644
--- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
+++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
@@ -111,34 +111,30 @@ def get_data(filters: Filters) -> List:
employee.leave_approver
)
- if (
- (leave_approvers and len(leave_approvers) and user in leave_approvers)
- or (user in ["Administrator", employee.user_id])
- or ("HR Manager" in frappe.get_roles(user))
- ):
- if len(active_employees) > 1:
- row = frappe._dict()
- row.employee = employee.name
- row.employee_name = employee.employee_name
-
- leaves_taken = (
- get_leaves_for_period(employee.name, leave_type, filters.from_date, filters.to_date) * -1
- )
+ if len(active_employees) > 1:
+ row = frappe._dict()
- new_allocation, expired_leaves, carry_forwarded_leaves = get_allocated_and_expired_leaves(
- filters.from_date, filters.to_date, employee.name, leave_type
- )
- opening = get_opening_balance(employee.name, leave_type, filters, carry_forwarded_leaves)
+ row.employee = employee.name
+ row.employee_name = employee.employee_name
+
+ leaves_taken = (
+ get_leaves_for_period(employee.name, leave_type, filters.from_date, filters.to_date) * -1
+ )
+
+ new_allocation, expired_leaves, carry_forwarded_leaves = get_allocated_and_expired_leaves(
+ filters.from_date, filters.to_date, employee.name, leave_type
+ )
+ opening = get_opening_balance(employee.name, leave_type, filters, carry_forwarded_leaves)
- row.leaves_allocated = new_allocation
- row.leaves_expired = expired_leaves
- row.opening_balance = opening
- row.leaves_taken = leaves_taken
+ row.leaves_allocated = new_allocation
+ row.leaves_expired = expired_leaves
+ row.opening_balance = opening
+ row.leaves_taken = leaves_taken
- # not be shown on the basis of days left it create in user mind for carry_forward leave
- row.closing_balance = new_allocation + opening - (row.leaves_expired + leaves_taken)
- row.indent = 1
- data.append(row)
+ # not be shown on the basis of days left it create in user mind for carry_forward leave
+ row.closing_balance = new_allocation + opening - (row.leaves_expired + leaves_taken)
+ row.indent = 1
+ data.append(row)
return data
diff --git a/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py
index 5354abf4f6f1..d167d1d86c9f 100644
--- a/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py
+++ b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py
@@ -9,13 +9,11 @@
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
-from erpnext.hr.doctype.leave_application.test_leave_application import (
- get_first_sunday,
- make_allocation_record,
-)
+from erpnext.hr.doctype.leave_application.test_leave_application import make_allocation_record
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
from erpnext.hr.report.employee_leave_balance.employee_leave_balance import execute
+from erpnext.hr.tests.test_utils import get_first_sunday
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
make_holiday_list,
make_leave_application,
diff --git a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py
index 986c686e5b3f..9750ad4e880c 100644
--- a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py
+++ b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py
@@ -22,9 +22,9 @@ def execute(filters=None):
def get_columns(leave_types):
columns = [
- _("Employee") + ":Link.Employee:150",
+ _("Employee") + ":Link/Employee:150",
_("Employee Name") + "::200",
- _("Department") + "::150",
+ _("Department") + ":Link/Department:150",
]
for leave_type in leave_types:
@@ -65,21 +65,16 @@ def get_data(filters, leave_types):
if employee.leave_approver:
leave_approvers.append(employee.leave_approver)
- if (
- (len(leave_approvers) and user in leave_approvers)
- or (user in ["Administrator", employee.user_id])
- or ("HR Manager" in frappe.get_roles(user))
- ):
- row = [employee.name, employee.employee_name, employee.department]
- available_leave = get_leave_details(employee.name, filters.date)
- for leave_type in leave_types:
- remaining = 0
- if leave_type in available_leave["leave_allocation"]:
- # opening balance
- remaining = available_leave["leave_allocation"][leave_type]["remaining_leaves"]
-
- row += [remaining]
-
- data.append(row)
+ row = [employee.name, employee.employee_name, employee.department]
+ available_leave = get_leave_details(employee.name, filters.date)
+ for leave_type in leave_types:
+ remaining = 0
+ if leave_type in available_leave["leave_allocation"]:
+ # opening balance
+ remaining = available_leave["leave_allocation"][leave_type]["remaining_leaves"]
+
+ row += [remaining]
+
+ data.append(row)
return data
diff --git a/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py b/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py
index 2fd74b7983b0..9eddbea02e2e 100644
--- a/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py
+++ b/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py
@@ -9,12 +9,10 @@
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
-from erpnext.hr.doctype.leave_application.test_leave_application import (
- get_first_sunday,
- make_allocation_record,
-)
+from erpnext.hr.doctype.leave_application.test_leave_application import make_allocation_record
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation
from erpnext.hr.report.employee_leave_balance_summary.employee_leave_balance_summary import execute
+from erpnext.hr.tests.test_utils import get_first_sunday
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
make_holiday_list,
make_leave_application,
diff --git a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py
index c6f5bf05891c..da94d96adb09 100644
--- a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py
+++ b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py
@@ -181,7 +181,6 @@ def add_data(
total_l += 1
elif status == "Half Day":
total_p += 0.5
- total_a += 0.5
total_l += 0.5
elif not status:
total_um += 1
diff --git a/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py
index 91da08eee508..84c66a7bf3f1 100644
--- a/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py
+++ b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py
@@ -1,7 +1,7 @@
import frappe
from dateutil.relativedelta import relativedelta
from frappe.tests.utils import FrappeTestCase
-from frappe.utils import now_datetime
+from frappe.utils import add_months, getdate
from erpnext.hr.doctype.attendance.attendance import mark_attendance
from erpnext.hr.doctype.employee.test_employee import make_employee
@@ -14,9 +14,7 @@ def setUp(self):
frappe.db.delete("Attendance", {"employee": self.employee})
def test_monthly_attendance_sheet_report(self):
- now = now_datetime()
- previous_month = now.month - 1
- previous_month_first = now.replace(day=1).replace(month=previous_month).date()
+ previous_month_first = add_months(getdate(), -1).replace(day=1)
company = frappe.db.get_value("Employee", self.employee, "company")
@@ -27,8 +25,8 @@ def test_monthly_attendance_sheet_report(self):
filters = frappe._dict(
{
- "month": previous_month,
- "year": now.year,
+ "month": previous_month_first.month,
+ "year": previous_month_first.year,
"company": company,
}
)
diff --git a/erpnext/hr/tests/test_utils.py b/erpnext/hr/tests/test_utils.py
new file mode 100644
index 000000000000..3d7c1ad3669e
--- /dev/null
+++ b/erpnext/hr/tests/test_utils.py
@@ -0,0 +1,19 @@
+import frappe
+from frappe.utils import get_first_day, get_last_day, getdate
+
+
+def get_first_sunday(holiday_list="Salary Slip Test Holiday List", for_date=None):
+ date = for_date or getdate()
+ month_start_date = get_first_day(date)
+ month_end_date = get_last_day(date)
+ first_sunday = frappe.db.sql(
+ """
+ select holiday_date from `tabHoliday`
+ where parent = %s
+ and holiday_date between %s and %s
+ order by holiday_date
+ """,
+ (holiday_list, month_start_date, month_end_date),
+ )[0][0]
+
+ return first_sunday
diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js
index 38328e696741..20e2b0b20139 100644
--- a/erpnext/loan_management/doctype/loan/loan.js
+++ b/erpnext/loan_management/doctype/loan/loan.js
@@ -61,6 +61,10 @@ frappe.ui.form.on('Loan', {
},
refresh: function (frm) {
+ if (frm.doc.repayment_schedule_type == "Pro-rated calendar months") {
+ frm.set_df_property("repayment_start_date", "label", "Interest Calculation Start Date");
+ }
+
if (frm.doc.docstatus == 1) {
if (["Disbursed", "Partially Disbursed"].includes(frm.doc.status) && (!frm.doc.repay_from_salary)) {
frm.add_custom_button(__('Request Loan Closure'), function() {
@@ -103,6 +107,14 @@ frappe.ui.form.on('Loan', {
frm.trigger("toggle_fields");
},
+ repayment_schedule_type: function(frm) {
+ if (frm.doc.repayment_schedule_type == "Pro-rated calendar months") {
+ frm.set_df_property("repayment_start_date", "label", "Interest Calculation Start Date");
+ } else {
+ frm.set_df_property("repayment_start_date", "label", "Repayment Start Date");
+ }
+ },
+
loan_type: function(frm) {
frm.toggle_reqd("repayment_method", frm.doc.is_term_loan);
frm.toggle_display("repayment_method", frm.doc.is_term_loan);
diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json
index 0e8feba2f125..73bd1c65ffc7 100644
--- a/erpnext/loan_management/doctype/loan/loan.json
+++ b/erpnext/loan_management/doctype/loan/loan.json
@@ -17,8 +17,10 @@
"posting_date",
"status",
"repay_from_salary",
+ "manually_update_paid_amount_in_salary_slip",
"section_break_8",
"loan_type",
+ "repayment_schedule_type",
"loan_amount",
"rate_of_interest",
"is_secured_loan",
@@ -51,10 +53,10 @@
"refund_amount",
"debit_adjustment_amount",
"credit_adjustment_amount",
- "is_npa",
"column_break_19",
"total_interest_payable",
"total_amount_paid",
+ "is_npa",
"amended_from"
],
"fields": [
@@ -166,7 +168,8 @@
"depends_on": "is_term_loan",
"fieldname": "repayment_start_date",
"fieldtype": "Date",
- "label": "Repayment Start Date"
+ "label": "Repayment Start Date",
+ "mandatory_depends_on": "is_term_loan"
},
{
"fieldname": "column_break_11",
@@ -410,16 +413,31 @@
"fieldname": "is_npa",
"fieldtype": "Check",
"label": "Is NPA"
+ },
+ {
+ "allow_on_submit": 1,
+ "default": "0",
+ "depends_on": "repay_from_salary",
+ "fieldname": "manually_update_paid_amount_in_salary_slip",
+ "fieldtype": "Check",
+ "label": "Manually Update Paid Amount in Salary Slip"
+ },
+ {
+ "depends_on": "is_term_loan",
+ "fetch_from": "loan_type.repayment_schedule_type",
+ "fieldname": "repayment_schedule_type",
+ "fieldtype": "Data",
+ "label": "Repayment Schedule Type",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-06-30 12:04:13.728880",
+ "modified": "2022-11-01 10:36:47.902903",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan",
- "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -445,6 +463,5 @@
"search_fields": "posting_date",
"sort_field": "creation",
"sort_order": "DESC",
- "states": [],
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py
index 540330b42b37..cdc693df1f7b 100644
--- a/erpnext/loan_management/doctype/loan/loan.py
+++ b/erpnext/loan_management/doctype/loan/loan.py
@@ -7,7 +7,16 @@
import frappe
from frappe import _
-from frappe.utils import add_months, flt, get_last_day, getdate, now_datetime, nowdate
+from frappe.utils import (
+ add_days,
+ add_months,
+ date_diff,
+ flt,
+ get_last_day,
+ getdate,
+ now_datetime,
+ nowdate,
+)
from six import string_types
import erpnext
@@ -115,30 +124,81 @@ def make_repayment_schedule(self):
if not self.repayment_start_date:
frappe.throw(_("Repayment Start Date is mandatory for term loans"))
+ schedule_type_details = frappe.db.get_value(
+ "Loan Type", self.loan_type, ["repayment_schedule_type", "repayment_date_on"], as_dict=1
+ )
+
self.repayment_schedule = []
payment_date = self.repayment_start_date
balance_amount = self.loan_amount
+
while balance_amount > 0:
- interest_amount = flt(balance_amount * flt(self.rate_of_interest) / (12 * 100))
- principal_amount = self.monthly_repayment_amount - interest_amount
- balance_amount = flt(balance_amount + interest_amount - self.monthly_repayment_amount)
- if balance_amount < 0:
- principal_amount += balance_amount
- balance_amount = 0.0
-
- total_payment = principal_amount + interest_amount
- self.append(
- "repayment_schedule",
- {
- "payment_date": payment_date,
- "principal_amount": principal_amount,
- "interest_amount": interest_amount,
- "total_payment": total_payment,
- "balance_loan_amount": balance_amount,
- },
+ interest_amount, principal_amount, balance_amount, total_payment = self.get_amounts(
+ payment_date,
+ balance_amount,
+ schedule_type_details.repayment_schedule_type,
+ schedule_type_details.repayment_date_on,
+ )
+
+ if schedule_type_details.repayment_schedule_type == "Pro-rated calendar months":
+ next_payment_date = get_last_day(payment_date)
+ if schedule_type_details.repayment_date_on == "Start of the next month":
+ next_payment_date = add_days(next_payment_date, 1)
+
+ payment_date = next_payment_date
+
+ self.add_repayment_schedule_row(
+ payment_date, principal_amount, interest_amount, total_payment, balance_amount
)
- next_payment_date = add_single_month(payment_date)
- payment_date = next_payment_date
+
+ if (
+ schedule_type_details.repayment_schedule_type == "Monthly as per repayment start date"
+ or schedule_type_details.repayment_date_on == "End of the current month"
+ ):
+ next_payment_date = add_single_month(payment_date)
+ payment_date = next_payment_date
+
+ def get_amounts(self, payment_date, balance_amount, schedule_type, repayment_date_on):
+ if schedule_type == "Monthly as per repayment start date":
+ days = 1
+ months = 12
+ else:
+ expected_payment_date = get_last_day(payment_date)
+ if repayment_date_on == "Start of the next month":
+ expected_payment_date = add_days(expected_payment_date, 1)
+
+ if expected_payment_date == payment_date:
+ # using 30 days for calculating interest for all full months
+ days = 30
+ months = 365
+ else:
+ days = date_diff(get_last_day(payment_date), payment_date)
+ months = 365
+
+ interest_amount = flt(balance_amount * flt(self.rate_of_interest) * days / (months * 100))
+ principal_amount = self.monthly_repayment_amount - interest_amount
+ balance_amount = flt(balance_amount + interest_amount - self.monthly_repayment_amount)
+ if balance_amount < 0:
+ principal_amount += balance_amount
+ balance_amount = 0.0
+
+ total_payment = principal_amount + interest_amount
+
+ return interest_amount, principal_amount, balance_amount, total_payment
+
+ def add_repayment_schedule_row(
+ self, payment_date, principal_amount, interest_amount, total_payment, balance_loan_amount
+ ):
+ self.append(
+ "repayment_schedule",
+ {
+ "payment_date": payment_date,
+ "principal_amount": principal_amount,
+ "interest_amount": interest_amount,
+ "total_payment": total_payment,
+ "balance_loan_amount": balance_loan_amount,
+ },
+ )
def set_repayment_period(self):
if self.repayment_method == "Repay Fixed Amount per Period":
diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py
index e2b0870c3226..276b05baf629 100644
--- a/erpnext/loan_management/doctype/loan/test_loan.py
+++ b/erpnext/loan_management/doctype/loan/test_loan.py
@@ -4,7 +4,16 @@
import unittest
import frappe
-from frappe.utils import add_days, add_months, add_to_date, date_diff, flt, get_datetime, nowdate
+from frappe.utils import (
+ add_days,
+ add_months,
+ add_to_date,
+ date_diff,
+ flt,
+ format_date,
+ get_datetime,
+ nowdate,
+)
from erpnext.loan_management.doctype.loan.loan import (
make_loan_write_off,
@@ -50,6 +59,51 @@ def setUp(self):
loan_account="Loan Account - _TC",
interest_income_account="Interest Income Account - _TC",
penalty_income_account="Penalty Income Account - _TC",
+ repayment_schedule_type="Monthly as per repayment start date",
+ )
+
+ create_loan_type(
+ "Term Loan Type 1",
+ 12000,
+ 7.5,
+ is_term_loan=1,
+ mode_of_payment="Cash",
+ disbursement_account="Disbursement Account - _TC",
+ payment_account="Payment Account - _TC",
+ loan_account="Loan Account - _TC",
+ interest_income_account="Interest Income Account - _TC",
+ penalty_income_account="Penalty Income Account - _TC",
+ repayment_schedule_type="Monthly as per repayment start date",
+ )
+
+ create_loan_type(
+ "Term Loan Type 2",
+ 12000,
+ 7.5,
+ is_term_loan=1,
+ mode_of_payment="Cash",
+ disbursement_account="Disbursement Account - _TC",
+ payment_account="Payment Account - _TC",
+ loan_account="Loan Account - _TC",
+ interest_income_account="Interest Income Account - _TC",
+ penalty_income_account="Penalty Income Account - _TC",
+ repayment_schedule_type="Pro-rated calendar months",
+ repayment_date_on="Start of the next month",
+ )
+
+ create_loan_type(
+ "Term Loan Type 3",
+ 12000,
+ 7.5,
+ is_term_loan=1,
+ mode_of_payment="Cash",
+ disbursement_account="Disbursement Account - _TC",
+ payment_account="Payment Account - _TC",
+ loan_account="Loan Account - _TC",
+ interest_income_account="Interest Income Account - _TC",
+ penalty_income_account="Penalty Income Account - _TC",
+ repayment_schedule_type="Pro-rated calendar months",
+ repayment_date_on="End of the current month",
)
create_loan_type(
@@ -65,6 +119,7 @@ def setUp(self):
"Loan Account - _TC",
"Interest Income Account - _TC",
"Penalty Income Account - _TC",
+ repayment_schedule_type="Monthly as per repayment start date",
)
create_loan_type(
@@ -912,6 +967,69 @@ def test_loan_amount_write_off(self):
amounts = calculate_amounts(loan.name, add_days(last_date, 5))
self.assertEqual(flt(amounts["pending_principal_amount"], 0), 0)
+ def test_term_loan_schedule_types(self):
+ loan = create_loan(
+ self.applicant1,
+ "Term Loan Type 1",
+ 12000,
+ "Repay Over Number of Periods",
+ 12,
+ repayment_start_date="2022-10-17",
+ )
+
+ # Check for first, second and last installment date
+ self.assertEqual(
+ format_date(loan.get("repayment_schedule")[0].payment_date, "dd-MM-yyyy"), "17-10-2022"
+ )
+ self.assertEqual(
+ format_date(loan.get("repayment_schedule")[1].payment_date, "dd-MM-yyyy"), "17-11-2022"
+ )
+ self.assertEqual(
+ format_date(loan.get("repayment_schedule")[-1].payment_date, "dd-MM-yyyy"), "17-09-2023"
+ )
+
+ loan.loan_type = "Term Loan Type 2"
+ loan.save()
+
+ # Check for first, second and last installment date
+ self.assertEqual(
+ format_date(loan.get("repayment_schedule")[0].payment_date, "dd-MM-yyyy"), "01-11-2022"
+ )
+ self.assertEqual(
+ format_date(loan.get("repayment_schedule")[1].payment_date, "dd-MM-yyyy"), "01-12-2022"
+ )
+ self.assertEqual(
+ format_date(loan.get("repayment_schedule")[-1].payment_date, "dd-MM-yyyy"), "01-10-2023"
+ )
+
+ loan.loan_type = "Term Loan Type 3"
+ loan.save()
+
+ # Check for first, second and last installment date
+ self.assertEqual(
+ format_date(loan.get("repayment_schedule")[0].payment_date, "dd-MM-yyyy"), "31-10-2022"
+ )
+ self.assertEqual(
+ format_date(loan.get("repayment_schedule")[1].payment_date, "dd-MM-yyyy"), "30-11-2022"
+ )
+ self.assertEqual(
+ format_date(loan.get("repayment_schedule")[-1].payment_date, "dd-MM-yyyy"), "30-09-2023"
+ )
+
+ loan.repayment_method = "Repay Fixed Amount per Period"
+ loan.monthly_repayment_amount = 1042
+ loan.save()
+
+ self.assertEqual(
+ format_date(loan.get("repayment_schedule")[0].payment_date, "dd-MM-yyyy"), "31-10-2022"
+ )
+ self.assertEqual(
+ format_date(loan.get("repayment_schedule")[1].payment_date, "dd-MM-yyyy"), "30-11-2022"
+ )
+ self.assertEqual(
+ format_date(loan.get("repayment_schedule")[-1].payment_date, "dd-MM-yyyy"), "30-09-2023"
+ )
+
def create_loan_scenario_for_penalty(doc):
pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}]
@@ -1043,6 +1161,8 @@ def create_loan_type(
penalty_income_account=None,
repayment_method=None,
repayment_periods=None,
+ repayment_schedule_type=None,
+ repayment_date_on=None,
):
if not frappe.db.exists("Loan Type", loan_name):
@@ -1052,6 +1172,7 @@ def create_loan_type(
"company": "_Test Company",
"loan_name": loan_name,
"is_term_loan": is_term_loan,
+ "repayment_schedule_type": "Monthly as per repayment start date",
"maximum_loan_amount": maximum_loan_amount,
"rate_of_interest": rate_of_interest,
"penalty_interest_rate": penalty_interest_rate,
@@ -1066,8 +1187,14 @@ def create_loan_type(
"repayment_periods": repayment_periods,
"write_off_amount": 100,
}
- ).insert()
+ )
+
+ if loan_type.is_term_loan:
+ loan_type.repayment_schedule_type = repayment_schedule_type
+ if loan_type.repayment_schedule_type != "Monthly as per repayment start date":
+ loan_type.repayment_date_on = repayment_date_on
+ loan_type.insert()
loan_type.submit()
diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
index 8a493a5fc77a..64e5cde0ace8 100644
--- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
+++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
@@ -191,7 +191,9 @@ def get_total_pledged_security_value(loan):
for security, qty in pledged_securities.items():
after_haircut_percentage = 100 - hair_cut_map.get(security)
- security_value += (loan_security_price_map.get(security) * qty * after_haircut_percentage) / 100
+ security_value += (
+ loan_security_price_map.get(security, 0) * qty * after_haircut_percentage
+ ) / 100
return security_value
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index 19d0d84a46f7..9bc963ab931a 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -520,6 +520,8 @@ def get_accrued_interest_entries(against_loan, posting_date=None):
if not posting_date:
posting_date = getdate()
+ precision = cint(frappe.db.get_default("currency_precision")) or 2
+
unpaid_accrued_entries = frappe.db.sql(
"""
SELECT name, posting_date, interest_amount - paid_interest_amount as interest_amount,
@@ -540,6 +542,13 @@ def get_accrued_interest_entries(against_loan, posting_date=None):
as_dict=1,
)
+ # Skip entries with zero interest amount & payable principal amount
+ unpaid_accrued_entries = [
+ d
+ for d in unpaid_accrued_entries
+ if flt(d.interest_amount, precision) > 0 or flt(d.payable_principal_amount, precision) > 0
+ ]
+
return unpaid_accrued_entries
@@ -568,8 +577,8 @@ def regenerate_repayment_schedule(loan, cancel=0):
loan_doc = frappe.get_doc("Loan", loan)
next_accrual_date = None
accrued_entries = 0
- last_repayment_amount = 0
- last_balance_amount = 0
+ last_repayment_amount = None
+ last_balance_amount = None
for term in reversed(loan_doc.get("repayment_schedule")):
if not term.is_accrued:
@@ -577,9 +586,9 @@ def regenerate_repayment_schedule(loan, cancel=0):
loan_doc.remove(term)
else:
accrued_entries += 1
- if not last_repayment_amount:
+ if last_repayment_amount is None:
last_repayment_amount = term.total_payment
- if not last_balance_amount:
+ if last_balance_amount is None:
last_balance_amount = term.balance_loan_amount
loan_doc.save()
diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.json b/erpnext/loan_management/doctype/loan_type/loan_type.json
index 00337e4b4c3c..3e784c256e74 100644
--- a/erpnext/loan_management/doctype/loan_type/loan_type.json
+++ b/erpnext/loan_management/doctype/loan_type/loan_type.json
@@ -16,6 +16,8 @@
"company",
"is_term_loan",
"disabled",
+ "repayment_schedule_type",
+ "repayment_date_on",
"description",
"account_details_section",
"mode_of_payment",
@@ -157,12 +159,30 @@
"label": "Disbursement Account",
"options": "Account",
"reqd": 1
+ },
+ {
+ "depends_on": "is_term_loan",
+ "description": "The schedule type that will be used for generating the term loan schedules (will affect the payment date and monthly repayment amount)",
+ "fieldname": "repayment_schedule_type",
+ "fieldtype": "Select",
+ "label": "Repayment Schedule Type",
+ "mandatory_depends_on": "is_term_loan",
+ "options": "\nMonthly as per repayment start date\nPro-rated calendar months"
+ },
+ {
+ "depends_on": "eval:doc.repayment_schedule_type == \"Pro-rated calendar months\"",
+ "description": "Select whether the repayment date should be the end of the current month or start of the upcoming month",
+ "fieldname": "repayment_date_on",
+ "fieldtype": "Select",
+ "label": "Repayment Date On",
+ "mandatory_depends_on": "eval:doc.repayment_schedule_type == \"Pro-rated calendar months\"",
+ "options": "\nStart of the next month\nEnd of the current month"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-01-25 16:23:57.009349",
+ "modified": "2022-11-01 17:43:03.954201",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Type",
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 4cf298663265..403b14757700 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -384,6 +384,7 @@ def update_cost(self, update_parent=True, from_child_bom=False, update_hour_rate
if self.docstatus == 2:
return
+ self.flags.cost_updated = False
existing_bom_cost = self.total_cost
if self.docstatus == 1:
@@ -406,7 +407,11 @@ def update_cost(self, update_parent=True, from_child_bom=False, update_hour_rate
frappe.get_doc("BOM", bom).update_cost(from_child_bom=True)
if not from_child_bom:
- frappe.msgprint(_("Cost Updated"), alert=True)
+ msg = "Cost Updated"
+ if not self.flags.cost_updated:
+ msg = "No changes in cost found"
+
+ frappe.msgprint(_(msg), alert=True)
def update_parent_cost(self):
if self.total_cost:
@@ -592,11 +597,16 @@ def calculate_cost(self, save_updates=False, update_hour_rate=False):
# not via doc event, table is not regenerated and needs updation
self.calculate_exploded_cost()
+ old_cost = self.total_cost
+
self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost
self.base_total_cost = (
self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost
)
+ if self.total_cost != old_cost:
+ self.flags.cost_updated = True
+
def calculate_op_cost(self, update_hour_rate=False):
"""Update workstation rate and calculates totals"""
self.operating_cost = 0
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 3c91c6d4d9bf..16280fc81061 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -583,6 +583,28 @@ def test_exploded_items_rate(self):
bom.submit()
self.assertEqual(bom.exploded_items[0].rate, bom.items[0].base_rate)
+ def test_bom_cost_update_flag(self):
+ rm_item = make_item(
+ properties={"is_stock_item": 1, "valuation_rate": 99, "last_purchase_rate": 89}
+ ).name
+ fg_item = make_item(properties={"is_stock_item": 1}).name
+
+ from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
+
+ bom = make_bom(item=fg_item, raw_materials=[rm_item])
+
+ create_stock_reconciliation(
+ item_code=rm_item, warehouse="_Test Warehouse - _TC", qty=100, rate=600
+ )
+
+ bom.load_from_db()
+ bom.update_cost()
+ self.assertTrue(bom.flags.cost_updated)
+
+ bom.load_from_db()
+ bom.update_cost()
+ self.assertFalse(bom.flags.cost_updated)
+
def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index c24d07d21946..49e5569644fa 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -7,6 +7,8 @@
from frappe import _, bold
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
+from frappe.query_builder import Criterion
+from frappe.query_builder.functions import IfNull, Max, Min
from frappe.utils import (
add_days,
add_to_date,
@@ -54,6 +56,9 @@ def onload(self):
self.set_onload("job_card_excess_transfer", excess_transfer)
self.set_onload("work_order_stopped", self.is_work_order_stopped())
+ def before_validate(self):
+ self.set_wip_warehouse()
+
def validate(self):
self.validate_time_logs()
self.set_status()
@@ -109,43 +114,44 @@ def validate_time_logs(self):
def get_overlap_for(self, args, check_next_available_slot=False):
production_capacity = 1
+ jc = frappe.qb.DocType("Job Card")
+ jctl = frappe.qb.DocType("Job Card Time Log")
+
+ time_conditions = [
+ ((jctl.from_time < args.from_time) & (jctl.to_time > args.from_time)),
+ ((jctl.from_time < args.to_time) & (jctl.to_time > args.to_time)),
+ ((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)
+ .select(jc.name.as_("name"), jctl.to_time)
+ .where(
+ (jctl.parent == jc.name)
+ & (Criterion.any(time_conditions))
+ & (jctl.name != f"{args.name or 'No Name'}")
+ & (jc.name != f"{args.parent or 'No Name'}")
+ & (jc.docstatus < 2)
+ )
+ .orderby(jctl.to_time, order=frappe.qb.desc)
+ )
+
if self.workstation:
production_capacity = (
frappe.get_cached_value("Workstation", self.workstation, "production_capacity") or 1
)
- validate_overlap_for = " and jc.workstation = %(workstation)s "
+ query = query.where(jc.workstation == self.workstation)
if args.get("employee"):
# override capacity for employee
production_capacity = 1
- validate_overlap_for = " and jctl.employee = %(employee)s "
+ query = query.where(jctl.employee == args.get("employee"))
- extra_cond = ""
- if check_next_available_slot:
- extra_cond = " or (%(from_time)s <= jctl.from_time and %(to_time)s <= jctl.to_time)"
-
- existing = frappe.db.sql(
- """select jc.name as name, jctl.to_time from
- `tabJob Card Time Log` jctl, `tabJob Card` jc where jctl.parent = jc.name and
- (
- (%(from_time)s > jctl.from_time and %(from_time)s < jctl.to_time) or
- (%(to_time)s > jctl.from_time and %(to_time)s < jctl.to_time) or
- (%(from_time)s <= jctl.from_time and %(to_time)s >= jctl.to_time) {0}
- )
- and jctl.name != %(name)s and jc.name != %(parent)s and jc.docstatus < 2 {1}
- order by jctl.to_time desc limit 1""".format(
- extra_cond, validate_overlap_for
- ),
- {
- "from_time": args.from_time,
- "to_time": args.to_time,
- "name": args.name or "No Name",
- "parent": args.parent or "No Name",
- "employee": args.get("employee"),
- "workstation": self.workstation,
- },
- as_dict=True,
- )
+ existing = query.run(as_dict=True)
if existing and production_capacity > len(existing):
return
@@ -485,18 +491,21 @@ def validate_produced_quantity(self, for_quantity, wo):
)
def update_work_order_data(self, for_quantity, time_in_mins, wo):
- time_data = frappe.db.sql(
- """
- SELECT
- min(from_time) as start_time, max(to_time) as end_time
- FROM `tabJob Card` jc, `tabJob Card Time Log` jctl
- WHERE
- jctl.parent = jc.name and jc.work_order = %s and jc.operation_id = %s
- and jc.docstatus = 1 and IFNULL(jc.is_corrective_job_card, 0) = 0
- """,
- (self.work_order, self.operation_id),
- as_dict=1,
- )
+ jc = frappe.qb.DocType("Job Card")
+ jctl = frappe.qb.DocType("Job Card Time Log")
+
+ time_data = (
+ frappe.qb.from_(jc)
+ .from_(jctl)
+ .select(Min(jctl.from_time).as_("start_time"), Max(jctl.to_time).as_("end_time"))
+ .where(
+ (jctl.parent == jc.name)
+ & (jc.work_order == self.work_order)
+ & (jc.operation_id == self.operation_id)
+ & (jc.docstatus == 1)
+ & (IfNull(jc.is_corrective_job_card, 0) == 0)
+ )
+ ).run(as_dict=True)
for data in wo.operations:
if data.get("name") == self.operation_id:
@@ -639,6 +648,12 @@ def set_status(self, update_status=False):
if update_status:
self.db_set("status", self.status)
+ def set_wip_warehouse(self):
+ if not self.wip_warehouse:
+ self.wip_warehouse = frappe.db.get_single_value(
+ "Manufacturing Settings", "default_wip_warehouse"
+ )
+
def validate_operation_id(self):
if (
self.get("operation_id")
diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py
index 71aa5ef0951a..87ee2bdaa218 100644
--- a/erpnext/manufacturing/doctype/job_card/test_job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py
@@ -133,6 +133,45 @@ def test_job_card_overlap(self):
)
self.assertRaises(OverlapError, jc2.save)
+ def test_job_card_overlap_with_capacity(self):
+ wo2 = make_wo_order_test_record(item="_Test FG Item 2", qty=2)
+
+ workstation = make_workstation(workstation_name=random_string(5)).name
+ frappe.db.set_value("Workstation", workstation, "production_capacity", 1)
+
+ jc1 = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
+ jc2 = frappe.get_last_doc("Job Card", {"work_order": wo2.name})
+
+ jc1.workstation = workstation
+ jc1.append(
+ "time_logs",
+ {"from_time": "2021-01-01 00:00:00", "to_time": "2021-01-01 08:00:00", "completed_qty": 1},
+ )
+ jc1.save()
+
+ jc2.workstation = workstation
+
+ # add a new entry in same time slice
+ jc2.append(
+ "time_logs",
+ {"from_time": "2021-01-01 00:01:00", "to_time": "2021-01-01 06:00:00", "completed_qty": 1},
+ )
+ self.assertRaises(OverlapError, jc2.save)
+
+ frappe.db.set_value("Workstation", workstation, "production_capacity", 2)
+ jc2.load_from_db()
+
+ jc2.workstation = workstation
+
+ # add a new entry in same time slice
+ jc2.append(
+ "time_logs",
+ {"from_time": "2021-01-01 00:01:00", "to_time": "2021-01-01 06:00:00", "completed_qty": 1},
+ )
+
+ jc2.save()
+ self.assertTrue(jc2.name)
+
def test_job_card_multiple_materials_transfer(self):
"Test transferring RMs separately against Job Card with multiple RMs."
self.transfer_material_against = "Job Card"
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index b6567ec15d80..1c0d19fa95bf 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -8,6 +8,7 @@
import frappe
from frappe import _, msgprint
from frappe.model.document import Document
+from frappe.query_builder.functions import IfNull, Sum
from frappe.utils import (
add_days,
ceil,
@@ -20,11 +21,13 @@
nowdate,
)
from frappe.utils.csvutils import build_csv_response
+from pypika.terms import ExistsCriterion
from six import iteritems
from erpnext.manufacturing.doctype.bom.bom import get_children, validate_bom_no
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
+from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.utilities.transaction_base import validate_uom_is_integer
@@ -100,39 +103,46 @@ def add_so_in_table(self, open_so):
@frappe.whitelist()
def get_pending_material_requests(self):
"""Pull Material Requests that are pending based on criteria selected"""
- mr_filter = item_filter = ""
+
+ bom = frappe.qb.DocType("BOM")
+ mr = frappe.qb.DocType("Material Request")
+ mr_item = frappe.qb.DocType("Material Request Item")
+
+ pending_mr_query = (
+ frappe.qb.from_(mr)
+ .from_(mr_item)
+ .select(mr.name, mr.transaction_date)
+ .distinct()
+ .where(
+ (mr_item.parent == mr.name)
+ & (mr.material_request_type == "Manufacture")
+ & (mr.docstatus == 1)
+ & (mr.status != "Stopped")
+ & (mr.company == self.company)
+ & (mr_item.qty > IfNull(mr_item.ordered_qty, 0))
+ & (
+ ExistsCriterion(
+ frappe.qb.from_(bom)
+ .select(bom.name)
+ .where((bom.item == mr_item.item_code) & (bom.is_active == 1))
+ )
+ )
+ )
+ )
+
if self.from_date:
- mr_filter += " and mr.transaction_date >= %(from_date)s"
+ pending_mr_query = pending_mr_query.where(mr.transaction_date >= self.from_date)
+
if self.to_date:
- mr_filter += " and mr.transaction_date <= %(to_date)s"
+ pending_mr_query = pending_mr_query.where(mr.transaction_date <= self.to_date)
+
if self.warehouse:
- mr_filter += " and mr_item.warehouse = %(warehouse)s"
+ pending_mr_query = pending_mr_query.where(mr_item.warehouse == self.warehouse)
if self.item_code:
- item_filter += " and mr_item.item_code = %(item)s"
-
- pending_mr = frappe.db.sql(
- """
- select distinct mr.name, mr.transaction_date
- from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item
- where mr_item.parent = mr.name
- and mr.material_request_type = "Manufacture"
- and mr.docstatus = 1 and mr.status != "Stopped" and mr.company = %(company)s
- and mr_item.qty > ifnull(mr_item.ordered_qty,0) {0} {1}
- and (exists (select name from `tabBOM` bom where bom.item=mr_item.item_code
- and bom.is_active = 1))
- """.format(
- mr_filter, item_filter
- ),
- {
- "from_date": self.from_date,
- "to_date": self.to_date,
- "warehouse": self.warehouse,
- "item": self.item_code,
- "company": self.company,
- },
- as_dict=1,
- )
+ pending_mr_query = pending_mr_query.where(mr_item.item_code == self.item_code)
+
+ pending_mr = pending_mr_query.run(as_dict=True)
self.add_mr_in_table(pending_mr)
@@ -160,16 +170,17 @@ def get_so_mr_list(self, field, table):
so_mr_list = [d.get(field) for d in self.get(table) if d.get(field)]
return so_mr_list
- def get_bom_item(self):
+ def get_bom_item_condition(self):
"""Check if Item or if its Template has a BOM."""
- bom_item = None
+ bom_item_condition = None
has_bom = frappe.db.exists({"doctype": "BOM", "item": self.item_code, "docstatus": 1})
+
if not has_bom:
+ bom = frappe.qb.DocType("BOM")
template_item = frappe.db.get_value("Item", self.item_code, ["variant_of"])
- bom_item = (
- "bom.item = {0}".format(frappe.db.escape(template_item)) if template_item else bom_item
- )
- return bom_item
+ bom_item_condition = bom.item == template_item or None
+
+ return bom_item_condition
def get_so_items(self):
# Check for empty table or empty rows
@@ -178,46 +189,75 @@ def get_so_items(self):
so_list = self.get_so_mr_list("sales_order", "sales_orders")
- item_condition = ""
- bom_item = "bom.item = so_item.item_code"
+ bom = frappe.qb.DocType("BOM")
+ so_item = frappe.qb.DocType("Sales Order Item")
+
+ items_subquery = frappe.qb.from_(bom).select(bom.name).where(bom.is_active == 1)
+ items_query = (
+ frappe.qb.from_(so_item)
+ .select(
+ so_item.parent,
+ so_item.item_code,
+ so_item.warehouse,
+ (
+ (so_item.qty - so_item.work_order_qty - so_item.delivered_qty) * so_item.conversion_factor
+ ).as_("pending_qty"),
+ so_item.description,
+ so_item.name,
+ )
+ .distinct()
+ .where(
+ (so_item.parent.isin(so_list))
+ & (so_item.docstatus == 1)
+ & (so_item.qty > so_item.work_order_qty)
+ )
+ )
+
if self.item_code and frappe.db.exists("Item", self.item_code):
- bom_item = self.get_bom_item() or bom_item
- item_condition = " and so_item.item_code = {0}".format(frappe.db.escape(self.item_code))
-
- items = frappe.db.sql(
- """
- select
- distinct parent, item_code, warehouse,
- (qty - work_order_qty) * conversion_factor as pending_qty,
- description, name
- from
- `tabSales Order Item` so_item
- where
- parent in (%s) and docstatus = 1 and qty > work_order_qty
- and exists (select name from `tabBOM` bom where %s
- and bom.is_active = 1) %s"""
- % (", ".join(["%s"] * len(so_list)), bom_item, item_condition),
- tuple(so_list),
- as_dict=1,
+ items_query = items_query.where(so_item.item_code == self.item_code)
+ items_subquery = items_subquery.where(
+ self.get_bom_item_condition() or bom.item == so_item.item_code
+ )
+
+ items_query = items_query.where(ExistsCriterion(items_subquery))
+
+ items = items_query.run(as_dict=True)
+
+ pi = frappe.qb.DocType("Packed Item")
+
+ packed_items_query = (
+ frappe.qb.from_(so_item)
+ .from_(pi)
+ .select(
+ pi.parent,
+ pi.item_code,
+ pi.warehouse.as_("warehouse"),
+ (((so_item.qty - so_item.work_order_qty) * pi.qty) / so_item.qty).as_("pending_qty"),
+ pi.parent_item,
+ pi.description,
+ so_item.name,
+ )
+ .distinct()
+ .where(
+ (so_item.parent == pi.parent)
+ & (so_item.docstatus == 1)
+ & (pi.parent_item == so_item.item_code)
+ & (so_item.parent.isin(so_list))
+ & (so_item.qty > so_item.work_order_qty)
+ & (
+ ExistsCriterion(
+ frappe.qb.from_(bom)
+ .select(bom.name)
+ .where((bom.item == pi.item_code) & (bom.is_active == 1))
+ )
+ )
+ )
)
if self.item_code:
- item_condition = " and so_item.item_code = {0}".format(frappe.db.escape(self.item_code))
-
- packed_items = frappe.db.sql(
- """select distinct pi.parent, pi.item_code, pi.warehouse as warehouse,
- (((so_item.qty - so_item.work_order_qty) * pi.qty) / so_item.qty)
- as pending_qty, pi.parent_item, pi.description, so_item.name
- from `tabSales Order Item` so_item, `tabPacked Item` pi
- where so_item.parent = pi.parent and so_item.docstatus = 1
- and pi.parent_item = so_item.item_code
- and so_item.parent in (%s) and so_item.qty > so_item.work_order_qty
- and exists (select name from `tabBOM` bom where bom.item=pi.item_code
- and bom.is_active = 1) %s"""
- % (", ".join(["%s"] * len(so_list)), item_condition),
- tuple(so_list),
- as_dict=1,
- )
+ packed_items_query = packed_items_query.where(so_item.item_code == self.item_code)
+
+ packed_items = packed_items_query.run(as_dict=True)
self.add_items(items + packed_items)
self.calculate_total_planned_qty()
@@ -233,22 +273,39 @@ def get_mr_items(self):
mr_list = self.get_so_mr_list("material_request", "material_requests")
- item_condition = ""
- if self.item_code:
- item_condition = " and mr_item.item_code ={0}".format(frappe.db.escape(self.item_code))
-
- items = frappe.db.sql(
- """select distinct parent, name, item_code, warehouse, description,
- (qty - ordered_qty) * conversion_factor as pending_qty
- from `tabMaterial Request Item` mr_item
- where parent in (%s) and docstatus = 1 and qty > ordered_qty
- and exists (select name from `tabBOM` bom where bom.item=mr_item.item_code
- and bom.is_active = 1) %s"""
- % (", ".join(["%s"] * len(mr_list)), item_condition),
- tuple(mr_list),
- as_dict=1,
+ bom = frappe.qb.DocType("BOM")
+ mr_item = frappe.qb.DocType("Material Request Item")
+
+ items_query = (
+ frappe.qb.from_(mr_item)
+ .select(
+ mr_item.parent,
+ mr_item.name,
+ mr_item.item_code,
+ mr_item.warehouse,
+ mr_item.description,
+ ((mr_item.qty - mr_item.ordered_qty) * mr_item.conversion_factor).as_("pending_qty"),
+ )
+ .distinct()
+ .where(
+ (mr_item.parent.isin(mr_list))
+ & (mr_item.docstatus == 1)
+ & (mr_item.qty > mr_item.ordered_qty)
+ & (
+ ExistsCriterion(
+ frappe.qb.from_(bom)
+ .select(bom.name)
+ .where((bom.item == mr_item.item_code) & (bom.is_active == 1))
+ )
+ )
+ )
)
+ if self.item_code:
+ items_query = items_query.where(mr_item.item_code == self.item_code)
+
+ items = items_query.run(as_dict=True)
+
self.add_items(items)
self.calculate_total_planned_qty()
@@ -754,29 +811,46 @@ def download_raw_materials(doc, warehouses=None):
def get_exploded_items(item_details, company, bom_no, include_non_stock_items, planned_qty=1):
- for d in frappe.db.sql(
- """select bei.item_code, item.default_bom as bom,
- ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0)*%s as qty, item.item_name,
- bei.description, bei.stock_uom, item.min_order_qty, bei.source_warehouse,
- item.default_material_request_type, item.min_order_qty, item_default.default_warehouse,
- item.purchase_uom, item_uom.conversion_factor, item.safety_stock
- from
- `tabBOM Explosion Item` bei
- JOIN `tabBOM` bom ON bom.name = bei.parent
- JOIN `tabItem` item ON item.name = bei.item_code
- LEFT JOIN `tabItem Default` item_default
- ON item_default.parent = item.name and item_default.company=%s
- LEFT JOIN `tabUOM Conversion Detail` item_uom
- ON item.name = item_uom.parent and item_uom.uom = item.purchase_uom
- where
- bei.docstatus < 2
- and bom.name=%s and item.is_stock_item in (1, {0})
- group by bei.item_code, bei.stock_uom""".format(
- 0 if include_non_stock_items else 1
- ),
- (planned_qty, company, bom_no),
- as_dict=1,
- ):
+ bei = frappe.qb.DocType("BOM Explosion Item")
+ bom = frappe.qb.DocType("BOM")
+ item = frappe.qb.DocType("Item")
+ item_default = frappe.qb.DocType("Item Default")
+ item_uom = frappe.qb.DocType("UOM Conversion Detail")
+
+ data = (
+ frappe.qb.from_(bei)
+ .join(bom)
+ .on(bom.name == bei.parent)
+ .join(item)
+ .on(item.name == bei.item_code)
+ .left_join(item_default)
+ .on((item_default.parent == item.name) & (item_default.company == company))
+ .left_join(item_uom)
+ .on((item.name == item_uom.parent) & (item_uom.uom == item.purchase_uom))
+ .select(
+ (IfNull(Sum(bei.stock_qty / IfNull(bom.quantity, 1)), 0) * planned_qty).as_("qty"),
+ item.item_name,
+ item.name.as_("item_code"),
+ bei.description,
+ bei.stock_uom,
+ item.min_order_qty,
+ bei.source_warehouse,
+ item.default_material_request_type,
+ item.min_order_qty,
+ item_default.default_warehouse,
+ item.purchase_uom,
+ item_uom.conversion_factor,
+ item.safety_stock,
+ )
+ .where(
+ (bei.docstatus < 2)
+ & (bom.name == bom_no)
+ & (item.is_stock_item.isin([0, 1]) if include_non_stock_items else item.is_stock_item == 1)
+ )
+ .groupby(bei.item_code, bei.stock_uom)
+ ).run(as_dict=True)
+
+ for d in data:
if not d.conversion_factor and d.purchase_uom:
d.conversion_factor = get_uom_conversion_factor(d.item_code, d.purchase_uom)
item_details.setdefault(d.get("item_code"), d)
@@ -801,33 +875,47 @@ def get_subitems(
parent_qty,
planned_qty=1,
):
- items = frappe.db.sql(
- """
- SELECT
- bom_item.item_code, default_material_request_type, item.item_name,
- ifnull(%(parent_qty)s * sum(bom_item.stock_qty/ifnull(bom.quantity, 1)) * %(planned_qty)s, 0) as qty,
- item.is_sub_contracted_item as is_sub_contracted, bom_item.source_warehouse,
- item.default_bom as default_bom, bom_item.description as description,
- bom_item.stock_uom as stock_uom, item.min_order_qty as min_order_qty, item.safety_stock as safety_stock,
- item_default.default_warehouse, item.purchase_uom, item_uom.conversion_factor
- FROM
- `tabBOM Item` bom_item
- JOIN `tabBOM` bom ON bom.name = bom_item.parent
- JOIN tabItem item ON bom_item.item_code = item.name
- LEFT JOIN `tabItem Default` item_default
- ON item.name = item_default.parent and item_default.company = %(company)s
- LEFT JOIN `tabUOM Conversion Detail` item_uom
- ON item.name = item_uom.parent and item_uom.uom = item.purchase_uom
- where
- bom.name = %(bom)s
- and bom_item.docstatus < 2
- and item.is_stock_item in (1, {0})
- group by bom_item.item_code""".format(
- 0 if include_non_stock_items else 1
- ),
- {"bom": bom_no, "parent_qty": parent_qty, "planned_qty": planned_qty, "company": company},
- as_dict=1,
- )
+ bom_item = frappe.qb.DocType("BOM Item")
+ bom = frappe.qb.DocType("BOM")
+ item = frappe.qb.DocType("Item")
+ item_default = frappe.qb.DocType("Item Default")
+ item_uom = frappe.qb.DocType("UOM Conversion Detail")
+
+ items = (
+ frappe.qb.from_(bom_item)
+ .join(bom)
+ .on(bom.name == bom_item.parent)
+ .join(item)
+ .on(bom_item.item_code == item.name)
+ .left_join(item_default)
+ .on((item.name == item_default.parent) & (item_default.company == company))
+ .left_join(item_uom)
+ .on((item.name == item_uom.parent) & (item_uom.uom == item.purchase_uom))
+ .select(
+ bom_item.item_code,
+ item.default_material_request_type,
+ item.item_name,
+ IfNull(parent_qty * Sum(bom_item.stock_qty / IfNull(bom.quantity, 1)) * planned_qty, 0).as_(
+ "qty"
+ ),
+ item.is_sub_contracted_item.as_("is_sub_contracted"),
+ bom_item.source_warehouse,
+ item.default_bom.as_("default_bom"),
+ bom_item.description.as_("description"),
+ bom_item.stock_uom.as_("stock_uom"),
+ item.min_order_qty.as_("min_order_qty"),
+ item.safety_stock.as_("safety_stock"),
+ item_default.default_warehouse,
+ item.purchase_uom,
+ item_uom.conversion_factor,
+ )
+ .where(
+ (bom.name == bom_no)
+ & (bom_item.docstatus < 2)
+ & (item.is_stock_item.isin([0, 1]) if include_non_stock_items else item.is_stock_item == 1)
+ )
+ .groupby(bom_item.item_code)
+ ).run(as_dict=True)
for d in items:
if not data.get("include_exploded_items") or not d.default_bom:
@@ -890,11 +978,25 @@ def get_material_request_items(
if include_safety_stock:
required_qty += flt(row["safety_stock"])
+ item_details = frappe.get_cached_value(
+ "Item", row.item_code, ["purchase_uom", "stock_uom"], as_dict=1
+ )
+
+ conversion_factor = 1.0
+ if (
+ row.get("default_material_request_type") == "Purchase"
+ and item_details.purchase_uom
+ and item_details.purchase_uom != item_details.stock_uom
+ ):
+ conversion_factor = (
+ get_conversion_factor(row.item_code, item_details.purchase_uom).get("conversion_factor") or 1.0
+ )
+
if required_qty > 0:
return {
"item_code": row.item_code,
"item_name": row.item_name,
- "quantity": required_qty,
+ "quantity": required_qty / conversion_factor,
"required_bom_qty": total_qty,
"stock_uom": row.get("stock_uom"),
"warehouse": warehouse
@@ -915,48 +1017,69 @@ def get_material_request_items(
def get_sales_orders(self):
- so_filter = item_filter = ""
- bom_item = "bom.item = so_item.item_code"
+ bom = frappe.qb.DocType("BOM")
+ pi = frappe.qb.DocType("Packed Item")
+ so = frappe.qb.DocType("Sales Order")
+ so_item = frappe.qb.DocType("Sales Order Item")
+
+ open_so_subquery1 = frappe.qb.from_(bom).select(bom.name).where(bom.is_active == 1)
+
+ open_so_subquery2 = (
+ frappe.qb.from_(pi)
+ .select(pi.name)
+ .where(
+ (pi.parent == so.name)
+ & (pi.parent_item == so_item.item_code)
+ & (
+ ExistsCriterion(
+ frappe.qb.from_(bom).select(bom.name).where((bom.item == pi.item_code) & (bom.is_active == 1))
+ )
+ )
+ )
+ )
+
+ open_so_query = (
+ frappe.qb.from_(so)
+ .from_(so_item)
+ .select(so.name, so.transaction_date, so.customer, so.base_grand_total)
+ .distinct()
+ .where(
+ (so_item.parent == so.name)
+ & (so.docstatus == 1)
+ & (so.status.notin(["Stopped", "Closed"]))
+ & (so.company == self.company)
+ & (so_item.qty > so_item.work_order_qty)
+ )
+ )
date_field_mapper = {
- "from_date": (">=", "so.transaction_date"),
- "to_date": ("<=", "so.transaction_date"),
- "from_delivery_date": (">=", "so_item.delivery_date"),
- "to_delivery_date": ("<=", "so_item.delivery_date"),
+ "from_date": self.from_date >= so.transaction_date,
+ "to_date": self.to_date <= so.transaction_date,
+ "from_delivery_date": self.from_delivery_date >= so_item.delivery_date,
+ "to_delivery_date": self.to_delivery_date <= so_item.delivery_date,
}
for field, value in date_field_mapper.items():
if self.get(field):
- so_filter += f" and {value[1]} {value[0]} %({field})s"
+ open_so_query = open_so_query.where(value)
- for field in ["customer", "project", "sales_order_status"]:
+ for field in ("customer", "project", "sales_order_status"):
if self.get(field):
so_field = "status" if field == "sales_order_status" else field
- so_filter += f" and so.{so_field} = %({field})s"
+ open_so_query = open_so_query.where(so[so_field] == self.get(field))
if self.item_code and frappe.db.exists("Item", self.item_code):
- bom_item = self.get_bom_item() or bom_item
- item_filter += " and so_item.item_code = %(item_code)s"
-
- open_so = frappe.db.sql(
- f"""
- select distinct so.name, so.transaction_date, so.customer, so.base_grand_total
- from `tabSales Order` so, `tabSales Order Item` so_item
- where so_item.parent = so.name
- and so.docstatus = 1 and so.status not in ("Stopped", "Closed")
- and so.company = %(company)s
- and so_item.qty > so_item.work_order_qty {so_filter} {item_filter}
- and (exists (select name from `tabBOM` bom where {bom_item}
- and bom.is_active = 1)
- or exists (select name from `tabPacked Item` pi
- where pi.parent = so.name and pi.parent_item = so_item.item_code
- and exists (select name from `tabBOM` bom where bom.item=pi.item_code
- and bom.is_active = 1)))
- """,
- self.as_dict(),
- as_dict=1,
+ open_so_query = open_so_query.where(so_item.item_code == self.item_code)
+ open_so_subquery1 = open_so_subquery1.where(
+ self.get_bom_item_condition() or bom.item == so_item.item_code
+ )
+
+ open_so_query = open_so_query.where(
+ (ExistsCriterion(open_so_subquery1) | ExistsCriterion(open_so_subquery2))
)
+ open_so = open_so_query.run(as_dict=True)
+
return open_so
@@ -965,37 +1088,34 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False):
if isinstance(row, str):
row = frappe._dict(json.loads(row))
- company = frappe.db.escape(company)
- conditions, warehouse = "", ""
+ bin = frappe.qb.DocType("Bin")
+ wh = frappe.qb.DocType("Warehouse")
+
+ subquery = frappe.qb.from_(wh).select(wh.name).where(wh.company == company)
- conditions = " and warehouse in (select name from `tabWarehouse` where company = {0})".format(
- company
- )
if not all_warehouse:
warehouse = for_warehouse or row.get("source_warehouse") or row.get("default_warehouse")
if warehouse:
lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"])
- conditions = """ and warehouse in (select name from `tabWarehouse`
- where lft >= {0} and rgt <= {1} and name=`tabBin`.warehouse and company = {2})
- """.format(
- lft, rgt, company
+ subquery = subquery.where((wh.lft >= lft) & (wh.rgt <= rgt) & (wh.name == bin.warehouse))
+
+ query = (
+ frappe.qb.from_(bin)
+ .select(
+ bin.warehouse,
+ IfNull(Sum(bin.projected_qty), 0).as_("projected_qty"),
+ IfNull(Sum(bin.actual_qty), 0).as_("actual_qty"),
+ IfNull(Sum(bin.ordered_qty), 0).as_("ordered_qty"),
+ IfNull(Sum(bin.reserved_qty_for_production), 0).as_("reserved_qty_for_production"),
+ IfNull(Sum(bin.planned_qty), 0).as_("planned_qty"),
)
-
- return frappe.db.sql(
- """ select ifnull(sum(projected_qty),0) as projected_qty,
- ifnull(sum(actual_qty),0) as actual_qty, ifnull(sum(ordered_qty),0) as ordered_qty,
- ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse,
- ifnull(sum(planned_qty),0) as planned_qty
- from `tabBin` where item_code = %(item_code)s {conditions}
- group by item_code, warehouse
- """.format(
- conditions=conditions
- ),
- {"item_code": row["item_code"]},
- as_dict=1,
+ .where((bin.item_code == row["item_code"]) & (bin.warehouse.isin(subquery)))
+ .groupby(bin.item_code, bin.warehouse)
)
+ return query.run(as_dict=True)
+
@frappe.whitelist()
def get_so_details(sales_order):
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index cbae275031e6..8cd79202dd8d 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -12,6 +12,7 @@
)
from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError
from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry as make_se_from_wo
+from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.item.test_item import create_item, make_item
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
@@ -538,15 +539,21 @@ def test_production_plan_pending_qty_with_sales_order(self):
"""
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
+ make_stock_entry(item_code="_Test Item", target="Work In Progress - _TC", qty=2, basic_rate=100)
make_stock_entry(
- item_code="Raw Material Item 1", target="Work In Progress - _TC", qty=2, basic_rate=100
- )
- make_stock_entry(
- item_code="Raw Material Item 2", target="Work In Progress - _TC", qty=2, basic_rate=100
+ item_code="_Test Item Home Desktop 100", target="Work In Progress - _TC", qty=4, basic_rate=100
)
- item = "Test Production Item 1"
- so = make_sales_order(item_code=item, qty=1)
+ item = "_Test FG Item"
+
+ make_stock_entry(item_code=item, target="_Test Warehouse - _TC", qty=1)
+
+ so = make_sales_order(item_code=item, qty=2)
+
+ dn = make_delivery_note(so.name)
+ dn.items[0].qty = 1
+ dn.save()
+ dn.submit()
pln = create_production_plan(
company=so.company, get_items_from="Sales Order", sales_order=so, skip_getting_mr_items=True
@@ -725,6 +732,40 @@ def test_produced_qty_for_multi_level_bom_item(self):
self.assertEqual(pln.status, "Completed")
self.assertEqual(pln.po_items[0].produced_qty, 5)
+ def test_material_request_item_for_purchase_uom(self):
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ fg_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name
+ bom_item = make_item(
+ properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1", "purchase_uom": "Nos"}
+ ).name
+
+ if not frappe.db.exists("UOM Conversion Detail", {"parent": bom_item, "uom": "Nos"}):
+ doc = frappe.get_doc("Item", bom_item)
+ doc.append("uoms", {"uom": "Nos", "conversion_factor": 10})
+ doc.save()
+
+ make_bom(item=fg_item, raw_materials=[bom_item], source_warehouse="_Test Warehouse - _TC")
+
+ pln = create_production_plan(
+ item_code=fg_item, planned_qty=10, ignore_existing_ordered_qty=1, stock_uom="_Test UOM 1"
+ )
+
+ pln.make_material_request()
+
+ for row in pln.mr_items:
+ self.assertEqual(row.uom, "Nos")
+ self.assertEqual(row.quantity, 1)
+
+ for row in frappe.get_all(
+ "Material Request Item",
+ filters={"production_plan": pln.name},
+ fields=["item_code", "uom", "qty"],
+ ):
+ self.assertEqual(row.item_code, bom_item)
+ self.assertEqual(row.uom, "Nos")
+ self.assertEqual(row.qty, 1)
+
def create_production_plan(**args):
"""
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index cfba0fff15ef..c5c9b5f0d312 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -625,6 +625,10 @@ def test_batch_size_for_fg_item(self):
bom.submit()
bom_name = bom.name
+ ste1 = test_stock_entry.make_stock_entry(
+ item_code=rm1, target="_Test Warehouse - _TC", qty=32, basic_rate=5000.0
+ )
+
work_order = make_wo_order_test_record(
item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1
)
@@ -649,11 +653,29 @@ def test_batch_size_for_fg_item(self):
work_order.insert()
work_order.submit()
self.assertEqual(work_order.has_batch_no, 1)
- ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 30))
+ batches = frappe.get_all("Batch", filters={"reference_name": work_order.name})
+ self.assertEqual(len(batches), 3)
+ batches = [batch.name for batch in batches]
+
+ ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 10))
for row in ste1.get("items"):
if row.is_finished_item:
self.assertEqual(row.item_code, fg_item)
self.assertEqual(row.qty, 10)
+ self.assertTrue(row.batch_no in batches)
+ batches.remove(row.batch_no)
+
+ ste1.submit()
+
+ remaining_batches = []
+ ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 20))
+ for row in ste1.get("items"):
+ if row.is_finished_item:
+ self.assertEqual(row.item_code, fg_item)
+ self.assertEqual(row.qty, 10)
+ remaining_batches.append(row.batch_no)
+
+ self.assertEqual(sorted(remaining_batches), sorted(batches))
frappe.db.set_value("Manufacturing Settings", None, "make_serial_no_batch_from_work_order", 0)
@@ -1123,6 +1145,37 @@ def test_auto_batch_creation(self):
except frappe.MandatoryError:
self.fail("Batch generation causing failing in Work Order")
+ @change_settings("Manufacturing Settings", {"make_serial_no_batch_from_work_order": 1})
+ def test_auto_serial_no_creation(self):
+ from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
+ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
+ fg_item = frappe.generate_hash(length=20)
+ child_item = frappe.generate_hash(length=20)
+
+ bom_tree = {fg_item: {child_item: {}}}
+
+ create_nested_bom(bom_tree, prefix="")
+
+ item = frappe.get_doc("Item", fg_item)
+ item.has_serial_no = 1
+ item.serial_no_series = f"{item.name}.#####"
+ item.save()
+
+ try:
+ wo_order = make_wo_order_test_record(item=fg_item, qty=2, skip_transfer=True)
+ serial_nos = wo_order.serial_no
+ stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
+ stock_entry.set_work_order_details()
+ stock_entry.set_serial_no_batch_for_finished_good()
+ for row in stock_entry.items:
+ if row.item_code == fg_item:
+ self.assertTrue(row.serial_no)
+ self.assertEqual(sorted(get_serial_nos(row.serial_no)), sorted(get_serial_nos(serial_nos)))
+
+ except frappe.MandatoryError:
+ self.fail("Batch generation causing failing in Work Order")
+
@change_settings(
"Manufacturing Settings",
{"backflush_raw_materials_based_on": "Material Transferred for Manufacture"},
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index 9b0c8382c535..b1ee3cd7a1bc 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -555,13 +555,10 @@ erpnext.work_order = {
}
}
- if(!frm.doc.skip_transfer){
+ if (frm.doc.status != 'Stopped') {
// If "Material Consumption is check in Manufacturing Settings, allow Material Consumption
- if ((flt(doc.produced_qty) < flt(doc.material_transferred_for_manufacturing))
- && frm.doc.status != 'Stopped') {
- frm.has_finish_btn = true;
-
- if (frm.doc.__onload && frm.doc.__onload.material_consumption == 1) {
+ if (frm.doc.__onload && frm.doc.__onload.material_consumption == 1) {
+ if (flt(doc.material_transferred_for_manufacturing) > 0 || frm.doc.skip_transfer) {
// Only show "Material Consumption" when required_qty > consumed_qty
var counter = 0;
var tbl = frm.doc.required_items || [];
@@ -580,26 +577,47 @@ erpnext.work_order = {
consumption_btn.addClass('btn-primary');
}
}
+ }
- var finish_btn = frm.add_custom_button(__('Finish'), function() {
- erpnext.work_order.make_se(frm, 'Manufacture');
- });
+ if(!frm.doc.skip_transfer){
+ if (flt(doc.material_transferred_for_manufacturing) > 0) {
+ if ((flt(doc.produced_qty) < flt(doc.material_transferred_for_manufacturing))) {
+ frm.has_finish_btn = true;
+
+ var finish_btn = frm.add_custom_button(__('Finish'), function() {
+ erpnext.work_order.make_se(frm, 'Manufacture');
+ });
- if(doc.material_transferred_for_manufacturing>=doc.qty) {
- // all materials transferred for manufacturing, make this primary
+ if(doc.material_transferred_for_manufacturing>=doc.qty) {
+ // all materials transferred for manufacturing, make this primary
+ finish_btn.addClass('btn-primary');
+ }
+ } else {
+ frappe.db.get_doc("Manufacturing Settings").then((doc) => {
+ let allowance_percentage = doc.overproduction_percentage_for_work_order;
+
+ if (allowance_percentage > 0) {
+ let allowed_qty = frm.doc.qty + ((allowance_percentage / 100) * frm.doc.qty);
+
+ if ((flt(doc.produced_qty) < allowed_qty)) {
+ frm.add_custom_button(__('Finish'), function() {
+ erpnext.work_order.make_se(frm, 'Manufacture');
+ });
+ }
+ }
+ });
+ }
+ }
+ } else {
+ if ((flt(doc.produced_qty) < flt(doc.qty))) {
+ var finish_btn = frm.add_custom_button(__('Finish'), function() {
+ erpnext.work_order.make_se(frm, 'Manufacture');
+ });
finish_btn.addClass('btn-primary');
}
}
- } else {
- if ((flt(doc.produced_qty) < flt(doc.qty)) && frm.doc.status != 'Stopped') {
- var finish_btn = frm.add_custom_button(__('Finish'), function() {
- erpnext.work_order.make_se(frm, 'Manufacture');
- });
- finish_btn.addClass('btn-primary');
- }
}
}
-
},
calculate_cost: function(doc) {
if (doc.operations){
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 2802310250bb..574ca185fee8 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -145,7 +145,7 @@ def check_sales_order_on_hold_or_close(self):
frappe.throw(_("Sales Order {0} is {1}").format(self.sales_order, status))
def set_default_warehouse(self):
- if not self.wip_warehouse:
+ if not self.wip_warehouse and not self.skip_transfer:
self.wip_warehouse = frappe.db.get_single_value(
"Manufacturing Settings", "default_wip_warehouse"
)
diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js
index 0d5bfcbaf402..a0fd91e866f2 100644
--- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js
+++ b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js
@@ -11,17 +11,24 @@ frappe.query_reports["BOM Stock Calculated"] = {
"options": "BOM",
"reqd": 1
},
- {
- "fieldname": "qty_to_make",
- "label": __("Quantity to Make"),
- "fieldtype": "Int",
- "default": "1"
- },
-
- {
+ {
+ "fieldname": "warehouse",
+ "label": __("Warehouse"),
+ "fieldtype": "Link",
+ "options": "Warehouse",
+ },
+ {
+ "fieldname": "qty_to_make",
+ "label": __("Quantity to Make"),
+ "fieldtype": "Float",
+ "default": "1.0",
+ "reqd": 1
+ },
+ {
"fieldname": "show_exploded_view",
"label": __("Show exploded view"),
- "fieldtype": "Check"
+ "fieldtype": "Check",
+ "default": false,
}
]
}
diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
index 933be3e01404..550445c1f77f 100644
--- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
+++ b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
@@ -4,29 +4,31 @@
import frappe
from frappe import _
+from frappe.query_builder.functions import IfNull, Sum
from frappe.utils.data import comma_and
+from pypika.terms import ExistsCriterion
def execute(filters=None):
- # if not filters: filters = {}
columns = get_columns()
- summ_data = []
+ data = []
- data = get_bom_stock(filters)
+ bom_data = get_bom_data(filters)
qty_to_make = filters.get("qty_to_make")
-
manufacture_details = get_manufacturer_records()
- for row in data:
- reqd_qty = qty_to_make * row.actual_qty
- last_pur_price = frappe.db.get_value("Item", row.item_code, "last_purchase_rate")
- summ_data.append(get_report_data(last_pur_price, reqd_qty, row, manufacture_details))
- return columns, summ_data
+ for row in bom_data:
+ required_qty = qty_to_make * row.qty_per_unit
+ last_purchase_rate = frappe.db.get_value("Item", row.item_code, "last_purchase_rate")
+
+ data.append(get_report_data(last_purchase_rate, required_qty, row, manufacture_details))
+ return columns, data
-def get_report_data(last_pur_price, reqd_qty, row, manufacture_details):
- to_build = row.to_build if row.to_build > 0 else 0
- diff_qty = to_build - reqd_qty
+
+def get_report_data(last_purchase_rate, required_qty, row, manufacture_details):
+ qty_per_unit = row.qty_per_unit if row.qty_per_unit > 0 else 0
+ difference_qty = row.actual_qty - required_qty
return [
row.item_code,
row.description,
@@ -34,85 +36,126 @@ def get_report_data(last_pur_price, reqd_qty, row, manufacture_details):
comma_and(
manufacture_details.get(row.item_code, {}).get("manufacturer_part", []), add_quotes=False
),
+ qty_per_unit,
row.actual_qty,
- str(to_build),
- reqd_qty,
- diff_qty,
- last_pur_price,
+ required_qty,
+ difference_qty,
+ last_purchase_rate,
]
def get_columns():
- """return columns"""
- columns = [
- _("Item") + ":Link/Item:100",
- _("Description") + "::150",
- _("Manufacturer") + "::250",
- _("Manufacturer Part Number") + "::250",
- _("Qty") + ":Float:50",
- _("Stock Qty") + ":Float:100",
- _("Reqd Qty") + ":Float:100",
- _("Diff Qty") + ":Float:100",
- _("Last Purchase Price") + ":Float:100",
+ return [
+ {
+ "fieldname": "item",
+ "label": _("Item"),
+ "fieldtype": "Link",
+ "options": "Item",
+ "width": 120,
+ },
+ {
+ "fieldname": "description",
+ "label": _("Description"),
+ "fieldtype": "Data",
+ "width": 150,
+ },
+ {
+ "fieldname": "manufacturer",
+ "label": _("Manufacturer"),
+ "fieldtype": "Data",
+ "width": 120,
+ },
+ {
+ "fieldname": "manufacturer_part_number",
+ "label": _("Manufacturer Part Number"),
+ "fieldtype": "Data",
+ "width": 150,
+ },
+ {
+ "fieldname": "qty_per_unit",
+ "label": _("Qty Per Unit"),
+ "fieldtype": "Float",
+ "width": 110,
+ },
+ {
+ "fieldname": "available_qty",
+ "label": _("Available Qty"),
+ "fieldtype": "Float",
+ "width": 120,
+ },
+ {
+ "fieldname": "required_qty",
+ "label": _("Required Qty"),
+ "fieldtype": "Float",
+ "width": 120,
+ },
+ {
+ "fieldname": "difference_qty",
+ "label": _("Difference Qty"),
+ "fieldtype": "Float",
+ "width": 130,
+ },
+ {
+ "fieldname": "last_purchase_rate",
+ "label": _("Last Purchase Rate"),
+ "fieldtype": "Float",
+ "width": 160,
+ },
]
- return columns
-def get_bom_stock(filters):
- conditions = ""
- bom = filters.get("bom")
-
- table = "`tabBOM Item`"
- qty_field = "qty"
-
+def get_bom_data(filters):
if filters.get("show_exploded_view"):
- table = "`tabBOM Explosion Item`"
- qty_field = "stock_qty"
+ bom_item_table = "BOM Explosion Item"
+ else:
+ bom_item_table = "BOM Item"
+
+ bom_item = frappe.qb.DocType(bom_item_table)
+ bin = frappe.qb.DocType("Bin")
+
+ query = (
+ frappe.qb.from_(bom_item)
+ .left_join(bin)
+ .on(bom_item.item_code == bin.item_code)
+ .select(
+ bom_item.item_code,
+ bom_item.description,
+ bom_item.qty_consumed_per_unit.as_("qty_per_unit"),
+ IfNull(Sum(bin.actual_qty), 0).as_("actual_qty"),
+ )
+ .where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM"))
+ .groupby(bom_item.item_code)
+ )
if filters.get("warehouse"):
warehouse_details = frappe.db.get_value(
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
)
+
if warehouse_details:
- conditions += (
- " and exists (select name from `tabWarehouse` wh \
- where wh.lft >= %s and wh.rgt <= %s and ledger.warehouse = wh.name)"
- % (warehouse_details.lft, warehouse_details.rgt)
+ wh = frappe.qb.DocType("Warehouse")
+ query = query.where(
+ ExistsCriterion(
+ frappe.qb.from_(wh)
+ .select(wh.name)
+ .where(
+ (wh.lft >= warehouse_details.lft)
+ & (wh.rgt <= warehouse_details.rgt)
+ & (bin.warehouse == wh.name)
+ )
+ )
)
else:
- conditions += " and ledger.warehouse = %s" % frappe.db.escape(filters.get("warehouse"))
+ query = query.where(bin.warehouse == filters.get("warehouse"))
- else:
- conditions += ""
-
- return frappe.db.sql(
- """
- SELECT
- bom_item.item_code,
- bom_item.description,
- bom_item.{qty_field},
- ifnull(sum(ledger.actual_qty), 0) as actual_qty,
- ifnull(sum(FLOOR(ledger.actual_qty / bom_item.{qty_field})), 0) as to_build
- FROM
- {table} AS bom_item
- LEFT JOIN `tabBin` AS ledger
- ON bom_item.item_code = ledger.item_code
- {conditions}
-
- WHERE
- bom_item.parent = '{bom}' and bom_item.parenttype='BOM'
-
- GROUP BY bom_item.item_code""".format(
- qty_field=qty_field, table=table, conditions=conditions, bom=bom
- ),
- as_dict=1,
- )
+ return query.run(as_dict=True)
def get_manufacturer_records():
details = frappe.get_all(
"Item Manufacturer", fields=["manufacturer", "manufacturer_part_no", "item_code"]
)
+
manufacture_details = frappe._dict()
for detail in details:
dic = manufacture_details.setdefault(detail.get("item_code"), {})
diff --git a/erpnext/manufacturing/report/bom_stock_calculated/test_bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/test_bom_stock_calculated.py
new file mode 100644
index 000000000000..8ad980fa19af
--- /dev/null
+++ b/erpnext/manufacturing/report/bom_stock_calculated/test_bom_stock_calculated.py
@@ -0,0 +1,115 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from frappe.tests.utils import FrappeTestCase
+
+from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
+from erpnext.manufacturing.report.bom_stock_calculated.bom_stock_calculated import (
+ execute as bom_stock_calculated_report,
+)
+from erpnext.stock.doctype.item.test_item import make_item
+
+
+class TestBOMStockCalculated(FrappeTestCase):
+ def setUp(self):
+ self.fg_item, self.rm_items = create_items()
+ self.boms = create_boms(self.fg_item, self.rm_items)
+
+ def test_bom_stock_calculated(self):
+ qty_to_make = 10
+
+ # Case 1: When Item(s) Qty and Stock Qty are equal.
+ data = bom_stock_calculated_report(
+ filters={
+ "qty_to_make": qty_to_make,
+ "bom": self.boms[0].name,
+ }
+ )[1]
+ expected_data = get_expected_data(self.boms[0], qty_to_make)
+ self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
+
+ # Case 2: When Item(s) Qty and Stock Qty are different and BOM Qty is 1.
+ data = bom_stock_calculated_report(
+ filters={
+ "qty_to_make": qty_to_make,
+ "bom": self.boms[1].name,
+ }
+ )[1]
+ expected_data = get_expected_data(self.boms[1], qty_to_make)
+ self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
+
+ # Case 3: When Item(s) Qty and Stock Qty are different and BOM Qty is greater than 1.
+ data = bom_stock_calculated_report(
+ filters={
+ "qty_to_make": qty_to_make,
+ "bom": self.boms[2].name,
+ }
+ )[1]
+ expected_data = get_expected_data(self.boms[2], qty_to_make)
+ self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
+
+
+def create_items():
+ fg_item = make_item(properties={"is_stock_item": 1}).name
+ rm_item1 = make_item(
+ properties={
+ "is_stock_item": 1,
+ "standard_rate": 100,
+ "opening_stock": 100,
+ "last_purchase_rate": 100,
+ }
+ ).name
+ rm_item2 = make_item(
+ properties={
+ "is_stock_item": 1,
+ "standard_rate": 200,
+ "opening_stock": 200,
+ "last_purchase_rate": 200,
+ }
+ ).name
+
+ return fg_item, [rm_item1, rm_item2]
+
+
+def create_boms(fg_item, rm_items):
+ def update_bom_items(bom, uom, conversion_factor):
+ for item in bom.items:
+ item.uom = uom
+ item.conversion_factor = conversion_factor
+
+ return bom
+
+ bom1 = make_bom(item=fg_item, quantity=1, raw_materials=rm_items, rm_qty=10)
+
+ bom2 = make_bom(item=fg_item, quantity=1, raw_materials=rm_items, rm_qty=10, do_not_submit=True)
+ bom2 = update_bom_items(bom2, "Box", 10)
+ bom2.save()
+ bom2.submit()
+
+ bom3 = make_bom(item=fg_item, quantity=2, raw_materials=rm_items, rm_qty=10, do_not_submit=True)
+ bom3 = update_bom_items(bom3, "Box", 10)
+ bom3.save()
+ bom3.submit()
+
+ return [bom1, bom2, bom3]
+
+
+def get_expected_data(bom, qty_to_make):
+ expected_data = []
+
+ for idx in range(len(bom.items)):
+ expected_data.append(
+ [
+ bom.items[idx].item_code,
+ bom.items[idx].item_code,
+ "",
+ "",
+ float(bom.items[idx].stock_qty / bom.quantity),
+ float(100 * (idx + 1)),
+ float(qty_to_make * (bom.items[idx].stock_qty / bom.quantity)),
+ float((100 * (idx + 1)) - (qty_to_make * (bom.items[idx].stock_qty / bom.quantity))),
+ float(100 * (idx + 1)),
+ ]
+ )
+
+ return expected_data
diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
index 34e9826305ef..cdf1541f8881 100644
--- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
+++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
@@ -4,6 +4,8 @@
import frappe
from frappe import _
+from frappe.query_builder.functions import Sum
+from pypika.terms import ExistsCriterion
def execute(filters=None):
@@ -11,7 +13,6 @@ def execute(filters=None):
filters = {}
columns = get_columns()
-
data = get_bom_stock(filters)
return columns, data
@@ -33,59 +34,57 @@ def get_columns():
def get_bom_stock(filters):
- conditions = ""
- bom = filters.get("bom")
-
- table = "`tabBOM Item`"
- qty_field = "stock_qty"
-
- qty_to_produce = filters.get("qty_to_produce", 1)
- if int(qty_to_produce) <= 0:
+ qty_to_produce = filters.get("qty_to_produce") or 1
+ if int(qty_to_produce) < 0:
frappe.throw(_("Quantity to Produce can not be less than Zero"))
if filters.get("show_exploded_view"):
- table = "`tabBOM Explosion Item`"
+ bom_item_table = "BOM Explosion Item"
+ else:
+ bom_item_table = "BOM Item"
+
+ bin = frappe.qb.DocType("Bin")
+ bom = frappe.qb.DocType("BOM")
+ bom_item = frappe.qb.DocType(bom_item_table)
+
+ query = (
+ frappe.qb.from_(bom)
+ .inner_join(bom_item)
+ .on(bom.name == bom_item.parent)
+ .left_join(bin)
+ .on(bom_item.item_code == bin.item_code)
+ .select(
+ bom_item.item_code,
+ bom_item.description,
+ bom_item.stock_qty,
+ bom_item.stock_uom,
+ (bom_item.stock_qty / bom.quantity) * qty_to_produce,
+ Sum(bin.actual_qty),
+ Sum(bin.actual_qty) / (bom_item.stock_qty / bom.quantity),
+ )
+ .where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM"))
+ .groupby(bom_item.item_code)
+ )
if filters.get("warehouse"):
warehouse_details = frappe.db.get_value(
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
)
+
if warehouse_details:
- conditions += (
- " and exists (select name from `tabWarehouse` wh \
- where wh.lft >= %s and wh.rgt <= %s and ledger.warehouse = wh.name)"
- % (warehouse_details.lft, warehouse_details.rgt)
+ wh = frappe.qb.DocType("Warehouse")
+ query = query.where(
+ ExistsCriterion(
+ frappe.qb.from_(wh)
+ .select(wh.name)
+ .where(
+ (wh.lft >= warehouse_details.lft)
+ & (wh.rgt <= warehouse_details.rgt)
+ & (bin.warehouse == wh.name)
+ )
+ )
)
else:
- conditions += " and ledger.warehouse = %s" % frappe.db.escape(filters.get("warehouse"))
+ query = query.where(bin.warehouse == filters.get("warehouse"))
- else:
- conditions += ""
-
- return frappe.db.sql(
- """
- SELECT
- bom_item.item_code,
- bom_item.description ,
- bom_item.{qty_field},
- bom_item.stock_uom,
- bom_item.{qty_field} * {qty_to_produce} / bom.quantity,
- sum(ledger.actual_qty) as actual_qty,
- sum(FLOOR(ledger.actual_qty / (bom_item.{qty_field} * {qty_to_produce} / bom.quantity)))
- FROM
- `tabBOM` AS bom INNER JOIN {table} AS bom_item
- ON bom.name = bom_item.parent
- LEFT JOIN `tabBin` AS ledger
- ON bom_item.item_code = ledger.item_code
- {conditions}
- WHERE
- bom_item.parent = {bom} and bom_item.parenttype='BOM'
-
- GROUP BY bom_item.item_code""".format(
- qty_field=qty_field,
- table=table,
- conditions=conditions,
- bom=frappe.db.escape(bom),
- qty_to_produce=qty_to_produce or 1,
- )
- )
+ return query.run()
diff --git a/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py b/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py
index 3fe2198966c6..70a1850fd0fd 100644
--- a/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py
+++ b/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py
@@ -64,22 +64,21 @@ def get_columns(filters):
def get_data(filters):
- cond = "1=1"
+ wo = frappe.qb.DocType("Work Order")
+ query = (
+ frappe.qb.from_(wo)
+ .select(wo.name.as_("work_order"), wo.qty, wo.produced_qty, wo.production_item, wo.bom_no)
+ .where((wo.produced_qty > wo.qty) & (wo.docstatus == 1))
+ )
if filters.get("bom_no") and not filters.get("work_order"):
- cond += " and bom_no = '%s'" % filters.get("bom_no")
+ query = query.where(wo.bom_no == filters.get("bom_no"))
if filters.get("work_order"):
- cond += " and name = '%s'" % filters.get("work_order")
+ query = query.where(wo.name == filters.get("work_order"))
results = []
- for d in frappe.db.sql(
- """ select name as work_order, qty, produced_qty, production_item, bom_no
- from `tabWork Order` where produced_qty > qty and docstatus = 1 and {0}""".format(
- cond
- ),
- as_dict=1,
- ):
+ for d in query.run(as_dict=True):
results.append(d)
for data in frappe.get_all(
@@ -95,16 +94,17 @@ def get_data(filters):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_work_orders(doctype, txt, searchfield, start, page_len, filters):
- cond = "1=1"
- if filters.get("bom_no"):
- cond += " and bom_no = '%s'" % filters.get("bom_no")
-
- return frappe.db.sql(
- """select name from `tabWork Order`
- where name like %(name)s and {0} and produced_qty > qty and docstatus = 1
- order by name limit {1}, {2}""".format(
- cond, start, page_len
- ),
- {"name": "%%%s%%" % txt},
- as_list=1,
+ wo = frappe.qb.DocType("Work Order")
+ query = (
+ frappe.qb.from_(wo)
+ .select(wo.name)
+ .where((wo.name.like(f"{txt}%")) & (wo.produced_qty > wo.qty) & (wo.docstatus == 1))
+ .orderby(wo.name)
+ .limit(page_len)
+ .offset(start)
)
+
+ if filters.get("bom_no"):
+ query = query.where(wo.bom_no == filters.get("bom_no"))
+
+ return query.run(as_list=True)
diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py
index 7500744c2280..d3bce8315518 100644
--- a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py
+++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py
@@ -96,38 +96,39 @@ def prepare_periodical_data(self):
value["avg"] = flt(sum(list_of_period_value)) / flt(sum(total_qty))
def get_data_for_forecast(self):
- cond = ""
- if self.filters.item_code:
- cond = " AND soi.item_code = %s" % (frappe.db.escape(self.filters.item_code))
-
- warehouses = []
- if self.filters.warehouse:
- warehouses = get_child_warehouses(self.filters.warehouse)
- cond += " AND soi.warehouse in ({})".format(",".join(["%s"] * len(warehouses)))
-
- input_data = [self.filters.from_date, self.filters.company]
- if warehouses:
- input_data.extend(warehouses)
+ parent = frappe.qb.DocType(self.doctype)
+ child = frappe.qb.DocType(self.child_doctype)
date_field = "posting_date" if self.doctype == "Delivery Note" else "transaction_date"
- return frappe.db.sql(
- """
- SELECT
- so.{date_field} as posting_date, soi.item_code, soi.warehouse,
- soi.item_name, soi.stock_qty as qty, soi.base_amount as amount
- FROM
- `tab{doc}` so, `tab{child_doc}` soi
- WHERE
- so.docstatus = 1 AND so.name = soi.parent AND
- so.{date_field} < %s AND so.company = %s {cond}
- """.format(
- doc=self.doctype, child_doc=self.child_doctype, date_field=date_field, cond=cond
- ),
- tuple(input_data),
- as_dict=1,
+ query = (
+ frappe.qb.from_(parent)
+ .from_(child)
+ .select(
+ parent[date_field].as_("posting_date"),
+ child.item_code,
+ child.warehouse,
+ child.item_name,
+ child.stock_qty.as_("qty"),
+ child.base_amount.as_("amount"),
+ )
+ .where(
+ (parent.docstatus == 1)
+ & (parent.name == child.parent)
+ & (parent[date_field] < self.filters.from_date)
+ & (parent.company == self.filters.company)
+ )
)
+ if self.filters.item_code:
+ query = query.where(child.item_code == self.filters.item_code)
+
+ if self.filters.warehouse:
+ warehouses = get_child_warehouses(self.filters.warehouse) or []
+ query = query.where(child.warehouse.isin(warehouses))
+
+ return query.run(as_dict=True)
+
def prepare_final_data(self):
self.data = []
diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.js b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js
index cb771e499415..782ce8110a8f 100644
--- a/erpnext/manufacturing/report/job_card_summary/job_card_summary.js
+++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js
@@ -54,11 +54,11 @@ frappe.query_reports["Job Card Summary"] = {
options: ["", "Open", "Work In Progress", "Completed", "On Hold"]
},
{
- label: __("Sales Orders"),
- fieldname: "sales_order",
+ label: __("Work Orders"),
+ fieldname: "work_order",
fieldtype: "MultiSelectList",
get_data: function(txt) {
- return frappe.db.get_link_options('Sales Order', txt);
+ return frappe.db.get_link_options('Work Order', txt);
}
},
{
diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.py b/erpnext/manufacturing/report/job_card_summary/job_card_summary.py
index a86c7a47c364..009dc862e999 100644
--- a/erpnext/manufacturing/report/job_card_summary/job_card_summary.py
+++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.py
@@ -36,10 +36,14 @@ def get_data(filters):
"total_time_in_mins",
]
- for field in ["work_order", "workstation", "operation", "company"]:
+ for field in ["work_order", "production_item"]:
if filters.get(field):
query_filters[field] = ("in", filters.get(field))
+ for field in ["workstation", "operation", "status", "company"]:
+ if filters.get(field):
+ query_filters[field] = filters.get(field)
+
data = frappe.get_all("Job Card", fields=fields, filters=query_filters)
if not data:
diff --git a/erpnext/manufacturing/report/process_loss_report/process_loss_report.py b/erpnext/manufacturing/report/process_loss_report/process_loss_report.py
index b10e6434223b..ce8f4f35a3f2 100644
--- a/erpnext/manufacturing/report/process_loss_report/process_loss_report.py
+++ b/erpnext/manufacturing/report/process_loss_report/process_loss_report.py
@@ -5,6 +5,7 @@
import frappe
from frappe import _
+from frappe.query_builder.functions import Sum
Filters = frappe._dict
Row = frappe._dict
@@ -14,15 +15,50 @@
def execute(filters: Filters) -> Tuple[Columns, Data]:
+ filters = frappe._dict(filters or {})
columns = get_columns()
data = get_data(filters)
return columns, data
def get_data(filters: Filters) -> Data:
- query_args = get_query_args(filters)
- data = run_query(query_args)
+ wo = frappe.qb.DocType("Work Order")
+ se = frappe.qb.DocType("Stock Entry")
+
+ query = (
+ frappe.qb.from_(wo)
+ .inner_join(se)
+ .on(wo.name == se.work_order)
+ .select(
+ wo.name,
+ wo.status,
+ wo.production_item,
+ wo.qty,
+ wo.produced_qty,
+ wo.process_loss_qty,
+ (wo.produced_qty - wo.process_loss_qty).as_("actual_produced_qty"),
+ Sum(se.total_incoming_value).as_("total_fg_value"),
+ Sum(se.total_outgoing_value).as_("total_rm_value"),
+ )
+ .where(
+ (wo.process_loss_qty > 0)
+ & (wo.company == filters.company)
+ & (se.docstatus == 1)
+ & (se.posting_date.between(filters.from_date, filters.to_date))
+ )
+ .groupby(se.work_order)
+ )
+
+ if "item" in filters:
+ query.where(wo.production_item == filters.item)
+
+ if "work_order" in filters:
+ query.where(wo.name == filters.work_order)
+
+ data = query.run(as_dict=True)
+
update_data_with_total_pl_value(data)
+
return data
@@ -67,54 +103,7 @@ def get_columns() -> Columns:
]
-def get_query_args(filters: Filters) -> QueryArgs:
- query_args = {}
- query_args.update(filters)
- query_args.update(get_filter_conditions(filters))
- return query_args
-
-
-def run_query(query_args: QueryArgs) -> Data:
- return frappe.db.sql(
- """
- SELECT
- wo.name, wo.status, wo.production_item, wo.qty,
- wo.produced_qty, wo.process_loss_qty,
- (wo.produced_qty - wo.process_loss_qty) as actual_produced_qty,
- sum(se.total_incoming_value) as total_fg_value,
- sum(se.total_outgoing_value) as total_rm_value
- FROM
- `tabWork Order` wo INNER JOIN `tabStock Entry` se
- ON wo.name=se.work_order
- WHERE
- process_loss_qty > 0
- AND wo.company = %(company)s
- AND se.docstatus = 1
- AND se.posting_date BETWEEN %(from_date)s AND %(to_date)s
- {item_filter}
- {work_order_filter}
- GROUP BY
- se.work_order
- """.format(
- **query_args
- ),
- query_args,
- as_dict=1,
- )
-
-
def update_data_with_total_pl_value(data: Data) -> None:
for row in data:
value_per_unit_fg = row["total_fg_value"] / row["actual_produced_qty"]
row["total_pl_value"] = row["process_loss_qty"] * value_per_unit_fg
-
-
-def get_filter_conditions(filters: Filters) -> QueryArgs:
- filter_conditions = dict(item_filter="", work_order_filter="")
- if "item" in filters:
- production_item = filters.get("item")
- filter_conditions.update({"item_filter": f"AND wo.production_item='{production_item}'"})
- if "work_order" in filters:
- work_order_name = filters.get("work_order")
- filter_conditions.update({"work_order_filter": f"AND wo.name='{work_order_name}'"})
- return filter_conditions
diff --git a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py
index 140488820a5c..109d9ab656bc 100644
--- a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py
+++ b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py
@@ -4,42 +4,10 @@
import frappe
from frappe import _
+from pypika import Order
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
-# and bom_no is not null and bom_no !=''
-
-mapper = {
- "Sales Order": {
- "fields": """ item_code as production_item, item_name as production_item_name, stock_uom,
- stock_qty as qty_to_manufacture, `tabSales Order Item`.parent as name, bom_no, warehouse,
- `tabSales Order Item`.delivery_date, `tabSales Order`.base_grand_total """,
- "filters": """`tabSales Order Item`.docstatus = 1 and stock_qty > produced_qty
- and `tabSales Order`.per_delivered < 100.0""",
- },
- "Material Request": {
- "fields": """ item_code as production_item, item_name as production_item_name, stock_uom,
- stock_qty as qty_to_manufacture, `tabMaterial Request Item`.parent as name, bom_no, warehouse,
- `tabMaterial Request Item`.schedule_date """,
- "filters": """`tabMaterial Request`.docstatus = 1 and `tabMaterial Request`.per_ordered < 100
- and `tabMaterial Request`.material_request_type = 'Manufacture' """,
- },
- "Work Order": {
- "fields": """ production_item, item_name as production_item_name, planned_start_date,
- stock_uom, qty as qty_to_manufacture, name, bom_no, fg_warehouse as warehouse """,
- "filters": "docstatus = 1 and status not in ('Completed', 'Stopped')",
- },
-}
-
-order_mapper = {
- "Sales Order": {
- "Delivery Date": "`tabSales Order Item`.delivery_date asc",
- "Total Amount": "`tabSales Order`.base_grand_total desc",
- },
- "Material Request": {"Required Date": "`tabMaterial Request Item`.schedule_date asc"},
- "Work Order": {"Planned Start Date": "planned_start_date asc"},
-}
-
def execute(filters=None):
return ProductionPlanReport(filters).execute_report()
@@ -63,40 +31,81 @@ def execute_report(self):
return self.columns, self.data
def get_open_orders(self):
- doctype = (
- "`tabWork Order`"
- if self.filters.based_on == "Work Order"
- else "`tab{doc}`, `tab{doc} Item`".format(doc=self.filters.based_on)
- )
+ doctype, order_by = self.filters.based_on, self.filters.order_by
+
+ parent = frappe.qb.DocType(doctype)
+ query = None
+
+ if doctype == "Work Order":
+ query = (
+ frappe.qb.from_(parent)
+ .select(
+ parent.production_item,
+ parent.item_name.as_("production_item_name"),
+ parent.planned_start_date,
+ parent.stock_uom,
+ parent.qty.as_("qty_to_manufacture"),
+ parent.name,
+ parent.bom_no,
+ parent.fg_warehouse.as_("warehouse"),
+ )
+ .where(parent.status.notin(["Completed", "Stopped", "Closed"]))
+ )
- filters = mapper.get(self.filters.based_on)["filters"]
- filters = self.prepare_other_conditions(filters, self.filters.based_on)
- order_by = " ORDER BY %s" % (order_mapper[self.filters.based_on][self.filters.order_by])
-
- self.orders = frappe.db.sql(
- """ SELECT {fields} from {doctype}
- WHERE {filters} {order_by}""".format(
- doctype=doctype,
- filters=filters,
- order_by=order_by,
- fields=mapper.get(self.filters.based_on)["fields"],
- ),
- tuple(self.filters.docnames),
- as_dict=1,
- )
+ if order_by == "Planned Start Date":
+ query = query.orderby(parent.planned_start_date, order=Order.asc)
+
+ if self.filters.docnames:
+ query = query.where(parent.name.isin(self.filters.docnames))
+
+ else:
+ child = frappe.qb.DocType(f"{doctype} Item")
+ query = (
+ frappe.qb.from_(parent)
+ .from_(child)
+ .select(
+ child.bom_no,
+ child.stock_uom,
+ child.warehouse,
+ child.parent.as_("name"),
+ child.item_code.as_("production_item"),
+ child.stock_qty.as_("qty_to_manufacture"),
+ child.item_name.as_("production_item_name"),
+ )
+ .where(parent.name == child.parent)
+ )
+
+ if self.filters.docnames:
+ query = query.where(child.parent.isin(self.filters.docnames))
- def prepare_other_conditions(self, filters, doctype):
- if self.filters.docnames:
- field = "name" if doctype == "Work Order" else "`tab{} Item`.parent".format(doctype)
- filters += " and %s in (%s)" % (field, ",".join(["%s"] * len(self.filters.docnames)))
+ if doctype == "Sales Order":
+ query = query.select(child.delivery_date, parent.base_grand_total,).where(
+ (child.stock_qty > child.produced_qty)
+ & (parent.per_delivered < 100.0)
+ & (parent.status.notin(["Completed", "Closed"]))
+ )
+
+ if order_by == "Delivery Date":
+ query = query.orderby(child.delivery_date, order=Order.asc)
+ elif order_by == "Total Amount":
+ query = query.orderby(parent.base_grand_total, order=Order.desc)
+
+ elif doctype == "Material Request":
+ query = query.select(child.schedule_date,).where(
+ (parent.per_ordered < 100)
+ & (parent.material_request_type == "Manufacture")
+ & (parent.status != "Stopped")
+ )
+
+ if order_by == "Required Date":
+ query = query.orderby(child.schedule_date, order=Order.asc)
- if doctype != "Work Order":
- filters += " and `tab{doc}`.name = `tab{doc} Item`.parent".format(doc=doctype)
+ query = query.where(parent.docstatus == 1)
if self.filters.company:
- filters += " and `tab%s`.company = %s" % (doctype, frappe.db.escape(self.filters.company))
+ query = query.where(parent.company == self.filters.company)
- return filters
+ self.orders = query.run(as_dict=True)
def get_raw_materials(self):
if not self.orders:
@@ -134,29 +143,29 @@ def get_raw_materials(self):
bom_nos.append(bom_no)
- bom_doctype = (
+ bom_item_doctype = (
"BOM Explosion Item" if self.filters.include_subassembly_raw_materials else "BOM Item"
)
- qty_field = (
- "qty_consumed_per_unit"
- if self.filters.include_subassembly_raw_materials
- else "(bom_item.qty / bom.quantity)"
- )
+ bom = frappe.qb.DocType("BOM")
+ bom_item = frappe.qb.DocType(bom_item_doctype)
- raw_materials = frappe.db.sql(
- """ SELECT bom_item.parent, bom_item.item_code,
- bom_item.item_name as raw_material_name, {0} as required_qty_per_unit
- FROM
- `tabBOM` as bom, `tab{1}` as bom_item
- WHERE
- bom_item.parent in ({2}) and bom_item.parent = bom.name and bom.docstatus = 1
- """.format(
- qty_field, bom_doctype, ",".join(["%s"] * len(bom_nos))
- ),
- tuple(bom_nos),
- as_dict=1,
- )
+ if self.filters.include_subassembly_raw_materials:
+ qty_field = bom_item.qty_consumed_per_unit
+ else:
+ qty_field = bom_item.qty / bom.quantity
+
+ raw_materials = (
+ frappe.qb.from_(bom)
+ .from_(bom_item)
+ .select(
+ bom_item.parent,
+ bom_item.item_code,
+ bom_item.item_name.as_("raw_material_name"),
+ qty_field.as_("required_qty_per_unit"),
+ )
+ .where((bom_item.parent.isin(bom_nos)) & (bom_item.parent == bom.name) & (bom.docstatus == 1))
+ ).run(as_dict=True)
if not raw_materials:
return
diff --git a/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py b/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py
index c6b7e58d656f..d2e3e4081c2f 100644
--- a/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py
+++ b/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py
@@ -3,6 +3,7 @@
import frappe
+from frappe.query_builder.functions import IfNull
from frappe.utils import cint
@@ -16,70 +17,70 @@ def execute(filters=None):
def get_item_list(wo_list, filters):
out = []
- # Add a row for each item/qty
- for wo_details in wo_list:
- desc = frappe.db.get_value("BOM", wo_details.bom_no, "description")
-
- for wo_item_details in frappe.db.get_values(
- "Work Order Item", {"parent": wo_details.name}, ["item_code", "source_warehouse"], as_dict=1
- ):
-
- item_list = frappe.db.sql(
- """SELECT
- bom_item.item_code as item_code,
- ifnull(ledger.actual_qty*bom.quantity/bom_item.stock_qty,0) as build_qty
- FROM
- `tabBOM` as bom, `tabBOM Item` AS bom_item
- LEFT JOIN `tabBin` AS ledger
- ON bom_item.item_code = ledger.item_code
- AND ledger.warehouse = ifnull(%(warehouse)s,%(filterhouse)s)
- WHERE
- bom.name = bom_item.parent
- and bom_item.item_code = %(item_code)s
- and bom.name = %(bom)s
- GROUP BY
- bom_item.item_code""",
- {
- "bom": wo_details.bom_no,
- "warehouse": wo_item_details.source_warehouse,
- "filterhouse": filters.warehouse,
- "item_code": wo_item_details.item_code,
- },
- as_dict=1,
- )
-
- stock_qty = 0
- count = 0
- buildable_qty = wo_details.qty
- for item in item_list:
- count = count + 1
- if item.build_qty >= (wo_details.qty - wo_details.produced_qty):
- stock_qty = stock_qty + 1
- elif buildable_qty >= item.build_qty:
- buildable_qty = item.build_qty
-
- if count == stock_qty:
- build = "Y"
- else:
- build = "N"
-
- row = frappe._dict(
- {
- "work_order": wo_details.name,
- "status": wo_details.status,
- "req_items": cint(count),
- "instock": stock_qty,
- "description": desc,
- "source_warehouse": wo_item_details.source_warehouse,
- "item_code": wo_item_details.item_code,
- "bom_no": wo_details.bom_no,
- "qty": wo_details.qty,
- "buildable_qty": buildable_qty,
- "ready_to_build": build,
- }
- )
-
- out.append(row)
+ if wo_list:
+ bin = frappe.qb.DocType("Bin")
+ bom = frappe.qb.DocType("BOM")
+ bom_item = frappe.qb.DocType("BOM Item")
+
+ # Add a row for each item/qty
+ for wo_details in wo_list:
+ desc = frappe.db.get_value("BOM", wo_details.bom_no, "description")
+
+ for wo_item_details in frappe.db.get_values(
+ "Work Order Item", {"parent": wo_details.name}, ["item_code", "source_warehouse"], as_dict=1
+ ):
+ item_list = (
+ frappe.qb.from_(bom)
+ .from_(bom_item)
+ .left_join(bin)
+ .on(
+ (bom_item.item_code == bin.item_code)
+ & (bin.warehouse == IfNull(wo_item_details.source_warehouse, filters.warehouse))
+ )
+ .select(
+ bom_item.item_code.as_("item_code"),
+ IfNull(bin.actual_qty * bom.quantity / bom_item.stock_qty, 0).as_("build_qty"),
+ )
+ .where(
+ (bom.name == bom_item.parent)
+ & (bom_item.item_code == wo_item_details.item_code)
+ & (bom.name == wo_details.bom_no)
+ )
+ .groupby(bom_item.item_code)
+ ).run(as_dict=1)
+
+ stock_qty = 0
+ count = 0
+ buildable_qty = wo_details.qty
+ for item in item_list:
+ count = count + 1
+ if item.build_qty >= (wo_details.qty - wo_details.produced_qty):
+ stock_qty = stock_qty + 1
+ elif buildable_qty >= item.build_qty:
+ buildable_qty = item.build_qty
+
+ if count == stock_qty:
+ build = "Y"
+ else:
+ build = "N"
+
+ row = frappe._dict(
+ {
+ "work_order": wo_details.name,
+ "status": wo_details.status,
+ "req_items": cint(count),
+ "instock": stock_qty,
+ "description": desc,
+ "source_warehouse": wo_item_details.source_warehouse,
+ "item_code": wo_item_details.item_code,
+ "bom_no": wo_details.bom_no,
+ "qty": wo_details.qty,
+ "buildable_qty": buildable_qty,
+ "ready_to_build": build,
+ }
+ )
+
+ out.append(row)
return out
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 2368bfdf2c6f..08a7e0ccd394 100644
--- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py
+++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py
@@ -39,10 +39,14 @@ def get_data(filters):
"lead_time",
]
- for field in ["sales_order", "production_item", "status", "company"]:
+ for field in ["sales_order", "production_item"]:
if filters.get(field):
query_filters[field] = ("in", filters.get(field))
+ for field in ["status", "company"]:
+ if filters.get(field):
+ query_filters[field] = filters.get(field)
+
query_filters["planned_start_date"] = (">=", filters.get("from_date"))
query_filters["planned_end_date"] = ("<=", filters.get("to_date"))
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 2e3a8e042c22..a35c21757886 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -374,3 +374,5 @@ erpnext.patches.v13_0.fix_number_and_frequency_for_monthly_depreciation
erpnext.patches.v13_0.reset_corrupt_defaults
erpnext.patches.v13_0.show_hr_payroll_deprecation_warning
erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair
+execute:frappe.db.set_value("Naming Series", "Naming Series", {"select_doc_for_series": "", "set_options": "", "prefix": "", "current_value": 0, "user_must_always_select": 0})
+erpnext.patches.v13_0.update_schedule_type_in_loans
\ No newline at end of file
diff --git a/erpnext/patches/v11_0/create_salary_structure_assignments.py b/erpnext/patches/v11_0/create_salary_structure_assignments.py
index b81e867b9dd6..51b2a2cc0b1e 100644
--- a/erpnext/patches/v11_0/create_salary_structure_assignments.py
+++ b/erpnext/patches/v11_0/create_salary_structure_assignments.py
@@ -13,8 +13,10 @@
def execute():
+ frappe.reload_doc("Payroll", "doctype", "Payroll Settings")
frappe.reload_doc("Payroll", "doctype", "Salary Structure")
frappe.reload_doc("Payroll", "doctype", "Salary Structure Assignment")
+
frappe.db.sql(
"""
delete from `tabSalary Structure Assignment`
diff --git a/erpnext/patches/v13_0/update_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py
index a1d40b739eba..0bd3fcdec4c5 100644
--- a/erpnext/patches/v13_0/update_old_loans.py
+++ b/erpnext/patches/v13_0/update_old_loans.py
@@ -100,6 +100,7 @@ def execute():
"mode_of_payment": loan.mode_of_payment,
"loan_account": loan.loan_account,
"payment_account": loan.payment_account,
+ "disbursement_account": loan.payment_account,
"interest_income_account": loan.interest_income_account,
"penalty_income_account": loan.penalty_income_account,
},
@@ -190,6 +191,7 @@ def create_loan_type(loan, loan_type_name, penalty_account):
loan_type_doc.company = loan.company
loan_type_doc.mode_of_payment = loan.mode_of_payment
loan_type_doc.payment_account = loan.payment_account
+ loan_type_doc.disbursement_account = loan.payment_account
loan_type_doc.loan_account = loan.loan_account
loan_type_doc.interest_income_account = loan.interest_income_account
loan_type_doc.penalty_income_account = penalty_account
diff --git a/erpnext/patches/v13_0/update_schedule_type_in_loans.py b/erpnext/patches/v13_0/update_schedule_type_in_loans.py
new file mode 100644
index 000000000000..0cff5c5fee91
--- /dev/null
+++ b/erpnext/patches/v13_0/update_schedule_type_in_loans.py
@@ -0,0 +1,17 @@
+import frappe
+
+
+def execute():
+ frappe.reload_doc("loan_management", "doctype", "loan")
+ frappe.reload_doc("loan_management", "doctype", "loan_type")
+
+ loan = frappe.qb.DocType("Loan")
+ loan_type = frappe.qb.DocType("Loan Type")
+
+ frappe.qb.update(loan_type).set(
+ loan_type.repayment_schedule_type, "Monthly as per repayment start date"
+ ).where(loan_type.is_term_loan == 1).run()
+
+ frappe.qb.update(loan).set(
+ loan.repayment_schedule_type, "Monthly as per repayment start date"
+ ).where(loan.is_term_loan == 1).run()
diff --git a/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.py
index de8f9b6a7ade..f2b86b3ff37d 100644
--- a/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.py
+++ b/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.py
@@ -9,7 +9,7 @@
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
-from erpnext.hr.doctype.leave_application.test_leave_application import get_first_sunday
+from erpnext.hr.tests.test_utils import get_first_sunday
from erpnext.hr.utils import get_holiday_dates_for_employee
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import (
calculate_lwp,
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
index 5f2af74dca68..e6b1bfac4a91 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
@@ -346,6 +346,8 @@ def make_accrual_jv_entry(self):
"credit_in_account_currency": flt(payable_amt, precision),
"exchange_rate": flt(exchange_rate),
"cost_center": self.cost_center,
+ "reference_type": self.doctype,
+ "reference_name": self.name,
},
accounting_dimensions,
)
@@ -720,12 +722,21 @@ def get_month_details(year, month):
def get_payroll_entry_bank_entries(payroll_entry_name):
journal_entries = frappe.db.sql(
- "select name from `tabJournal Entry Account` "
- 'where reference_type="Payroll Entry" '
- "and reference_name=%s and docstatus=1",
+ """
+ select
+ je.name
+ from
+ `tabJournal Entry` je,
+ `tabJournal Entry Account` jea
+ where
+ je.name = jea.parent
+ and je.voucher_type = 'Bank Entry'
+ and jea.reference_type = 'Payroll Entry'
+ and jea.reference_name = %s
+ """,
payroll_entry_name,
- as_dict=1,
- )
+ as_dict=True,
+ ) # nosemgrep
return journal_entries
diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
index 35cea017e8c9..21dab42b80f1 100644
--- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
@@ -133,9 +133,17 @@ def test_multi_currency_payroll_entry(self): # pylint: disable=no-self-use
payment_entry = frappe.db.sql(
"""
- Select ifnull(sum(je.total_debit),0) as total_debit, ifnull(sum(je.total_credit),0) as total_credit from `tabJournal Entry` je, `tabJournal Entry Account` jea
- Where je.name = jea.parent
- And jea.reference_name = %s
+ select
+ ifnull(sum(je.total_debit),0) as total_debit,
+ ifnull(sum(je.total_credit),0) as total_credit
+ from
+ `tabJournal Entry` je,
+ `tabJournal Entry Account` jea
+ Where
+ je.name = jea.parent
+ and je.voucher_type = 'Bank Entry'
+ and jea.reference_type = 'Payroll Entry'
+ and jea.reference_name = %s
""",
(payroll_entry.name),
as_dict=1,
@@ -295,6 +303,7 @@ def test_loan(self):
loan_account="Loan Account - _TC",
interest_income_account="Interest Income Account - _TC",
penalty_income_account="Penalty Income Account - _TC",
+ repayment_schedule_type="Monthly as per repayment start date",
)
loan = create_loan(
diff --git a/erpnext/payroll/doctype/payroll_settings/payroll_settings.json b/erpnext/payroll/doctype/payroll_settings/payroll_settings.json
index 54377e94b30f..f4db6f099a63 100644
--- a/erpnext/payroll/doctype/payroll_settings/payroll_settings.json
+++ b/erpnext/payroll/doctype/payroll_settings/payroll_settings.json
@@ -11,6 +11,7 @@
"max_working_hours_against_timesheet",
"include_holidays_in_total_working_days",
"disable_rounded_total",
+ "define_opening_balance_for_earning_and_deductions",
"column_break_11",
"daily_wages_fraction_for_half_day",
"email_salary_slip_to_employee",
@@ -91,13 +92,20 @@
"fieldname": "show_leave_balances_in_salary_slip",
"fieldtype": "Check",
"label": "Show Leave Balances in Salary Slip"
+ },
+ {
+ "default": "0",
+ "description": "If checked, then the system will enable the provision to set the opening balance for earnings and deductions till date while creating a Salary Structure Assignment (if any)",
+ "fieldname": "define_opening_balance_for_earning_and_deductions",
+ "fieldtype": "Check",
+ "label": "Define Opening Balance for Earning and Deductions"
}
],
"icon": "fa fa-cog",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-03-03 17:49:59.579723",
+ "modified": "2022-12-21 17:30:08.704247",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Payroll Settings",
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index 14a2a3ae398d..0563541eaa5a 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -587,7 +587,7 @@ def calculate_net_pay(self):
if self.salary_structure:
self.calculate_component_amounts("deductions")
- self.set_loan_repayment()
+ self.add_applicable_loans()
self.set_precision_for_component_amounts()
self.set_net_pay()
@@ -632,9 +632,19 @@ def add_structure_components(self, component_type):
continue
amount = self.eval_condition_and_formula(struct_row, data)
- if (
- amount or (struct_row.amount_based_on_formula and amount is not None)
- ) and struct_row.statistical_component == 0:
+
+ if struct_row.statistical_component:
+ # update statitical component amount in reference data based on payment days
+ # since row for statistical component is not added to salary slip
+ if struct_row.depends_on_payment_days:
+ joining_date, relieving_date = self.get_joining_and_relieving_dates()
+ default_data[struct_row.abbr] = amount
+ data[struct_row.abbr] = flt(
+ (flt(amount) * flt(self.payment_days) / cint(self.total_working_days)),
+ struct_row.precision("amount"),
+ )
+
+ elif amount or struct_row.amount_based_on_formula and amount is not None:
default_amount = self.eval_condition_and_formula(struct_row, default_data)
self.update_component_row(
struct_row, amount, component_type, data=data, default_amount=default_amount
@@ -687,8 +697,8 @@ def get_data_for_eval(self):
for key in ("earnings", "deductions"):
for d in self.get(key):
- default_data[d.abbr] = d.default_amount
- data[d.abbr] = d.amount
+ default_data[d.abbr] = d.default_amount or 0
+ data[d.abbr] = d.amount or 0
return data, default_data
@@ -1053,7 +1063,25 @@ def get_taxable_earnings_for_prev_period(self, start_date, end_date, allow_tax_e
)
exempted_amount = flt(exempted_amount[0][0]) if exempted_amount else 0
- return taxable_earnings - exempted_amount
+ opening_taxable_earning = self.get_opening_balance_for(
+ "taxable_earnings_till_date", start_date, end_date
+ )
+
+ return (taxable_earnings + opening_taxable_earning) - exempted_amount
+
+ def get_opening_balance_for(self, field_to_select, start_date, end_date):
+ opening_balance = frappe.db.get_all(
+ "Salary Structure Assignment",
+ {
+ "employee": self.employee,
+ "salary_structure": self.salary_structure,
+ "from_date": ["between", (start_date, end_date)],
+ "docstatus": 1,
+ },
+ field_to_select,
+ )
+
+ return opening_balance[0].get(field_to_select) if opening_balance else 0.0
def get_tax_paid_in_period(self, start_date, end_date, tax_component):
# find total_tax_paid, tax paid for benefit, additional_salary
@@ -1082,7 +1110,11 @@ def get_tax_paid_in_period(self, start_date, end_date, tax_component):
)[0][0]
)
- return total_tax_paid
+ tax_deducted_till_date = self.get_opening_balance_for(
+ "tax_deducted_till_date", start_date, end_date
+ )
+
+ return total_tax_paid + tax_deducted_till_date
def get_taxable_earnings(
self, allow_tax_exemption=False, based_on_payment_days=0, payroll_period=None
@@ -1370,44 +1402,47 @@ def get_joining_and_relieving_dates(self):
return joining_date, relieving_date
- def set_loan_repayment(self):
+ def add_applicable_loans(self):
self.total_loan_repayment = 0
self.total_interest_amount = 0
self.total_principal_amount = 0
- self.set("loans", [])
- for loan in self.get_loan_details():
- amounts = calculate_amounts(loan.name, self.posting_date, "Regular Payment")
-
- if (amounts["interest_amount"] or amounts["payable_principal_amount"]) and (
- amounts["payable_principal_amount"] + amounts["interest_amount"]
- > amounts["written_off_amount"]
- ):
-
- if amounts["interest_amount"] > amounts["written_off_amount"]:
- amounts["interest_amount"] -= amounts["written_off_amount"]
- amounts["written_off_amount"] = 0
- else:
- amounts["written_off_amount"] -= amounts["interest_amount"]
- amounts["interest_amount"] = 0
-
- if amounts["payable_principal_amount"] > amounts["written_off_amount"]:
- amounts["payable_principal_amount"] -= amounts["written_off_amount"]
- amounts["written_off_amount"] = 0
- else:
- amounts["written_off_amount"] -= amounts["payable_principal_amount"]
- amounts["payable_principal_amount"] = 0
+ loans = [d.loan for d in self.get("loans")]
- self.append(
- "loans",
- {
- "loan": loan.name,
- "interest_amount": amounts["interest_amount"],
- "principal_amount": amounts["payable_principal_amount"],
- "loan_account": loan.loan_account,
- "interest_income_account": loan.interest_income_account,
- },
- )
+ for loan in self.get_loan_details():
+ if loan.name not in loans:
+ amounts = calculate_amounts(loan.name, self.posting_date, "Regular Payment")
+ if (
+ amounts["interest_amount"] + amounts["payable_principal_amount"]
+ > amounts["written_off_amount"]
+ ):
+ if amounts["interest_amount"] > amounts["written_off_amount"]:
+ amounts["interest_amount"] -= amounts["written_off_amount"]
+ amounts["written_off_amount"] = 0
+ else:
+ amounts["written_off_amount"] -= amounts["interest_amount"]
+ amounts["interest_amount"] = 0
+
+ if amounts["payable_principal_amount"] > amounts["written_off_amount"]:
+ amounts["payable_principal_amount"] -= amounts["written_off_amount"]
+ amounts["written_off_amount"] = 0
+ else:
+ amounts["written_off_amount"] -= amounts["payable_principal_amount"]
+ amounts["payable_principal_amount"] = 0
+
+ self.append(
+ "loans",
+ {
+ "loan": loan.name,
+ "interest_amount": amounts["interest_amount"],
+ "principal_amount": amounts["payable_principal_amount"],
+ "total_payment": amounts["interest_amount"] + amounts["payable_principal_amount"]
+ if not loan.manually_update_paid_amount_in_salary_slip
+ else 0,
+ "loan_account": loan.loan_account,
+ "interest_income_account": loan.interest_income_account,
+ },
+ )
for payment in self.get("loans"):
amounts = calculate_amounts(payment.loan, self.posting_date, "Regular Payment")
@@ -1432,7 +1467,14 @@ def set_loan_repayment(self):
def get_loan_details(self):
loan_details = frappe.get_all(
"Loan",
- fields=["name", "interest_income_account", "loan_account", "loan_type", "is_term_loan"],
+ fields=[
+ "name",
+ "interest_income_account",
+ "loan_account",
+ "loan_type",
+ "is_term_loan",
+ "manually_update_paid_amount_in_salary_slip",
+ ],
filters={
"applicant": self.employee,
"docstatus": 1,
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index e33f8cce4c4c..32d0c7ed08fa 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -27,6 +27,7 @@
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
+from erpnext.hr.tests.test_utils import get_first_sunday
from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import (
create_exemption_category,
create_payroll_period,
@@ -55,18 +56,7 @@ def test_payment_days_based_on_attendance(self):
frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0)
- month_start_date = get_first_day(nowdate())
- month_end_date = get_last_day(nowdate())
-
- first_sunday = frappe.db.sql(
- """
- select holiday_date from `tabHoliday`
- where parent = 'Salary Slip Test Holiday List'
- and holiday_date between %s and %s
- order by holiday_date
- """,
- (month_start_date, month_end_date),
- )[0][0]
+ first_sunday = get_first_sunday()
mark_attendance(emp_id, first_sunday, "Absent", ignore_validate=True) # invalid lwp
mark_attendance(
@@ -273,19 +263,7 @@ def test_payment_days_based_on_leave_application(self):
frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0)
- month_start_date = get_first_day(nowdate())
- month_end_date = get_last_day(nowdate())
-
- first_sunday = frappe.db.sql(
- """
- select holiday_date from `tabHoliday`
- where parent = 'Salary Slip Test Holiday List'
- and holiday_date between %s and %s
- order by holiday_date
- """,
- (month_start_date, month_end_date),
- )[0][0]
-
+ first_sunday = get_first_sunday()
make_leave_application(emp_id, first_sunday, add_days(first_sunday, 3), "Leave Without Pay")
leave_type_ppl = create_leave_type(leave_type_name="Test Partially Paid Leave", is_ppl=1)
@@ -338,19 +316,7 @@ def test_payment_days_in_salary_slip_based_on_timesheet(self):
frappe.db.set_value("Employee", emp, {"relieving_date": None, "status": "Active"})
# mark attendance
- month_start_date = get_first_day(nowdate())
- month_end_date = get_last_day(nowdate())
-
- first_sunday = frappe.db.sql(
- """
- select holiday_date from `tabHoliday`
- where parent = 'Salary Slip Test Holiday List'
- and holiday_date between %s and %s
- order by holiday_date
- """,
- (month_start_date, month_end_date),
- )[0][0]
-
+ first_sunday = get_first_sunday()
mark_attendance(
emp, add_days(first_sunday, 1), "Absent", ignore_validate=True
) # counted as absent
@@ -359,8 +325,8 @@ def test_payment_days_in_salary_slip_based_on_timesheet(self):
make_salary_structure_for_timesheet(emp)
timesheet = make_timesheet(emp, simulate=True, is_billable=1)
salary_slip = make_salary_slip_for_timesheet(timesheet.name)
- salary_slip.start_date = month_start_date
- salary_slip.end_date = month_end_date
+ salary_slip.start_date = get_first_day(nowdate())
+ salary_slip.end_date = get_last_day(nowdate())
salary_slip.save()
salary_slip.submit()
salary_slip.reload()
@@ -402,18 +368,7 @@ def test_component_amount_dependent_on_another_payment_days_based_component(self
)
# mark employee absent for a day since this case works fine if payment days are equal to working days
- month_start_date = get_first_day(nowdate())
- month_end_date = get_last_day(nowdate())
-
- first_sunday = frappe.db.sql(
- """
- select holiday_date from `tabHoliday`
- where parent = 'Salary Slip Test Holiday List'
- and holiday_date between %s and %s
- order by holiday_date
- """,
- (month_start_date, month_end_date),
- )[0][0]
+ first_sunday = get_first_sunday()
mark_attendance(
employee, add_days(first_sunday, 1), "Absent", ignore_validate=True
@@ -1032,6 +987,42 @@ def test_do_not_show_statistical_component_in_slip(self):
components = [row.salary_component for row in new_ss.get("earnings")]
self.assertNotIn("Statistical Component", components)
+ @change_settings(
+ "Payroll Settings",
+ {"payroll_based_on": "Attendance", "consider_unmarked_attendance_as": "Present"},
+ )
+ def test_statistical_component_based_on_payment_days(self):
+ """
+ Tests whether component using statistical component in the formula
+ gets the updated value based on payment days
+ """
+ from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
+ create_salary_structure_assignment,
+ )
+
+ emp = make_employee("test_statistical_component@salary.com")
+ first_sunday = get_first_sunday()
+ mark_attendance(emp, add_days(first_sunday, 1), "Absent", ignore_validate=True)
+ salary_structure = make_salary_structure_for_payment_days_based_component_dependency(
+ test_statistical_comp=True
+ )
+ create_salary_structure_assignment(
+ emp, salary_structure.name, company="_Test Company", currency="INR"
+ )
+ # make salary slip and assert payment days
+ ss = make_salary_slip_for_payment_days_dependency_test(
+ "test_statistical_component@salary.com", salary_structure.name
+ )
+
+ amount = precision = None
+ for entry in ss.earnings:
+ if entry.salary_component == "Dependency Component":
+ amount = entry.amount
+ precision = entry.precision("amount")
+ break
+
+ self.assertEqual(amount, flt((1000 * ss.payment_days / ss.total_working_days) * 0.5, precision))
+
def make_activity_for_employee(self):
activity_type = frappe.get_doc("Activity Type", "_Test Activity Type")
activity_type.billing_rate = 50
@@ -1039,6 +1030,104 @@ def make_activity_for_employee(self):
activity_type.wage_rate = 25
activity_type.save()
+ def test_salary_slip_generation_against_opening_entries_in_ssa(self):
+ import math
+
+ from erpnext.payroll.doctype.payroll_period.payroll_period import get_period_factor
+ from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
+
+ payroll_period = frappe.db.get_value(
+ "Payroll Period",
+ {
+ "company": "_Test Company",
+ "start_date": ["<=", "2023-03-31"],
+ "end_date": [">=", "2022-04-01"],
+ },
+ "name",
+ )
+
+ if not payroll_period:
+ payroll_period = create_payroll_period(
+ name="_Test Payroll Period for Tax",
+ company="_Test Company",
+ start_date="2022-04-01",
+ end_date="2023-03-31",
+ )
+ else:
+ payroll_period = frappe.get_cached_doc("Payroll Period", payroll_period)
+
+ emp = make_employee(
+ "test_employee_ss_with_opening_balance@salary.com",
+ company="_Test Company",
+ **{"date_of_joining": "2021-12-01"},
+ )
+ employee_doc = frappe.get_doc("Employee", emp)
+
+ create_tax_slab(payroll_period, allow_tax_exemption=True)
+
+ salary_structure_name = "Test Salary Structure for Opening Balance"
+ if not frappe.db.exists("Salary Structure", salary_structure_name):
+ salary_structure_doc = make_salary_structure(
+ salary_structure_name,
+ "Monthly",
+ company="_Test Company",
+ employee=emp,
+ from_date="2022-04-01",
+ payroll_period=payroll_period,
+ test_tax=True,
+ )
+
+ # validate no salary slip exists for the employee
+ self.assertTrue(
+ frappe.db.count(
+ "Salary Slip",
+ {
+ "employee": emp,
+ "salary_structure": salary_structure_doc.name,
+ "docstatus": 1,
+ "start_date": [">=", "2022-04-01"],
+ },
+ )
+ == 0
+ )
+
+ remaining_sub_periods = get_period_factor(
+ emp,
+ get_first_day("2022-10-01"),
+ get_last_day("2022-10-01"),
+ "Monthly",
+ payroll_period,
+ depends_on_payment_days=0,
+ )[1]
+
+ prev_period = math.ceil(remaining_sub_periods)
+
+ annual_tax = 93036 # 89220 #data[0].get('applicable_tax')
+ monthly_tax_amount = 7732.40 # 7435 #annual_tax/12
+ annual_earnings = 933600 # data[0].get('ctc')
+ monthly_earnings = 77800 # annual_earnings/12
+
+ # Get Salary Structure Assignment
+ ssa = frappe.get_value(
+ "Salary Structure Assignment",
+ {"employee": emp, "salary_structure": salary_structure_doc.name},
+ "name",
+ )
+ ssa_doc = frappe.get_doc("Salary Structure Assignment", ssa)
+
+ # Set opening balance for earning and tax deduction in Salary Structure Assignment
+ ssa_doc.taxable_earnings_till_date = monthly_earnings * prev_period
+ ssa_doc.tax_deducted_till_date = monthly_tax_amount * prev_period
+ ssa_doc.save()
+
+ # Create Salary Slip
+ salary_slip = make_salary_slip(
+ salary_structure_doc.name, employee=employee_doc.name, posting_date=getdate("2022-10-01")
+ )
+ for deduction in salary_slip.deductions:
+ if deduction.salary_component == "TDS":
+ self.assertEqual(deduction.amount, rounded(monthly_tax_amount))
+
def get_no_of_days():
no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, getdate(nowdate()).month)
@@ -1151,7 +1240,11 @@ def create_account(account_name, company, parent_account, account_type=None):
def make_earning_salary_component(
- setup=False, test_tax=False, company_list=None, include_flexi_benefits=False
+ setup=False,
+ test_tax=False,
+ company_list=None,
+ include_flexi_benefits=False,
+ test_statistical_comp=False,
):
data = [
{
@@ -1517,7 +1610,7 @@ def make_holiday_list(list_name=None, from_date=None, to_date=None):
return holiday_list
-def make_salary_structure_for_payment_days_based_component_dependency():
+def make_salary_structure_for_payment_days_based_component_dependency(test_statistical_comp=False):
earnings = [
{
"salary_component": "Basic Salary - Payment Days",
@@ -1535,6 +1628,27 @@ def make_salary_structure_for_payment_days_based_component_dependency():
"formula": "base * 0.20",
},
]
+ if test_statistical_comp:
+ earnings.extend(
+ [
+ {
+ "salary_component": "Statistical Component",
+ "abbr": "SC",
+ "type": "Earning",
+ "statistical_component": 1,
+ "amount": 1000,
+ "depends_on_payment_days": 1,
+ },
+ {
+ "salary_component": "Dependency Component",
+ "abbr": "DC",
+ "type": "Earning",
+ "amount_based_on_formula": 1,
+ "formula": "SC * 0.5",
+ "depends_on_payment_days": 0,
+ },
+ ]
+ )
make_salary_component(earnings, False, company_list=["_Test Company"])
diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js
index 6cd897e95d17..7cb573d6307e 100644
--- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js
+++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js
@@ -42,6 +42,13 @@ frappe.ui.form.on('Salary Structure Assignment', {
});
},
+ refresh: function(frm) {
+ if(frm.doc.__onload){
+ frm.unhide_earnings_and_taxation_section = frm.doc.__onload.earning_and_deduction_entries_does_not_exists;
+ frm.trigger("set_earnings_and_taxation_section_visibility");
+ }
+ },
+
employee: function(frm) {
if(frm.doc.employee){
frappe.call({
@@ -59,6 +66,8 @@ frappe.ui.form.on('Salary Structure Assignment', {
}
}
});
+
+ frm.trigger("valiadte_joining_date_and_salary_slips");
}
else{
frm.set_value("company", null);
@@ -71,5 +80,33 @@ frappe.ui.form.on('Salary Structure Assignment', {
frm.set_value("payroll_payable_account", r.default_payroll_payable_account);
});
}
- }
+ },
+
+ valiadte_joining_date_and_salary_slips: function(frm) {
+ frappe.call({
+ method: "earning_and_deduction_entries_does_not_exists",
+ doc: frm.doc,
+ callback: function(data) {
+ let earning_and_deduction_entries_does_not_exists = data.message;
+ frm.unhide_earnings_and_taxation_section = earning_and_deduction_entries_does_not_exists;
+ frm.trigger("set_earnings_and_taxation_section_visibility");
+ }
+ });
+ },
+
+ set_earnings_and_taxation_section_visibility: function(frm) {
+ if(frm.unhide_earnings_and_taxation_section){
+ frm.set_df_property('earnings_and_taxation_section', 'hidden', 0);
+ }
+ else{
+ frm.set_df_property('earnings_and_taxation_section', 'hidden', 1);
+ }
+ },
+
+ from_date: function(frm) {
+ if (frm.doc.from_date) {
+ frm.trigger("valiadte_joining_date_and_salary_slips" );
+ }
+ },
+
});
diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json
index c8b98e5aafc9..4db023c6d519 100644
--- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json
+++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json
@@ -22,6 +22,10 @@
"base",
"column_break_9",
"variable",
+ "earnings_and_taxation_section",
+ "taxable_earnings_till_date",
+ "column_break_18",
+ "tax_deducted_till_date",
"amended_from"
],
"fields": [
@@ -141,11 +145,31 @@
"fieldtype": "Link",
"label": "Payroll Payable Account",
"options": "Account"
+ },
+ {
+ "fieldname": "earnings_and_taxation_section",
+ "fieldtype": "Section Break"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "tax_deducted_till_date",
+ "fieldtype": "Currency",
+ "label": "Tax Deducted Till Date"
+ },
+ {
+ "fieldname": "column_break_18",
+ "fieldtype": "Column Break"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "taxable_earnings_till_date",
+ "fieldtype": "Currency",
+ "label": "Taxable Earnings Till Date"
}
],
"is_submittable": 1,
"links": [],
- "modified": "2021-03-31 22:44:46.267974",
+ "modified": "2022-12-26 12:47:42.521891",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Structure Assignment",
diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py
index e34e48e6c055..21196c3e52d2 100644
--- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py
+++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py
@@ -13,10 +13,32 @@ class DuplicateAssignment(frappe.ValidationError):
class SalaryStructureAssignment(Document):
+ def onload(self):
+ if self.employee:
+ self.set_onload(
+ "earning_and_deduction_entries_exists", self.earning_and_deduction_entries_does_not_exists()
+ )
+
def validate(self):
self.validate_dates()
self.validate_income_tax_slab()
self.set_payroll_payable_account()
+ self.valiadte_missing_taxable_earnings_and_deductions_till_date()
+
+ def valiadte_missing_taxable_earnings_and_deductions_till_date(self):
+ if self.earning_and_deduction_entries_does_not_exists():
+ if not self.taxable_earnings_till_date and not self.tax_deducted_till_date:
+ frappe.msgprint(
+ _(
+ """Not found any salary slip record(s) for the employee {0}. Please specify {1} and {2} (if any), for the correct tax calculation in future salary slips."""
+ ).format(
+ self.employee,
+ " " + _("Taxable Earnings Till Date") + "",
+ " " + _("Tax Deducted Till Date") + "",
+ ),
+ indicator="orange",
+ title=_("Warning"),
+ )
def validate_dates(self):
joining_date, relieving_date = frappe.db.get_value(
@@ -76,6 +98,56 @@ def set_payroll_payable_account(self):
)
self.payroll_payable_account = payroll_payable_account
+ @frappe.whitelist()
+ def earning_and_deduction_entries_does_not_exists(self):
+ if self.enabled_settings_to_specify_earnings_and_deductions_till_date():
+ if not self.joined_in_the_same_month() and not self.have_salary_slips():
+ return True
+ else:
+ if self.docstatus in [1, 2] and (
+ self.taxable_earnings_till_date or self.tax_deducted_till_date
+ ):
+ return True
+ return False
+ else:
+ return False
+
+ def enabled_settings_to_specify_earnings_and_deductions_till_date(self):
+ """returns True if settings are enabled to specify earnings and deductions till date else False"""
+
+ if frappe.db.get_single_value(
+ "Payroll Settings", "define_opening_balance_for_earning_and_deductions"
+ ):
+ return True
+ return False
+
+ def have_salary_slips(self):
+ """returns True if salary structure assignment has salary slips else False"""
+
+ salary_slip = frappe.db.get_value(
+ "Salary Slip", filters={"employee": self.employee, "docstatus": 1}
+ )
+
+ if salary_slip:
+ return True
+
+ return False
+
+ def joined_in_the_same_month(self):
+ """returns True if employee joined in same month as salary structure assignment from date else False"""
+
+ date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining")
+ from_date = getdate(self.from_date)
+
+ if not self.from_date or not date_of_joining:
+ return False
+
+ elif date_of_joining.month == from_date.month:
+ return True
+
+ else:
+ return False
+
def get_assigned_salary_structure(employee, on_date):
if not employee or not on_date:
diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js
index 31460f66ea30..9f1bdb0a11d5 100644
--- a/erpnext/projects/doctype/project/project.js
+++ b/erpnext/projects/doctype/project/project.js
@@ -134,6 +134,7 @@ function open_form(frm, doctype, child_doctype, parentfield) {
new_child_doc.parentfield = parentfield;
new_child_doc.parenttype = doctype;
new_doc[parentfield] = [new_child_doc];
+ new_doc.project = frm.doc.name;
frappe.ui.form.make_quick_entry(doctype, null, null, new_doc);
});
diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
index bb799af36eab..9f5e3c882aa6 100644
--- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
+++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
@@ -371,12 +371,14 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
fieldname: "deposit",
fieldtype: "Currency",
label: "Deposit",
+ options: "currency",
read_only: 1,
},
{
fieldname: "withdrawal",
fieldtype: "Currency",
label: "Withdrawal",
+ options: "currency",
read_only: 1,
},
{
@@ -394,6 +396,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
fieldname: "allocated_amount",
fieldtype: "Currency",
label: "Allocated Amount",
+ options: "Currency",
read_only: 1,
},
@@ -401,8 +404,17 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
fieldname: "unallocated_amount",
fieldtype: "Currency",
label: "Unallocated Amount",
+ options: "Currency",
read_only: 1,
},
+ {
+ fieldname: "currency",
+ fieldtype: "Link",
+ label: "Currency",
+ options: "Currency",
+ read_only: 1,
+ hidden: 1,
+ }
];
me.dialog = new frappe.ui.Dialog({
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index 068342effbfe..c5aeb5f349b4 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -261,7 +261,8 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
args: {
item_code: item.item_code,
warehouse: item.warehouse,
- company: doc.company
+ company: doc.company,
+ include_child_warehouses: true
}
});
}
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index d7892be082a2..0ca27c20facb 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -545,6 +545,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
if(!this.validate_company_and_party()) {
this.frm.fields_dict["items"].grid.grid_rows[item.idx - 1].remove();
} else {
+ item.pricing_rules = ''
return this.frm.call({
method: "erpnext.stock.get_item_details.get_item_details",
child: item,
@@ -1160,6 +1161,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
uom: function(doc, cdt, cdn) {
var me = this;
var item = frappe.get_doc(cdt, cdn);
+ item.pricing_rules = ''
if(item.item_code && item.uom) {
return this.frm.call({
method: "erpnext.stock.get_item_details.get_conversion_factor",
@@ -1236,6 +1238,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
qty: function(doc, cdt, cdn) {
let item = frappe.get_doc(cdt, cdn);
+ item.pricing_rules = ''
this.conversion_factor(doc, cdt, cdn, true);
this.calculate_stock_uom_rate(doc, cdt, cdn);
this.apply_pricing_rule(item, true);
diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js
index 58594b0a13d4..644adff1e273 100644
--- a/erpnext/public/js/utils/party.js
+++ b/erpnext/public/js/utils/party.js
@@ -242,20 +242,29 @@ erpnext.utils.set_taxes = function(frm, triggered_from_field) {
});
};
-erpnext.utils.get_contact_details = function(frm) {
+erpnext.utils.get_contact_details = function (frm) {
if (frm.updating_party_details) return;
if (frm.doc["contact_person"]) {
frappe.call({
method: "frappe.contacts.doctype.contact.contact.get_contact_details",
- args: {contact: frm.doc.contact_person },
- callback: function(r) {
- if (r.message)
- frm.set_value(r.message);
- }
- })
+ args: { contact: frm.doc.contact_person },
+ callback: function (r) {
+ if (r.message) frm.set_value(r.message);
+ },
+ });
+ } else {
+ frm.set_value({
+ contact_person: "",
+ contact_display: "",
+ contact_email: "",
+ contact_mobile: "",
+ contact_phone: "",
+ contact_designation: "",
+ contact_department: "",
+ });
}
-}
+};
erpnext.utils.validate_mandatory = function(frm, label, value, trigger_on) {
if (!value) {
diff --git a/erpnext/public/scss/point-of-sale.scss b/erpnext/public/scss/point-of-sale.scss
index 7a3854cc611a..7b7530b1501b 100644
--- a/erpnext/public/scss/point-of-sale.scss
+++ b/erpnext/public/scss/point-of-sale.scss
@@ -159,6 +159,12 @@
}
}
+ .item-img {
+ @extend .image;
+ border-radius: 8px 8px 0 0;
+ object-fit: cover;
+ }
+
> .item-detail {
display: flex;
flex-direction: column;
diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py
index 72f9e6d6e441..e8604080fbf4 100644
--- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py
+++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py
@@ -79,7 +79,7 @@ def get_children(doctype, parent=None, parent_quality_procedure=None, is_root=Fa
]
else:
return frappe.get_all(
- doctype,
+ "Quality Procedure",
fields=["name as value", "is_group as expandable"],
filters=dict(parent_quality_procedure=parent),
order_by="name asc",
diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py
index 897d8d86da41..3ce55c2622ae 100644
--- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py
+++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py
@@ -10,3 +10,8 @@ class EInvoiceSettings(Document):
def validate(self):
if self.enable and not self.credentials:
frappe.throw(_("You must add atleast one credentials to be able to use E Invoicing."))
+
+ prev_doc = self.get_doc_before_save()
+ if prev_doc.client_secret != self.client_secret or prev_doc.client_id != self.client_id:
+ self.auth_token = None
+ self.token_expiry = None
diff --git a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py
index 77c4d7c6ca3f..6db213aa2759 100644
--- a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py
+++ b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py
@@ -146,7 +146,9 @@ def process_file_data(self):
def publish(self, title, message, count, total):
frappe.publish_realtime(
- "import_invoice_update", {"title": title, "message": message, "count": count, "total": total}
+ "import_invoice_update",
+ {"title": title, "message": message, "count": count, "total": total},
+ user=self.modified_by,
)
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index 9292fc1cfecf..45ceb1566c68 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -894,6 +894,7 @@ def get_auth_token(self):
return self.e_invoice_settings.auth_token
def make_request(self, request_type, url, headers=None, data=None):
+ res = None
try:
if request_type == "post":
res = make_post_request(url, headers=headers, data=data)
diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js
index 88973e36b6ab..49af669fc3fc 100644
--- a/erpnext/regional/india/taxes.js
+++ b/erpnext/regional/india/taxes.js
@@ -47,6 +47,12 @@ erpnext.setup_auto_gst_taxation = (doctype) => {
}
}
});
+ },
+
+ reverse_charge: function(frm) {
+ if (frm.doc.reverse_charge == "Y") {
+ frm.set_value('eligibility_for_itc', 'ITC on Reverse Charge');
+ }
}
});
}
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index 1f5212857aa1..0b61e7faf9be 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -257,9 +257,16 @@ def get_regional_address_details(party_details, doctype, company):
update_party_details(party_details, doctype)
+ customer_gst_category = frappe.get_value(
+ "Customer", party_details.customer, ["gst_category", "export_type"]
+ )
+
party_details.place_of_supply = get_place_of_supply(party_details, doctype)
- if is_internal_transfer(party_details, doctype):
+ if is_internal_transfer(party_details, doctype) or customer_gst_category == (
+ "SEZ",
+ "Without Payment of Tax",
+ ):
party_details.taxes_and_charges = ""
party_details.taxes = []
return party_details
@@ -603,6 +610,10 @@ def get_ewb_data(dt, dn):
data = get_address_details(data, doc, company_address, billing_address, dispatch_address)
+ if is_intrastate_transfer_eway_bill(data):
+ data.docType = "CHL"
+ data.subSupplyType = 8
+
data.itemList = []
data.totalValue = doc.total
@@ -645,6 +656,10 @@ def get_ewb_data(dt, dn):
return data
+def is_intrastate_transfer_eway_bill(data):
+ return data.fromGstin == data.toGstin
+
+
@frappe.whitelist()
def generate_ewb_json(dt, dn):
dn = json.loads(dn)
@@ -968,8 +983,6 @@ def validate_reverse_charge_transaction(doc, method):
frappe.throw(msg)
- doc.eligibility_for_itc = "ITC on Reverse Charge"
-
def update_itc_availed_fields(doc, method):
country = frappe.get_cached_value("Company", doc.company, "country")
diff --git a/erpnext/regional/report/gst_itemised_sales_register/gst_itemised_sales_register.js b/erpnext/regional/report/gst_itemised_sales_register/gst_itemised_sales_register.js
index 3a0f0c966d6f..d5e832a83f48 100644
--- a/erpnext/regional/report/gst_itemised_sales_register/gst_itemised_sales_register.js
+++ b/erpnext/regional/report/gst_itemised_sales_register/gst_itemised_sales_register.js
@@ -15,12 +15,6 @@ filters = filters.concat({
"placeholder":"Company GSTIN",
"options": [""],
"width": "80"
-}, {
- "fieldname":"invoice_type",
- "label": __("Invoice Type"),
- "fieldtype": "Select",
- "placeholder":"Invoice Type",
- "options": ["", "Regular", "SEZ", "Export", "Deemed Export"]
});
// Handle company on change
diff --git a/erpnext/regional/report/gst_itemised_sales_register/gst_itemised_sales_register.py b/erpnext/regional/report/gst_itemised_sales_register/gst_itemised_sales_register.py
index bb1843f1bd92..aef69b178689 100644
--- a/erpnext/regional/report/gst_itemised_sales_register/gst_itemised_sales_register.py
+++ b/erpnext/regional/report/gst_itemised_sales_register/gst_itemised_sales_register.py
@@ -5,31 +5,48 @@
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import _execute
+def get_conditions(filters, additional_query_columns):
+ conditions = ""
+
+ for opts in additional_query_columns:
+ if filters.get(opts):
+ conditions += f" and {opts}=%({opts})s"
+
+ return conditions
+
+
def execute(filters=None):
+ additional_table_columns = [
+ dict(fieldtype="Data", label="Customer GSTIN", fieldname="customer_gstin", width=120),
+ dict(
+ fieldtype="Data", label="Billing Address GSTIN", fieldname="billing_address_gstin", width=140
+ ),
+ dict(fieldtype="Data", label="Company GSTIN", fieldname="company_gstin", width=120),
+ dict(fieldtype="Data", label="Place of Supply", fieldname="place_of_supply", width=120),
+ dict(fieldtype="Data", label="Reverse Charge", fieldname="reverse_charge", width=120),
+ dict(fieldtype="Data", label="GST Category", fieldname="gst_category", width=120),
+ dict(fieldtype="Data", label="Export Type", fieldname="export_type", width=120),
+ dict(fieldtype="Data", label="E-Commerce GSTIN", fieldname="ecommerce_gstin", width=130),
+ dict(fieldtype="Data", label="HSN Code", fieldname="gst_hsn_code", width=120),
+ ]
+
+ additional_query_columns = [
+ "customer_gstin",
+ "billing_address_gstin",
+ "company_gstin",
+ "place_of_supply",
+ "reverse_charge",
+ "gst_category",
+ "export_type",
+ "ecommerce_gstin",
+ "gst_hsn_code",
+ ]
+
+ additional_conditions = get_conditions(filters, additional_query_columns)
+
return _execute(
filters,
- additional_table_columns=[
- dict(fieldtype="Data", label="Customer GSTIN", fieldname="customer_gstin", width=120),
- dict(
- fieldtype="Data", label="Billing Address GSTIN", fieldname="billing_address_gstin", width=140
- ),
- dict(fieldtype="Data", label="Company GSTIN", fieldname="company_gstin", width=120),
- dict(fieldtype="Data", label="Place of Supply", fieldname="place_of_supply", width=120),
- dict(fieldtype="Data", label="Reverse Charge", fieldname="reverse_charge", width=120),
- dict(fieldtype="Data", label="GST Category", fieldname="gst_category", width=120),
- dict(fieldtype="Data", label="Export Type", fieldname="export_type", width=120),
- dict(fieldtype="Data", label="E-Commerce GSTIN", fieldname="ecommerce_gstin", width=130),
- dict(fieldtype="Data", label="HSN Code", fieldname="gst_hsn_code", width=120),
- ],
- additional_query_columns=[
- "customer_gstin",
- "billing_address_gstin",
- "company_gstin",
- "place_of_supply",
- "reverse_charge",
- "gst_category",
- "export_type",
- "ecommerce_gstin",
- "gst_hsn_code",
- ],
+ additional_table_columns=additional_table_columns,
+ additional_query_columns=additional_query_columns,
+ additional_conditions=additional_conditions,
)
diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py
index 68815bf1edff..338a06653dcc 100644
--- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py
+++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py
@@ -35,7 +35,7 @@ def _execute(filters=None):
data = []
added_item = []
for d in item_list:
- if (d.parent, d.item_code) not in added_item:
+ if (d.parent, d.gst_hsn_code, d.item_code) not in added_item:
row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty]
total_tax = 0
tax_rate = 0
@@ -52,7 +52,7 @@ def _execute(filters=None):
item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {})
row += [item_tax.get("tax_amount", 0)]
data.append(row)
- added_item.append((d.parent, d.item_code))
+ added_item.append((d.parent, d.gst_hsn_code, d.item_code))
if data:
data = get_merged_data(columns, data) # merge same hsn code data
return columns, data
@@ -161,11 +161,9 @@ def get_items(filters):
GROUP BY
`tabSales Invoice Item`.parent,
`tabSales Invoice Item`.item_code,
- `tabSales Invoice Item`.gst_hsn_code,
- `tabSales Invoice Item`.uom
+ `tabSales Invoice Item`.gst_hsn_code
ORDER BY
- `tabSales Invoice Item`.gst_hsn_code,
- `tabSales Invoice Item`.uom
+ `tabSales Invoice Item`.gst_hsn_code
""".format(
conditions=conditions
),
diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js
index 3bb9774044aa..e33fa4b1d1a1 100644
--- a/erpnext/selling/doctype/quotation/quotation.js
+++ b/erpnext/selling/doctype/quotation/quotation.js
@@ -83,11 +83,12 @@ erpnext.selling.QuotationController = erpnext.selling.SellingController.extend({
}
}
- if(doc.docstatus == 1 && !(['Lost', 'Ordered']).includes(doc.status)) {
- if(!doc.valid_till || frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) {
- cur_frm.add_custom_button(__('Sales Order'),
- cur_frm.cscript['Make Sales Order'], __('Create'));
- }
+ if (doc.docstatus == 1 && !["Lost", "Ordered"].includes(doc.status)) {
+ this.frm.add_custom_button(
+ __("Sales Order"),
+ this.frm.cscript["Make Sales Order"],
+ __("Create")
+ );
if(doc.status!=="Ordered") {
this.frm.add_custom_button(__('Set as Lost'), () => {
diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json
index 09fd310040ce..e4d2065c13db 100644
--- a/erpnext/selling/doctype/quotation/quotation.json
+++ b/erpnext/selling/doctype/quotation/quotation.json
@@ -461,7 +461,6 @@
"fieldname": "ignore_pricing_rule",
"fieldtype": "Check",
"label": "Ignore Pricing Rule",
- "no_copy": 1,
"permlevel": 1,
"print_hide": 1,
"show_days": 1,
@@ -1174,7 +1173,7 @@
"idx": 82,
"is_submittable": 1,
"links": [],
- "modified": "2022-06-15 20:35:32.635804",
+ "modified": "2022-09-16 17:44:43.221804",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation",
diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index 436852913f1e..3bfe79e3f387 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -191,14 +191,7 @@ def get_list_context(context=None):
@frappe.whitelist()
-def make_sales_order(source_name, target_doc=None):
- quotation = frappe.db.get_value(
- "Quotation", source_name, ["transaction_date", "valid_till"], as_dict=1
- )
- if quotation.valid_till and (
- quotation.valid_till < quotation.transaction_date or quotation.valid_till < getdate(nowdate())
- ):
- frappe.throw(_("Validity period of this quotation has ended."))
+def make_sales_order(source_name: str, target_doc=None):
return _make_sales_order(source_name, target_doc)
diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py
index 6f0b381fc163..6ab4a52d9d2a 100644
--- a/erpnext/selling/doctype/quotation/test_quotation.py
+++ b/erpnext/selling/doctype/quotation/test_quotation.py
@@ -118,17 +118,20 @@ def test_make_sales_order_with_terms(self):
sales_order.payment_schedule[1].due_date, getdate(add_days(quotation.transaction_date, 30))
)
- def test_valid_till(self):
- from erpnext.selling.doctype.quotation.quotation import make_sales_order
-
+ def test_valid_till_before_transaction_date(self):
quotation = frappe.copy_doc(test_records[0])
quotation.valid_till = add_days(quotation.transaction_date, -1)
self.assertRaises(frappe.ValidationError, quotation.validate)
+ def test_so_from_expired_quotation(self):
+ from erpnext.selling.doctype.quotation.quotation import make_sales_order
+
+ quotation = frappe.copy_doc(test_records[0])
quotation.valid_till = add_days(nowdate(), -1)
quotation.insert()
quotation.submit()
- self.assertRaises(frappe.ValidationError, make_sales_order, quotation.name)
+
+ make_sales_order(quotation.name)
def test_shopping_cart_without_website_item(self):
if frappe.db.exists("Website Item", {"item_code": "_Test Item Home Desktop 100"}):
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index 213909b9b999..1a40725552dd 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -95,6 +95,11 @@ frappe.ui.form.on("Sales Order", {
return query;
});
+ // On cancel and amending a sales order with advance payment, reset advance paid amount
+ if (frm.is_new()) {
+ frm.set_value("advance_paid", 0)
+ }
+
frm.ignore_doctypes_on_cancel_all = ['Purchase Order'];
},
diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json
index fc00fa6ea684..f77f494d800b 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.json
+++ b/erpnext/selling/doctype/sales_order/sales_order.json
@@ -544,7 +544,6 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Ignore Pricing Rule",
- "no_copy": 1,
"permlevel": 1,
"print_hide": 1
},
@@ -1549,7 +1548,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2022-06-10 03:52:22.212953",
+ "modified": "2022-09-16 17:43:57.007441",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 4bbeb21a1f22..8b28e02797d8 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -19,6 +19,7 @@
update_linked_doc,
validate_inter_company_party,
)
+from erpnext.accounts.party import get_party_account
from erpnext.controllers.selling_controller import SellingController
from erpnext.manufacturing.doctype.production_plan.production_plan import (
get_items_for_material_requests,
@@ -797,6 +798,8 @@ def set_missing_values(source, target):
if source.loyalty_points and source.order_type == "Shopping Cart":
target.redeem_loyalty_points = 1
+ target.debit_to = get_party_account("Customer", source.customer, source.company)
+
def update_item(source, target, source_parent):
target.amount = flt(source.amount) - flt(source.billed_amt)
target.base_amount = target.amount * flt(source_parent.conversion_rate)
@@ -950,6 +953,9 @@ def get_events(start, end, filters=None):
@frappe.whitelist()
def make_purchase_order_for_default_supplier(source_name, selected_items=None, target_doc=None):
"""Creates Purchase Order for each Supplier. Returns a list of doc objects."""
+
+ from erpnext.setup.utils import get_exchange_rate
+
if not selected_items:
return
@@ -958,10 +964,20 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
def set_missing_values(source, target):
target.supplier = supplier
+ target.currency = frappe.db.get_value(
+ "Supplier", filters={"name": supplier}, fieldname=["default_currency"]
+ )
+ company_currency = frappe.db.get_value(
+ "Company", filters={"name": target.company}, fieldname=["default_currency"]
+ )
+
+ target.conversion_rate = get_exchange_rate(target.currency, company_currency, args="for_buying")
+
target.apply_discount_on = ""
target.additional_discount_percentage = 0.0
target.discount_amount = 0.0
target.inter_company_order_reference = ""
+ target.shipping_rule = ""
default_price_list = frappe.get_value("Supplier", supplier, "default_price_list")
if default_price_list:
@@ -1074,14 +1090,30 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
]
items_to_map = list(set(items_to_map))
+ def is_drop_ship_order(target):
+ drop_ship = True
+ for item in target.items:
+ if not item.delivered_by_supplier:
+ drop_ship = False
+ break
+
+ return drop_ship
+
def set_missing_values(source, target):
target.supplier = ""
target.apply_discount_on = ""
target.additional_discount_percentage = 0.0
target.discount_amount = 0.0
target.inter_company_order_reference = ""
- target.customer = ""
- target.customer_name = ""
+ target.shipping_rule = ""
+
+ if is_drop_ship_order(target):
+ target.customer = source.customer
+ target.customer_name = source.customer_name
+ target.shipping_address = source.shipping_address_name
+ else:
+ target.customer = target.customer_name = target.shipping_address = None
+
target.run_method("set_missing_values")
target.run_method("calculate_taxes_and_totals")
diff --git a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py
index ace2e29c2b47..cbc40bbf90be 100644
--- a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py
+++ b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py
@@ -12,7 +12,9 @@ def get_data():
"Auto Repeat": "reference_document",
"Maintenance Visit": "prevdoc_docname",
},
- "internal_links": {"Quotation": ["items", "prevdoc_docname"]},
+ "internal_links": {
+ "Quotation": ["items", "prevdoc_docname"],
+ },
"transactions": [
{
"label": _("Fulfillment"),
diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
index 21abb94557ca..2b783758c189 100644
--- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json
+++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
@@ -809,7 +809,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2022-04-27 03:15:34.366563",
+ "modified": "2022-12-25 02:51:10.247569",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",
@@ -820,4 +820,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py
index 8bce1f607250..a4d20e2e7ae4 100644
--- a/erpnext/selling/page/point_of_sale/point_of_sale.py
+++ b/erpnext/selling/page/point_of_sale/point_of_sale.py
@@ -5,6 +5,7 @@
import json
import frappe
+from frappe.utils import cint
from frappe.utils.nestedset import get_root_of
from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability
@@ -105,11 +106,11 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te
ORDER BY
item.name asc
LIMIT
- {start}, {page_length}""".format(
- start=start,
- page_length=page_length,
- lft=lft,
- rgt=rgt,
+ {page_length} offset {start}""".format(
+ start=cint(start),
+ page_length=cint(page_length),
+ lft=cint(lft),
+ rgt=cint(rgt),
condition=condition,
bin_join_selection=bin_join_selection,
bin_join_condition=bin_join_condition,
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index da7576e08def..595b9196e848 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -67,7 +67,7 @@ erpnext.PointOfSale.Controller = class {
{
fieldtype: 'Link', label: __('POS Profile'),
options: 'POS Profile', fieldname: 'pos_profile', reqd: 1,
- get_query: () => pos_profile_query,
+ get_query: () => pos_profile_query(),
onchange: () => fetch_pos_payment_methods()
},
{
@@ -101,9 +101,11 @@ erpnext.PointOfSale.Controller = class {
primary_action_label: __('Submit')
});
dialog.show();
- const pos_profile_query = {
- query: 'erpnext.accounts.doctype.pos_profile.pos_profile.pos_profile_query',
- filters: { company: dialog.fields_dict.company.get_value() }
+ const pos_profile_query = () => {
+ return {
+ query: 'erpnext.accounts.doctype.pos_profile.pos_profile.pos_profile_query',
+ filters: { company: dialog.fields_dict.company.get_value() }
+ }
};
}
@@ -660,7 +662,7 @@ erpnext.PointOfSale.Controller = class {
} else {
return;
}
- } else if (available_qty < qty_needed) {
+ } else if (is_stock_item && available_qty < qty_needed) {
frappe.throw({
message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]),
indicator: 'orange'
@@ -694,7 +696,7 @@ erpnext.PointOfSale.Controller = class {
callback(res) {
if (!me.item_stock_map[item_code])
me.item_stock_map[item_code] = {};
- me.item_stock_map[item_code][warehouse] = res.message[0];
+ me.item_stock_map[item_code][warehouse] = res.message;
}
});
}
diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js
index 1d720f7291a0..f9b5bb2e4529 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_details.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_details.js
@@ -242,13 +242,14 @@ erpnext.PointOfSale.ItemDetails = class {
if (this.value) {
me.events.form_updated(me.current_item, 'warehouse', this.value).then(() => {
me.item_stock_map = me.events.get_item_stock_map();
- const available_qty = me.item_stock_map[me.item_row.item_code][this.value];
+ const available_qty = me.item_stock_map[me.item_row.item_code][this.value][0];
+ const is_stock_item = Boolean(me.item_stock_map[me.item_row.item_code][this.value][1]);
if (available_qty === undefined) {
me.events.get_available_stock(me.item_row.item_code, this.value).then(() => {
// item stock map is updated now reset warehouse
me.warehouse_control.set_value(this.value);
})
- } else if (available_qty === 0) {
+ } else if (available_qty === 0 && is_stock_item) {
me.warehouse_control.set_value('');
const bold_item_code = me.item_row.item_code.bold();
const bold_warehouse = this.value.bold();
diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js
index 7a90fb044f3b..b5eb0489f9d2 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_selector.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js
@@ -103,9 +103,9 @@ erpnext.PointOfSale.ItemSelector = class {
+ >
`;
} else {
return `
diff --git a/erpnext/setup/doctype/brand/brand.js b/erpnext/setup/doctype/brand/brand.js
index 3680906057fa..0abb71a3629b 100644
--- a/erpnext/setup/doctype/brand/brand.js
+++ b/erpnext/setup/doctype/brand/brand.js
@@ -1,13 +1,71 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
+frappe.ui.form.on('Brand', {
+ setup: (frm) => {
+ frm.fields_dict["brand_defaults"].grid.get_field("default_warehouse").get_query = function(doc, cdt, cdn) {
+ const row = locals[cdt][cdn];
+ return {
+ filters: { company: row.company }
+ }
+ }
+ frm.fields_dict["brand_defaults"].grid.get_field("default_discount_account").get_query = function(doc, cdt, cdn) {
+ const row = locals[cdt][cdn];
+ return {
+ filters: {
+ 'report_type': 'Profit and Loss',
+ 'company': row.company,
+ "is_group": 0
+ }
+ };
+ }
-//--------- ONLOAD -------------
-cur_frm.cscript.onload = function(doc, cdt, cdn) {
+ frm.fields_dict["brand_defaults"].grid.get_field("buying_cost_center").get_query = function(doc, cdt, cdn) {
+ const row = locals[cdt][cdn];
+ return {
+ filters: {
+ "is_group": 0,
+ "company": row.company
+ }
+ }
+ }
-}
+ frm.fields_dict["brand_defaults"].grid.get_field("expense_account").get_query = function(doc, cdt, cdn) {
+ const row = locals[cdt][cdn];
+ return {
+ query: "erpnext.controllers.queries.get_expense_account",
+ filters: { company: row.company }
+ }
+ }
-cur_frm.cscript.refresh = function(doc, cdt, cdn) {
+ frm.fields_dict["brand_defaults"].grid.get_field("default_provisional_account").get_query = function(doc, cdt, cdn) {
+ const row = locals[cdt][cdn];
+ return {
+ filters: {
+ "company": row.company,
+ "root_type": ["in", ["Liability", "Asset"]],
+ "is_group": 0
+ }
+ };
+ }
-}
+ frm.fields_dict["brand_defaults"].grid.get_field("selling_cost_center").get_query = function(doc, cdt, cdn) {
+ const row = locals[cdt][cdn];
+ return {
+ filters: {
+ "is_group": 0,
+ "company": row.company
+ }
+ }
+ }
+
+ frm.fields_dict["brand_defaults"].grid.get_field("income_account").get_query = function(doc, cdt, cdn) {
+ const row = locals[cdt][cdn];
+ return {
+ query: "erpnext.controllers.queries.get_income_account",
+ filters: { company: row.company }
+ }
+ }
+ }
+});
\ No newline at end of file
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index ee39d3a4ac5b..51bae6091bd7 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -611,11 +611,11 @@ def get_children(doctype, parent=None, company=None, is_root=False):
name as value,
is_group as expandable
from
- `tab{doctype}` comp
+ `tabCompany` comp
where
ifnull(parent_company, "")={parent}
""".format(
- doctype=doctype, parent=frappe.db.escape(parent)
+ parent=frappe.db.escape(parent)
),
as_dict=1,
)
diff --git a/erpnext/setup/doctype/customer_group/customer_group.json b/erpnext/setup/doctype/customer_group/customer_group.json
index 0e2ed9efcf8c..d6a431ea616e 100644
--- a/erpnext/setup/doctype/customer_group/customer_group.json
+++ b/erpnext/setup/doctype/customer_group/customer_group.json
@@ -139,10 +139,11 @@
"idx": 1,
"is_tree": 1,
"links": [],
- "modified": "2021-02-08 17:01:52.162202",
+ "modified": "2022-12-24 11:15:17.142746",
"modified_by": "Administrator",
"module": "Setup",
"name": "Customer Group",
+ "naming_rule": "By fieldname",
"nsm_parent_field": "parent_customer_group",
"owner": "Administrator",
"permissions": [
@@ -198,10 +199,19 @@
"role": "Customer",
"select": 1,
"share": 1
+ },
+ {
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1
}
],
"search_fields": "parent_customer_group",
"show_name_in_global_search": 1,
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/setup/doctype/item_group/item_group.json b/erpnext/setup/doctype/item_group/item_group.json
index 50f923d87e0b..2986087277c8 100644
--- a/erpnext/setup/doctype/item_group/item_group.json
+++ b/erpnext/setup/doctype/item_group/item_group.json
@@ -123,6 +123,7 @@
"fieldname": "route",
"fieldtype": "Data",
"label": "Route",
+ "no_copy": 1,
"unique": 1
},
{
@@ -232,11 +233,10 @@
"is_tree": 1,
"links": [],
"max_attachments": 3,
- "modified": "2022-03-09 12:27:11.055782",
+ "modified": "2023-01-05 12:21:30.458628",
"modified_by": "Administrator",
"module": "Setup",
"name": "Item Group",
- "name_case": "Title Case",
"naming_rule": "By fieldname",
"nsm_parent_field": "parent_item_group",
"owner": "Administrator",
diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py
index a4e23267cd4e..b0ff1ccfcd74 100644
--- a/erpnext/setup/doctype/item_group/item_group.py
+++ b/erpnext/setup/doctype/item_group/item_group.py
@@ -153,7 +153,7 @@ def get_parent_item_groups(item_group_name, from_item=False):
if from_item and frappe.request.environ.get("HTTP_REFERER"):
# base page after 'Home' will vary on Item page
- last_page = frappe.request.environ["HTTP_REFERER"].split("/")[-1]
+ last_page = frappe.request.environ["HTTP_REFERER"].split("/")[-1].split("?")[0]
if last_page and last_page in ("shop-by-category", "all-products"):
base_nav_page_title = " ".join(last_page.split("-")).title()
base_nav_page = {"name": _(base_nav_page_title), "route": "/" + last_page}
diff --git a/erpnext/setup/doctype/supplier_group/supplier_group.json b/erpnext/setup/doctype/supplier_group/supplier_group.json
index 9119bb947cbd..b3ed608cd033 100644
--- a/erpnext/setup/doctype/supplier_group/supplier_group.json
+++ b/erpnext/setup/doctype/supplier_group/supplier_group.json
@@ -6,6 +6,7 @@
"creation": "2013-01-10 16:34:24",
"doctype": "DocType",
"document_type": "Setup",
+ "engine": "InnoDB",
"field_order": [
"supplier_group_name",
"parent_supplier_group",
@@ -106,10 +107,11 @@
"idx": 1,
"is_tree": 1,
"links": [],
- "modified": "2020-03-18 18:10:49.228407",
+ "modified": "2022-12-24 11:16:12.486719",
"modified_by": "Administrator",
"module": "Setup",
"name": "Supplier Group",
+ "naming_rule": "By fieldname",
"nsm_parent_field": "parent_supplier_group",
"owner": "Administrator",
"permissions": [
@@ -156,8 +158,18 @@
"permlevel": 1,
"read": 1,
"role": "Purchase User"
+ },
+ {
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1
}
],
"show_name_in_global_search": 1,
- "sort_order": "ASC"
+ "sort_field": "modified",
+ "sort_order": "ASC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/setup/doctype/territory/territory.json b/erpnext/setup/doctype/territory/territory.json
index a25bda054b9c..c3a499337468 100644
--- a/erpnext/setup/doctype/territory/territory.json
+++ b/erpnext/setup/doctype/territory/territory.json
@@ -123,11 +123,12 @@
"idx": 1,
"is_tree": 1,
"links": [],
- "modified": "2021-02-08 17:10:03.767426",
+ "modified": "2022-12-24 11:16:39.964956",
"modified_by": "Administrator",
"module": "Setup",
"name": "Territory",
"name_case": "Title Case",
+ "naming_rule": "By fieldname",
"nsm_parent_field": "parent_territory",
"owner": "Administrator",
"permissions": [
@@ -175,10 +176,19 @@
"role": "Customer",
"select": 1,
"share": 1
+ },
+ {
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1
}
],
"search_fields": "parent_territory,territory_manager",
"show_name_in_global_search": 1,
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/startup/report_data_map.py b/erpnext/startup/report_data_map.py
deleted file mode 100644
index f8c1b6cca070..000000000000
--- a/erpnext/startup/report_data_map.py
+++ /dev/null
@@ -1,327 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-
-
-# mappings for table dumps
-# "remember to add indexes!"
-
-data_map = {
- "Company": {"columns": ["name"], "conditions": ["docstatus < 2"]},
- "Fiscal Year": {
- "columns": ["name", "year_start_date", "year_end_date"],
- "conditions": ["docstatus < 2"],
- },
- # Accounts
- "Account": {
- "columns": ["name", "parent_account", "lft", "rgt", "report_type", "company", "is_group"],
- "conditions": ["docstatus < 2"],
- "order_by": "lft",
- "links": {
- "company": ["Company", "name"],
- },
- },
- "Cost Center": {
- "columns": ["name", "lft", "rgt"],
- "conditions": ["docstatus < 2"],
- "order_by": "lft",
- },
- "GL Entry": {
- "columns": [
- "name",
- "account",
- "posting_date",
- "cost_center",
- "debit",
- "credit",
- "is_opening",
- "company",
- "voucher_type",
- "voucher_no",
- "remarks",
- ],
- "order_by": "posting_date, account",
- "links": {
- "account": ["Account", "name"],
- "company": ["Company", "name"],
- "cost_center": ["Cost Center", "name"],
- },
- },
- # Stock
- "Item": {
- "columns": [
- "name",
- "if(item_name=name, '', item_name) as item_name",
- "description",
- "item_group as parent_item_group",
- "stock_uom",
- "brand",
- "valuation_method",
- ],
- # "conditions": ["docstatus < 2"],
- "order_by": "name",
- "links": {"parent_item_group": ["Item Group", "name"], "brand": ["Brand", "name"]},
- },
- "Item Group": {
- "columns": ["name", "parent_item_group"],
- # "conditions": ["docstatus < 2"],
- "order_by": "lft",
- },
- "Brand": {"columns": ["name"], "conditions": ["docstatus < 2"], "order_by": "name"},
- "Project": {"columns": ["name"], "conditions": ["docstatus < 2"], "order_by": "name"},
- "Warehouse": {"columns": ["name"], "conditions": ["docstatus < 2"], "order_by": "name"},
- "Stock Ledger Entry": {
- "columns": [
- "name",
- "posting_date",
- "posting_time",
- "item_code",
- "warehouse",
- "actual_qty as qty",
- "voucher_type",
- "voucher_no",
- "project",
- "incoming_rate as incoming_rate",
- "stock_uom",
- "serial_no",
- "qty_after_transaction",
- "valuation_rate",
- ],
- "order_by": "posting_date, posting_time, creation",
- "links": {
- "item_code": ["Item", "name"],
- "warehouse": ["Warehouse", "name"],
- "project": ["Project", "name"],
- },
- "force_index": "posting_sort_index",
- },
- "Serial No": {
- "columns": ["name", "purchase_rate as incoming_rate"],
- "conditions": ["docstatus < 2"],
- "order_by": "name",
- },
- "Stock Entry": {
- "columns": ["name", "purpose"],
- "conditions": ["docstatus=1"],
- "order_by": "posting_date, posting_time, name",
- },
- "Material Request Item": {
- "columns": ["item.name as name", "item_code", "warehouse", "(qty - ordered_qty) as qty"],
- "from": "`tabMaterial Request Item` item, `tabMaterial Request` main",
- "conditions": [
- "item.parent = main.name",
- "main.docstatus=1",
- "main.status != 'Stopped'",
- "ifnull(warehouse, '')!=''",
- "qty > ordered_qty",
- ],
- "links": {"item_code": ["Item", "name"], "warehouse": ["Warehouse", "name"]},
- },
- "Purchase Order Item": {
- "columns": [
- "item.name as name",
- "item_code",
- "warehouse",
- "(qty - received_qty)*conversion_factor as qty",
- ],
- "from": "`tabPurchase Order Item` item, `tabPurchase Order` main",
- "conditions": [
- "item.parent = main.name",
- "main.docstatus=1",
- "main.status != 'Stopped'",
- "ifnull(warehouse, '')!=''",
- "qty > received_qty",
- ],
- "links": {"item_code": ["Item", "name"], "warehouse": ["Warehouse", "name"]},
- },
- "Sales Order Item": {
- "columns": [
- "item.name as name",
- "item_code",
- "(qty - delivered_qty)*conversion_factor as qty",
- "warehouse",
- ],
- "from": "`tabSales Order Item` item, `tabSales Order` main",
- "conditions": [
- "item.parent = main.name",
- "main.docstatus=1",
- "main.status != 'Stopped'",
- "ifnull(warehouse, '')!=''",
- "qty > delivered_qty",
- ],
- "links": {"item_code": ["Item", "name"], "warehouse": ["Warehouse", "name"]},
- },
- # Sales
- "Customer": {
- "columns": [
- "name",
- "if(customer_name=name, '', customer_name) as customer_name",
- "customer_group as parent_customer_group",
- "territory as parent_territory",
- ],
- "conditions": ["docstatus < 2"],
- "order_by": "name",
- "links": {
- "parent_customer_group": ["Customer Group", "name"],
- "parent_territory": ["Territory", "name"],
- },
- },
- "Customer Group": {
- "columns": ["name", "parent_customer_group"],
- "conditions": ["docstatus < 2"],
- "order_by": "lft",
- },
- "Territory": {
- "columns": ["name", "parent_territory"],
- "conditions": ["docstatus < 2"],
- "order_by": "lft",
- },
- "Sales Invoice": {
- "columns": ["name", "customer", "posting_date", "company"],
- "conditions": ["docstatus=1"],
- "order_by": "posting_date",
- "links": {"customer": ["Customer", "name"], "company": ["Company", "name"]},
- },
- "Sales Invoice Item": {
- "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"],
- "conditions": ["docstatus=1", "ifnull(parent, '')!=''"],
- "order_by": "parent",
- "links": {"parent": ["Sales Invoice", "name"], "item_code": ["Item", "name"]},
- },
- "Sales Order": {
- "columns": ["name", "customer", "transaction_date as posting_date", "company"],
- "conditions": ["docstatus=1"],
- "order_by": "transaction_date",
- "links": {"customer": ["Customer", "name"], "company": ["Company", "name"]},
- },
- "Sales Order Item[Sales Analytics]": {
- "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"],
- "conditions": ["docstatus=1", "ifnull(parent, '')!=''"],
- "order_by": "parent",
- "links": {"parent": ["Sales Order", "name"], "item_code": ["Item", "name"]},
- },
- "Delivery Note": {
- "columns": ["name", "customer", "posting_date", "company"],
- "conditions": ["docstatus=1"],
- "order_by": "posting_date",
- "links": {"customer": ["Customer", "name"], "company": ["Company", "name"]},
- },
- "Delivery Note Item[Sales Analytics]": {
- "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"],
- "conditions": ["docstatus=1", "ifnull(parent, '')!=''"],
- "order_by": "parent",
- "links": {"parent": ["Delivery Note", "name"], "item_code": ["Item", "name"]},
- },
- "Supplier": {
- "columns": [
- "name",
- "if(supplier_name=name, '', supplier_name) as supplier_name",
- "supplier_group as parent_supplier_group",
- ],
- "conditions": ["docstatus < 2"],
- "order_by": "name",
- "links": {
- "parent_supplier_group": ["Supplier Group", "name"],
- },
- },
- "Supplier Group": {
- "columns": ["name", "parent_supplier_group"],
- "conditions": ["docstatus < 2"],
- "order_by": "name",
- },
- "Purchase Invoice": {
- "columns": ["name", "supplier", "posting_date", "company"],
- "conditions": ["docstatus=1"],
- "order_by": "posting_date",
- "links": {"supplier": ["Supplier", "name"], "company": ["Company", "name"]},
- },
- "Purchase Invoice Item": {
- "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"],
- "conditions": ["docstatus=1", "ifnull(parent, '')!=''"],
- "order_by": "parent",
- "links": {"parent": ["Purchase Invoice", "name"], "item_code": ["Item", "name"]},
- },
- "Purchase Order": {
- "columns": ["name", "supplier", "transaction_date as posting_date", "company"],
- "conditions": ["docstatus=1"],
- "order_by": "posting_date",
- "links": {"supplier": ["Supplier", "name"], "company": ["Company", "name"]},
- },
- "Purchase Order Item[Purchase Analytics]": {
- "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"],
- "conditions": ["docstatus=1", "ifnull(parent, '')!=''"],
- "order_by": "parent",
- "links": {"parent": ["Purchase Order", "name"], "item_code": ["Item", "name"]},
- },
- "Purchase Receipt": {
- "columns": ["name", "supplier", "posting_date", "company"],
- "conditions": ["docstatus=1"],
- "order_by": "posting_date",
- "links": {"supplier": ["Supplier", "name"], "company": ["Company", "name"]},
- },
- "Purchase Receipt Item[Purchase Analytics]": {
- "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"],
- "conditions": ["docstatus=1", "ifnull(parent, '')!=''"],
- "order_by": "parent",
- "links": {"parent": ["Purchase Receipt", "name"], "item_code": ["Item", "name"]},
- },
- # Support
- "Issue": {
- "columns": ["name", "status", "creation", "resolution_date", "first_responded_on"],
- "conditions": ["docstatus < 2"],
- "order_by": "creation",
- },
- # Manufacturing
- "Work Order": {
- "columns": [
- "name",
- "status",
- "creation",
- "planned_start_date",
- "planned_end_date",
- "status",
- "actual_start_date",
- "actual_end_date",
- "modified",
- ],
- "conditions": ["docstatus = 1"],
- "order_by": "creation",
- },
- # Medical
- "Patient": {
- "columns": [
- "name",
- "creation",
- "owner",
- "if(patient_name=name, '', patient_name) as patient_name",
- ],
- "conditions": ["docstatus < 2"],
- "order_by": "name",
- "links": {"owner": ["User", "name"]},
- },
- "Patient Appointment": {
- "columns": [
- "name",
- "appointment_type",
- "patient",
- "practitioner",
- "appointment_date",
- "department",
- "status",
- "company",
- ],
- "order_by": "name",
- "links": {
- "practitioner": ["Healthcare Practitioner", "name"],
- "appointment_type": ["Appointment Type", "name"],
- },
- },
- "Healthcare Practitioner": {
- "columns": ["name", "department"],
- "order_by": "name",
- "links": {
- "department": ["Department", "name"],
- },
- },
- "Appointment Type": {"columns": ["name"], "order_by": "name"},
- "Medical Department": {"columns": ["name"], "order_by": "name"},
-}
diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py
index acf7dfdaa111..58c8b9c8dcbf 100644
--- a/erpnext/stock/doctype/batch/batch.py
+++ b/erpnext/stock/doctype/batch/batch.py
@@ -285,7 +285,7 @@ def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None):
batches = get_batches(item_code, warehouse, qty, throw, serial_no)
for batch in batches:
- if cint(qty) <= cint(batch.qty):
+ if flt(qty) <= flt(batch.qty):
batch_no = batch.batch_id
break
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json
index f9e934921d8b..a8f907ed7113 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -490,7 +490,6 @@
"fieldname": "ignore_pricing_rule",
"fieldtype": "Check",
"label": "Ignore Pricing Rule",
- "no_copy": 1,
"permlevel": 1,
"print_hide": 1
},
@@ -1336,7 +1335,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
- "modified": "2022-06-10 03:52:04.197415",
+ "modified": "2022-09-16 17:46:17.701904",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 7205758a8e4b..3efe584a7937 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -832,6 +832,9 @@ def update_details(source_doc, target_doc, source_parent):
update_address(
target_doc, "shipping_address", "shipping_address_display", source_doc.customer_address
)
+ update_address(
+ target_doc, "billing_address", "billing_address_display", source_doc.customer_address
+ )
update_taxes(
target_doc,
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 6bcab737b376..d747383d6a53 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -6,7 +6,7 @@
import frappe
from frappe.tests.utils import FrappeTestCase
-from frappe.utils import cstr, flt, nowdate, nowtime
+from frappe.utils import add_days, cstr, flt, nowdate, nowtime, today
from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.accounts.utils import get_balance_on
@@ -1050,9 +1050,22 @@ def test_internal_transfer_with_valuation_only(self):
do_not_submit=True,
)
+ dn.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": "_Test Account Service Tax - _TC",
+ "description": "Tax 1",
+ "rate": 14,
+ "cost_center": "_Test Cost Center - _TC",
+ "included_in_print_rate": 1,
+ },
+ )
+
self.assertEqual(dn.items[0].rate, 500) # haven't saved yet
dn.save()
self.assertEqual(dn.ignore_pricing_rule, 1)
+ self.assertEqual(dn.taxes[0].included_in_print_rate, 0)
# rate should reset to incoming rate
self.assertEqual(dn.items[0].rate, rate)
@@ -1063,6 +1076,7 @@ def test_internal_transfer_with_valuation_only(self):
dn.save()
self.assertEqual(dn.items[0].rate, rate)
+ self.assertEqual(dn.items[0].net_rate, rate)
def test_internal_transfer_precision_gle(self):
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
@@ -1091,6 +1105,36 @@ def test_internal_transfer_precision_gle(self):
frappe.db.exists("GL Entry", {"voucher_no": dn.name, "voucher_type": dn.doctype})
)
+ def test_batch_expiry_for_delivery_note(self):
+ from erpnext.controllers.sales_and_purchase_return import make_return_doc
+ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
+
+ item = make_item(
+ "_Test Batch Item For Return Check",
+ {
+ "is_purchase_item": 1,
+ "is_stock_item": 1,
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ "batch_number_series": "TBIRC.#####",
+ },
+ )
+
+ pi = make_purchase_receipt(qty=1, item_code=item.name)
+
+ dn = create_delivery_note(qty=1, item_code=item.name, batch_no=pi.items[0].batch_no)
+
+ dn.load_from_db()
+ batch_no = dn.items[0].batch_no
+ self.assertTrue(batch_no)
+
+ frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1))
+
+ return_dn = make_return_doc(dn.doctype, dn.name)
+ return_dn.save().submit()
+
+ self.assertTrue(return_dn.docstatus == 1)
+
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")
@@ -1117,6 +1161,7 @@ def create_delivery_note(**args):
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"serial_no": args.serial_no,
+ "batch_no": args.batch_no or None,
"target_warehouse": args.target_warehouse,
},
)
diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
index 2d7abc8a0d68..fb89aef5b262 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -746,6 +746,7 @@
"fieldtype": "Currency",
"label": "Incoming Rate",
"no_copy": 1,
+ "precision": "6",
"print_hide": 1,
"read_only": 1
},
@@ -780,7 +781,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2022-05-02 12:09:39.610075",
+ "modified": "2022-10-12 03:36:05.344847",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index 24550b718290..aa6264033625 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -10,6 +10,31 @@ frappe.ui.form.on("Item", {
frm.add_fetch('attribute', 'to_range', 'to_range');
frm.add_fetch('attribute', 'increment', 'increment');
frm.add_fetch('tax_type', 'tax_rate', 'tax_rate');
+
+ frm.make_methods = {
+ 'Sales Order': () => {
+ open_form(frm, "Sales Order", "Sales Order Item", "items");
+ },
+ 'Delivery Note': () => {
+ open_form(frm, "Delivery Note", "Delivery Note Item", "items");
+ },
+ 'Sales Invoice': () => {
+ open_form(frm, "Sales Invoice", "Sales Invoice Item", "items");
+ },
+ 'Purchase Order': () => {
+ open_form(frm, "Purchase Order", "Purchase Order Item", "items");
+ },
+ 'Purchase Receipt': () => {
+ open_form(frm, "Purchase Receipt", "Purchase Receipt Item", "items");
+ },
+ 'Purchase Invoice': () => {
+ open_form(frm, "Purchase Invoice", "Purchase Invoice Item", "items");
+ },
+ 'Material Request': () => {
+ open_form(frm, "Material Request", "Material Request Item", "items");
+ },
+ };
+
},
onload: function(frm) {
erpnext.item.setup_queries(frm);
@@ -813,3 +838,17 @@ frappe.ui.form.on("UOM Conversion Detail", {
}
}
});
+
+function open_form(frm, doctype, child_doctype, parentfield) {
+ frappe.model.with_doctype(doctype, () => {
+ let new_doc = frappe.model.get_new_doc(doctype);
+
+ let new_child_doc = frappe.model.add_child(new_doc, child_doctype, parentfield);
+ new_child_doc.item_code = frm.doc.name;
+ new_child_doc.item_name = frm.doc.item_name;
+ new_child_doc.uom = frm.doc.stock_uom;
+ new_child_doc.description = frm.doc.description;
+
+ frappe.ui.form.make_quick_entry(doctype, null, null, new_doc);
+ });
+}
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index a1c71a3f3770..9e47c80de172 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -30,7 +30,7 @@
test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand", "Item Attribute"]
-def make_item(item_code=None, properties=None):
+def make_item(item_code=None, properties=None, uoms=None):
if not item_code:
item_code = frappe.generate_hash(length=16)
@@ -54,6 +54,11 @@ def make_item(item_code=None, properties=None):
for item_default in [doc for doc in item.get("item_defaults") if not doc.default_warehouse]:
item_default.default_warehouse = "_Test Warehouse - _TC"
item_default.company = "_Test Company"
+
+ if uoms:
+ for uom in uoms:
+ item.append("uoms", uom)
+
item.insert()
return item
@@ -76,6 +81,7 @@ def get_item(self, idx):
def test_get_item_details(self):
# delete modified item price record and make as per test_records
frappe.db.sql("""delete from `tabItem Price`""")
+ frappe.db.sql("""delete from `tabBin`""")
to_check = {
"item_code": "_Test Item",
@@ -96,9 +102,25 @@ def test_get_item_details(self):
"batch_no": None,
"uom": "_Test UOM",
"conversion_factor": 1.0,
+ "reserved_qty": 1,
+ "actual_qty": 5,
+ "projected_qty": 14,
}
make_test_objects("Item Price")
+ make_test_objects(
+ "Bin",
+ [
+ {
+ "item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC",
+ "reserved_qty": 1,
+ "actual_qty": 5,
+ "ordered_qty": 10,
+ "projected_qty": 14,
+ }
+ ],
+ )
company = "_Test Company"
currency = frappe.get_cached_value("Company", company, "default_currency")
@@ -122,7 +144,7 @@ def test_get_item_details(self):
)
for key, value in to_check.items():
- self.assertEqual(value, details.get(key))
+ self.assertEqual(value, details.get(key), key)
def test_item_tax_template(self):
expected_item_tax_template = [
diff --git a/erpnext/stock/doctype/item_barcode/item_barcode.json b/erpnext/stock/doctype/item_barcode/item_barcode.json
index eef70c95d05c..06413a844fac 100644
--- a/erpnext/stock/doctype/item_barcode/item_barcode.json
+++ b/erpnext/stock/doctype/item_barcode/item_barcode.json
@@ -16,6 +16,7 @@
"in_list_view": 1,
"label": "Barcode",
"no_copy": 1,
+ "reqd": 1,
"unique": 1
},
{
@@ -28,7 +29,7 @@
],
"istable": 1,
"links": [],
- "modified": "2022-04-01 05:54:27.314030",
+ "modified": "2022-08-24 19:59:47.871677",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Barcode",
diff --git a/erpnext/stock/doctype/item_supplier/item_supplier.json b/erpnext/stock/doctype/item_supplier/item_supplier.json
index 6cff8e0892e4..84649a67d001 100644
--- a/erpnext/stock/doctype/item_supplier/item_supplier.json
+++ b/erpnext/stock/doctype/item_supplier/item_supplier.json
@@ -1,95 +1,43 @@
{
- "allow_copy": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2013-02-22 01:28:01",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "creation": "2013-02-22 01:28:01",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "supplier",
+ "supplier_part_no"
+ ],
"fields": [
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "supplier",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Supplier",
- "length": 0,
- "no_copy": 0,
- "options": "Supplier",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "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",
+ "reqd": 1
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "supplier_part_no",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 1,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Supplier Part Number",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "200px",
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "fieldname": "supplier_part_no",
+ "fieldtype": "Data",
+ "in_global_search": 1,
+ "in_list_view": 1,
+ "label": "Supplier Part Number",
+ "print_width": "200px",
"width": "200px"
}
- ],
- "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": "2017-02-20 13:29:32.569715",
- "modified_by": "Administrator",
- "module": "Stock",
- "name": "Item Supplier",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "track_changes": 1,
- "track_seen": 0
+ ],
+ "idx": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2022-09-07 12:33:55.780062",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Item Supplier",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js
index cc64b5caa523..e83deae7fbab 100644
--- a/erpnext/stock/doctype/material_request/material_request.js
+++ b/erpnext/stock/doctype/material_request/material_request.js
@@ -370,9 +370,6 @@ frappe.ui.form.on("Material Request Item", {
if (flt(d.qty) < flt(d.min_order_qty)) {
frappe.msgprint(__("Warning: Material Requested Qty is less than Minimum Order Qty"));
}
-
- const item = locals[doctype][name];
- frm.events.get_item_data(frm, item, false);
},
from_warehouse: function(frm, doctype, name) {
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index 3474ca0db683..c1201ef8f9ac 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -10,7 +10,7 @@
import frappe
from frappe import _, msgprint
from frappe.model.mapper import get_mapped_doc
-from frappe.utils import cstr, flt, get_link_to_form, getdate, new_line_sep, nowdate
+from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, new_line_sep, nowdate
from six import string_types
from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items
@@ -504,13 +504,13 @@ def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, pa
and mr.per_ordered < 99.99
and mr.docstatus = 1
and mr.status != 'Stopped'
- and mr.company = '{1}'
- {2}
+ and mr.company = %s
+ {1}
order by mr_item.item_code ASC
- limit {3} offset {4} """.format(
- ", ".join(["%s"] * len(supplier_items)), filters.get("company"), conditions, page_len, start
+ limit {2} offset {3} """.format(
+ ", ".join(["%s"] * len(supplier_items)), conditions, cint(page_len), cint(start)
),
- tuple(supplier_items),
+ tuple(supplier_items) + (filters.get("company"),),
as_dict=1,
)
diff --git a/erpnext/stock/doctype/material_request/material_request_dashboard.py b/erpnext/stock/doctype/material_request/material_request_dashboard.py
index b073e6a22eee..691a8b39b1b5 100644
--- a/erpnext/stock/doctype/material_request/material_request_dashboard.py
+++ b/erpnext/stock/doctype/material_request/material_request_dashboard.py
@@ -4,10 +4,13 @@
def get_data():
return {
"fieldname": "material_request",
+ "internal_links": {
+ "Sales Order": ["items", "sales_order"],
+ },
"transactions": [
{
"label": _("Reference"),
- "items": ["Request for Quotation", "Supplier Quotation", "Purchase Order"],
+ "items": ["Sales Order", "Request for Quotation", "Supplier Quotation", "Purchase Order"],
},
{"label": _("Stock"), "items": ["Stock Entry", "Purchase Receipt", "Pick List"]},
{"label": _("Manufacturing"), "items": ["Work Order"]},
diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py
index 4d05d7a345c0..5412d4c00205 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.py
+++ b/erpnext/stock/doctype/packed_item/packed_item.py
@@ -83,8 +83,8 @@ def reset_packing_list(doc):
# 1. items were deleted
# 2. if bundle item replaced by another item (same no. of items but different items)
# we maintain list to track recurring item rows as well
- items_before_save = [item.item_code for item in doc_before_save.get("items")]
- items_after_save = [item.item_code for item in doc.get("items")]
+ items_before_save = [(item.name, item.item_code) for item in doc_before_save.get("items")]
+ items_after_save = [(item.name, item.item_code) for item in doc.get("items")]
reset_table = items_before_save != items_after_save
else:
# reset: if via Update Items OR
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 49cfbe2ce271..effba8e579b2 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -174,20 +174,20 @@ def aggregate_item_qty(self):
frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx))
item_code = item.item_code
reference = item.sales_order_item or item.material_request_item
- key = (item_code, item.uom, reference)
+ key = (item_code, item.uom, item.warehouse, item.batch_no, reference)
item.idx = None
item.name = None
if item_map.get(key):
item_map[key].qty += item.qty
- item_map[key].stock_qty += item.stock_qty
+ item_map[key].stock_qty += flt(item.stock_qty, item.precision("stock_qty"))
else:
item_map[key] = item
# maintain count of each item (useful to limit get query)
self.item_count_map.setdefault(item_code, 0)
- self.item_count_map[item_code] += item.stock_qty
+ self.item_count_map[item_code] += flt(item.stock_qty, item.precision("stock_qty"))
return item_map.values()
@@ -198,7 +198,8 @@ def validate_for_qty(self):
frappe.throw(_("Qty of Finished Goods Item should be greater than 0."))
def before_print(self, settings=None):
- self.group_similar_items()
+ if self.group_same_items:
+ self.group_similar_items()
def group_similar_items(self):
group_item_qty = defaultdict(float)
diff --git a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py
index 92e57bed2205..7fbcbafbac1f 100644
--- a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py
+++ b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py
@@ -1,7 +1,10 @@
def get_data():
return {
"fieldname": "pick_list",
+ "internal_links": {
+ "Sales Order": ["locations", "sales_order"],
+ },
"transactions": [
- {"items": ["Stock Entry", "Delivery Note"]},
+ {"items": ["Stock Entry", "Sales Order", "Delivery Note"]},
],
}
diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py
index e8cebc8e6224..fbb9fb5f051a 100644
--- a/erpnext/stock/doctype/pick_list/test_pick_list.py
+++ b/erpnext/stock/doctype/pick_list/test_pick_list.py
@@ -432,10 +432,10 @@ def _compare_dicts(a, b):
pl.before_print()
self.assertEqual(len(pl.locations), 4)
- # grouping should halve the number of items
+ # grouping should not happen if group_same_items is False
pl = frappe.get_doc(
doctype="Pick List",
- group_same_items=True,
+ group_same_items=False,
locations=[
_dict(item_code="A", warehouse="X", qty=5, picked_qty=1),
_dict(item_code="B", warehouse="Y", qty=4, picked_qty=2),
@@ -444,6 +444,11 @@ def _compare_dicts(a, b):
],
)
pl.before_print()
+ self.assertEqual(len(pl.locations), 4)
+
+ # grouping should halve the number of items
+ pl.group_same_items = True
+ pl.before_print()
self.assertEqual(len(pl.locations), 2)
expected_items = [
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
index abc90e6ca570..70b4e422fdd2 100755
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
@@ -405,7 +405,6 @@
"fieldname": "ignore_pricing_rule",
"fieldtype": "Check",
"label": "Ignore Pricing Rule",
- "no_copy": 1,
"permlevel": 1,
"print_hide": 1
},
@@ -1157,7 +1156,7 @@
"idx": 261,
"is_submittable": 1,
"links": [],
- "modified": "2022-05-27 15:59:18.550583",
+ "modified": "2022-09-16 17:45:58.430132",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index f859791de62c..390704fa3edd 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -367,6 +367,12 @@ def make_item_gl_entries(self, gl_entries, warehouse_account=None):
if credit_currency == self.company_currency
else flt(d.net_amount, d.precision("net_amount"))
)
+
+ outgoing_amount = d.base_net_amount
+ if self.is_internal_supplier and d.valuation_rate:
+ outgoing_amount = d.valuation_rate * d.stock_qty
+ credit_amount = outgoing_amount
+
if credit_amount:
account = warehouse_account[d.from_warehouse]["account"] if d.from_warehouse else stock_rbnb
@@ -374,7 +380,7 @@ def make_item_gl_entries(self, gl_entries, warehouse_account=None):
gl_entries=gl_entries,
account=account,
cost_center=d.cost_center,
- debit=-1 * flt(d.base_net_amount, d.precision("base_net_amount")),
+ debit=-1 * flt(outgoing_amount, d.precision("base_net_amount")),
credit=0.0,
remarks=remarks,
against_account=warehouse_account_name,
@@ -423,7 +429,7 @@ def make_item_gl_entries(self, gl_entries, warehouse_account=None):
# divisional loss adjustment
valuation_amount_as_per_doc = (
- flt(d.base_net_amount, d.precision("base_net_amount"))
+ flt(outgoing_amount, d.precision("base_net_amount"))
+ flt(d.landed_cost_voucher_amount)
+ flt(d.rm_supp_cost)
+ flt(d.item_tax_amount)
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py
index 06ba93655619..b3ae7b58b498 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py
@@ -12,13 +12,17 @@ def get_data():
"Purchase Receipt": "return_against",
},
"internal_links": {
+ "Material Request": ["items", "material_request"],
"Purchase Order": ["items", "purchase_order"],
"Project": ["items", "project"],
"Quality Inspection": ["items", "quality_inspection"],
},
"transactions": [
{"label": _("Related"), "items": ["Purchase Invoice", "Landed Cost Voucher", "Asset"]},
- {"label": _("Reference"), "items": ["Purchase Order", "Quality Inspection", "Project"]},
+ {
+ "label": _("Reference"),
+ "items": ["Material Request", "Purchase Order", "Quality Inspection", "Project"],
+ },
{"label": _("Returns"), "items": ["Purchase Receipt"]},
{"label": _("Subscription"), "items": ["Auto Repeat"]},
],
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 2d9402e5325e..2457c8e80abc 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -9,6 +9,7 @@
import frappe
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, cint, cstr, flt, today
+from pypika import functions as fn
from six import iteritems
import erpnext
@@ -1361,6 +1362,386 @@ def test_neg_to_positive(self):
if gle.account == account:
self.assertEqual(gle.credit, 50)
+ def test_backdated_transaction_for_internal_transfer(self):
+ from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
+ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+
+ prepare_data_for_internal_transfer()
+ customer = "_Test Internal Customer 2"
+ company = "_Test Company with perpetual inventory"
+
+ from_warehouse = create_warehouse("_Test Internal From Warehouse New", company=company)
+ to_warehouse = create_warehouse("_Test Internal To Warehouse New", company=company)
+ item_doc = create_item("Test Internal Transfer Item")
+
+ target_warehouse = create_warehouse("_Test Internal GIT Warehouse New", company=company)
+
+ make_purchase_receipt(
+ item_code=item_doc.name,
+ company=company,
+ posting_date=add_days(today(), -1),
+ warehouse=from_warehouse,
+ qty=1,
+ rate=100,
+ )
+
+ dn1 = create_delivery_note(
+ item_code=item_doc.name,
+ company=company,
+ customer=customer,
+ cost_center="Main - TCP1",
+ expense_account="Cost of Goods Sold - TCP1",
+ qty=1,
+ rate=500,
+ warehouse=from_warehouse,
+ target_warehouse=target_warehouse,
+ )
+
+ self.assertEqual(dn1.items[0].rate, 100)
+
+ pr1 = make_inter_company_purchase_receipt(dn1.name)
+ pr1.items[0].warehouse = to_warehouse
+ self.assertEqual(pr1.items[0].rate, 100)
+ pr1.submit()
+
+ self.assertEqual(pr1.is_internal_supplier, 1)
+
+ # Backdated purchase receipt entry, the valuation rate should be updated for DN1 and PR1
+ make_purchase_receipt(
+ item_code=item_doc.name,
+ company=company,
+ posting_date=add_days(today(), -2),
+ warehouse=from_warehouse,
+ qty=1,
+ rate=200,
+ )
+
+ dn_value = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Delivery Note", "voucher_no": dn1.name, "warehouse": target_warehouse},
+ "stock_value_difference",
+ )
+
+ self.assertEqual(abs(dn_value), 200.00)
+
+ pr_value = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr1.name, "warehouse": to_warehouse},
+ "stock_value_difference",
+ )
+
+ self.assertEqual(abs(pr_value), 200.00)
+ pr1.load_from_db()
+
+ self.assertEqual(pr1.items[0].valuation_rate, 200)
+ self.assertEqual(pr1.items[0].rate, 100)
+
+ Gl = frappe.qb.DocType("GL Entry")
+
+ query = (
+ frappe.qb.from_(Gl)
+ .select(
+ (fn.Sum(Gl.debit) - fn.Sum(Gl.credit)).as_("value"),
+ )
+ .where((Gl.voucher_type == pr1.doctype) & (Gl.voucher_no == pr1.name))
+ ).run(as_dict=True)
+
+ self.assertEqual(query[0].value, 0)
+
+ def test_backdated_transaction_for_internal_transfer_in_trasit_warehouse_for_purchase_receipt(
+ self,
+ ):
+ from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
+ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+
+ prepare_data_for_internal_transfer()
+ customer = "_Test Internal Customer 2"
+ company = "_Test Company with perpetual inventory"
+
+ from_warehouse = create_warehouse("_Test Internal From Warehouse New", company=company)
+ to_warehouse = create_warehouse("_Test Internal To Warehouse New", company=company)
+ item_doc = create_item("Test Internal Transfer Item")
+
+ target_warehouse = create_warehouse("_Test Internal GIT Warehouse New", company=company)
+
+ make_purchase_receipt(
+ item_code=item_doc.name,
+ company=company,
+ posting_date=add_days(today(), -1),
+ warehouse=from_warehouse,
+ qty=1,
+ rate=100,
+ )
+
+ # Keep stock in advance and make sure that systen won't pick this stock while reposting backdated transaction
+ for i in range(1, 4):
+ make_purchase_receipt(
+ item_code=item_doc.name,
+ company=company,
+ posting_date=add_days(today(), -1 * i),
+ warehouse=target_warehouse,
+ qty=1,
+ rate=320 * i,
+ )
+
+ dn1 = create_delivery_note(
+ item_code=item_doc.name,
+ company=company,
+ customer=customer,
+ cost_center="Main - TCP1",
+ expense_account="Cost of Goods Sold - TCP1",
+ qty=1,
+ rate=500,
+ warehouse=from_warehouse,
+ target_warehouse=target_warehouse,
+ )
+
+ self.assertEqual(dn1.items[0].rate, 100)
+
+ pr1 = make_inter_company_purchase_receipt(dn1.name)
+ pr1.items[0].warehouse = to_warehouse
+ self.assertEqual(pr1.items[0].rate, 100)
+ pr1.submit()
+
+ stk_ledger = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr1.name, "warehouse": target_warehouse},
+ ["stock_value_difference", "outgoing_rate"],
+ as_dict=True,
+ )
+
+ self.assertEqual(abs(stk_ledger.stock_value_difference), 100)
+ self.assertEqual(stk_ledger.outgoing_rate, 100)
+
+ # Backdated purchase receipt entry, the valuation rate should be updated for DN1 and PR1
+ make_purchase_receipt(
+ item_code=item_doc.name,
+ company=company,
+ posting_date=add_days(today(), -2),
+ warehouse=from_warehouse,
+ qty=1,
+ rate=200,
+ )
+
+ dn_value = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Delivery Note", "voucher_no": dn1.name, "warehouse": target_warehouse},
+ "stock_value_difference",
+ )
+
+ self.assertEqual(abs(dn_value), 200.00)
+
+ pr_value = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr1.name, "warehouse": to_warehouse},
+ "stock_value_difference",
+ )
+
+ self.assertEqual(abs(pr_value), 200.00)
+ pr1.load_from_db()
+
+ self.assertEqual(pr1.items[0].valuation_rate, 200)
+ self.assertEqual(pr1.items[0].rate, 100)
+
+ Gl = frappe.qb.DocType("GL Entry")
+
+ query = (
+ frappe.qb.from_(Gl)
+ .select(
+ (fn.Sum(Gl.debit) - fn.Sum(Gl.credit)).as_("value"),
+ )
+ .where((Gl.voucher_type == pr1.doctype) & (Gl.voucher_no == pr1.name))
+ ).run(as_dict=True)
+
+ self.assertEqual(query[0].value, 0)
+
+ def test_backdated_transaction_for_internal_transfer_in_trasit_warehouse_for_purchase_invoice(
+ self,
+ ):
+ from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
+ make_purchase_invoice as make_purchase_invoice_for_si,
+ )
+ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
+ make_inter_company_purchase_invoice,
+ )
+ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+
+ prepare_data_for_internal_transfer()
+ customer = "_Test Internal Customer 2"
+ company = "_Test Company with perpetual inventory"
+
+ from_warehouse = create_warehouse("_Test Internal From Warehouse New", company=company)
+ to_warehouse = create_warehouse("_Test Internal To Warehouse New", company=company)
+ item_doc = create_item("Test Internal Transfer Item")
+
+ target_warehouse = create_warehouse("_Test Internal GIT Warehouse New", company=company)
+
+ make_purchase_invoice_for_si(
+ item_code=item_doc.name,
+ company=company,
+ posting_date=add_days(today(), -1),
+ warehouse=from_warehouse,
+ qty=1,
+ update_stock=1,
+ expense_account="Cost of Goods Sold - TCP1",
+ cost_center="Main - TCP1",
+ rate=100,
+ )
+
+ # Keep stock in advance and make sure that systen won't pick this stock while reposting backdated transaction
+ for i in range(1, 4):
+ make_purchase_invoice_for_si(
+ item_code=item_doc.name,
+ company=company,
+ posting_date=add_days(today(), -1 * i),
+ warehouse=target_warehouse,
+ update_stock=1,
+ qty=1,
+ expense_account="Cost of Goods Sold - TCP1",
+ cost_center="Main - TCP1",
+ rate=320 * i,
+ )
+
+ si1 = create_sales_invoice(
+ item_code=item_doc.name,
+ company=company,
+ customer=customer,
+ cost_center="Main - TCP1",
+ income_account="Sales - TCP1",
+ qty=1,
+ rate=500,
+ update_stock=1,
+ warehouse=from_warehouse,
+ target_warehouse=target_warehouse,
+ )
+
+ self.assertEqual(si1.items[0].rate, 100)
+
+ pi1 = make_inter_company_purchase_invoice(si1.name)
+ pi1.items[0].warehouse = to_warehouse
+ self.assertEqual(pi1.items[0].rate, 100)
+ pi1.update_stock = 1
+ pi1.save()
+ pi1.submit()
+
+ stk_ledger = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": pi1.doctype, "voucher_no": pi1.name, "warehouse": target_warehouse},
+ ["stock_value_difference", "outgoing_rate"],
+ as_dict=True,
+ )
+
+ self.assertEqual(abs(stk_ledger.stock_value_difference), 100)
+ self.assertEqual(stk_ledger.outgoing_rate, 100)
+
+ # Backdated purchase receipt entry, the valuation rate should be updated for si1 and pi1
+ make_purchase_receipt(
+ item_code=item_doc.name,
+ company=company,
+ posting_date=add_days(today(), -2),
+ warehouse=from_warehouse,
+ qty=1,
+ rate=200,
+ )
+
+ si_value = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": si1.doctype, "voucher_no": si1.name, "warehouse": target_warehouse},
+ "stock_value_difference",
+ )
+
+ self.assertEqual(abs(si_value), 200.00)
+
+ pi_value = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": pi1.doctype, "voucher_no": pi1.name, "warehouse": to_warehouse},
+ "stock_value_difference",
+ )
+
+ self.assertEqual(abs(pi_value), 200.00)
+ pi1.load_from_db()
+
+ self.assertEqual(pi1.items[0].valuation_rate, 200)
+ self.assertEqual(pi1.items[0].rate, 100)
+
+ Gl = frappe.qb.DocType("GL Entry")
+
+ query = (
+ frappe.qb.from_(Gl)
+ .select(
+ (fn.Sum(Gl.debit) - fn.Sum(Gl.credit)).as_("value"),
+ )
+ .where((Gl.voucher_type == pi1.doctype) & (Gl.voucher_no == pi1.name))
+ ).run(as_dict=True)
+
+ self.assertEqual(query[0].value, 0)
+
+ def test_batch_expiry_for_purchase_receipt(self):
+ from erpnext.controllers.sales_and_purchase_return import make_return_doc
+
+ item = make_item(
+ "_Test Batch Item For Return Check",
+ {
+ "is_purchase_item": 1,
+ "is_stock_item": 1,
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ "batch_number_series": "TBIRC.#####",
+ },
+ )
+
+ pi = make_purchase_receipt(
+ qty=1,
+ item_code=item.name,
+ update_stock=True,
+ )
+
+ pi.load_from_db()
+ batch_no = pi.items[0].batch_no
+ self.assertTrue(batch_no)
+
+ frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1))
+
+ return_pi = make_return_doc(pi.doctype, pi.name)
+ return_pi.save().submit()
+
+ self.assertTrue(return_pi.docstatus == 1)
+
+
+def prepare_data_for_internal_transfer():
+ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
+ from erpnext.selling.doctype.customer.test_customer import create_internal_customer
+
+ company = "_Test Company with perpetual inventory"
+
+ create_internal_customer(
+ "_Test Internal Customer 2",
+ company,
+ company,
+ )
+
+ create_internal_supplier(
+ "_Test Internal Supplier 2",
+ company,
+ company,
+ )
+
+ if not frappe.db.get_value("Company", company, "unrealized_profit_loss_account"):
+ account = "Unrealized Profit and Loss - TCP1"
+ if not frappe.db.exists("Account", account):
+ frappe.get_doc(
+ {
+ "doctype": "Account",
+ "account_name": "Unrealized Profit and Loss",
+ "parent_account": "Direct Income - TCP1",
+ "company": company,
+ "is_group": 0,
+ "account_type": "Income Account",
+ }
+ ).insert()
+
+ frappe.db.set_value("Company", company, "unrealized_profit_loss_account", account)
+
def get_sl_entries(voucher_type, voucher_no):
return frappe.db.sql(
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index 5a04e7d2eece..3d30533a5e70 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -738,6 +738,7 @@
"oldfieldname": "valuation_rate",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
+ "precision": "6",
"print_hide": 1,
"print_width": "80px",
"read_only": 1,
@@ -992,7 +993,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2022-07-28 19:27:54.880781",
+ "modified": "2022-10-12 03:37:59.516609",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json
index edfe7e98b2e8..db9322f32632 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json
@@ -10,6 +10,7 @@
"naming_series",
"report_date",
"status",
+ "manual_inspection",
"column_break_4",
"inspection_type",
"reference_type",
@@ -231,6 +232,12 @@
"label": "Status",
"options": "\nAccepted\nRejected",
"reqd": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "manual_inspection",
+ "fieldtype": "Check",
+ "label": "Manual Inspection"
}
],
"icon": "fa fa-search",
@@ -238,10 +245,11 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-12-18 19:59:55.710300",
+ "modified": "2022-10-04 22:00:13.995221",
"modified_by": "Administrator",
"module": "Stock",
"name": "Quality Inspection",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
@@ -262,5 +270,6 @@
"search_fields": "item_code, report_date, reference_name",
"show_name_in_global_search": 1,
"sort_field": "modified",
- "sort_order": "ASC"
+ "sort_order": "ASC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
index 331d3e812b24..9321c2c166b5 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
@@ -6,7 +6,7 @@
from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
-from frappe.utils import cint, flt
+from frappe.utils import cint, cstr, flt
from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template import (
get_template_details,
@@ -30,6 +30,9 @@ def validate(self):
if self.readings:
self.inspect_and_set_status()
+ def before_submit(self):
+ self.validate_readings_status_mandatory()
+
@frappe.whitelist()
def get_item_specification_details(self):
if not self.quality_inspection_template:
@@ -65,6 +68,11 @@ def on_submit(self):
def on_cancel(self):
self.update_qc_reference()
+ def validate_readings_status_mandatory(self):
+ for reading in self.readings:
+ if not reading.status:
+ frappe.throw(_("Row #{0}: Status is mandatory").format(reading.idx))
+
def update_qc_reference(self):
quality_inspection = self.name if self.docstatus == 1 else ""
@@ -124,6 +132,16 @@ def inspect_and_set_status(self):
# if not formula based check acceptance values set
self.set_status_based_on_acceptance_values(reading)
+ if not self.manual_inspection:
+ self.status = "Accepted"
+ for reading in self.readings:
+ if reading.status == "Rejected":
+ self.status = "Rejected"
+ frappe.msgprint(
+ _("Status set to rejected as there are one or more rejected readings."), alert=True
+ )
+ break
+
def set_status_based_on_acceptance_values(self, reading):
if not cint(reading.numeric):
result = reading.get("reading_value") == reading.get("value")
@@ -201,68 +219,71 @@ def calculate_mean(self, reading):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def item_query(doctype, txt, searchfield, start, page_len, filters):
- if filters.get("from"):
- from frappe.desk.reportview import get_match_cond
-
- mcond = get_match_cond(filters["from"])
- cond, qi_condition = "", "and (quality_inspection is null or quality_inspection = '')"
-
- if filters.get("parent"):
- if (
- filters.get("from") in ["Purchase Invoice Item", "Purchase Receipt Item"]
- and filters.get("inspection_type") != "In Process"
- ):
- cond = """and item_code in (select name from `tabItem` where
- inspection_required_before_purchase = 1)"""
- elif (
- filters.get("from") in ["Sales Invoice Item", "Delivery Note Item"]
- and filters.get("inspection_type") != "In Process"
- ):
- cond = """and item_code in (select name from `tabItem` where
- inspection_required_before_delivery = 1)"""
- elif filters.get("from") == "Stock Entry Detail":
- cond = """and s_warehouse is null"""
-
- if filters.get("from") in ["Supplier Quotation Item"]:
- qi_condition = ""
-
- return frappe.db.sql(
- """
- SELECT item_code
- FROM `tab{doc}`
- WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s
- {qi_condition} {cond} {mcond}
- ORDER BY item_code limit {start}, {page_len}
- """.format(
- doc=filters.get("from"),
- cond=cond,
- mcond=mcond,
- start=start,
- page_len=page_len,
- qi_condition=qi_condition,
- ),
- {"parent": filters.get("parent"), "txt": "%%%s%%" % txt},
- )
-
- elif filters.get("reference_name"):
- return frappe.db.sql(
- """
- SELECT production_item
- FROM `tab{doc}`
- WHERE name = %(reference_name)s and docstatus < 2 and production_item like %(txt)s
- {qi_condition} {cond} {mcond}
- ORDER BY production_item
- LIMIT {start}, {page_len}
- """.format(
- doc=filters.get("from"),
- cond=cond,
- mcond=mcond,
- start=start,
- page_len=page_len,
- qi_condition=qi_condition,
- ),
- {"reference_name": filters.get("reference_name"), "txt": "%%%s%%" % txt},
- )
+ from frappe.desk.reportview import get_match_cond
+
+ from_doctype = cstr(filters.get("doctype"))
+ if not from_doctype or not frappe.db.exists("DocType", from_doctype):
+ return []
+
+ mcond = get_match_cond(from_doctype)
+ cond, qi_condition = "", "and (quality_inspection is null or quality_inspection = '')"
+
+ if filters.get("parent"):
+ if (
+ from_doctype in ["Purchase Invoice Item", "Purchase Receipt Item"]
+ and filters.get("inspection_type") != "In Process"
+ ):
+ cond = """and item_code in (select name from `tabItem` where
+ inspection_required_before_purchase = 1)"""
+ elif (
+ from_doctype in ["Sales Invoice Item", "Delivery Note Item"]
+ and filters.get("inspection_type") != "In Process"
+ ):
+ cond = """and item_code in (select name from `tabItem` where
+ inspection_required_before_delivery = 1)"""
+ elif from_doctype == "Stock Entry Detail":
+ cond = """and s_warehouse is null"""
+
+ if from_doctype in ["Supplier Quotation Item"]:
+ qi_condition = ""
+
+ return frappe.db.sql(
+ """
+ SELECT item_code
+ FROM `tab{doc}`
+ WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s
+ {qi_condition} {cond} {mcond}
+ ORDER BY item_code limit {page_len} offset {start}
+ """.format(
+ doc=from_doctype,
+ cond=cond,
+ mcond=mcond,
+ start=cint(start),
+ page_len=cint(page_len),
+ qi_condition=qi_condition,
+ ),
+ {"parent": filters.get("parent"), "txt": "%%%s%%" % txt},
+ )
+
+ elif filters.get("reference_name"):
+ return frappe.db.sql(
+ """
+ SELECT production_item
+ FROM `tab{doc}`
+ WHERE name = %(reference_name)s and docstatus < 2 and production_item like %(txt)s
+ {qi_condition} {cond} {mcond}
+ ORDER BY production_item
+ limit {page_len} offset {start}
+ """.format(
+ doc=from_doctype,
+ cond=cond,
+ mcond=mcond,
+ start=cint(start),
+ page_len=cint(page_len),
+ qi_condition=qi_condition,
+ ),
+ {"reference_name": filters.get("reference_name"), "txt": "%%%s%%" % txt},
+ )
@frappe.whitelist()
diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
index 144f13880b16..4f19643ad526 100644
--- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
@@ -160,7 +160,7 @@ def test_rejected_qi_validation(self):
)
readings = [
- {"specification": "Iron Content", "min_value": 0.1, "max_value": 0.9, "reading_1": "0.4"}
+ {"specification": "Iron Content", "min_value": 0.1, "max_value": 0.9, "reading_1": "1.0"}
]
qa = create_quality_inspection(
@@ -184,6 +184,38 @@ def test_rejected_qi_validation(self):
se.cancel()
frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Stop")
+ def test_qi_status(self):
+ make_stock_entry(
+ item_code="_Test Item with QA", target="_Test Warehouse - _TC", qty=1, basic_rate=100
+ )
+ dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
+ qa = create_quality_inspection(
+ reference_type="Delivery Note", reference_name=dn.name, status="Accepted", do_not_save=True
+ )
+ qa.readings[0].manual_inspection = 1
+ qa.save()
+
+ # Case - 1: When there are one or more readings with rejected status and parent manual inspection is unchecked, then parent status should be set to rejected.
+ qa.status = "Accepted"
+ qa.manual_inspection = 0
+ qa.readings[0].status = "Rejected"
+ qa.save()
+ self.assertEqual(qa.status, "Rejected")
+
+ # Case - 2: When all readings have accepted status and parent manual inspection is unchecked, then parent status should be set to accepted.
+ qa.status = "Rejected"
+ qa.manual_inspection = 0
+ qa.readings[0].status = "Accepted"
+ qa.save()
+ self.assertEqual(qa.status, "Accepted")
+
+ # Case - 3: When parent manual inspection is checked, then parent status should not be changed.
+ qa.status = "Accepted"
+ qa.manual_inspection = 1
+ qa.readings[0].status = "Rejected"
+ qa.save()
+ self.assertEqual(qa.status, "Accepted")
+
def create_quality_inspection(**args):
args = frappe._dict(args)
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js
index e53476e0dd32..e78e57cd275d 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js
@@ -34,6 +34,22 @@ frappe.ui.form.on('Repost Item Valuation', {
frm.trigger('setup_realtime_progress');
},
+ based_on: function(frm) {
+ var fields_to_reset = [];
+
+ if (frm.doc.based_on == 'Transaction') {
+ fields_to_reset = ['item_code', 'warehouse'];
+ } else if (frm.doc.based_on == 'Item and Warehouse') {
+ fields_to_reset = ['voucher_type', 'voucher_no'];
+ }
+
+ if (fields_to_reset) {
+ fields_to_reset.forEach(field => {
+ frm.set_value(field, undefined);
+ });
+ }
+ },
+
setup_realtime_progress: function(frm) {
frappe.realtime.on('item_reposting_progress', data => {
if (frm.doc.name !== data.name) {
@@ -58,6 +74,21 @@ frappe.ui.form.on('Repost Item Valuation', {
}
frm.trigger('show_reposting_progress');
+
+ if (frm.doc.status === 'Queued' && frm.doc.docstatus === 1) {
+ frm.trigger('execute_reposting');
+ }
+ },
+
+ execute_reposting(frm) {
+ frm.add_custom_button(__("Start Reposting"), () => {
+ frappe.call({
+ method: 'erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.execute_repost_item_valuation',
+ callback: function() {
+ frappe.msgprint(__('Reposting has been started in the background.'));
+ }
+ });
+ });
},
show_reposting_progress: function(frm) {
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json
index 2c97d0f51731..d07366197610 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json
@@ -50,13 +50,15 @@
"fieldname": "posting_date",
"fieldtype": "Date",
"label": "Posting Date",
+ "read_only_depends_on": "eval: doc.based_on == \"Transaction\"",
"reqd": 1
},
{
"fetch_from": "voucher_no.posting_time",
"fieldname": "posting_time",
"fieldtype": "Time",
- "label": "Posting Time"
+ "label": "Posting Time",
+ "read_only_depends_on": "eval: doc.based_on == \"Transaction\""
},
{
"default": "Queued",
@@ -195,7 +197,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-06-13 12:20:22.182322",
+ "modified": "2022-11-28 16:00:05.637440",
"modified_by": "Administrator",
"module": "Stock",
"name": "Repost Item Valuation",
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 ee4d7bd8643b..14f5e548eccf 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -128,6 +128,9 @@ def repost(doc):
if not frappe.db.exists("Repost Item Valuation", doc.name):
return
+ # This is to avoid TooManyWritesError in case of large reposts
+ frappe.db.MAX_WRITES_PER_TRANSACTION *= 4
+
doc.set_status("In Progress")
if not frappe.flags.in_test:
frappe.db.commit()
@@ -302,3 +305,9 @@ def in_configured_timeslot(repost_settings=None, current_time=None):
return end_time >= now_time >= start_time
else:
return now_time >= start_time or now_time <= end_time
+
+
+@frappe.whitelist()
+def execute_repost_item_valuation():
+ """Execute repost item valuation via scheduler."""
+ frappe.get_doc("Scheduled Job Type", "repost_item_valuation.repost_entries").enqueue(force=True)
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index cfa5cee453a0..76029f0e0017 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -864,16 +864,15 @@ def get_pos_reserved_serial_nos(filters):
pos_transacted_sr_nos = query.run(as_dict=True)
- reserved_sr_nos = []
- returned_sr_nos = []
+ reserved_sr_nos = set()
+ returned_sr_nos = set()
for d in pos_transacted_sr_nos:
if d.is_return == 0:
- reserved_sr_nos += get_serial_nos(d.serial_no)
+ [reserved_sr_nos.add(x) for x in get_serial_nos(d.serial_no)]
elif d.is_return == 1:
- returned_sr_nos += get_serial_nos(d.serial_no)
+ [returned_sr_nos.add(x) for x in get_serial_nos(d.serial_no)]
- for sr_no in returned_sr_nos:
- reserved_sr_nos.remove(sr_no)
+ reserved_sr_nos = list(reserved_sr_nos - returned_sr_nos)
return reserved_sr_nos
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 9cc8e237b6a4..3503e3e4b294 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -1050,7 +1050,8 @@ erpnext.stock.select_batch_and_serial_no = (frm, item) => {
if (frm.doc.purpose === 'Material Receipt') return;
frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", function() {
- new erpnext.SerialNoBatchSelector({
+ if (frm.batch_selector?.dialog?.display) return;
+ frm.batch_selector = new erpnext.SerialNoBatchSelector({
frm: frm,
item: item,
warehouse_details: get_warehouse_type_and_name(item),
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index fa7900bd3b2a..3da249749198 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -879,6 +879,27 @@ def validate_purchase_order(self):
se_item.idx, se_item.item_code, total_allowed, self.purchase_order
)
)
+ elif not se_item.get("po_detail"):
+ filters = {
+ "parent": self.purchase_order,
+ "docstatus": 1,
+ "rm_item_code": se_item.item_code,
+ "main_item_code": se_item.subcontracted_item,
+ }
+
+ order_rm_detail = frappe.db.get_value("Purchase Order Item Supplied", filters, "name")
+ if order_rm_detail:
+ se_item.db_set("po_detail", order_rm_detail)
+ else:
+ if not se_item.allow_alternative_item:
+ frappe.throw(
+ _("Row {0}# Item {1} not found in 'Raw Materials Supplied' table in {2} {3}").format(
+ se_item.idx,
+ se_item.item_code,
+ "Purchase Order",
+ self.purchase_order,
+ )
+ )
elif backflush_raw_materials_based_on == "Material Transferred for Subcontract":
for row in self.items:
if not row.subcontracted_item:
@@ -994,8 +1015,8 @@ def validate_finished_goods(self):
# No work order could mean independent Manufacture entry, if so skip validation
if self.work_order and self.fg_completed_qty > allowed_qty:
frappe.throw(
- _("For quantity {0} should not be greater than work order quantity {1}").format(
- flt(self.fg_completed_qty), wo_qty
+ _("For quantity {0} should not be greater than allowed quantity {1}").format(
+ flt(self.fg_completed_qty), allowed_qty
)
)
@@ -1453,6 +1474,7 @@ def set_batchwise_finished_goods(self, args, item):
"reference_name": self.pro_doc.name,
"reference_doctype": self.pro_doc.doctype,
"qty_to_produce": (">", 0),
+ "batch_qty": ("=", 0),
}
fields = ["qty_to_produce as qty", "produced_qty", "name"]
@@ -2170,16 +2192,16 @@ def update_items_for_process_loss(self):
d.qty -= process_loss_dict[d.item_code][1]
def set_serial_no_batch_for_finished_good(self):
- args = {}
+ serial_nos = []
if self.pro_doc.serial_no:
- self.get_serial_nos_for_fg(args)
+ serial_nos = self.get_serial_nos_for_fg() or []
for row in self.items:
if row.is_finished_item and row.item_code == self.pro_doc.production_item:
- if args.get("serial_no"):
- row.serial_no = "\n".join(args["serial_no"][0 : cint(row.qty)])
+ if serial_nos:
+ row.serial_no = "\n".join(serial_nos[0 : cint(row.qty)])
- def get_serial_nos_for_fg(self, args):
+ def get_serial_nos_for_fg(self):
fields = [
"`tabStock Entry`.`name`",
"`tabStock Entry Detail`.`qty`",
@@ -2190,14 +2212,12 @@ def get_serial_nos_for_fg(self, args):
filters = [
["Stock Entry", "work_order", "=", self.work_order],
["Stock Entry", "purpose", "=", "Manufacture"],
- ["Stock Entry", "docstatus", "=", 1],
+ ["Stock Entry", "docstatus", "<", 2],
["Stock Entry Detail", "item_code", "=", self.pro_doc.production_item],
]
stock_entries = frappe.get_all("Stock Entry", fields=fields, filters=filters)
-
- if self.pro_doc.serial_no:
- args["serial_no"] = self.get_available_serial_nos(stock_entries)
+ return self.get_available_serial_nos(stock_entries)
def get_available_serial_nos(self, stock_entries):
used_serial_nos = []
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 64ea0435e168..4fe3cc1fdab9 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -5,7 +5,7 @@
import frappe
from frappe.permissions import add_user_permission, remove_user_permission
from frappe.tests.utils import FrappeTestCase, change_settings
-from frappe.utils import add_days, flt, nowdate, nowtime, today
+from frappe.utils import add_days, flt, now, nowdate, nowtime, today
from six import iteritems
from erpnext.accounts.doctype.account.test_account import get_inventory_account
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 5c1da420e242..854b22a6fcef 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -153,6 +153,11 @@ def scrub_posting_time(self):
def validate_batch(self):
if self.batch_no and self.voucher_type != "Stock Entry":
+ if (self.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and self.actual_qty < 0) or (
+ self.voucher_type in ["Delivery Note", "Sales Invoice"] and self.actual_qty > 0
+ ):
+ return
+
expiry_date = frappe.db.get_value("Batch", self.batch_no, "expiry_date")
if expiry_date:
if getdate(self.posting_date) > getdate(expiry_date):
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 e810c7933cc7..1ec229d2e1ff 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
@@ -902,13 +902,15 @@ def create_product_bundle_item(new_item_code, packed_items):
item.save()
-def create_items():
- items = [
- "_Test Item for Reposting",
- "_Test Finished Item for Reposting",
- "_Test Subcontracted Item for Reposting",
- "_Test Bundled Item for Reposting",
- ]
+def create_items(items=None, uoms=None):
+ if not items:
+ items = [
+ "_Test Item for Reposting",
+ "_Test Finished Item for Reposting",
+ "_Test Subcontracted Item for Reposting",
+ "_Test Bundled Item for Reposting",
+ ]
+
for d in items:
properties = {"valuation_method": "FIFO"}
if d == "_Test Bundled Item for Reposting":
@@ -916,7 +918,7 @@ def create_items():
elif d == "_Test Subcontracted Item for Reposting":
properties.update({"is_sub_contracted_item": 1})
- make_item(d, properties=properties)
+ make_item(d, properties=properties, uoms=uoms)
return items
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 87cc8c6a957d..b8ba53475108 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -131,7 +131,9 @@ def _get_msg(row_num, msg):
key.append(row.get(field))
if key in item_warehouse_combinations:
- self.validation_messages.append(_get_msg(row_num, _("Duplicate entry")))
+ self.validation_messages.append(
+ _get_msg(row_num, _("Same item and warehouse combination already entered."))
+ )
else:
item_warehouse_combinations.append(key)
@@ -227,7 +229,7 @@ def update_stock_ledger(self):
if item.has_serial_no or item.has_batch_no:
has_serial_no = True
- self.get_sle_for_serialized_items(row, sl_entries)
+ self.get_sle_for_serialized_items(row, sl_entries, item)
else:
if row.serial_no or row.batch_no:
frappe.throw(
@@ -279,7 +281,7 @@ def update_stock_ledger(self):
if has_serial_no and sl_entries:
self.update_valuation_rate_for_serial_no()
- def get_sle_for_serialized_items(self, row, sl_entries):
+ def get_sle_for_serialized_items(self, row, sl_entries, item):
from erpnext.stock.stock_ledger import get_previous_sle
serial_nos = get_serial_nos(row.serial_no)
@@ -345,6 +347,9 @@ def get_sle_for_serialized_items(self, row, sl_entries):
if row.qty:
args = self.get_sle_for_items(row)
+ if item.has_serial_no and item.has_batch_no:
+ args["qty_after_transaction"] = row.qty
+
args.update(
{
"actual_qty": row.qty,
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 190ae9edaf99..a7f5f4a8ae20 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -643,6 +643,38 @@ def test_serial_no_creation_and_inactivation(self):
)
self.assertEqual(len(active_sr_no), 0)
+ def test_serial_no_batch_no_item(self):
+ item = self.make_item(
+ "Test Serial No Batch No Item",
+ {
+ "is_stock_item": 1,
+ "has_serial_no": 1,
+ "has_batch_no": 1,
+ "serial_no_series": "SRS9.####",
+ "batch_number_series": "BNS9.####",
+ "create_new_batch": 1,
+ },
+ )
+
+ warehouse = "_Test Warehouse - _TC"
+
+ sr = create_stock_reconciliation(
+ item_code=item.name,
+ warehouse=warehouse,
+ qty=1,
+ rate=100,
+ )
+
+ sl_entry = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Stock Reconciliation", "voucher_no": sr.name},
+ ["actual_qty", "qty_after_transaction"],
+ as_dict=1,
+ )
+
+ self.assertEqual(flt(sl_entry.actual_qty), 1.0)
+ self.assertEqual(flt(sl_entry.qty_after_transaction), 1.0)
+
def create_batch_item_with_batch(item_name, batch_id):
batch_item_doc = create_item(item_name, is_stock_item=1)
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 4db12dcb98bf..6fb9205d4b90 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -102,9 +102,11 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
elif out.get("warehouse"):
if doc and doc.get("doctype") == "Purchase Order":
# calculate company_total_stock only for po
- bin_details = get_bin_details(args.item_code, out.warehouse, args.company)
+ bin_details = get_bin_details(
+ args.item_code, out.warehouse, args.company, include_child_warehouses=True
+ )
else:
- bin_details = get_bin_details(args.item_code, out.warehouse)
+ bin_details = get_bin_details(args.item_code, out.warehouse, include_child_warehouses=True)
out.update(bin_details)
@@ -315,6 +317,9 @@ def get_basic_details(args, item, overwrite_warehouse=True):
else:
args.uom = item.stock_uom
+ # Set stock UOM in args, so that it can be used while fetching item price
+ args.stock_uom = item.stock_uom
+
if args.get("batch_no") and item.name != frappe.get_cached_value(
"Batch", args.get("batch_no"), "item"
):
@@ -810,9 +815,9 @@ def insert_item_price(args):
):
if frappe.has_permission("Item Price", "write"):
price_list_rate = (
- (args.rate + args.discount_amount) / args.get("conversion_factor")
+ (flt(args.rate) + flt(args.discount_amount)) / args.get("conversion_factor")
if args.get("conversion_factor")
- else (args.rate + args.discount_amount)
+ else (flt(args.rate) + flt(args.discount_amount))
)
item_price = frappe.db.get_value(
@@ -1042,7 +1047,9 @@ def get_pos_profile_item_details(company, args, pos_profile=None, update_data=Fa
res[fieldname] = pos_profile.get(fieldname)
if res.get("warehouse"):
- res.actual_qty = get_bin_details(args.item_code, res.warehouse).get("actual_qty")
+ res.actual_qty = get_bin_details(
+ args.item_code, res.warehouse, include_child_warehouses=True
+ ).get("actual_qty")
return res
@@ -1153,16 +1160,30 @@ def get_projected_qty(item_code, warehouse):
@frappe.whitelist()
-def get_bin_details(item_code, warehouse, company=None):
- bin_details = frappe.db.get_value(
- "Bin",
- {"item_code": item_code, "warehouse": warehouse},
- ["projected_qty", "actual_qty", "reserved_qty"],
- as_dict=True,
- cache=True,
- ) or {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0}
+def get_bin_details(item_code, warehouse, company=None, include_child_warehouses=False):
+ bin_details = {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0}
+
+ if warehouse:
+ from frappe.query_builder.functions import Coalesce, Sum
+
+ from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
+
+ warehouses = get_child_warehouses(warehouse) if include_child_warehouses else [warehouse]
+
+ bin = frappe.qb.DocType("Bin")
+ bin_details = (
+ frappe.qb.from_(bin)
+ .select(
+ Coalesce(Sum(bin.projected_qty), 0).as_("projected_qty"),
+ Coalesce(Sum(bin.actual_qty), 0).as_("actual_qty"),
+ Coalesce(Sum(bin.reserved_qty), 0).as_("reserved_qty"),
+ )
+ .where((bin.item_code == item_code) & (bin.warehouse.isin(warehouses)))
+ ).run(as_dict=True)[0]
+
if company:
bin_details["company_total_stock"] = get_company_total_stock(item_code, company)
+
return bin_details
diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
index 8a13300dc836..949452d43bad 100644
--- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
+++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
@@ -74,10 +74,21 @@ def get_conditions(filters):
else:
frappe.throw(_("'To Date' is required"))
- for field in ["item_code", "warehouse", "batch_no", "company"]:
+ for field in ["item_code", "batch_no", "company"]:
if filters.get(field):
conditions += " and {0} = {1}".format(field, frappe.db.escape(filters.get(field)))
+ if filters.get("warehouse"):
+ warehouse_details = frappe.db.get_value(
+ "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
+ )
+ if warehouse_details:
+ conditions += (
+ " and exists (select name from `tabWarehouse` wh \
+ where wh.lft >= %s and wh.rgt <= %s and sle.warehouse = wh.name)"
+ % (warehouse_details.lft, warehouse_details.rgt)
+ )
+
return conditions
@@ -87,7 +98,7 @@ def get_stock_ledger_entries(filters):
return frappe.db.sql(
"""
select item_code, batch_no, warehouse, posting_date, sum(actual_qty) as actual_qty
- from `tabStock Ledger Entry`
+ from `tabStock Ledger Entry` as sle
where is_cancelled = 0 and docstatus < 2 and ifnull(batch_no, '') != '' %s
group by voucher_no, batch_no, item_code, warehouse
order by item_code, warehouse"""
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 15f211127d7d..46bcd94e278b 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
@@ -4,6 +4,8 @@
import frappe
from frappe import _
+from frappe.query_builder import Field
+from frappe.query_builder.functions import Min, Timestamp
from frappe.utils import add_days, getdate, today
from six import iteritems
@@ -29,7 +31,7 @@ def execute(filters=None):
def get_unsync_date(filters):
date = filters.from_date
if not date:
- date = frappe.db.sql(""" SELECT min(posting_date) from `tabStock Ledger Entry`""")
+ date = (frappe.qb.from_("Stock Ledger Entry").select(Min(Field("posting_date")))).run()
date = date[0][0]
if not date:
@@ -55,22 +57,27 @@ def get_data(report_filters):
result = []
voucher_wise_dict = {}
- data = frappe.db.sql(
- """
- SELECT
- name, posting_date, posting_time, voucher_type, voucher_no,
- stock_value_difference, stock_value, warehouse, item_code
- FROM
- `tabStock Ledger Entry`
- WHERE
- posting_date
- = %s and company = %s
- and is_cancelled = 0
- ORDER BY timestamp(posting_date, posting_time) asc, creation asc
- """,
- (from_date, report_filters.company),
- as_dict=1,
- )
+ sle = frappe.qb.DocType("Stock Ledger Entry")
+ data = (
+ frappe.qb.from_(sle)
+ .select(
+ sle.name,
+ sle.posting_date,
+ sle.posting_time,
+ sle.voucher_type,
+ sle.voucher_no,
+ sle.stock_value_difference,
+ sle.stock_value,
+ sle.warehouse,
+ sle.item_code,
+ )
+ .where(
+ (sle.posting_date == from_date)
+ & (sle.company == report_filters.company)
+ & (sle.is_cancelled == 0)
+ )
+ .orderby(Timestamp(sle.posting_date, sle.posting_time), sle.creation)
+ ).run(as_dict=True)
for d in data:
voucher_wise_dict.setdefault((d.item_code, d.warehouse), []).append(d)
diff --git a/erpnext/stock/report/item_price_stock/item_price_stock.py b/erpnext/stock/report/item_price_stock/item_price_stock.py
index 15218e63a876..1b07f596c7b3 100644
--- a/erpnext/stock/report/item_price_stock/item_price_stock.py
+++ b/erpnext/stock/report/item_price_stock/item_price_stock.py
@@ -62,22 +62,28 @@ def get_data(filters, columns):
def get_item_price_qty_data(filters):
- conditions = ""
- if filters.get("item_code"):
- conditions += "where a.item_code=%(item_code)s"
-
- item_results = frappe.db.sql(
- """select a.item_code, a.item_name, a.name as price_list_name,
- a.brand as brand, b.warehouse as warehouse, b.actual_qty as actual_qty
- from `tabItem Price` a left join `tabBin` b
- ON a.item_code = b.item_code
- {conditions}""".format(
- conditions=conditions
- ),
- filters,
- as_dict=1,
+ item_price = frappe.qb.DocType("Item Price")
+ bin = frappe.qb.DocType("Bin")
+
+ query = (
+ frappe.qb.from_(item_price)
+ .left_join(bin)
+ .on(item_price.item_code == bin.item_code)
+ .select(
+ item_price.item_code,
+ item_price.item_name,
+ item_price.name.as_("price_list_name"),
+ item_price.brand.as_("brand"),
+ bin.warehouse.as_("warehouse"),
+ bin.actual_qty.as_("actual_qty"),
+ )
)
+ if filters.get("item_code"):
+ query = query.where(item_price.item_code == filters.get("item_code"))
+
+ item_results = query.run(as_dict=True)
+
price_list_names = list(set(item.price_list_name for item in item_results))
buying_price_map = get_price_map(price_list_names, buying=1)
diff --git a/erpnext/stock/report/item_shortage_report/item_shortage_report.py b/erpnext/stock/report/item_shortage_report/item_shortage_report.py
index 03a3a6a0b83b..9fafe91c3f96 100644
--- a/erpnext/stock/report/item_shortage_report/item_shortage_report.py
+++ b/erpnext/stock/report/item_shortage_report/item_shortage_report.py
@@ -8,8 +8,7 @@
def execute(filters=None):
columns = get_columns()
- conditions = get_conditions(filters)
- data = get_data(conditions, filters)
+ data = get_data(filters)
if not data:
return [], [], None, []
@@ -19,49 +18,39 @@ def execute(filters=None):
return columns, data, None, chart_data
-def get_conditions(filters):
- conditions = ""
+def get_data(filters):
+ bin = frappe.qb.DocType("Bin")
+ wh = frappe.qb.DocType("Warehouse")
+ item = frappe.qb.DocType("Item")
- if filters.get("warehouse"):
- conditions += "AND warehouse in %(warehouse)s"
- if filters.get("company"):
- conditions += "AND company = %(company)s"
-
- return conditions
-
-
-def get_data(conditions, filters):
- data = frappe.db.sql(
- """
- SELECT
+ query = (
+ frappe.qb.from_(bin)
+ .from_(wh)
+ .from_(item)
+ .select(
bin.warehouse,
bin.item_code,
- bin.actual_qty ,
- bin.ordered_qty ,
- bin.planned_qty ,
- bin.reserved_qty ,
+ bin.actual_qty,
+ bin.ordered_qty,
+ bin.planned_qty,
+ bin.reserved_qty,
bin.reserved_qty_for_production,
- bin.projected_qty ,
- warehouse.company,
- item.item_name ,
- item.description
- FROM
- `tabBin` bin,
- `tabWarehouse` warehouse,
- `tabItem` item
- WHERE
- bin.projected_qty<0
- AND warehouse.name = bin.warehouse
- AND bin.item_code=item.name
- {0}
- ORDER BY bin.projected_qty;""".format(
- conditions
- ),
- filters,
- as_dict=1,
+ bin.projected_qty,
+ wh.company,
+ item.item_name,
+ item.description,
+ )
+ .where((bin.projected_qty < 0) & (wh.name == bin.warehouse) & (bin.item_code == item.name))
+ .orderby(bin.projected_qty)
)
- return data
+ if filters.get("warehouse"):
+ query = query.where(bin.warehouse.isin(filters.get("warehouse")))
+
+ if filters.get("company"):
+ query = query.where(wh.company == filters.get("company"))
+
+ return query.run(as_dict=True)
def get_chart_data(data):
diff --git a/erpnext/stock/report/item_shortage_report/test_item_shortage_report.py b/erpnext/stock/report/item_shortage_report/test_item_shortage_report.py
new file mode 100644
index 000000000000..5884c32acc7a
--- /dev/null
+++ b/erpnext/stock/report/item_shortage_report/test_item_shortage_report.py
@@ -0,0 +1,51 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+
+from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
+from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.stock.report.item_shortage_report.item_shortage_report import (
+ execute as item_shortage_report,
+)
+
+
+class TestItemShortageReport(FrappeTestCase):
+ def test_item_shortage_report(self):
+ item = make_item().name
+ so = make_sales_order(item_code=item)
+
+ reserved_qty, projected_qty = frappe.db.get_value(
+ "Bin",
+ {
+ "item_code": item,
+ "warehouse": so.items[0].warehouse,
+ },
+ ["reserved_qty", "projected_qty"],
+ )
+ self.assertEqual(reserved_qty, so.items[0].qty)
+ self.assertEqual(projected_qty, -(so.items[0].qty))
+
+ filters = {
+ "company": so.company,
+ }
+ report_data = item_shortage_report(filters)[1]
+ item_code_list = [row.get("item_code") for row in report_data]
+ self.assertIn(item, item_code_list)
+
+ filters = {
+ "company": so.company,
+ "warehouse": [so.items[0].warehouse],
+ }
+ report_data = item_shortage_report(filters)[1]
+ item_code_list = [row.get("item_code") for row in report_data]
+ self.assertIn(item, item_code_list)
+
+ filters = {
+ "company": so.company,
+ "warehouse": ["Work In Progress - _TC"],
+ }
+ report_data = item_shortage_report(filters)[1]
+ item_code_list = [row.get("item_code") for row in report_data]
+ self.assertNotIn(item, item_code_list)
diff --git a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py
index f308e9e41f11..1c54d775a098 100644
--- a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py
+++ b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py
@@ -75,6 +75,7 @@ def get_item_info(filters):
if filters.get("brand"):
conditions.append("item.brand=%(brand)s")
conditions.append("is_stock_item = 1")
+ conditions.append("disabled = 0")
return frappe.db.sql(
"""select name, item_name, description, brand, item_group,
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py
index 1956238331e0..7c430e491abd 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/stock_ageing.py
@@ -34,6 +34,9 @@ def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> Li
precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True))
for item, item_dict in item_details.items():
+ if not flt(item_dict.get("total_qty"), precision):
+ continue
+
earliest_age, latest_age = 0, 0
details = item_dict["details"]
diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py
index 99f820ecac62..106e877c4cd4 100644
--- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py
+++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py
@@ -41,7 +41,7 @@ def get_data(report_filters):
key = (d.voucher_type, d.voucher_no)
gl_data = voucher_wise_gl_data.get(key) or {}
d.account_value = gl_data.get("account_value", 0)
- d.difference_value = abs(d.stock_value - d.account_value)
+ d.difference_value = d.stock_value - d.account_value
if abs(d.difference_value) > 0.1:
data.append(d)
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py
index b1fdaacac9d8..7ca771f0a73a 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.py
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.py
@@ -355,7 +355,7 @@ def get_opening_balance(filters, columns, sl_entries):
)
# check if any SLEs are actually Opening Stock Reconciliation
- for sle in sl_entries:
+ for sle in list(sl_entries):
if (
sle.get("voucher_type") == "Stock Reconciliation"
and sle.get("date").split()[0] == filters.from_date
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 270acb869c50..4e0528e536da 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -532,6 +532,14 @@ def process_sle(self, sle):
if not self.args.get("sle_id"):
self.get_dynamic_incoming_outgoing_rate(sle)
+ if (
+ sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
+ and sle.voucher_detail_no
+ and sle.actual_qty < 0
+ and frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_internal_supplier")
+ ):
+ sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle)
+
if get_serial_nos(sle.serial_no):
self.get_serialized_values(sle)
self.wh_data.qty_after_transaction += flt(sle.actual_qty)
@@ -579,6 +587,7 @@ def process_sle(self, sle):
sle.stock_queue = json.dumps(self.wh_data.stock_queue)
sle.stock_value_difference = stock_value_difference
sle.doctype = "Stock Ledger Entry"
+
frappe.get_doc(sle).db_update()
if not self.args.get("sle_id"):
@@ -638,21 +647,10 @@ def get_incoming_outgoing_rate_from_transaction(self, sle):
elif (
sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
- and sle.actual_qty > 0
+ and sle.voucher_detail_no
and frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_internal_supplier")
):
- sle_details = frappe.db.get_value(
- "Stock Ledger Entry",
- {
- "voucher_type": sle.voucher_type,
- "voucher_no": sle.voucher_no,
- "dependant_sle_voucher_detail_no": sle.voucher_detail_no,
- },
- ["stock_value_difference", "actual_qty"],
- as_dict=1,
- )
-
- rate = abs(sle_details.stock_value_difference / sle.actual_qty)
+ rate = get_incoming_rate_for_inter_company_transfer(sle)
else:
if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
rate_field = "valuation_rate"
@@ -727,9 +725,12 @@ def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
- frappe.db.set_value(
- sle.voucher_type + " Item", sle.voucher_detail_no, "base_net_rate", outgoing_rate
- )
+ if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value(
+ sle.voucher_type, sle.voucher_no, "is_internal_supplier"
+ ):
+ frappe.db.set_value(
+ f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate
+ )
else:
frappe.db.set_value(
"Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
@@ -1183,20 +1184,6 @@ def get_valuation_rate(
(item_code, warehouse, voucher_no, voucher_type),
)
- if not last_valuation_rate:
- # Get valuation rate from last sle for the item against any warehouse
- last_valuation_rate = frappe.db.sql(
- """select valuation_rate
- from `tabStock Ledger Entry` force index (item_code)
- where
- item_code = %s
- 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""",
- (item_code, voucher_no, voucher_type),
- )
-
if last_valuation_rate:
return flt(last_valuation_rate[0][0])
@@ -1473,3 +1460,25 @@ def _round_off_if_near_zero(number: float, precision: int = 6) -> float:
return 0.0
return flt(number)
+
+
+def get_incoming_rate_for_inter_company_transfer(sle) -> float:
+ """
+ For inter company transfer, incoming rate is the average of the outgoing rate
+ """
+ rate = 0.0
+
+ field = "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item"
+
+ doctype = "Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item"
+
+ reference_name = frappe.get_cached_value(sle.voucher_type + " Item", sle.voucher_detail_no, field)
+
+ if reference_name:
+ rate = frappe.get_cached_value(
+ doctype,
+ reference_name,
+ "incoming_rate",
+ )
+
+ return rate
diff --git a/erpnext/templates/generators/item/item.html b/erpnext/templates/generators/item/item.html
index 89436343c7a1..1f9f6317bb7f 100644
--- a/erpnext/templates/generators/item/item.html
+++ b/erpnext/templates/generators/item/item.html
@@ -32,7 +32,7 @@
-
+
{% if show_tabs and tabs %}
diff --git a/erpnext/templates/pages/order.html b/erpnext/templates/pages/order.html
index a10870db2787..ec1d49788bda 100644
--- a/erpnext/templates/pages/order.html
+++ b/erpnext/templates/pages/order.html
@@ -18,7 +18,7 @@ {{ doc.name }}
|