Merge pull request #30080 from deepeshgarg007/item_wise_sales_history
fix: Item-wise sales history report
diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py
index 9e2cdff..ab1061b 100644
--- a/erpnext/accounts/deferred_revenue.py
+++ b/erpnext/accounts/deferred_revenue.py
@@ -120,6 +120,7 @@
prev_gl_entry = frappe.db.sql('''
select name, posting_date from `tabGL Entry` where company=%s and account=%s and
voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
+ and is_cancelled = 0
order by posting_date desc limit 1
''', (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True)
@@ -227,6 +228,7 @@
gl_entries_details = frappe.db.sql('''
select sum({0}) as total_credit, sum({1}) as total_credit_in_account_currency, voucher_detail_no
from `tabGL Entry` where company=%s and account=%s and voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
+ and is_cancelled = 0
group by voucher_detail_no
'''.format(total_credit_debit, total_credit_debit_currency),
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True)
@@ -282,7 +284,7 @@
return
# check if books nor frozen till endate:
- if getdate(end_date) >= getdate(accounts_frozen_upto):
+ if accounts_frozen_upto and (end_date) <= getdate(accounts_frozen_upto):
end_date = get_last_day(add_days(accounts_frozen_upto, 1))
if via_journal_entry:
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index a9bc028..b2b818a 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -194,8 +194,14 @@
frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency));
frm.toggle_display("base_paid_amount", frm.doc.paid_from_account_currency != company_currency);
- frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges &&
- (frm.doc.paid_from_account_currency != company_currency));
+
+ if (frm.doc.payment_type == "Pay") {
+ frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges &&
+ (frm.doc.paid_to_account_currency != company_currency));
+ } else {
+ frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges &&
+ (frm.doc.paid_from_account_currency != company_currency));
+ }
frm.toggle_display("base_received_amount", (
frm.doc.paid_to_account_currency != company_currency
@@ -230,7 +236,8 @@
var company_currency = frm.doc.company? frappe.get_doc(":Company", frm.doc.company).default_currency: "";
frm.set_currency_labels(["base_paid_amount", "base_received_amount", "base_total_allocated_amount",
- "difference_amount", "base_paid_amount_after_tax", "base_received_amount_after_tax"], company_currency);
+ "difference_amount", "base_paid_amount_after_tax", "base_received_amount_after_tax",
+ "base_total_taxes_and_charges"], company_currency);
frm.set_currency_labels(["paid_amount"], frm.doc.paid_from_account_currency);
frm.set_currency_labels(["received_amount"], frm.doc.paid_to_account_currency);
@@ -339,6 +346,8 @@
}
frm.set_party_account_based_on_party = true;
+ let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
+
return frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_details",
args: {
@@ -372,7 +381,11 @@
if (r.message.bank_account) {
frm.set_value("bank_account", r.message.bank_account);
}
- }
+ },
+ () => frm.events.set_current_exchange_rate(frm, "source_exchange_rate",
+ frm.doc.paid_from_account_currency, company_currency),
+ () => frm.events.set_current_exchange_rate(frm, "target_exchange_rate",
+ frm.doc.paid_to_account_currency, company_currency)
]);
}
}
@@ -476,14 +489,14 @@
},
paid_from_account_currency: function(frm) {
- if(!frm.doc.paid_from_account_currency) return;
- var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
+ if(!frm.doc.paid_from_account_currency || !frm.doc.company) return;
+ let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.paid_from_account_currency == company_currency) {
frm.set_value("source_exchange_rate", 1);
} else if (frm.doc.paid_from){
if (in_list(["Internal Transfer", "Pay"], frm.doc.payment_type)) {
- var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
+ let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
frappe.call({
method: "erpnext.setup.utils.get_exchange_rate",
args: {
@@ -503,8 +516,8 @@
},
paid_to_account_currency: function(frm) {
- if(!frm.doc.paid_to_account_currency) return;
- var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
+ if(!frm.doc.paid_to_account_currency || !frm.doc.company) return;
+ let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
frm.events.set_current_exchange_rate(frm, "target_exchange_rate",
frm.doc.paid_to_account_currency, company_currency);
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json
index c8d1db9..3fc1adf 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.json
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json
@@ -66,7 +66,9 @@
"tax_withholding_category",
"section_break_56",
"taxes",
+ "section_break_60",
"base_total_taxes_and_charges",
+ "column_break_61",
"total_taxes_and_charges",
"deductions_or_loss_section",
"deductions",
@@ -715,12 +717,21 @@
"fieldtype": "Data",
"hidden": 1,
"label": "Paid To Account Type"
+ },
+ {
+ "fieldname": "column_break_61",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_60",
+ "fieldtype": "Section Break",
+ "hide_border": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-11-24 18:58:24.919764",
+ "modified": "2022-02-23 20:08:39.559814",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",
@@ -763,6 +774,7 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"title_field": "title",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index c36c843..f9f3350 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -934,8 +934,12 @@
tax.base_total = tax.total * self.source_exchange_rate
- self.total_taxes_and_charges += current_tax_amount
- self.base_total_taxes_and_charges += current_tax_amount * self.source_exchange_rate
+ 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 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 cc3528e..349b8bb 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -633,6 +633,45 @@
self.assertEqual(flt(expected_party_balance), party_balance)
self.assertEqual(flt(expected_party_account_balance), party_account_balance)
+ def test_multi_currency_payment_entry_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': 10,
+ 'add_deduct_tax': 'Add',
+ 'description': 'Test'
+ })
+
+ payment_entry.save()
+ self.assertEqual(payment_entry.base_total_taxes_and_charges, 10)
+ self.assertEqual(flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2))
+
+def create_payment_entry(**args):
+ payment_entry = frappe.new_doc('Payment Entry')
+ payment_entry.company = args.get('company') or '_Test Company'
+ payment_entry.payment_type = args.get('payment_type') or 'Pay'
+ payment_entry.party_type = args.get('party_type') or 'Supplier'
+ payment_entry.party = args.get('party') or '_Test Supplier'
+ payment_entry.paid_from = args.get('paid_from') or '_Test Bank - _TC'
+ payment_entry.paid_to = args.get('paid_to') or 'Creditors - _TC'
+ payment_entry.paid_amount = args.get('paid_amount') or 1000
+
+ payment_entry.setup_party_account_field()
+ payment_entry.set_missing_values()
+ payment_entry.set_exchange_rate()
+ payment_entry.received_amount = payment_entry.paid_amount / payment_entry.target_exchange_rate
+ payment_entry.reference_no = 'Test001'
+ payment_entry.reference_date = nowdate()
+
+ if args.get('save'):
+ payment_entry.save()
+ if args.get('submit'):
+ payment_entry.submit()
+
+ return payment_entry
+
def create_payment_terms_template():
create_payment_term('Basic Amount Receivable')
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 ddca68a..d4513c6 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
@@ -84,20 +84,12 @@
sales_invoice.set_posting_time = 1
sales_invoice.posting_date = getdate(self.posting_date)
sales_invoice.save()
- self.write_off_fractional_amount(sales_invoice, data)
sales_invoice.submit()
self.consolidated_invoice = sales_invoice.name
return sales_invoice.name
- def write_off_fractional_amount(self, invoice, data):
- pos_invoice_grand_total = sum(d.grand_total for d in data)
-
- if abs(pos_invoice_grand_total - invoice.grand_total) < 1:
- invoice.write_off_amount += -1 * (pos_invoice_grand_total - invoice.grand_total)
- invoice.save()
-
def process_merging_into_credit_note(self, data):
credit_note = self.get_new_sales_invoice()
credit_note.is_return = 1
@@ -110,7 +102,6 @@
# TODO: return could be against multiple sales invoice which could also have been consolidated?
# credit_note.return_against = self.consolidated_invoice
credit_note.save()
- self.write_off_fractional_amount(credit_note, data)
credit_note.submit()
self.consolidated_credit_note = credit_note.name
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
index 5930aa0..89f7f18 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
@@ -5,6 +5,7 @@
import unittest
import frappe
+from frappe.tests.utils import change_settings
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
@@ -280,3 +281,100 @@
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")
+
+ @change_settings("System Settings", {"number_format": "#,###.###", "currency_precision": 3, "float_precision": 3})
+ def test_consolidation_round_off_error_3(self):
+ frappe.db.sql("delete from `tabPOS Invoice`")
+
+ try:
+ make_stock_entry(
+ to_warehouse="_Test Warehouse - _TC",
+ item_code="_Test Item",
+ rate=8000,
+ qty=10,
+ )
+ init_user_and_profile()
+
+ item_rates = [69, 59, 29]
+ for i in [1, 2]:
+ inv = create_pos_invoice(is_return=1, do_not_save=1)
+ inv.items = []
+ for rate in item_rates:
+ inv.append("items", {
+ "item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC",
+ "qty": -1,
+ "rate": rate,
+ "income_account": "Sales - _TC",
+ "expense_account": "Cost of Goods Sold - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ })
+ inv.append("taxes", {
+ "account_head": "_Test Account VAT - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "VAT",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 15,
+ "included_in_print_rate": 1
+ })
+ inv.payments = []
+ inv.append('payments', {
+ 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -157
+ })
+ inv.paid_amount = -157
+ inv.save()
+ inv.submit()
+
+ consolidate_pos_invoices()
+
+ inv.load_from_db()
+ consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
+ self.assertEqual(consolidated_invoice.status, 'Return')
+ self.assertEqual(consolidated_invoice.rounding_adjustment, -0.001)
+
+ finally:
+ frappe.set_user("Administrator")
+ frappe.db.sql("delete from `tabPOS Profile`")
+ frappe.db.sql("delete from `tabPOS Invoice`")
+
+ def test_consolidation_rounding_adjustment(self):
+ '''
+ Test if the rounding adjustment is calculated correctly
+ '''
+ frappe.db.sql("delete from `tabPOS Invoice`")
+
+ try:
+ make_stock_entry(
+ to_warehouse="_Test Warehouse - _TC",
+ item_code="_Test Item",
+ rate=8000,
+ qty=10,
+ )
+
+ init_user_and_profile()
+
+ inv = create_pos_invoice(qty=1, rate=69.5, do_not_save=True)
+ inv.append('payments', {
+ 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 70
+ })
+ inv.insert()
+ inv.submit()
+
+ inv2 = create_pos_invoice(qty=1, rate=59.5, do_not_save=True)
+ inv2.append('payments', {
+ 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60
+ })
+ inv2.insert()
+ inv2.submit()
+
+ consolidate_pos_invoices()
+
+ inv.load_from_db()
+ consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
+ self.assertEqual(consolidated_invoice.rounding_adjustment, 1)
+
+ finally:
+ frappe.set_user("Administrator")
+ frappe.db.sql("delete from `tabPOS Profile`")
+ frappe.db.sql("delete from `tabPOS Invoice`")
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index b894f90..573da27 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -263,6 +263,9 @@
self.process_common_party_accounting()
def validate_pos_return(self):
+ if self.is_consolidated:
+ # pos return is already validated in pos invoice
+ return
if self.is_pos and self.is_return:
total_amount_in_payments = 0
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 07591e7..6d929e4 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1595,6 +1595,56 @@
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
+ def test_rounding_adjustment_3(self):
+ si = create_sales_invoice(do_not_save=True)
+ si.items = []
+ for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]:
+ si.append("items", {
+ "item_code": "_Test Item",
+ "gst_hsn_code": "999800",
+ "warehouse": "_Test Warehouse - _TC",
+ "qty": d[1],
+ "rate": d[0],
+ "income_account": "Sales - _TC",
+ "cost_center": "_Test Cost Center - _TC"
+ })
+ for tax_account in ["_Test Account VAT - _TC", "_Test Account Service Tax - _TC"]:
+ si.append("taxes", {
+ "charge_type": "On Net Total",
+ "account_head": tax_account,
+ "description": tax_account,
+ "rate": 6,
+ "cost_center": "_Test Cost Center - _TC",
+ "included_in_print_rate": 1
+ })
+ si.save()
+ si.submit()
+ self.assertEqual(si.net_total, 4007.16)
+ self.assertEqual(si.grand_total, 4488.02)
+ self.assertEqual(si.total_taxes_and_charges, 480.86)
+ self.assertEqual(si.rounding_adjustment, -0.02)
+
+ expected_values = dict((d[0], d) for d in [
+ [si.debit_to, 4488.0, 0.0],
+ ["_Test Account Service Tax - _TC", 0.0, 240.43],
+ ["_Test Account VAT - _TC", 0.0, 240.43],
+ ["Sales - _TC", 0.0, 4007.15],
+ ["Round Off - _TC", 0.01, 0]
+ ])
+
+ gl_entries = frappe.db.sql("""select account, debit, credit
+ from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
+ order by account asc""", si.name, as_dict=1)
+
+ debit_credit_diff = 0
+ for gle in gl_entries:
+ self.assertEqual(expected_values[gle.account][0], gle.account)
+ self.assertEqual(expected_values[gle.account][1], gle.debit)
+ self.assertEqual(expected_values[gle.account][2], gle.credit)
+ debit_credit_diff += (gle.debit - gle.credit)
+
+ self.assertEqual(debit_credit_diff, 0)
+
def test_sales_invoice_with_shipping_rule(self):
from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule
diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py
index 1d30934..8043a1b 100644
--- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py
+++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py
@@ -55,5 +55,8 @@
frappe.throw(_("Disabled template must not be default template"))
def validate_for_tax_category(doc):
+ if not doc.tax_category:
+ return
+
if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0, "name": ["!=", doc.name]}):
frappe.throw(_("A template with tax category {0} already exists. Only one template is allowed with each tax category").format(frappe.bold(doc.tax_category)))
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index d24d56b..0cd5e86 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -274,7 +274,7 @@
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)):
+ if round_off_account_exists and abs(debit_credit_diff) < (1.0 / (10**precision)):
gl_map.remove(round_off_gle)
return
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js
index d554d52..3fe6b2d 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.js
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.js
@@ -68,6 +68,28 @@
});
frappe.ui.form.on('Asset Repair Consumed Item', {
+ item_code: function(frm, cdt, cdn) {
+ var item = locals[cdt][cdn];
+
+ let item_args = {
+ 'item_code': item.item_code,
+ 'warehouse': frm.doc.warehouse,
+ 'qty': item.consumed_quantity,
+ 'serial_no': item.serial_no,
+ 'company': frm.doc.company
+ };
+
+ frappe.call({
+ method: 'erpnext.stock.utils.get_incoming_rate',
+ args: {
+ args: item_args
+ },
+ callback: function(r) {
+ frappe.model.set_value(cdt, cdn, 'valuation_rate', r.message);
+ }
+ });
+ },
+
consumed_quantity: function(frm, cdt, cdn) {
var row = locals[cdt][cdn];
frappe.model.set_value(cdt, cdn, 'total_value', row.consumed_quantity * row.valuation_rate);
diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json
index f63add1..4685a09 100644
--- a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json
+++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json
@@ -13,12 +13,10 @@
],
"fields": [
{
- "fetch_from": "item.valuation_rate",
"fieldname": "valuation_rate",
"fieldtype": "Currency",
"in_list_view": 1,
- "label": "Valuation Rate",
- "read_only": 1
+ "label": "Valuation Rate"
},
{
"fieldname": "consumed_quantity",
@@ -49,7 +47,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-11-11 18:23:00.492483",
+ "modified": "2022-02-08 17:37:20.028290",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Repair Consumed Item",
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 1b5f35e..2e7d306 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -316,6 +316,16 @@
'target_ref_field': 'stock_qty',
'source_field': 'stock_qty'
})
+ self.status_updater.append({
+ 'source_dt': 'Purchase Order Item',
+ 'target_dt': 'Packed Item',
+ 'target_field': 'ordered_qty',
+ 'target_parent_dt': 'Sales Order',
+ 'target_parent_field': '',
+ 'join_field': 'sales_order_packed_item',
+ 'target_ref_field': 'qty',
+ 'source_field': 'stock_qty'
+ })
def update_delivered_qty_in_sales_order(self):
"""Update delivered qty in Sales Order for drop ship"""
diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
index 87cd575..a18c527 100644
--- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
+++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
@@ -63,6 +63,7 @@
"material_request_item",
"sales_order",
"sales_order_item",
+ "sales_order_packed_item",
"supplier_quotation",
"supplier_quotation_item",
"col_break5",
@@ -837,21 +838,30 @@
"label": "Product Bundle",
"options": "Product Bundle",
"read_only": 1
+ },
+ {
+ "fieldname": "sales_order_packed_item",
+ "fieldtype": "Data",
+ "label": "Sales Order Packed Item",
+ "no_copy": 1,
+ "print_hide": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-08-30 20:06:26.712097",
+ "modified": "2022-02-02 13:10:18.398976",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",
+ "naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"search_fields": "item_name",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py
index f98e5f1..60a8f92 100644
--- a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py
+++ b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py
@@ -6,6 +6,7 @@
import frappe
from frappe import _
+from frappe.query_builder.functions import Coalesce, Sum
from frappe.utils import date_diff, flt, getdate
@@ -16,12 +17,9 @@
validate_filters(filters)
columns = get_columns(filters)
- conditions = get_conditions(filters)
+ data = get_data(filters)
- #get queried data
- data = get_data(filters, conditions)
-
- #prepare data for report and chart views
+ # prepare data for report and chart views
data, chart_data = prepare_data(data, filters)
return columns, data, None, chart_data
@@ -34,53 +32,70 @@
elif date_diff(to_date, from_date) < 0:
frappe.throw(_("To Date cannot be before From Date."))
-def get_conditions(filters):
- conditions = ''
+def get_data(filters):
+ mr = frappe.qb.DocType("Material Request")
+ mr_item = frappe.qb.DocType("Material Request Item")
+ query = (
+ frappe.qb.from_(mr)
+ .join(mr_item).on(mr_item.parent == mr.name)
+ .select(
+ mr.name.as_("material_request"),
+ mr.transaction_date.as_("date"),
+ mr_item.schedule_date.as_("required_date"),
+ mr_item.item_code.as_("item_code"),
+ Sum(Coalesce(mr_item.stock_qty, 0)).as_("qty"),
+ Coalesce(mr_item.stock_uom, '').as_("uom"),
+ Sum(Coalesce(mr_item.ordered_qty, 0)).as_("ordered_qty"),
+ Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"),
+ (
+ Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.received_qty, 0))
+ ).as_("qty_to_receive"),
+ Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"),
+ (
+ Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.ordered_qty, 0))
+ ).as_("qty_to_order"),
+ mr_item.item_name,
+ mr_item.description,
+ mr.company
+ ).where(
+ (mr.material_request_type == "Purchase")
+ & (mr.docstatus == 1)
+ & (mr.status != "Stopped")
+ & (mr.per_received < 100)
+ )
+ )
+
+ query = get_conditions(filters, query, mr, mr_item) # add conditional conditions
+
+ query = (
+ query.groupby(
+ mr.name, mr_item.item_code
+ ).orderby(
+ mr.transaction_date, mr.schedule_date
+ )
+ )
+ data = query.run(as_dict=True)
+ return data
+
+def get_conditions(filters, query, mr, mr_item):
if filters.get("from_date") and filters.get("to_date"):
- conditions += " and mr.transaction_date between '{0}' and '{1}'".format(filters.get("from_date"),filters.get("to_date"))
-
+ query = (
+ query.where(
+ (mr.transaction_date >= filters.get("from_date"))
+ & (mr.transaction_date <= filters.get("to_date"))
+ )
+ )
if filters.get("company"):
- conditions += " and mr.company = '{0}'".format(filters.get("company"))
+ query = query.where(mr.company == filters.get("company"))
if filters.get("material_request"):
- conditions += " and mr.name = '{0}'".format(filters.get("material_request"))
+ query = query.where(mr.name == filters.get("material_request"))
if filters.get("item_code"):
- conditions += " and mr_item.item_code = '{0}'".format(filters.get("item_code"))
+ query = query.where(mr_item.item_code == filters.get("item_code"))
- return conditions
-
-def get_data(filters, conditions):
- data = frappe.db.sql("""
- select
- mr.name as material_request,
- mr.transaction_date as date,
- mr_item.schedule_date as required_date,
- mr_item.item_code as item_code,
- sum(ifnull(mr_item.stock_qty, 0)) as qty,
- ifnull(mr_item.stock_uom, '') as uom,
- sum(ifnull(mr_item.ordered_qty, 0)) as ordered_qty,
- sum(ifnull(mr_item.received_qty, 0)) as received_qty,
- (sum(ifnull(mr_item.stock_qty, 0)) - sum(ifnull(mr_item.received_qty, 0))) as qty_to_receive,
- (sum(ifnull(mr_item.stock_qty, 0)) - sum(ifnull(mr_item.ordered_qty, 0))) as qty_to_order,
- mr_item.item_name as item_name,
- mr_item.description as "description",
- mr.company as company
- from
- `tabMaterial Request` mr, `tabMaterial Request Item` mr_item
- where
- mr_item.parent = mr.name
- and mr.material_request_type = "Purchase"
- and mr.docstatus = 1
- and mr.status != "Stopped"
- {conditions}
- group by mr.name, mr_item.item_code
- having
- sum(ifnull(mr_item.ordered_qty, 0)) < sum(ifnull(mr_item.stock_qty, 0))
- order by mr.transaction_date, mr.schedule_date""".format(conditions=conditions), as_dict=1)
-
- return data
+ return query
def update_qty_columns(row_to_update, data_row):
fields = ["qty", "ordered_qty", "received_qty", "qty_to_receive", "qty_to_order"]
diff --git a/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py b/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py
new file mode 100644
index 0000000..f3c751c
--- /dev/null
+++ b/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py
@@ -0,0 +1,69 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import add_days, today
+
+from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
+from erpnext.buying.report.requested_items_to_order_and_receive.requested_items_to_order_and_receive import (
+ get_data,
+)
+from erpnext.stock.doctype.item.test_item import create_item
+from erpnext.stock.doctype.material_request.material_request import make_purchase_order
+
+
+class TestRequestedItemsToOrderAndReceive(FrappeTestCase):
+ def setUp(self) -> None:
+ create_item("Test MR Report Item")
+ self.setup_material_request() # to order and receive
+ self.setup_material_request(order=True) # to receive (ordered)
+ self.setup_material_request(order=True, receive=True) # complete (ordered & received)
+
+ self.filters = frappe._dict(
+ company="_Test Company", from_date=today(), to_date=add_days(today(), 30),
+ item_code="Test MR Report Item"
+ )
+
+ def tearDown(self) -> None:
+ frappe.db.rollback()
+
+ def test_date_range(self):
+ data = get_data(self.filters)
+ self.assertEqual(len(data), 2) # MRs today should be fetched
+
+ self.filters.from_date = add_days(today(), 1)
+ data = get_data(self.filters)
+ self.assertEqual(len(data), 0) # MRs today should not be fetched as from date is tomorrow
+
+ def test_ordered_received_material_requests(self):
+ data = get_data(self.filters)
+
+ # from the 3 MRs made, only 2 (to receive) should be fetched
+ self.assertEqual(len(data), 2)
+ self.assertEqual(data[0].ordered_qty, 0.0)
+ self.assertEqual(data[1].ordered_qty, 57.0)
+
+ def setup_material_request(self, order=False, receive=False):
+ po = None
+ test_records = frappe.get_test_records('Material Request')
+
+ mr = frappe.copy_doc(test_records[0])
+ mr.transaction_date = today()
+ mr.schedule_date = add_days(today(), 1)
+ for row in mr.items:
+ row.item_code = "Test MR Report Item"
+ row.item_name = "Test MR Report Item"
+ row.description = "Test MR Report Item"
+ row.uom = "Nos"
+ row.schedule_date = add_days(today(), 1)
+ mr.submit()
+
+ if order or receive:
+ po = make_purchase_order(mr.name)
+ po.supplier = "_Test Supplier"
+ po.submit()
+ if receive:
+ pr = make_purchase_receipt(po.name)
+ pr.submit()
+
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index c8e5edd..8972c32 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -507,13 +507,41 @@
"voucher_no": self.name,
"company": self.company
})
- if future_sle_exists(args):
+
+ if future_sle_exists(args) or repost_required_for_queue(self):
item_based_reposting = cint(frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"))
if item_based_reposting:
create_item_wise_repost_entries(voucher_type=self.doctype, voucher_no=self.name)
else:
create_repost_item_valuation_entry(args)
+def repost_required_for_queue(doc: StockController) -> bool:
+ """check if stock document contains repeated item-warehouse with queue based valuation.
+
+ if queue exists for repeated items then SLEs need to reprocessed in background again.
+ """
+
+ consuming_sles = frappe.db.get_all("Stock Ledger Entry",
+ filters={
+ "voucher_type": doc.doctype,
+ "voucher_no": doc.name,
+ "actual_qty": ("<", 0),
+ "is_cancelled": 0
+ },
+ fields=["item_code", "warehouse", "stock_queue"]
+ )
+ item_warehouses = [(sle.item_code, sle.warehouse) for sle in consuming_sles]
+
+ unique_item_warehouses = set(item_warehouses)
+
+ if len(unique_item_warehouses) == len(item_warehouses):
+ return False
+
+ for sle in consuming_sles:
+ if sle.stock_queue != "[]": # using FIFO/LIFO valuation
+ return True
+ return False
+
@frappe.whitelist()
def make_quality_inspections(doctype, docname, items):
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 2776628..a1bb667 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -113,17 +113,24 @@
for item in self.doc.get("items"):
self.doc.round_floats_in(item)
+ if not item.rate:
+ item.rate = item.price_list_rate
+
if item.discount_percentage == 100:
item.rate = 0.0
elif item.price_list_rate:
- if not item.rate or (item.pricing_rules and item.discount_percentage > 0):
+ if item.pricing_rules or abs(item.discount_percentage) > 0:
item.rate = flt(item.price_list_rate *
(1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
- item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0)
- elif item.discount_amount and item.pricing_rules:
+
+ if abs(item.discount_percentage) > 0:
+ item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0)
+
+ elif item.discount_amount or item.pricing_rules:
item.rate = item.price_list_rate - item.discount_amount
- if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item', 'POS Invoice Item', 'Purchase Invoice Item', 'Purchase Order Item', 'Purchase Receipt Item']:
+ if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item',
+ 'POS Invoice Item', 'Purchase Invoice Item', 'Purchase Order Item', 'Purchase Receipt Item']:
item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item)
if flt(item.rate_with_margin) > 0:
item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
@@ -270,7 +277,8 @@
shipping_rule.apply(self.doc)
def calculate_taxes(self):
- if not self.doc.get('is_consolidated'):
+ rounding_adjustment_computed = self.doc.get('is_consolidated') and self.doc.get('rounding_adjustment')
+ if not rounding_adjustment_computed:
self.doc.rounding_adjustment = 0
# maintain actual tax rate based on idx
@@ -326,7 +334,7 @@
if i == (len(self.doc.get("taxes")) - 1) and self.discount_amount_applied \
and self.doc.discount_amount \
and self.doc.apply_discount_on == "Grand Total" \
- and not self.doc.get('is_consolidated'):
+ and not rounding_adjustment_computed:
self.doc.rounding_adjustment = flt(self.doc.grand_total
- flt(self.doc.discount_amount) - tax.total,
self.doc.precision("rounding_adjustment"))
@@ -465,20 +473,22 @@
self.doc.total_net_weight += d.total_weight
def set_rounded_total(self):
- if not self.doc.get('is_consolidated'):
- if self.doc.meta.get_field("rounded_total"):
- if self.doc.is_rounded_total_disabled():
- self.doc.rounded_total = self.doc.base_rounded_total = 0
- return
+ if self.doc.get('is_consolidated') and self.doc.get('rounding_adjustment'):
+ return
- self.doc.rounded_total = round_based_on_smallest_currency_fraction(self.doc.grand_total,
- self.doc.currency, self.doc.precision("rounded_total"))
+ if self.doc.meta.get_field("rounded_total"):
+ if self.doc.is_rounded_total_disabled():
+ self.doc.rounded_total = self.doc.base_rounded_total = 0
+ return
- #if print_in_rate is set, we would have already calculated rounding adjustment
- self.doc.rounding_adjustment += flt(self.doc.rounded_total - self.doc.grand_total,
- self.doc.precision("rounding_adjustment"))
+ self.doc.rounded_total = round_based_on_smallest_currency_fraction(self.doc.grand_total,
+ self.doc.currency, self.doc.precision("rounded_total"))
- self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"])
+ #if print_in_rate is set, we would have already calculated rounding adjustment
+ self.doc.rounding_adjustment += flt(self.doc.rounded_total - self.doc.grand_total,
+ self.doc.precision("rounding_adjustment"))
+
+ self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"])
def _cleanup(self):
if not self.doc.get('is_consolidated'):
diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py
index b1eaaf8..b1e373e 100644
--- a/erpnext/hr/doctype/attendance/attendance.py
+++ b/erpnext/hr/doctype/attendance/attendance.py
@@ -174,16 +174,22 @@
def get_unmarked_days(employee, month, exclude_holidays=0):
import calendar
month_map = get_month_map()
-
today = get_datetime()
- dates_of_month = ['{}-{}-{}'.format(today.year, month_map[month], r) for r in range(1, calendar.monthrange(today.year, month_map[month])[1] + 1)]
+ 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
- length = len(dates_of_month)
- month_start, month_end = dates_of_month[0], dates_of_month[length-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
- records = frappe.get_all("Attendance", fields = ['attendance_date', 'employee'] , filters = [
+ 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]
+
+ records = frappe.get_all("Attendance", fields=['attendance_date', 'employee'], filters=[
["attendance_date", ">=", month_start],
["attendance_date", "<=", month_end],
["employee", "=", employee],
@@ -200,7 +206,7 @@
for date in dates_of_month:
date_time = get_datetime(date)
- if today.day == date_time.day and today.month == date_time.month:
+ if today.day <= date_time.day and today.month <= date_time.month:
break
if date_time not in marked_days:
unmarked_days.append(date)
diff --git a/erpnext/hr/doctype/attendance/test_attendance.py b/erpnext/hr/doctype/attendance/test_attendance.py
index a770d70..c74967d 100644
--- a/erpnext/hr/doctype/attendance/test_attendance.py
+++ b/erpnext/hr/doctype/attendance/test_attendance.py
@@ -1,20 +1,108 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
# See license.txt
-import unittest
-
import frappe
-from frappe.utils import nowdate
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import add_days, get_first_day, getdate, now_datetime, nowdate
+
+from erpnext.hr.doctype.attendance.attendance import (
+ get_month_map,
+ 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
test_records = frappe.get_test_records('Attendance')
-class TestAttendance(unittest.TestCase):
+class TestAttendance(FrappeTestCase):
def test_mark_absent(self):
- from erpnext.hr.doctype.employee.test_employee import make_employee
employee = make_employee("test_mark_absent@example.com")
date = nowdate()
frappe.db.delete('Attendance', {'employee':employee, 'attendance_date':date})
- from erpnext.hr.doctype.attendance.attendance import mark_attendance
attendance = mark_attendance(employee, date, 'Absent')
fetch_attendance = frappe.get_value('Attendance', {'employee':employee, 'attendance_date':date, 'status':'Absent'})
self.assertEqual(attendance, fetch_attendance)
+
+ def test_unmarked_days(self):
+ first_day = get_first_day(getdate())
+
+ employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1))
+ frappe.db.delete('Attendance', {'employee': employee})
+
+ from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
+ holiday_list = make_holiday_list()
+ frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list)
+
+ first_sunday = get_first_sunday(holiday_list)
+ mark_attendance(employee, first_day, 'Present')
+ month_name = get_month_name(first_day)
+
+ unmarked_days = get_unmarked_days(employee, month_name)
+ unmarked_days = [getdate(date) for date in unmarked_days]
+
+ # attendance already marked for the day
+ self.assertNotIn(first_day, unmarked_days)
+ # attendance unmarked
+ self.assertIn(getdate(add_days(first_day, 1)), unmarked_days)
+ # holiday considered in unmarked days
+ self.assertIn(first_sunday, unmarked_days)
+
+ def test_unmarked_days_excluding_holidays(self):
+ first_day = get_first_day(getdate())
+
+ employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1))
+ frappe.db.delete('Attendance', {'employee': employee})
+
+ from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
+ holiday_list = make_holiday_list()
+ frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list)
+
+ first_sunday = get_first_sunday(holiday_list)
+ mark_attendance(employee, first_day, 'Present')
+ month_name = get_month_name(first_day)
+
+ unmarked_days = get_unmarked_days(employee, month_name, exclude_holidays=True)
+ unmarked_days = [getdate(date) for date in unmarked_days]
+
+ # attendance already marked for the day
+ self.assertNotIn(first_day, unmarked_days)
+ # attendance unmarked
+ self.assertIn(getdate(add_days(first_day, 1)), unmarked_days)
+ # holidays not considered in unmarked days
+ self.assertNotIn(first_sunday, unmarked_days)
+
+ def test_unmarked_days_as_per_joining_and_relieving_dates(self):
+ now = now_datetime()
+ previous_month = now.month - 1
+ first_day = now.replace(day=1).replace(month=previous_month).date()
+
+ doj = add_days(first_day, 1)
+ relieving_date = add_days(first_day, 5)
+ employee = make_employee('test_unmarked_days_as_per_doj@example.com', date_of_joining=doj,
+ relieving_date=relieving_date)
+ frappe.db.delete('Attendance', {'employee': employee})
+
+ attendance_date = add_days(first_day, 2)
+ mark_attendance(employee, attendance_date, 'Present')
+ month_name = get_month_name(first_day)
+
+ unmarked_days = get_unmarked_days(employee, month_name)
+ unmarked_days = [getdate(date) for date in unmarked_days]
+
+ # attendance already marked for the day
+ self.assertNotIn(attendance_date, unmarked_days)
+ # date before doj not in unmarked days
+ self.assertNotIn(add_days(doj, -1), unmarked_days)
+ # date after relieving not in unmarked days
+ self.assertNotIn(add_days(relieving_date, 1), unmarked_days)
+
+ 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/employee_advance/employee_advance.json b/erpnext/hr/doctype/employee_advance/employee_advance.json
index 0475453..b050183 100644
--- a/erpnext/hr/doctype/employee_advance/employee_advance.json
+++ b/erpnext/hr/doctype/employee_advance/employee_advance.json
@@ -2,7 +2,7 @@
"actions": [],
"allow_import": 1,
"autoname": "naming_series:",
- "creation": "2017-10-09 14:26:29.612365",
+ "creation": "2022-01-17 18:36:51.450395",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
@@ -121,7 +121,7 @@
"fieldtype": "Select",
"label": "Status",
"no_copy": 1,
- "options": "Draft\nPaid\nUnpaid\nClaimed\nCancelled",
+ "options": "Draft\nPaid\nUnpaid\nClaimed\nReturned\nPartly Claimed and Returned\nCancelled",
"read_only": 1
},
{
@@ -200,7 +200,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2021-09-11 18:38:38.617478",
+ "modified": "2022-01-17 19:33:52.345823",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Advance",
@@ -237,5 +237,41 @@
"search_fields": "employee,employee_name",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [
+ {
+ "color": "Red",
+ "custom": 1,
+ "title": "Draft"
+ },
+ {
+ "color": "Green",
+ "custom": 1,
+ "title": "Paid"
+ },
+ {
+ "color": "Orange",
+ "custom": 1,
+ "title": "Unpaid"
+ },
+ {
+ "color": "Blue",
+ "custom": 1,
+ "title": "Claimed"
+ },
+ {
+ "color": "Gray",
+ "title": "Returned"
+ },
+ {
+ "color": "Yellow",
+ "title": "Partly Claimed and Returned"
+ },
+ {
+ "color": "Red",
+ "custom": 1,
+ "title": "Cancelled"
+ }
+ ],
+ "title_field": "employee_name",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py
index 7aac2b6..79d389d 100644
--- a/erpnext/hr/doctype/employee_advance/employee_advance.py
+++ b/erpnext/hr/doctype/employee_advance/employee_advance.py
@@ -27,19 +27,33 @@
def on_cancel(self):
self.ignore_linked_doctypes = ('GL Entry')
+ self.set_status(update=True)
- def set_status(self):
+ def set_status(self, update=False):
+ precision = self.precision("paid_amount")
+ total_amount = flt(flt(self.claimed_amount) + flt(self.return_amount), precision)
+ status = None
+
if self.docstatus == 0:
- self.status = "Draft"
- if self.docstatus == 1:
- if self.claimed_amount and flt(self.claimed_amount) == flt(self.paid_amount):
- self.status = "Claimed"
- elif self.paid_amount and self.advance_amount == flt(self.paid_amount):
- self.status = "Paid"
+ status = "Draft"
+ elif self.docstatus == 1:
+ if flt(self.claimed_amount) > 0 and flt(self.claimed_amount, precision) == flt(self.paid_amount, precision):
+ status = "Claimed"
+ elif flt(self.return_amount) > 0 and flt(self.return_amount, precision) == flt(self.paid_amount, precision):
+ status = "Returned"
+ elif flt(self.claimed_amount) > 0 and (flt(self.return_amount) > 0) and total_amount == flt(self.paid_amount, precision):
+ status = "Partly Claimed and Returned"
+ elif flt(self.paid_amount) > 0 and flt(self.advance_amount, precision) == flt(self.paid_amount, precision):
+ status = "Paid"
else:
- self.status = "Unpaid"
+ status = "Unpaid"
elif self.docstatus == 2:
- self.status = "Cancelled"
+ status = "Cancelled"
+
+ if update:
+ self.db_set("status", status)
+ else:
+ self.status = status
def set_total_advance_paid(self):
gle = frappe.qb.DocType("GL Entry")
@@ -85,9 +99,7 @@
self.db_set("paid_amount", paid_amount)
self.db_set("return_amount", return_amount)
- self.set_status()
- frappe.db.set_value("Employee Advance", self.name , "status", self.status)
-
+ self.set_status(update=True)
def update_claimed_amount(self):
claimed_amount = frappe.db.sql("""
@@ -103,8 +115,8 @@
frappe.db.set_value("Employee Advance", self.name, "claimed_amount", flt(claimed_amount))
self.reload()
- self.set_status()
- frappe.db.set_value("Employee Advance", self.name, "status", self.status)
+ self.set_status(update=True)
+
@frappe.whitelist()
def get_pending_amount(employee, posting_date):
@@ -222,7 +234,8 @@
'reference_name': employee_advance_name,
'party_type': 'Employee',
'party': employee,
- 'is_advance': 'Yes'
+ 'is_advance': 'Yes',
+ 'cost_center': erpnext.get_default_cost_center(company)
})
bank_amount = flt(return_amount) if bank_cash_account.account_currency==currency \
@@ -233,7 +246,8 @@
"debit_in_account_currency": bank_amount,
"account_currency": bank_cash_account.account_currency,
"account_type": bank_cash_account.account_type,
- "exchange_rate": flt(exchange_rate) if bank_cash_account.account_currency == currency else 1
+ "exchange_rate": flt(exchange_rate) if bank_cash_account.account_currency == currency else 1,
+ "cost_center": erpnext.get_default_cost_center(company)
})
return je.as_dict()
diff --git a/erpnext/hr/doctype/employee_advance/test_employee_advance.py b/erpnext/hr/doctype/employee_advance/test_employee_advance.py
index 5f2e720..e3c1487 100644
--- a/erpnext/hr/doctype/employee_advance/test_employee_advance.py
+++ b/erpnext/hr/doctype/employee_advance/test_employee_advance.py
@@ -4,7 +4,7 @@
import unittest
import frappe
-from frappe.utils import nowdate
+from frappe.utils import flt, nowdate
import erpnext
from erpnext.hr.doctype.employee.test_employee import make_employee
@@ -12,12 +12,21 @@
EmployeeAdvanceOverPayment,
create_return_through_additional_salary,
make_bank_entry,
+ make_return_entry,
+)
+from erpnext.hr.doctype.expense_claim.expense_claim import get_advances
+from erpnext.hr.doctype.expense_claim.test_expense_claim import (
+ get_payable_account,
+ make_expense_claim,
)
from erpnext.payroll.doctype.salary_component.test_salary_component import create_salary_component
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
class TestEmployeeAdvance(unittest.TestCase):
+ def setUp(self):
+ frappe.db.delete("Employee Advance")
+
def test_paid_amount_and_status(self):
employee_name = make_employee("_T@employe.advance")
advance = make_employee_advance(employee_name)
@@ -52,9 +61,102 @@
self.assertEqual(advance.paid_amount, 0)
self.assertEqual(advance.status, "Unpaid")
+ advance.cancel()
+ advance.reload()
+ self.assertEqual(advance.status, "Cancelled")
+
+ def test_claimed_status(self):
+ # CLAIMED Status check, full amount claimed
+ payable_account = get_payable_account("_Test Company")
+ claim = make_expense_claim(payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True)
+
+ advance = make_employee_advance(claim.employee)
+ pe = make_payment_entry(advance)
+ pe.submit()
+
+ claim = get_advances_for_claim(claim, advance.name)
+ claim.save()
+ claim.submit()
+
+ advance.reload()
+ self.assertEqual(advance.claimed_amount, 1000)
+ self.assertEqual(advance.status, "Claimed")
+
+ # advance should not be shown in claims
+ advances = get_advances(claim.employee)
+ advances = [entry.name for entry in advances]
+ self.assertTrue(advance.name not in advances)
+
+ # cancel claim; status should be Paid
+ claim.cancel()
+ advance.reload()
+ self.assertEqual(advance.claimed_amount, 0)
+ self.assertEqual(advance.status, "Paid")
+
+ def test_partly_claimed_and_returned_status(self):
+ payable_account = get_payable_account("_Test Company")
+ claim = make_expense_claim(payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True)
+
+ advance = make_employee_advance(claim.employee)
+ pe = make_payment_entry(advance)
+ pe.submit()
+
+ # PARTLY CLAIMED AND RETURNED status check
+ # 500 Claimed, 500 Returned
+ claim = make_expense_claim(payable_account, 500, 500, "_Test Company", "Travel Expenses - _TC", do_not_submit=True)
+
+ advance = make_employee_advance(claim.employee)
+ pe = make_payment_entry(advance)
+ pe.submit()
+
+ claim = get_advances_for_claim(claim, advance.name, amount=500)
+ claim.save()
+ claim.submit()
+
+ advance.reload()
+ self.assertEqual(advance.claimed_amount, 500)
+ self.assertEqual(advance.status, "Paid")
+
+ entry = make_return_entry(
+ employee=advance.employee,
+ company=advance.company,
+ employee_advance_name=advance.name,
+ return_amount=flt(advance.paid_amount - advance.claimed_amount),
+ advance_account=advance.advance_account,
+ mode_of_payment=advance.mode_of_payment,
+ currency=advance.currency,
+ exchange_rate=advance.exchange_rate
+ )
+
+ entry = frappe.get_doc(entry)
+ entry.insert()
+ entry.submit()
+
+ advance.reload()
+ self.assertEqual(advance.return_amount, 500)
+ self.assertEqual(advance.status, "Partly Claimed and Returned")
+
+ # advance should not be shown in claims
+ advances = get_advances(claim.employee)
+ advances = [entry.name for entry in advances]
+ self.assertTrue(advance.name not in advances)
+
+ # Cancel return entry; status should change to PAID
+ entry.cancel()
+ advance.reload()
+ self.assertEqual(advance.return_amount, 0)
+ self.assertEqual(advance.status, "Paid")
+
+ # advance should be shown in claims
+ advances = get_advances(claim.employee)
+ advances = [entry.name for entry in advances]
+ self.assertTrue(advance.name in advances)
+
def test_repay_unclaimed_amount_from_salary(self):
employee_name = make_employee("_T@employe.advance")
advance = make_employee_advance(employee_name, {"repay_unclaimed_amount_from_salary": 1})
+ pe = make_payment_entry(advance)
+ pe.submit()
args = {"type": "Deduction"}
create_salary_component("Advance Salary - Deduction", **args)
@@ -82,11 +184,13 @@
advance.reload()
self.assertEqual(advance.return_amount, 1000)
+ self.assertEqual(advance.status, "Returned")
# update advance return amount on additional salary cancellation
additional_salary.cancel()
advance.reload()
self.assertEqual(advance.return_amount, 700)
+ self.assertEqual(advance.status, "Paid")
def tearDown(self):
frappe.db.rollback()
@@ -118,3 +222,24 @@
doc.submit()
return doc
+
+
+def get_advances_for_claim(claim, advance_name, amount=None):
+ advances = get_advances(claim.employee, advance_name)
+
+ for entry in advances:
+ if amount:
+ allocated_amount = amount
+ else:
+ allocated_amount = flt(entry.paid_amount) - flt(entry.claimed_amount)
+
+ claim.append("advances", {
+ "employee_advance": entry.name,
+ "posting_date": entry.posting_date,
+ "advance_account": entry.advance_account,
+ "advance_paid": entry.paid_amount,
+ "unclaimed_amount": allocated_amount,
+ "allocated_amount": allocated_amount
+ })
+
+ return claim
\ No newline at end of file
diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js
index 0479457..af80b63 100644
--- a/erpnext/hr/doctype/expense_claim/expense_claim.js
+++ b/erpnext/hr/doctype/expense_claim/expense_claim.js
@@ -171,7 +171,7 @@
['docstatus', '=', 1],
['employee', '=', frm.doc.employee],
['paid_amount', '>', 0],
- ['status', '!=', 'Claimed']
+ ['status', 'not in', ['Claimed', 'Returned', 'Partly Claimed and Returned']]
]
};
});
diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py
index 7e3898b..fe04efb 100644
--- a/erpnext/hr/doctype/expense_claim/expense_claim.py
+++ b/erpnext/hr/doctype/expense_claim/expense_claim.py
@@ -23,10 +23,10 @@
def validate(self):
validate_active_employee(self.employee)
- self.validate_advances()
+ set_employee_name(self)
self.validate_sanctioned_amount()
self.calculate_total_amount()
- set_employee_name(self)
+ self.validate_advances()
self.set_expense_account(validate=True)
self.set_payable_account()
self.set_cost_center()
@@ -341,18 +341,27 @@
@frappe.whitelist()
def get_advances(employee, advance_id=None):
- if not advance_id:
- condition = 'docstatus=1 and employee={0} and paid_amount > 0 and paid_amount > claimed_amount + return_amount'.format(frappe.db.escape(employee))
- else:
- condition = 'name={0}'.format(frappe.db.escape(advance_id))
+ advance = frappe.qb.DocType("Employee Advance")
- return frappe.db.sql("""
- select
- name, posting_date, paid_amount, claimed_amount, advance_account
- from
- `tabEmployee Advance`
- where {0}
- """.format(condition), as_dict=1)
+ query = (
+ frappe.qb.from_(advance)
+ .select(
+ advance.name, advance.posting_date, advance.paid_amount,
+ advance.claimed_amount, advance.advance_account
+ )
+ )
+
+ if not advance_id:
+ query = query.where(
+ (advance.docstatus == 1)
+ & (advance.employee == employee)
+ & (advance.paid_amount > 0)
+ & (advance.status.notin(["Claimed", "Returned", "Partly Claimed and Returned"]))
+ )
+ else:
+ query = query.where(advance.name == advance_id)
+
+ return query.run(as_dict=True)
@frappe.whitelist()
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 70250f5..ef5f4bc 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -4,6 +4,7 @@
import frappe
from frappe import _
+from frappe.query_builder.functions import Max, Min, Sum
from frappe.utils import (
add_days,
cint,
@@ -567,28 +568,39 @@
return get_remaining_leaves(allocation, leaves_taken, date, expiry)
def get_leave_allocation_records(employee, date, leave_type=None):
- ''' returns the total allocated leaves and carry forwarded leaves based on ledger entries '''
+ """Returns the total allocated leaves and carry forwarded leaves based on ledger entries"""
+ Ledger = frappe.qb.DocType("Leave Ledger Entry")
- conditions = ("and leave_type='%s'" % leave_type) if leave_type else ""
- allocation_details = frappe.db.sql("""
- SELECT
- SUM(CASE WHEN is_carry_forward = 1 THEN leaves ELSE 0 END) as cf_leaves,
- SUM(CASE WHEN is_carry_forward = 0 THEN leaves ELSE 0 END) as new_leaves,
- MIN(from_date) as from_date,
- MAX(to_date) as to_date,
- leave_type
- FROM `tabLeave Ledger Entry`
- WHERE
- from_date <= %(date)s
- AND to_date >= %(date)s
- AND docstatus=1
- AND transaction_type="Leave Allocation"
- AND employee=%(employee)s
- AND is_expired=0
- AND is_lwp=0
- {0}
- GROUP BY employee, leave_type
- """.format(conditions), dict(date=date, employee=employee), as_dict=1) #nosec
+ cf_leave_case = frappe.qb.terms.Case().when(Ledger.is_carry_forward == "1", Ledger.leaves).else_(0)
+ sum_cf_leaves = Sum(cf_leave_case).as_("cf_leaves")
+
+ new_leaves_case = frappe.qb.terms.Case().when(Ledger.is_carry_forward == "0", Ledger.leaves).else_(0)
+ sum_new_leaves = Sum(new_leaves_case).as_("new_leaves")
+
+ query = (
+ frappe.qb.from_(Ledger)
+ .select(
+ sum_cf_leaves,
+ sum_new_leaves,
+ Min(Ledger.from_date).as_("from_date"),
+ Max(Ledger.to_date).as_("to_date"),
+ Ledger.leave_type
+ ).where(
+ (Ledger.from_date <= date)
+ & (Ledger.to_date >= date)
+ & (Ledger.docstatus == 1)
+ & (Ledger.transaction_type == "Leave Allocation")
+ & (Ledger.employee == employee)
+ & (Ledger.is_expired == 0)
+ & (Ledger.is_lwp == 0)
+ )
+ )
+
+ if leave_type:
+ query = query.where((Ledger.leave_type == leave_type))
+ query = query.groupby(Ledger.employee, Ledger.leave_type)
+
+ allocation_details = query.run(as_dict=True)
allocated_leaves = frappe._dict()
for d in allocation_details:
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index 6d27f4a..7b3aa49 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -792,4 +792,4 @@
order by holiday_date
""", (holiday_list, month_start_date, month_end_date))[0][0]
- return first_sunday
\ No newline at end of file
+ return first_sunday
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index d640f3f..37d2b9f 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -918,7 +918,7 @@
frappe.throw(_("BOM {0} does not belong to Item {1}").format(bom_no, item))
@frappe.whitelist()
-def get_children(doctype, parent=None, is_root=False, **filters):
+def get_children(parent=None, is_root=False, **filters):
if not parent or parent=="BOM":
frappe.msgprint(_('Please select a BOM'))
return
diff --git a/erpnext/manufacturing/doctype/operation/operation_dashboard.py b/erpnext/manufacturing/doctype/operation/operation_dashboard.py
index 4fbcf49..9f7efa2 100644
--- a/erpnext/manufacturing/doctype/operation/operation_dashboard.py
+++ b/erpnext/manufacturing/doctype/operation/operation_dashboard.py
@@ -7,7 +7,7 @@
'transactions': [
{
'label': _('Manufacture'),
- 'items': ['BOM', 'Work Order', 'Job Card', 'Timesheet']
+ 'items': ['BOM', 'Work Order', 'Job Card']
}
]
}
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index c33e646..e8759f5 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -232,7 +232,7 @@
});
},
combine_items: function (frm) {
- frm.clear_table('prod_plan_references');
+ frm.clear_table("prod_plan_references");
frappe.call({
method: "get_items",
@@ -247,6 +247,13 @@
});
},
+ combine_sub_items: (frm) => {
+ if (frm.doc.sub_assembly_items.length > 0) {
+ frm.clear_table("sub_assembly_items");
+ frm.trigger("get_sub_assembly_items");
+ }
+ },
+
get_sub_assembly_items: function(frm) {
frm.dirty();
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json
index 56cf2b4..3bfb764 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.json
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json
@@ -36,6 +36,7 @@
"prod_plan_references",
"section_break_24",
"get_sub_assembly_items",
+ "combine_sub_items",
"sub_assembly_items",
"material_request_planning",
"include_non_stock_items",
@@ -340,7 +341,6 @@
{
"fieldname": "prod_plan_references",
"fieldtype": "Table",
- "hidden": 1,
"label": "Production Plan Item Reference",
"options": "Production Plan Item Reference"
},
@@ -370,16 +370,23 @@
"fieldname": "to_delivery_date",
"fieldtype": "Date",
"label": "To Delivery Date"
+ },
+ {
+ "default": "0",
+ "fieldname": "combine_sub_items",
+ "fieldtype": "Check",
+ "label": "Consolidate Sub Assembly Items"
}
],
"icon": "fa fa-calendar",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-09-06 18:35:59.642232",
+ "modified": "2022-02-23 17:16:10.629378",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 80003da..48cd753 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -21,7 +21,8 @@
)
from frappe.utils.csvutils import build_csv_response
-from erpnext.manufacturing.doctype.bom.bom import get_children, validate_bom_no
+from erpnext.manufacturing.doctype.bom.bom import get_children as get_bom_children
+from erpnext.manufacturing.doctype.bom.bom import 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
@@ -570,17 +571,28 @@
@frappe.whitelist()
def get_sub_assembly_items(self, manufacturing_type=None):
+ "Fetch sub assembly items and optionally combine them."
self.sub_assembly_items = []
+ sub_assembly_items_store = [] # temporary store to process all subassembly items
+
for row in self.po_items:
bom_data = []
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty)
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
+ sub_assembly_items_store.extend(bom_data)
- self.sub_assembly_items.sort(key= lambda d: d.bom_level, reverse=True)
- for idx, row in enumerate(self.sub_assembly_items, start=1):
- row.idx = idx
+ if self.combine_sub_items:
+ # Combine subassembly items
+ sub_assembly_items_store = self.combine_subassembly_items(sub_assembly_items_store)
+
+ sub_assembly_items_store.sort(key= lambda d: d.bom_level, reverse=True) # sort by bom level
+
+ for idx, row in enumerate(sub_assembly_items_store):
+ row.idx = idx + 1
+ self.append("sub_assembly_items", row)
def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None):
+ "Modify bom_data, set additional details."
for data in bom_data:
data.qty = data.stock_qty
data.production_plan_item = row.name
@@ -589,7 +601,32 @@
data.type_of_manufacturing = manufacturing_type or ("Subcontract" if data.is_sub_contracted_item
else "In House")
- self.append("sub_assembly_items", data)
+ def combine_subassembly_items(self, sub_assembly_items_store):
+ "Aggregate if same: Item, Warehouse, Inhouse/Outhouse Manu.g, BOM No."
+ key_wise_data = {}
+ for row in sub_assembly_items_store:
+ key = (
+ row.get("production_item"), row.get("fg_warehouse"),
+ row.get("bom_no"), row.get("type_of_manufacturing")
+ )
+ if key not in key_wise_data:
+ # intialise (item, wh, bom no, man.g type) wise dict
+ key_wise_data[key] = row
+ continue
+
+ existing_row = key_wise_data[key]
+ if existing_row:
+ # if row with same (item, wh, bom no, man.g type) key, merge
+ existing_row.qty += flt(row.qty)
+ existing_row.stock_qty += flt(row.stock_qty)
+ existing_row.bom_level = max(existing_row.bom_level, row.bom_level)
+ continue
+ else:
+ # add row with key
+ key_wise_data[key] = row
+
+ sub_assembly_items_store = [key_wise_data[key] for key in key_wise_data] # unpack into single level list
+ return sub_assembly_items_store
def all_items_completed(self):
all_items_produced = all(flt(d.planned_qty) - flt(d.produced_qty) < 0.000001
@@ -1031,7 +1068,7 @@
}
def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
- data = get_children('BOM', parent = bom_no)
+ data = get_bom_children(parent=bom_no)
for d in data:
if d.expandable:
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index b2a41ff..eeab788 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -38,6 +38,9 @@
if not frappe.db.get_value('BOM', {'item': item}):
make_bom(item = item, raw_materials = raw_materials)
+ def tearDown(self) -> None:
+ frappe.db.rollback()
+
def test_production_plan_mr_creation(self):
"Test if MRs are created for unavailable raw materials."
pln = create_production_plan(item_code='Test Production Item 1')
@@ -110,7 +113,7 @@
item_code='Test Production Item 1',
ignore_existing_ordered_qty=1
)
- self.assertTrue(len(pln.mr_items), 1)
+ self.assertTrue(len(pln.mr_items))
self.assertTrue(flt(pln.mr_items[0].quantity), 1.0)
sr1.cancel()
@@ -151,7 +154,7 @@
use_multi_level_bom=0,
ignore_existing_ordered_qty=0
)
- self.assertTrue(len(pln.mr_items), 0)
+ self.assertFalse(len(pln.mr_items))
sr1.cancel()
sr2.cancel()
@@ -258,6 +261,51 @@
pln.reload()
pln.cancel()
+ def test_production_plan_combine_subassembly(self):
+ """
+ Test combining Sub assembly items belonging to the same BOM in Prod Plan.
+ 1) Red-Car -> Wheel (sub assembly) > BOM-WHEEL-001
+ 2) Green-Car -> Wheel (sub assembly) > BOM-WHEEL-001
+ """
+ from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
+
+ bom_tree_1 = {
+ "Red-Car": {"Wheel": {"Rubber": {}}}
+ }
+ bom_tree_2 = {
+ "Green-Car": {"Wheel": {"Rubber": {}}}
+ }
+
+ parent_bom_1 = create_nested_bom(bom_tree_1, prefix="")
+ parent_bom_2 = create_nested_bom(bom_tree_2, prefix="")
+
+ # make sure both boms use same subassembly bom
+ subassembly_bom = parent_bom_1.items[0].bom_no
+ frappe.db.set_value("BOM Item", parent_bom_2.items[0].name, "bom_no", subassembly_bom)
+
+ plan = create_production_plan(item_code="Red-Car", use_multi_level_bom=1, do_not_save=True)
+ plan.append("po_items", { # Add Green-Car to Prod Plan
+ 'use_multi_level_bom': 1,
+ 'item_code': "Green-Car",
+ 'bom_no': frappe.db.get_value('Item', "Green-Car", 'default_bom'),
+ 'planned_qty': 1,
+ 'planned_start_date': now_datetime()
+ })
+ plan.get_sub_assembly_items()
+ self.assertTrue(len(plan.sub_assembly_items), 2)
+
+ plan.combine_sub_items = 1
+ plan.get_sub_assembly_items()
+
+ self.assertTrue(len(plan.sub_assembly_items), 1) # check if sub-assembly items merged
+ self.assertEqual(plan.sub_assembly_items[0].qty, 2.0)
+ self.assertEqual(plan.sub_assembly_items[0].stock_qty, 2.0)
+
+ # change warehouse in one row, sub-assemblies should not merge
+ plan.po_items[0].warehouse = "Finished Goods - _TC"
+ plan.get_sub_assembly_items()
+ self.assertTrue(len(plan.sub_assembly_items), 2)
+
def test_pp_to_mr_customer_provided(self):
" Test Material Request from Production Plan for Customer Provided Item."
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
@@ -532,6 +580,7 @@
wip_warehouse='Work In Progress - _TC',
fg_warehouse='Finished Goods - _TC',
skip_transfer=1,
+ use_multi_level_bom=1,
do_not_submit=True
)
wo.production_plan = pln.name
@@ -576,6 +625,7 @@
wip_warehouse='Work In Progress - _TC',
fg_warehouse='Finished Goods - _TC',
skip_transfer=1,
+ use_multi_level_bom=1,
do_not_submit=True
)
wo.production_plan = pln.name
diff --git a/erpnext/manufacturing/doctype/routing/routing.js b/erpnext/manufacturing/doctype/routing/routing.js
index 33a313e..b480c70 100644
--- a/erpnext/manufacturing/doctype/routing/routing.js
+++ b/erpnext/manufacturing/doctype/routing/routing.js
@@ -17,7 +17,7 @@
},
calculate_operating_cost: function(frm, child) {
- const operating_cost = flt(flt(child.hour_rate) * flt(child.time_in_mins) / 60, 2);
+ const operating_cost = flt(flt(child.hour_rate) * flt(child.time_in_mins) / 60, precision("operating_cost", child));
frappe.model.set_value(child.doctype, child.name, "operating_cost", operating_cost);
}
});
diff --git a/erpnext/manufacturing/doctype/routing/routing.py b/erpnext/manufacturing/doctype/routing/routing.py
index 1c76634..b207906 100644
--- a/erpnext/manufacturing/doctype/routing/routing.py
+++ b/erpnext/manufacturing/doctype/routing/routing.py
@@ -20,7 +20,8 @@
for operation in self.operations:
if not operation.hour_rate:
operation.hour_rate = frappe.db.get_value("Workstation", operation.workstation, 'hour_rate')
- operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60, 2)
+ operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60,
+ operation.precision("operating_cost"))
def set_routing_id(self):
sequence_id = 0
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index 549ec7b..bc07d22 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -1040,7 +1040,7 @@
wo_order.scrap_warehouse = args.fg_warehouse or "_Test Scrap Warehouse - _TC"
wo_order.company = args.company or "_Test Company"
wo_order.stock_uom = args.stock_uom or "_Test UOM"
- wo_order.use_multi_level_bom=0
+ wo_order.use_multi_level_bom= args.use_multi_level_bom or 0
wo_order.skip_transfer=args.skip_transfer or 0
wo_order.get_items_and_operations_from_bom()
wo_order.sales_order = args.sales_order or None
diff --git a/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py b/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py
index bc481ca..1fa1494 100644
--- a/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py
+++ b/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py
@@ -11,9 +11,9 @@
},
{
'label': _('Transaction'),
- 'items': ['Work Order', 'Job Card', 'Timesheet']
+ 'items': ['Work Order', 'Job Card',]
}
],
'disable_create_buttons': ['BOM', 'Routing', 'Operation',
- 'Work Order', 'Job Card', 'Timesheet']
+ 'Work Order', 'Job Card',]
}
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 12af8f8..13f0e7b 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -356,4 +356,5 @@
erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr
erpnext.patches.v13_0.update_accounts_in_loan_docs
erpnext.patches.v14_0.update_batch_valuation_flag
-erpnext.patches.v14_0.delete_non_profit_doctypes
\ No newline at end of file
+erpnext.patches.v14_0.delete_non_profit_doctypes
+erpnext.patches.v14_0.update_employee_advance_status
\ No newline at end of file
diff --git a/erpnext/patches/v14_0/update_employee_advance_status.py b/erpnext/patches/v14_0/update_employee_advance_status.py
new file mode 100644
index 0000000..a20e35a
--- /dev/null
+++ b/erpnext/patches/v14_0/update_employee_advance_status.py
@@ -0,0 +1,26 @@
+import frappe
+
+
+def execute():
+ frappe.reload_doc('hr', 'doctype', 'employee_advance')
+
+ advance = frappe.qb.DocType('Employee Advance')
+ (frappe.qb
+ .update(advance)
+ .set(advance.status, 'Returned')
+ .where(
+ (advance.docstatus == 1)
+ & ((advance.return_amount) & (advance.paid_amount == advance.return_amount))
+ & (advance.status == 'Paid')
+ )
+ ).run()
+
+ (frappe.qb
+ .update(advance)
+ .set(advance.status, 'Partly Claimed and Returned')
+ .where(
+ (advance.docstatus == 1)
+ & ((advance.claimed_amount & advance.return_amount) & (advance.paid_amount == (advance.return_amount + advance.claimed_amount)))
+ & (advance.status == 'Paid')
+ )
+ ).run()
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py
index bf8bd05..d618568 100644
--- a/erpnext/payroll/doctype/additional_salary/additional_salary.py
+++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py
@@ -105,6 +105,8 @@
return_amount += self.amount
frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", return_amount)
+ advance = frappe.get_doc("Employee Advance", self.ref_docname)
+ advance.set_status(update=True)
def update_employee_referral(self, cancel=False):
if self.ref_doctype == "Employee Referral":
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index d2a3998..b44dbb9 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -307,28 +307,59 @@
if payroll_based_on == "Attendance":
self.payment_days -= flt(absent)
- unmarked_days = self.get_unmarked_days()
consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, "consider_unmarked_attendance_as") or "Present"
if payroll_based_on == "Attendance" and consider_unmarked_attendance_as =="Absent":
+ unmarked_days = self.get_unmarked_days(include_holidays_in_total_working_days)
self.absent_days += unmarked_days #will be treated as absent
self.payment_days -= unmarked_days
- if include_holidays_in_total_working_days:
- for holiday in holidays:
- if not frappe.db.exists("Attendance", {"employee": self.employee, "attendance_date": holiday, "docstatus": 1 }):
- self.payment_days += 1
else:
self.payment_days = 0
- def get_unmarked_days(self):
- marked_days = frappe.get_all("Attendance", filters = {
- "attendance_date": ["between", [self.start_date, self.end_date]],
- "employee": self.employee,
- "docstatus": 1
- }, fields = ["COUNT(*) as marked_days"])[0].marked_days
+ def get_unmarked_days(self, include_holidays_in_total_working_days):
+ unmarked_days = self.total_working_days
+ joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
+ ["date_of_joining", "relieving_date"])
+ start_date = self.start_date
+ end_date = self.end_date
- return self.total_working_days - marked_days
+ if joining_date and (getdate(self.start_date) < joining_date <= getdate(self.end_date)):
+ start_date = joining_date
+ unmarked_days = self.get_unmarked_days_based_on_doj_or_relieving(unmarked_days,
+ include_holidays_in_total_working_days, self.start_date, joining_date)
+ if relieving_date and (getdate(self.start_date) <= relieving_date < getdate(self.end_date)):
+ end_date = relieving_date
+ unmarked_days = self.get_unmarked_days_based_on_doj_or_relieving(unmarked_days,
+ include_holidays_in_total_working_days, relieving_date, self.end_date)
+
+ # exclude days for which attendance has been marked
+ unmarked_days -= frappe.get_all("Attendance", filters = {
+ "attendance_date": ["between", [start_date, end_date]],
+ "employee": self.employee,
+ "docstatus": 1
+ }, fields = ["COUNT(*) as marked_days"])[0].marked_days
+
+ return unmarked_days
+
+ def get_unmarked_days_based_on_doj_or_relieving(self, unmarked_days,
+ include_holidays_in_total_working_days, start_date, end_date):
+ """
+ Exclude days before DOJ or after
+ Relieving Date from unmarked days
+ """
+ from erpnext.hr.doctype.employee.employee import is_holiday
+
+ if include_holidays_in_total_working_days:
+ unmarked_days -= date_diff(end_date, start_date)
+ else:
+ # exclude only if not holidays
+ for days in range(date_diff(end_date, start_date)):
+ date = add_days(end_date, -days)
+ if not is_holiday(self.employee, date):
+ unmarked_days -= 1
+
+ return unmarked_days
def get_payment_days(self, joining_date, relieving_date, include_holidays_in_total_working_days):
if not joining_date:
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index 6a5debf..fe15f2d 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -7,10 +7,12 @@
import frappe
from frappe.model.document import Document
+from frappe.tests.utils import change_settings
from frappe.utils import (
add_days,
add_months,
cstr,
+ date_diff,
flt,
get_first_day,
get_last_day,
@@ -21,6 +23,7 @@
import erpnext
from erpnext.accounts.utils import get_fiscal_year
+from erpnext.hr.doctype.attendance.attendance import mark_attendance
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
@@ -37,17 +40,17 @@
setup_test()
def tearDown(self):
+ frappe.db.rollback()
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0)
frappe.set_user("Administrator")
+ @change_settings("Payroll Settings", {
+ "payroll_based_on": "Attendance",
+ "daily_wages_fraction_for_half_day": 0.75
+ })
def test_payment_days_based_on_attendance(self):
- from erpnext.hr.doctype.attendance.attendance import mark_attendance
no_of_days = self.get_no_of_days()
- # Payroll based on attendance
- frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance")
- frappe.db.set_value("Payroll Settings", None, "daily_wages_fraction_for_half_day", 0.75)
-
emp_id = make_employee("test_payment_days_based_on_attendance@salary.com")
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
@@ -85,14 +88,78 @@
self.assertEqual(ss.gross_pay, gross_pay)
- frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
+ @change_settings("Payroll Settings", {
+ "payroll_based_on": "Attendance",
+ "consider_unmarked_attendance_as": "Absent",
+ "include_holidays_in_total_working_days": True
+ })
+ def test_payment_days_for_mid_joinee_including_holidays(self):
+ from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
+ no_of_days = self.get_no_of_days()
+ month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
+
+ new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
+ joining_date, relieving_date = add_days(month_start_date, 3), add_days(month_end_date, -5)
+ frappe.db.set_value("Employee", new_emp_id, {
+ "date_of_joining": joining_date,
+ "relieving_date": relieving_date,
+ "status": "Left"
+ })
+
+ holidays = 0
+
+ for days in range(date_diff(relieving_date, joining_date) + 1):
+ date = add_days(joining_date, days)
+ if not is_holiday("Salary Slip Test Holiday List", date):
+ mark_attendance(new_emp_id, date, 'Present', ignore_validate=True)
+ else:
+ holidays += 1
+
+ new_ss = make_employee_salary_slip("test_payment_days_based_on_joining_date@salary.com", "Monthly", "Test Payment Based On Attendence")
+
+ self.assertEqual(new_ss.total_working_days, no_of_days[0])
+ self.assertEqual(new_ss.payment_days, no_of_days[0] - holidays - 8)
+
+ @change_settings("Payroll Settings", {
+ "payroll_based_on": "Attendance",
+ "consider_unmarked_attendance_as": "Absent",
+ "include_holidays_in_total_working_days": False
+ })
+ def test_payment_days_for_mid_joinee_excluding_holidays(self):
+ from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
+
+ no_of_days = self.get_no_of_days()
+ month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
+
+ new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
+ joining_date, relieving_date = add_days(month_start_date, 3), add_days(month_end_date, -5)
+ frappe.db.set_value("Employee", new_emp_id, {
+ "date_of_joining": joining_date,
+ "relieving_date": relieving_date,
+ "status": "Left"
+ })
+
+ holidays = 0
+
+ for days in range(date_diff(relieving_date, joining_date) + 1):
+ date = add_days(joining_date, days)
+ if not is_holiday("Salary Slip Test Holiday List", date):
+ mark_attendance(new_emp_id, date, 'Present', ignore_validate=True)
+ else:
+ holidays += 1
+
+ new_ss = make_employee_salary_slip("test_payment_days_based_on_joining_date@salary.com", "Monthly", "Test Payment Based On Attendence")
+
+ self.assertEqual(new_ss.total_working_days, no_of_days[0] - no_of_days[1])
+ self.assertEqual(new_ss.payment_days, no_of_days[0] - holidays - 8)
+
+ @change_settings("Payroll Settings", {
+ "payroll_based_on": "Leave"
+ })
def test_payment_days_based_on_leave_application(self):
no_of_days = self.get_no_of_days()
- # Payroll based on attendance
- frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
-
emp_id = make_employee("test_payment_days_based_on_leave_application@salary.com")
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
@@ -133,8 +200,9 @@
self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 4)
- frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
-
+ @change_settings("Payroll Settings", {
+ "payroll_based_on": "Attendance"
+ })
def test_payment_days_in_salary_slip_based_on_timesheet(self):
from erpnext.hr.doctype.attendance.attendance import mark_attendance
from erpnext.projects.doctype.timesheet.test_timesheet import (
@@ -145,9 +213,6 @@
make_salary_slip as make_salary_slip_for_timesheet,
)
- # Payroll based on attendance
- frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance")
-
emp = make_employee("test_employee_timesheet@salary.com", company="_Test Company", holiday_list="Salary Slip Test Holiday List")
frappe.db.set_value("Employee", emp, {"relieving_date": None, "status": "Active"})
@@ -185,17 +250,15 @@
self.assertEqual(salary_slip.gross_pay, flt(gross_pay, 2))
- frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
-
+ @change_settings("Payroll Settings", {
+ "payroll_based_on": "Attendance"
+ })
def test_component_amount_dependent_on_another_payment_days_based_component(self):
from erpnext.hr.doctype.attendance.attendance import mark_attendance
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
create_salary_structure_assignment,
)
- # Payroll based on attendance
- frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance")
-
salary_structure = make_salary_structure_for_payment_days_based_component_dependency()
employee = make_employee("test_payment_days_based_component@salary.com", company="_Test Company")
@@ -238,11 +301,12 @@
expected_amount = flt((flt(ss.gross_pay) - payment_days_based_comp_amount) * 0.12, precision)
self.assertEqual(actual_amount, expected_amount)
- frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
+ @change_settings("Payroll Settings", {
+ "include_holidays_in_total_working_days": 1
+ })
def test_salary_slip_with_holidays_included(self):
no_of_days = self.get_no_of_days()
- frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1)
make_employee("test_salary_slip_with_holidays_included@salary.com")
frappe.db.set_value("Employee", frappe.get_value("Employee",
{"employee_name":"test_salary_slip_with_holidays_included@salary.com"}, "name"), "relieving_date", None)
@@ -256,9 +320,11 @@
self.assertEqual(ss.earnings[1].amount, 3000)
self.assertEqual(ss.gross_pay, 78000)
+ @change_settings("Payroll Settings", {
+ "include_holidays_in_total_working_days": 0
+ })
def test_salary_slip_with_holidays_excluded(self):
no_of_days = self.get_no_of_days()
- frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0)
make_employee("test_salary_slip_with_holidays_excluded@salary.com")
frappe.db.set_value("Employee", frappe.get_value("Employee",
{"employee_name":"test_salary_slip_with_holidays_excluded@salary.com"}, "name"), "relieving_date", None)
@@ -273,14 +339,15 @@
self.assertEqual(ss.earnings[1].amount, 3000)
self.assertEqual(ss.gross_pay, 78000)
+ @change_settings("Payroll Settings", {
+ "include_holidays_in_total_working_days": 1
+ })
def test_payment_days(self):
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
create_salary_structure_assignment,
)
no_of_days = self.get_no_of_days()
- # Holidays not included in working days
- frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1)
# set joinng date in the same month
employee = make_employee("test_payment_days@salary.com")
@@ -338,11 +405,12 @@
frappe.set_user("test_employee_salary_slip_read_permission@salary.com")
self.assertTrue(salary_slip_test_employee.has_permission("read"))
+ @change_settings("Payroll Settings", {
+ "email_salary_slip_to_employee": 1
+ })
def test_email_salary_slip(self):
frappe.db.sql("delete from `tabEmail Queue`")
- frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 1)
-
make_employee("test_email_salary_slip@salary.com")
ss = make_employee_salary_slip("test_email_salary_slip@salary.com", "Monthly", "Test Salary Slip Email")
ss.company = "_Test Company"
diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js
index f615f05..453d46c 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.js
+++ b/erpnext/projects/doctype/timesheet/timesheet.js
@@ -116,7 +116,7 @@
currency: function(frm) {
let base_currency = frappe.defaults.get_global_default('currency');
- if (base_currency != frm.doc.currency) {
+ if (frm.doc.currency && (base_currency != frm.doc.currency)) {
frappe.call({
method: "erpnext.setup.utils.get_exchange_rate",
args: {
diff --git a/erpnext/projects/report/project_profitability/test_project_profitability.py b/erpnext/projects/report/project_profitability/test_project_profitability.py
index 1eb3d0d..3ca28c1 100644
--- a/erpnext/projects/report/project_profitability/test_project_profitability.py
+++ b/erpnext/projects/report/project_profitability/test_project_profitability.py
@@ -1,6 +1,5 @@
-import unittest
-
import frappe
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, getdate
from erpnext.hr.doctype.employee.test_employee import make_employee
@@ -12,7 +11,7 @@
from erpnext.projects.report.project_profitability.project_profitability import execute
-class TestProjectProfitability(unittest.TestCase):
+class TestProjectProfitability(FrappeTestCase):
def setUp(self):
frappe.db.sql('delete from `tabTimesheet`')
emp = make_employee('test_employee_9@salary.com', company='_Test Company')
@@ -67,6 +66,3 @@
fractional_cost = self.salary_slip.base_gross_pay * utilization
self.assertEqual(fractional_cost, row.fractional_cost)
-
- def tearDown(self):
- frappe.db.rollback()
diff --git a/erpnext/regional/india/e_invoice/einv_item_template.json b/erpnext/regional/india/e_invoice/einv_item_template.json
index 78e5651..2c04c6d 100644
--- a/erpnext/regional/india/e_invoice/einv_item_template.json
+++ b/erpnext/regional/india/e_invoice/einv_item_template.json
@@ -23,9 +23,5 @@
"StateCesAmt": "{item.state_cess_amount}",
"StateCesNonAdvlAmt": "{item.state_cess_nadv_amount}",
"OthChrg": "{item.other_charges}",
- "TotItemVal": "{item.total_value}",
- "BchDtls": {{
- "Nm": "{item.batch_no}",
- "ExpDt": "{item.batch_expiry_date}"
- }}
+ "TotItemVal": "{item.total_value}"
}}
\ No newline at end of file
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index e3f7e90..64c75c4 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -214,8 +214,6 @@
item.taxable_value = abs(item.taxable_value)
item.discount_amount = 0
- item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None
- item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None
item.is_service_item = 'Y' if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else 'N'
item.serial_no = ""
diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js
index 0e1a915..34e9a52 100644
--- a/erpnext/selling/doctype/quotation/quotation.js
+++ b/erpnext/selling/doctype/quotation/quotation.js
@@ -40,7 +40,6 @@
erpnext.selling.QuotationController = class QuotationController extends erpnext.selling.SellingController {
onload(doc, dt, dn) {
- var me = this;
super.onload(doc, dt, dn);
}
party_name() {
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index eb98e6c..f80eaf2 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -562,6 +562,7 @@
var me = this;
var dialog = new frappe.ui.Dialog({
title: __("Select Items"),
+ size: "large",
fields: [
{
"fieldtype": "Check",
@@ -663,7 +664,8 @@
} else {
let po_items = [];
me.frm.doc.items.forEach(d => {
- let pending_qty = (flt(d.stock_qty) - flt(d.ordered_qty)) / flt(d.conversion_factor);
+ let ordered_qty = me.get_ordered_qty(d, me.frm.doc);
+ let pending_qty = (flt(d.stock_qty) - ordered_qty) / flt(d.conversion_factor);
if (pending_qty > 0) {
po_items.push({
"doctype": "Sales Order Item",
@@ -689,6 +691,24 @@
dialog.show();
}
+ get_ordered_qty(item, so) {
+ let ordered_qty = item.ordered_qty;
+ if (so.packed_items) {
+ // calculate ordered qty based on packed items in case of product bundle
+ let packed_items = so.packed_items.filter(
+ (pi) => pi.parent_detail_docname == item.name
+ );
+ if (packed_items) {
+ ordered_qty = packed_items.reduce(
+ (sum, pi) => sum + flt(pi.ordered_qty),
+ 0
+ );
+ ordered_qty = ordered_qty / packed_items.length;
+ }
+ }
+ return ordered_qty;
+ }
+
hold_sales_order(){
var me = this;
var d = new frappe.ui.Dialog({
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 0f5b1e3..abbb3c9 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -877,6 +877,9 @@
target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty))
target.project = source_parent.project
+ def update_item_for_packed_item(source, target, source_parent):
+ target.qty = flt(source.qty) - flt(source.ordered_qty)
+
# po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")})
doc = get_mapped_doc("Sales Order", source_name, {
"Sales Order": {
@@ -920,6 +923,7 @@
"Packed Item": {
"doctype": "Purchase Order Item",
"field_map": [
+ ["name", "sales_order_packed_item"],
["parent", "sales_order"],
["uom", "uom"],
["conversion_factor", "conversion_factor"],
@@ -934,6 +938,7 @@
"supplier",
"pricing_rules"
],
+ "postprocess": update_item_for_packed_item,
"condition": lambda doc: doc.parent_item in items_to_map
}
}, target_doc, set_missing_values)
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index f5a34c0..b528479 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -959,6 +959,42 @@
self.assertEqual(purchase_order.items[0].item_code, "_Test Bundle Item 1")
self.assertEqual(purchase_order.items[1].item_code, "_Test Bundle Item 2")
+ def test_purchase_order_updates_packed_item_ordered_qty(self):
+ """
+ Tests if the packed item's `ordered_qty` is updated with the quantity of the Purchase Order
+ """
+ from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order
+
+ product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0})
+ make_item("_Test Bundle Item 1", {"is_stock_item": 1})
+ make_item("_Test Bundle Item 2", {"is_stock_item": 1})
+
+ make_product_bundle("_Test Product Bundle",
+ ["_Test Bundle Item 1", "_Test Bundle Item 2"])
+
+ so_items = [
+ {
+ "item_code": product_bundle.item_code,
+ "warehouse": "",
+ "qty": 2,
+ "rate": 400,
+ "delivered_by_supplier": 1,
+ "supplier": '_Test Supplier'
+ }
+ ]
+
+ so = make_sales_order(item_list=so_items)
+
+ purchase_order = make_purchase_order(so.name, selected_items=so_items)
+ purchase_order.supplier = "_Test Supplier"
+ purchase_order.set_warehouse = "_Test Warehouse - _TC"
+ purchase_order.save()
+ purchase_order.submit()
+
+ so.reload()
+ self.assertEqual(so.packed_items[0].ordered_qty, 2)
+ self.assertEqual(so.packed_items[1].ordered_qty, 2)
+
def test_reserved_qty_for_closing_so(self):
bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"},
fields=["reserved_qty"])
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 95b1e8b..36ad8fe 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -3,7 +3,6 @@
import json
-import os
import frappe
import frappe.defaults
@@ -422,14 +421,14 @@
return " - ".join(parts)
def install_country_fixtures(company, country):
- path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(country))
- if os.path.exists(path.encode("utf-8")):
- try:
- module_name = "erpnext.regional.{0}.setup.setup".format(frappe.scrub(country))
- frappe.get_attr(module_name)(company, False)
- except Exception as e:
- frappe.log_error()
- frappe.throw(_("Failed to setup defaults for country {0}. Please contact support.").format(frappe.bold(country)))
+ try:
+ module_name = f"erpnext.regional.{frappe.scrub(country)}.setup.setup"
+ frappe.get_attr(module_name)(company, False)
+ except ImportError:
+ pass
+ except Exception:
+ frappe.log_error()
+ frappe.throw(_("Failed to setup defaults for country {0}. Please contact support.").format(frappe.bold(country)))
def update_company_current_month_sales(company):
diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
index dbaefc1..6dc4fee 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
@@ -11,6 +11,7 @@
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.utils import update_gl_entries_after
from erpnext.assets.doctype.asset.test_asset import create_asset_category, create_fixed_asset_item
+from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
get_gl_entries,
make_purchase_receipt,
@@ -177,6 +178,53 @@
self.assertEqual(serial_no.purchase_rate - serial_no_rate, 5.0)
self.assertEqual(serial_no.warehouse, "Stores - TCP1")
+ def test_serialized_lcv_delivered(self):
+ """In some cases you'd want to deliver before you can know all the
+ landed costs, this should be allowed for serial nos too.
+
+ Case:
+ - receipt a serial no @ X rate
+ - delivery the serial no @ X rate
+ - add LCV to receipt X + Y
+ - LCV should be successful
+ - delivery should reflect X+Y valuation.
+ """
+ serial_no = "LCV_TEST_SR_NO"
+ item_code = "_Test Serialized Item"
+ warehouse = "Stores - TCP1"
+
+ pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
+ warehouse=warehouse, qty=1, rate=200,
+ item_code=item_code, serial_no=serial_no)
+
+ serial_no_rate = frappe.db.get_value("Serial No", serial_no, "purchase_rate")
+
+ # deliver it before creating LCV
+ dn = create_delivery_note(item_code=item_code,
+ company='_Test Company with perpetual inventory', warehouse='Stores - TCP1',
+ serial_no=serial_no, qty=1, rate=500,
+ cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1")
+
+ charges = 10
+ create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=charges)
+
+ new_purchase_rate = serial_no_rate + charges
+
+ serial_no = frappe.db.get_value("Serial No", serial_no,
+ ["warehouse", "purchase_rate"], as_dict=1)
+
+ self.assertEqual(serial_no.purchase_rate, new_purchase_rate)
+
+ stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
+ filters={
+ "voucher_no": dn.name,
+ "voucher_type": dn.doctype,
+ "is_cancelled": 0 # LCV cancels with same name.
+ },
+ fieldname="stock_value_difference")
+
+ # reposting should update the purchase rate in future delivery
+ self.assertEqual(stock_value_difference, -new_purchase_rate)
def test_landed_cost_voucher_for_odd_numbers (self):
pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", do_not_save=True)
diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py
index 1cda781..866f3ab 100644
--- a/erpnext/stock/doctype/material_request/test_material_request.py
+++ b/erpnext/stock/doctype/material_request/test_material_request.py
@@ -626,13 +626,13 @@
mr.schedule_date = today()
if not frappe.db.get_value('UOM Conversion Detail',
- {'parent': item.item_code, 'uom': 'Kg'}):
- item_doc = frappe.get_doc('Item', item.item_code)
- item_doc.append('uoms', {
- 'uom': 'Kg',
- 'conversion_factor': 5
- })
- item_doc.save(ignore_permissions=True)
+ {'parent': item.item_code, 'uom': 'Kg'}):
+ item_doc = frappe.get_doc('Item', item.item_code)
+ item_doc.append('uoms', {
+ 'uom': 'Kg',
+ 'conversion_factor': 5
+ })
+ item_doc.save(ignore_permissions=True)
item.uom = 'Kg'
for item in mr.items:
diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json
index d2d4789..d6e2e9c 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.json
+++ b/erpnext/stock/doctype/packed_item/packed_item.json
@@ -26,6 +26,7 @@
"section_break_13",
"actual_qty",
"projected_qty",
+ "ordered_qty",
"column_break_16",
"incoming_rate",
"page_break",
@@ -224,13 +225,21 @@
"label": "Rate",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "ordered_qty",
+ "fieldtype": "Float",
+ "label": "Ordered Qty",
+ "no_copy": 1,
+ "read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2022-01-28 16:03:30.780111",
+ "modified": "2022-02-22 12:57:45.325488",
"modified_by": "Administrator",
"module": "Stock",
"name": "Packed Item",
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 5484a11..b2eaecb 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -272,9 +272,9 @@
and IFNULL(batch.`expiry_date`, '2200-01-01') > %(today)s
{warehouse_condition}
GROUP BY
- `warehouse`,
- `batch_no`,
- `item_code`
+ sle.`warehouse`,
+ sle.`batch_no`,
+ sle.`item_code`
HAVING `qty` > 0
ORDER BY IFNULL(batch.`expiry_date`, '2200-01-01'), batch.`creation`
""".format(warehouse_condition=warehouse_condition), { #nosec
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index a24acb1..fa28f22 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -161,6 +161,15 @@
qty=abs(existing_bin_qty)
)
+ existing_bin_qty, existing_bin_stock_value = frappe.db.get_value(
+ "Bin",
+ {
+ "item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC"
+ },
+ ["actual_qty", "stock_value"]
+ )
+
pr = make_purchase_receipt()
stock_value_difference = frappe.db.get_value(
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 01d25b2..684a8d4 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
@@ -389,10 +389,13 @@
)
- def assertSLEs(self, doc, expected_sles):
+ def assertSLEs(self, doc, expected_sles, sle_filters=None):
""" Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line"""
- sles = frappe.get_all("Stock Ledger Entry", fields=["*"],
- filters={"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled":0},
+
+ filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0}
+ if sle_filters:
+ filters.update(sle_filters)
+ sles = frappe.get_all("Stock Ledger Entry", fields=["*"], filters=filters,
order_by="timestamp(posting_date, posting_time), creation")
for exp_sle, act_sle in zip(expected_sles, sles):
@@ -665,6 +668,78 @@
{"actual_qty": -10, "stock_value_difference": -10*40, "stock_queue": []},
]))
+ def test_fifo_dependent_consumption(self):
+ item = make_item("_TestFifoTransferRates")
+ source = "_Test Warehouse - _TC"
+ target = "Stores - _TC"
+
+ rates = [10 * i for i in range(1, 20)]
+
+ receipt = make_stock_entry(item_code=item.name, target=source, qty=10, do_not_save=True, rate=10)
+ for rate in rates[1:]:
+ row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False)
+ row.basic_rate = rate
+ receipt.append("items", row)
+
+ receipt.save()
+ receipt.submit()
+
+ expected_queues = []
+ for idx, rate in enumerate(rates, start=1):
+ expected_queues.append(
+ {"stock_queue": [[10, 10 * i] for i in range(1, idx + 1)]}
+ )
+ self.assertSLEs(receipt, expected_queues)
+
+ transfer = make_stock_entry(item_code=item.name, source=source, target=target, qty=10, do_not_save=True, rate=10)
+ for rate in rates[1:]:
+ row = frappe.copy_doc(transfer.items[0], ignore_no_copy=False)
+ transfer.append("items", row)
+
+ transfer.save()
+ transfer.submit()
+
+ # same exact queue should be transferred
+ self.assertSLEs(transfer, expected_queues, sle_filters={"warehouse": target})
+
+ def test_fifo_multi_item_repack_consumption(self):
+ rm = make_item("_TestFifoRepackRM")
+ packed = make_item("_TestFifoRepackFinished")
+ warehouse = "_Test Warehouse - _TC"
+
+ rates = [10 * i for i in range(1, 5)]
+
+ receipt = make_stock_entry(item_code=rm.name, target=warehouse, qty=10, do_not_save=True, rate=10)
+ for rate in rates[1:]:
+ row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False)
+ row.basic_rate = rate
+ receipt.append("items", row)
+
+ receipt.save()
+ receipt.submit()
+
+ repack = make_stock_entry(item_code=rm.name, source=warehouse, qty=10,
+ do_not_save=True, rate=10, purpose="Repack")
+ for rate in rates[1:]:
+ row = frappe.copy_doc(repack.items[0], ignore_no_copy=False)
+ repack.append("items", row)
+
+ repack.append("items", {
+ "item_code": packed.name,
+ "t_warehouse": warehouse,
+ "qty": 1,
+ "transfer_qty": 1,
+ })
+
+ repack.save()
+ repack.submit()
+
+ # same exact queue should be transferred
+ self.assertSLEs(repack, [
+ {"incoming_rate": sum(rates) * 10}
+ ], sle_filters={"item_code": packed.name})
+
+
def create_repack_entry(**args):
args = frappe._dict(args)
repack = frappe.new_doc("Stock Entry")
diff --git a/erpnext/stock/doctype/warehouse/warehouse.json b/erpnext/stock/doctype/warehouse/warehouse.json
index 05076b5..c695d54 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.json
+++ b/erpnext/stock/doctype/warehouse/warehouse.json
@@ -244,7 +244,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
- "modified": "2021-12-03 04:40:06.414630",
+ "modified": "2022-03-01 02:37:48.034944",
"modified_by": "Administrator",
"module": "Stock",
"name": "Warehouse",
@@ -301,5 +301,7 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
- "title_field": "warehouse_name"
+ "states": [],
+ "title_field": "warehouse_name",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py
index 2630805..ca963b7 100644
--- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
index 7826d34..1ba2482 100644
--- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
+++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
@@ -21,6 +21,7 @@
"stock_value",
"stock_value_difference",
"valuation_rate",
+ "voucher_detail_no",
)
@@ -66,7 +67,9 @@
balance_qty += sle.actual_qty
balance_stock_value += sle.stock_value_difference
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
- balance_qty = sle.qty_after_transaction
+ balance_qty = frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "qty")
+ if balance_qty is None:
+ balance_qty = sle.qty_after_transaction
sle.fifo_queue_qty = fifo_qty
sle.fifo_stock_value = fifo_value
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 1b90086..ba1081f 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -28,6 +28,16 @@
def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
+ """ Create SL entries from SL entry dicts
+
+ args:
+ - allow_negative_stock: disable negative stock valiations if true
+ - via_landed_cost_voucher: landed cost voucher cancels and reposts
+ entries of purchase document. This flag is used to identify if
+ cancellation and repost is happening via landed cost voucher, in
+ such cases certain validations need to be ignored (like negative
+ stock)
+ """
from erpnext.controllers.stock_controller import future_sle_exists
if sl_entries:
cancel = sl_entries[0].get("is_cancelled")
@@ -39,7 +49,7 @@
future_sle_exists(args, sl_entries)
for sle in sl_entries:
- if sle.serial_no:
+ if sle.serial_no and not via_landed_cost_voucher:
validate_serial_no(sle)
if cancel:
@@ -819,7 +829,7 @@
if msg_list:
message = "\n\n".join(msg_list)
if self.verbose:
- frappe.throw(message, NegativeStockError, title='Insufficient Stock')
+ frappe.throw(message, NegativeStockError, title=_('Insufficient Stock'))
else:
raise NegativeStockError(message)
@@ -1147,7 +1157,7 @@
neg_sle[0]["posting_date"], neg_sle[0]["posting_time"],
frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]))
- frappe.throw(message, NegativeStockError, title='Insufficient Stock')
+ frappe.throw(message, NegativeStockError, title=_('Insufficient Stock'))
if not args.batch_no:
@@ -1161,7 +1171,7 @@
frappe.get_desk_link('Warehouse', args.warehouse),
neg_batch_sle[0]["posting_date"], neg_batch_sle[0]["posting_time"],
frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]))
- frappe.throw(message, NegativeStockError, title="Insufficient Stock for Batch")
+ frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
def get_future_sle_with_negative_qty(args):
diff --git a/erpnext/templates/includes/footer/footer_powered.html b/erpnext/templates/includes/footer/footer_powered.html
index 82b2716..faf5e92 100644
--- a/erpnext/templates/includes/footer/footer_powered.html
+++ b/erpnext/templates/includes/footer/footer_powered.html
@@ -1,27 +1 @@
-{% set domains = frappe.get_doc("Domain Settings").active_domains %}
-{% set links = {
- 'Manufacturing': '/manufacturing',
- 'Services': '/services',
- 'Retail': '/retail',
- 'Distribution': '/distribution',
- 'Non Profit': '/non-profit',
- 'Education': '/education',
- 'Healthcare': '/healthcare',
- 'Agriculture': '/agriculture',
- 'Hospitality': ''
-} %}
-
-{% set link = '' %}
-{% set label = '' %}
-{% if domains %}
- {% set label = domains[0].domain %}
- {% set link = links[label] %}
-{% endif %}
-
-{% if label == "Services" %}
- {% set label = "Service" %}
-{% endif %}
-
-
-
-<a href="https://erpnext.com{{ link }}?source=website_footer" target="_blank" class="text-muted">Powered by ERPNext - {{ '' if domains else 'Open Source' }} ERP Software {{ ('for ' + label + ' Companies') if domains else '' }}</a>
+<a href="https://erpnext.com?source=website_footer" target="_blank" class="text-muted">Powered by ERPNext</a>
diff --git a/erpnext/tests/test_point_of_sale.py b/erpnext/tests/test_point_of_sale.py
index 3299c88..38f2c16 100644
--- a/erpnext/tests/test_point_of_sale.py
+++ b/erpnext/tests/test_point_of_sale.py
@@ -25,7 +25,7 @@
Test Stock and Service Item Search.
"""
- pos_profile = make_pos_profile()
+ pos_profile = make_pos_profile(name="Test POS Profile for Search")
item1 = make_item("Test Search Stock Item", {"is_stock_item": 1})
make_stock_entry(
item_code="Test Search Stock Item",