Merge pull request #29979 from deepeshgarg007/bank_reco_multicurrency_fix
fix: Multi-currency bank reconciliation fixes
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/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
index f8d191c..82705a9 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
@@ -64,10 +64,10 @@
<td></td>
<td><b>{{ frappe.format(row.account, {fieldtype: "Link"}) or " " }}</b></td>
<td style="text-align: right">
- {{ row.account and frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }}
+ {{ row.get('account', '') and frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }}
</td>
<td style="text-align: right">
- {{ row.account and frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }}
+ {{ row.get('account', '') and frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }}
</td>
{% endif %}
<td style="text-align: right">
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
index 088c190..29f2e98 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
@@ -51,6 +51,13 @@
}
}
});
+ frm.set_query("account", function() {
+ return {
+ filters: {
+ 'company': frm.doc.company
+ }
+ };
+ });
if(frm.doc.__islocal){
frm.set_value('from_date', frappe.datetime.add_months(frappe.datetime.get_today(), -1));
frm.set_value('to_date', frappe.datetime.get_today());
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 5062c1c..80b95db 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -2,7 +2,7 @@
"actions": [],
"allow_import": 1,
"autoname": "naming_series:",
- "creation": "2013-05-24 19:29:05",
+ "creation": "2022-01-25 10:29:57.771398",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
@@ -651,7 +651,6 @@
"hide_seconds": 1,
"label": "Ignore Pricing Rule",
"no_copy": 1,
- "permlevel": 0,
"print_hide": 1
},
{
@@ -1974,9 +1973,10 @@
},
{
"default": "0",
+ "description": "Issue a debit note with 0 qty against an existing Sales Invoice",
"fieldname": "is_debit_note",
"fieldtype": "Check",
- "label": "Is Debit Note"
+ "label": "Is Rate Adjustment Entry (Debit Note)"
},
{
"default": "0",
@@ -2038,7 +2038,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2021-12-23 20:19:38.667508",
+ "modified": "2022-03-08 16:08:53.517903",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
@@ -2089,8 +2089,9 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"timeline_field": "customer",
"title_field": "title",
"track_changes": 1,
"track_seen": 1
-}
+}
\ 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..54217fb 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -42,7 +42,11 @@
from erpnext.setup.doctype.company.company import update_company_current_month_sales
from erpnext.stock.doctype.batch.batch import set_batch_nos
from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so
-from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no, get_serial_nos
+from erpnext.stock.doctype.serial_no.serial_no import (
+ get_delivery_note_serial_no,
+ get_serial_nos,
+ update_serial_nos_after_submit,
+)
from erpnext.stock.utils import calculate_mapped_packed_items_return
form_grid_templates = {
@@ -226,6 +230,9 @@
# because updating reserved qty in bin depends upon updated delivered qty in SO
if self.update_stock == 1:
self.update_stock_ledger()
+ if self.is_return and self.update_stock:
+ update_serial_nos_after_submit(self, "items")
+
# this sequence because outstanding may get -ve
self.make_gl_entries()
@@ -263,6 +270,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
@@ -1252,14 +1262,14 @@
def update_billing_status_in_dn(self, update_modified=True):
updated_delivery_notes = []
for d in self.get("items"):
- if d.so_detail:
- updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified)
- elif d.dn_detail:
+ if d.dn_detail:
billed_amt = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item`
where dn_detail=%s and docstatus=1""", d.dn_detail)
billed_amt = billed_amt and billed_amt[0][0] or 0
frappe.db.set_value("Delivery Note Item", d.dn_detail, "billed_amt", billed_amt, update_modified=update_modified)
updated_delivery_notes.append(d.delivery_note)
+ elif d.so_detail:
+ updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified)
for dn in set(updated_delivery_notes):
frappe.get_doc("Delivery Note", dn).update_billing_percentage(update_modified=update_modified)
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 6d929e4..62c3508 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -2615,6 +2615,12 @@
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
+ def test_standalone_serial_no_return(self):
+ si = create_sales_invoice(item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1)
+ si.reload()
+ self.assertTrue(si.items[0].serial_no)
+
+
def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####'
diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py
index 792e7d2..7e51299 100644
--- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py
+++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py
@@ -71,8 +71,7 @@
if doc.currency != doc.company_currency:
shipping_amount = flt(shipping_amount / doc.conversion_rate, 2)
- if shipping_amount:
- self.add_shipping_rule_to_tax_table(doc, shipping_amount)
+ self.add_shipping_rule_to_tax_table(doc, shipping_amount)
def get_shipping_amount_from_rules(self, value):
for condition in self.get("conditions"):
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index d6f6c5b..791cd1d 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -151,7 +151,7 @@
def set_other_values(party_details, party, party_type):
# copy
- if party_type=="Customer":
+ if party_type == "Customer":
to_copy = ["customer_name", "customer_group", "territory", "language"]
else:
to_copy = ["supplier_name", "supplier_group", "language"]
@@ -170,12 +170,8 @@
return party.default_price_list
if party.doctype == "Customer":
- price_list = frappe.get_cached_value("Customer Group",
- party.customer_group, "default_price_list")
- if price_list:
- return price_list
+ return frappe.db.get_value("Customer Group", party.customer_group, "default_price_list")
- return None
def set_price_list(party_details, party, party_type, given_price_list, pos=None):
# price list
diff --git a/erpnext/accounts/test_party.py b/erpnext/accounts/test_party.py
new file mode 100644
index 0000000..f7a1a85
--- /dev/null
+++ b/erpnext/accounts/test_party.py
@@ -0,0 +1,16 @@
+import frappe
+from frappe.tests.utils import FrappeTestCase
+
+from erpnext.accounts.party import get_default_price_list
+
+
+class PartyTestCase(FrappeTestCase):
+ def test_get_default_price_list_should_return_none_for_invalid_group(self):
+ customer = frappe.get_doc({
+ 'doctype': 'Customer',
+ 'customer_name': 'test customer',
+ }).insert(ignore_permissions=True, ignore_mandatory=True)
+ customer.customer_group = None
+ customer.save()
+ price_list = get_default_price_list(customer)
+ assert price_list is None
diff --git a/erpnext/assets/doctype/asset/asset_dashboard.py b/erpnext/assets/doctype/asset/asset_dashboard.py
index 00d0847..c81b611 100644
--- a/erpnext/assets/doctype/asset/asset_dashboard.py
+++ b/erpnext/assets/doctype/asset/asset_dashboard.py
@@ -1,3 +1,6 @@
+from frappe import _
+
+
def get_data():
return {
'non_standard_fieldnames': {
@@ -5,7 +8,7 @@
},
'transactions': [
{
- 'label': ['Movement'],
+ 'label': _('Movement'),
'items': ['Asset Movement']
}
]
diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py
index 874fb63..6e04242 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -23,7 +23,7 @@
frappe.db.commit()
def get_depreciable_assets(date):
- return frappe.db.sql_list("""select a.name
+ return frappe.db.sql_list("""select distinct a.name
from tabAsset a, `tabDepreciation Schedule` ds
where a.name = ds.parent and a.docstatus=1 and ds.schedule_date<=%s and a.calculate_depreciation = 1
and a.status in ('Submitted', 'Partially Depreciated')
diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js
index 52996e9..5c03b98 100644
--- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js
+++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js
@@ -48,7 +48,7 @@
<div class='col-sm-3 small'>
<a onclick="frappe.set_route('List', 'Asset Maintenance Log',
{'asset_name': '${d.asset_name}','maintenance_status': '${d.maintenance_status}' });">
- ${d.maintenance_status} <span class="badge">${d.count}</span>
+ ${__(d.maintenance_status)} <span class="badge">${d.count}</span>
</a>
</div>
</div>`).appendTo(rows);
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/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.js b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.js
index a739cc3..0073170 100644
--- a/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.js
+++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.js
@@ -3,15 +3,11 @@
frappe.ui.form.on('Bulk Transaction Log', {
- before_load: function(frm) {
- query(frm);
- },
-
refresh: function(frm) {
frm.disable_save();
frm.add_custom_button(__('Retry Failed Transactions'), ()=>{
frappe.confirm(__("Retry Failing Transactions ?"), ()=>{
- query(frm);
+ query(frm, 1);
}
);
});
@@ -25,8 +21,8 @@
log_date: frm.doc.log_date
}
}).then((r) => {
- if (r.message) {
- frm.remove_custom_button("Retry Failed Transactions");
+ if (r.message === "No Failed Records") {
+ frappe.show_alert(__(r.message), 5);
} else {
frappe.show_alert(__("Retrying Failed Transactions"), 5);
}
diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.py b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.py
index de7cde5..92f37f5 100644
--- a/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.py
+++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.py
@@ -15,6 +15,8 @@
@frappe.whitelist()
def retry_failing_transaction(log_date=None):
+ if not log_date:
+ log_date = str(date.today())
btp = frappe.qb.DocType("Bulk Transaction Log Detail")
data = (
frappe.qb.from_(btp)
@@ -25,9 +27,7 @@
.where(btp.date == log_date)
).run(as_dict=True)
- if data:
- if not log_date:
- log_date = str(date.today())
+ if data :
if len(data) > 10:
frappe.enqueue(job, queue="long", job_name="bulk_retry", data=data, log_date=log_date)
else:
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..a533da0
--- /dev/null
+++ b/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py
@@ -0,0 +1,68 @@
+# 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, days=1) # to receive (ordered)
+ self.setup_material_request(order=True, receive=True, days=2) # 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
+
+ data = get_data(self.filters.update({"from_date": add_days(today(), 10)}))
+ self.assertEqual(len(data), 0) # MRs today should not be fetched as from date is in future
+
+ 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, days=0):
+ po = None
+ test_records = frappe.get_test_records('Material Request')
+
+ mr = frappe.copy_doc(test_records[0])
+ mr.transaction_date = add_days(today(), days)
+ mr.schedule_date = add_days(mr.transaction_date, 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 = mr.schedule_date
+ 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/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 8c3aab4..2166633 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -208,10 +208,15 @@
return items
-def get_returned_qty_map_for_row(row_name, doctype):
+def get_returned_qty_map_for_row(return_against, party, row_name, doctype):
child_doctype = doctype + " Item"
reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype)
+ if doctype in ('Purchase Receipt', 'Purchase Invoice'):
+ party_type = 'supplier'
+ else:
+ party_type = 'customer'
+
fields = [
"sum(abs(`tab{0}`.qty)) as qty".format(child_doctype),
"sum(abs(`tab{0}`.stock_qty)) as stock_qty".format(child_doctype)
@@ -226,9 +231,12 @@
if doctype == "Purchase Receipt":
fields += ["sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)]
+ # Used retrun against and supplier and is_retrun because there is an index added for it
data = frappe.db.get_list(doctype,
fields = fields,
filters = [
+ [doctype, "return_against", "=", return_against],
+ [doctype, party_type, "=", party],
[doctype, "docstatus", "=", 1],
[doctype, "is_return", "=", 1],
[child_doctype, reference_field, "=", row_name]
@@ -307,7 +315,7 @@
target_doc.serial_no = '\n'.join(serial_nos)
if doctype == "Purchase Receipt":
- returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
+ returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.supplier, source_doc.name, doctype)
target_doc.received_qty = -1 * flt(source_doc.received_qty - (returned_qty_map.get('received_qty') or 0))
target_doc.rejected_qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get('rejected_qty') or 0))
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
@@ -321,7 +329,7 @@
target_doc.purchase_receipt_item = source_doc.name
elif doctype == "Purchase Invoice":
- returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
+ returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.supplier, source_doc.name, doctype)
target_doc.received_qty = -1 * flt(source_doc.received_qty - (returned_qty_map.get('received_qty') or 0))
target_doc.rejected_qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get('rejected_qty') or 0))
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
@@ -335,7 +343,7 @@
target_doc.purchase_invoice_item = source_doc.name
elif doctype == "Delivery Note":
- returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
+ returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.customer, source_doc.name, doctype)
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0))
@@ -348,7 +356,7 @@
if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return
elif doctype == "Sales Invoice" or doctype == "POS Invoice":
- returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
+ returned_qty_map = get_returned_qty_map_for_row(source_parent.name, source_parent.customer, source_doc.name, doctype)
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0))
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 2776628..d362cdd 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -37,6 +37,8 @@
self.set_discount_amount()
self.apply_discount_amount()
+ self.calculate_shipping_charges()
+
if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
self.calculate_total_advance()
@@ -50,7 +52,6 @@
self.initialize_taxes()
self.determine_exclusive_rate()
self.calculate_net_total()
- self.calculate_shipping_charges()
self.calculate_taxes()
self.manipulate_grand_total_for_inclusive_tax()
self.calculate_totals()
@@ -113,17 +114,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"))
@@ -269,8 +277,11 @@
shipping_rule = frappe.get_doc("Shipping Rule", self.doc.shipping_rule)
shipping_rule.apply(self.doc)
+ self._calculate()
+
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 +337,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 +476,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/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index c31b068..33ec552 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -214,6 +214,7 @@
})
contact.insert(ignore_permissions=True)
+ contact.reload() # load changes by hooks on contact
return contact
diff --git a/erpnext/e_commerce/api.py b/erpnext/e_commerce/api.py
index 43cb36c..84554ae 100644
--- a/erpnext/e_commerce/api.py
+++ b/erpnext/e_commerce/api.py
@@ -48,7 +48,6 @@
sub_categories = []
if item_group:
- field_filters['item_group'] = item_group
sub_categories = get_child_groups_for_website(item_group, immediate=True)
engine = ProductQuery()
diff --git a/erpnext/e_commerce/product_data_engine/filters.py b/erpnext/e_commerce/product_data_engine/filters.py
index c4a3cb9..89d4ffd 100644
--- a/erpnext/e_commerce/product_data_engine/filters.py
+++ b/erpnext/e_commerce/product_data_engine/filters.py
@@ -14,6 +14,8 @@
self.item_group = item_group
def get_field_filters(self):
+ from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website
+
if not self.item_group and not self.doc.enable_field_filters:
return
@@ -25,18 +27,26 @@
fields = [item_meta.get_field(field) for field in filter_fields if item_meta.has_field(field)]
for df in fields:
- item_filters, item_or_filters = {}, []
+ item_filters, item_or_filters = {"published_in_website": 1}, []
link_doctype_values = self.get_filtered_link_doctype_records(df)
if df.fieldtype == "Link":
if self.item_group:
- item_or_filters.extend([
- ["item_group", "=", self.item_group],
- ["Website Item Group", "item_group", "=", self.item_group] # consider website item groups
- ])
+ include_child = frappe.db.get_value("Item Group", self.item_group, "include_descendants")
+ if include_child:
+ include_groups = get_child_groups_for_website(self.item_group, include_self=True)
+ include_groups = [x.name for x in include_groups]
+ item_or_filters.extend([
+ ["item_group", "in", include_groups],
+ ["Website Item Group", "item_group", "=", self.item_group] # consider website item groups
+ ])
+ else:
+ item_or_filters.extend([
+ ["item_group", "=", self.item_group],
+ ["Website Item Group", "item_group", "=", self.item_group] # consider website item groups
+ ])
# Get link field values attached to published items
- item_filters['published_in_website'] = 1
item_values = frappe.get_all(
"Item",
fields=[df.fieldname],
diff --git a/erpnext/e_commerce/product_data_engine/query.py b/erpnext/e_commerce/product_data_engine/query.py
index cfc3c7b..5c92e3d 100644
--- a/erpnext/e_commerce/product_data_engine/query.py
+++ b/erpnext/e_commerce/product_data_engine/query.py
@@ -46,10 +46,10 @@
self.filter_with_discount = bool(fields.get("discount"))
result, discount_list, website_item_groups, cart_items, count = [], [], [], [], 0
- website_item_groups = self.get_website_item_group_results(item_group, website_item_groups)
-
if fields:
self.build_fields_filters(fields)
+ if item_group:
+ self.build_item_group_filters(item_group)
if search_term:
self.build_search_filters(search_term)
if self.settings.hide_variants:
@@ -61,8 +61,6 @@
else:
result, count = self.query_items(start=start)
- result = self.combine_web_item_group_results(item_group, result, website_item_groups)
-
# sort combined results by ranking
result = sorted(result, key=lambda x: x.get("ranking"), reverse=True)
@@ -167,6 +165,25 @@
# `=` will be faster than `IN` for most cases
self.filters.append([field, "=", values])
+ def build_item_group_filters(self, item_group):
+ "Add filters for Item group page and include Website Item Groups."
+ from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website
+ item_group_filters = []
+
+ item_group_filters.append(["Website Item", "item_group", "=", item_group])
+ # Consider Website Item Groups
+ item_group_filters.append(["Website Item Group", "item_group", "=", item_group])
+
+ if frappe.db.get_value("Item Group", item_group, "include_descendants"):
+ # include child item group's items as well
+ # eg. Group Node A, will show items of child 1 and child 2 as well
+ # on it's web page
+ include_groups = get_child_groups_for_website(item_group, include_self=True)
+ include_groups = [x.name for x in include_groups]
+ item_group_filters.append(["Website Item", "item_group", "in", include_groups])
+
+ self.or_filters.extend(item_group_filters)
+
def build_search_filters(self, search_term):
"""Query search term in specified fields
@@ -190,19 +207,6 @@
for field in search_fields:
self.or_filters.append([field, "like", search])
- def get_website_item_group_results(self, item_group, website_item_groups):
- """Get Web Items for Item Group Page via Website Item Groups."""
- if item_group:
- website_item_groups = frappe.db.get_all(
- "Website Item",
- fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"],
- filters=[
- ["Website Item Group", "item_group", "=", item_group],
- ["published", "=", 1]
- ]
- )
- return website_item_groups
-
def add_display_details(self, result, discount_list, cart_items):
"""Add price and availability details in result."""
for item in result:
@@ -278,16 +282,6 @@
return []
- def combine_web_item_group_results(self, item_group, result, website_item_groups):
- """Combine results with context of website item groups into item results."""
- if item_group and website_item_groups:
- items_list = {row.name for row in result}
- for row in website_item_groups:
- if row.wig_parent not in items_list:
- result.append(row)
-
- return result
-
def filter_results_by_discount(self, fields, result):
if fields and fields.get("discount"):
discount_percent = frappe.utils.flt(fields["discount"][0])
diff --git a/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py b/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py
index f0f7918..6549ba6 100644
--- a/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py
+++ b/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py
@@ -13,8 +13,7 @@
class TestItemGroupProductDataEngine(unittest.TestCase):
"Test Products & Sub-Category Querying for Product Listing on Item Group Page."
- @classmethod
- def setUpClass(cls):
+ def setUp(self):
item_codes = [
("Test Mobile A", "_Test Item Group B"),
("Test Mobile B", "_Test Item Group B"),
@@ -28,8 +27,10 @@
if not frappe.db.exists("Website Item", {"item_code": item_code}):
create_regular_web_item(item_code, item_args=item_args)
- @classmethod
- def tearDownClass(cls):
+ frappe.db.set_value("Item Group", "_Test Item Group B - 1", "show_in_website", 1)
+ frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 1)
+
+ def tearDown(self):
frappe.db.rollback()
def test_product_listing_in_item_group(self):
@@ -87,7 +88,6 @@
def test_item_group_with_sub_groups(self):
"Test Valid Sub Item Groups in Item Group Page."
- frappe.db.set_value("Item Group", "_Test Item Group B - 1", "show_in_website", 1)
frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 0)
result = get_product_filter_data(query_args={
@@ -114,4 +114,45 @@
# check if child group is fetched if shown in website
self.assertIn("_Test Item Group B - 1", child_groups)
- self.assertIn("_Test Item Group B - 2", child_groups)
\ No newline at end of file
+ self.assertIn("_Test Item Group B - 2", child_groups)
+
+ def test_item_group_page_with_descendants_included(self):
+ """
+ Test if 'include_descendants' pulls Items belonging to descendant Item Groups (Level 2 & 3).
+ > _Test Item Group B [Level 1]
+ > _Test Item Group B - 1 [Level 2]
+ > _Test Item Group B - 1 - 1 [Level 3]
+ """
+ frappe.get_doc({ # create Level 3 nested child group
+ "doctype": "Item Group",
+ "is_group": 1,
+ "item_group_name": "_Test Item Group B - 1 - 1",
+ "parent_item_group": "_Test Item Group B - 1"
+ }).insert()
+
+ create_regular_web_item( # create an item belonging to level 3 item group
+ "Test Mobile F",
+ item_args={"item_group": "_Test Item Group B - 1 - 1"}
+ )
+
+ frappe.db.set_value("Item Group", "_Test Item Group B - 1 - 1", "show_in_website", 1)
+
+ # enable 'include descendants' in Level 1
+ frappe.db.set_value("Item Group", "_Test Item Group B", "include_descendants", 1)
+
+ result = get_product_filter_data(query_args={
+ "field_filters": {},
+ "attribute_filters": {},
+ "start": 0,
+ "item_group": "_Test Item Group B"
+ })
+
+ items = result.get("items")
+ item_codes = [item.get("item_code") for item in items]
+
+ # check if all sub groups' items are pulled
+ self.assertEqual(len(items), 6)
+ self.assertIn("Test Mobile A", item_codes)
+ self.assertIn("Test Mobile C", item_codes)
+ self.assertIn("Test Mobile E", item_codes)
+ self.assertIn("Test Mobile F", item_codes)
\ No newline at end of file
diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py
index 372aed0..0f3f69d 100644
--- a/erpnext/e_commerce/shopping_cart/cart.py
+++ b/erpnext/e_commerce/shopping_cart/cart.py
@@ -120,7 +120,11 @@
def request_for_quotation():
quotation = _get_cart_quotation()
quotation.flags.ignore_permissions = True
- quotation.submit()
+
+ if get_shopping_cart_settings().save_quotations_as_draft:
+ quotation.save()
+ else:
+ quotation.submit()
return quotation.name
@frappe.whitelist()
diff --git a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
index 9c389d0..4a3787a 100644
--- a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
+++ b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
@@ -6,7 +6,7 @@
import frappe
from frappe.tests.utils import change_settings
-from frappe.utils import add_months, nowdate
+from frappe.utils import add_months, cint, nowdate
from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
@@ -14,22 +14,17 @@
_get_cart_quotation,
get_cart_quotation,
get_party,
+ request_for_quotation,
update_cart,
)
from erpnext.tests.utils import create_test_contact_and_address
-# test_dependencies = ['Payment Terms Template']
class TestShoppingCart(unittest.TestCase):
"""
Note:
Shopping Cart == Quotation
"""
-
- @classmethod
- def tearDownClass(cls):
- frappe.db.sql("delete from `tabTax Rule`")
-
def setUp(self):
frappe.set_user("Administrator")
create_test_contact_and_address()
@@ -45,6 +40,10 @@
frappe.set_user("Administrator")
self.disable_shopping_cart()
+ @classmethod
+ def tearDownClass(cls):
+ frappe.db.sql("delete from `tabTax Rule`")
+
def test_get_cart_new_user(self):
self.login_as_new_user()
@@ -179,6 +178,28 @@
# test if items are rendered without error
frappe.render_template("templates/includes/cart/cart_items.html", cart)
+ @change_settings("E Commerce Settings",{
+ "save_quotations_as_draft": 1
+ })
+ def test_cart_without_checkout_and_draft_quotation(self):
+ "Test impact of 'save_quotations_as_draft' checkbox."
+ frappe.local.shopping_cart_settings = None
+
+ # add item to cart
+ update_cart("_Test Item", 1)
+ quote_name = request_for_quotation() # Request for Quote
+ quote_doctstatus = cint(frappe.db.get_value("Quotation", quote_name, "docstatus"))
+
+ self.assertEqual(quote_doctstatus, 0)
+
+ frappe.db.set_value("E Commerce Settings", None, "save_quotations_as_draft", 0)
+ frappe.local.shopping_cart_settings = None
+ update_cart("_Test Item", 1)
+ quote_name = request_for_quotation() # Request for Quote
+ quote_doctstatus = cint(frappe.db.get_value("Quotation", quote_name, "docstatus"))
+
+ self.assertEqual(quote_doctstatus, 1)
+
def create_tax_rule(self):
tax_rule = frappe.get_test_records("Tax Rule")[0]
try:
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..6095413 100644
--- a/erpnext/hr/doctype/attendance/test_attendance.py
+++ b/erpnext/hr/doctype/attendance/test_attendance.py
@@ -1,20 +1,116 @@
# 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):
+ now = now_datetime()
+ previous_month = now.month - 1
+ first_day = now.replace(day=1).replace(month=previous_month).date()
+
+ 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, for_date=first_day)
+ 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):
+ now = now_datetime()
+ previous_month = now.month - 1
+ first_day = now.replace(day=1).replace(month=previous_month).date()
+
+ 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, for_date=first_day)
+ 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})
+
+ 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)
+
+ 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/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 70250f5..345d8dc 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,
@@ -494,7 +495,6 @@
number_of_days = date_diff(to_date, from_date) + .5
else:
number_of_days = date_diff(to_date, from_date) + 1
-
else:
number_of_days = date_diff(to_date, from_date) + 1
@@ -567,28 +567,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..01e0ca0 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -133,7 +133,9 @@
holiday_list = make_holiday_list()
employee = get_employee()
- frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list)
+ original_holiday_list = employee.holiday_list
+ frappe.db.set_value("Employee", employee.name, "holiday_list", holiday_list)
+
first_sunday = get_first_sunday(holiday_list)
leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name)
@@ -143,6 +145,8 @@
leave_application.cancel()
+ frappe.db.set_value("Employee", employee.name, "holiday_list", original_holiday_list)
+
def test_attendance_update_for_exclude_holidays(self):
# Case 2: leave type with 'Include holidays within leaves as leaves' disabled
frappe.delete_doc_if_exists("Leave Type", "Test Do Not Include Holidays", force=1)
@@ -157,7 +161,8 @@
holiday_list = make_holiday_list()
employee = get_employee()
- frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list)
+ original_holiday_list = employee.holiday_list
+ frappe.db.set_value("Employee", employee.name, "holiday_list", holiday_list)
first_sunday = get_first_sunday(holiday_list)
# already marked attendance on a holiday should be deleted in this case
@@ -177,7 +182,7 @@
attendance.flags.ignore_validate = True
attendance.save()
- leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name)
+ leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name, employee.company)
leave_application.reload()
# holiday should be excluded while marking attendance
self.assertEqual(leave_application.total_leave_days, 3)
@@ -189,6 +194,8 @@
# attendance on non-holiday updated
self.assertEqual(frappe.db.get_value("Attendance", attendance.name, "status"), "On Leave")
+ frappe.db.set_value("Employee", employee.name, "holiday_list", original_holiday_list)
+
def test_block_list(self):
self._clear_roles()
@@ -327,7 +334,8 @@
employee = get_employee()
default_holiday_list = make_holiday_list()
- frappe.db.set_value("Company", employee.company, "default_holiday_list", default_holiday_list)
+ original_holiday_list = employee.holiday_list
+ frappe.db.set_value("Employee", employee.name, "holiday_list", default_holiday_list)
first_sunday = get_first_sunday(default_holiday_list)
optional_leave_date = add_days(first_sunday, 1)
@@ -378,6 +386,8 @@
# check leave balance is reduced
self.assertEqual(get_leave_balance_on(employee.name, leave_type, optional_leave_date), 9)
+ frappe.db.set_value("Employee", employee.name, "holiday_list", original_holiday_list)
+
def test_leaves_allowed(self):
employee = get_employee()
leave_period = get_leave_period()
@@ -782,9 +792,10 @@
allocate_leave.submit()
-def get_first_sunday(holiday_list):
- month_start_date = get_first_day(nowdate())
- month_end_date = get_last_day(nowdate())
+def get_first_sunday(holiday_list, for_date=None):
+ date = for_date or getdate()
+ month_start_date = get_first_day(date)
+ month_end_date = get_last_day(date)
first_sunday = frappe.db.sql("""
select holiday_date from `tabHoliday`
where parent = %s
@@ -792,4 +803,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/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py
new file mode 100644
index 0000000..952af81
--- /dev/null
+++ b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py
@@ -0,0 +1,44 @@
+import frappe
+from dateutil.relativedelta import relativedelta
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import now_datetime
+
+from erpnext.hr.doctype.attendance.attendance import mark_attendance
+from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.report.monthly_attendance_sheet.monthly_attendance_sheet import execute
+
+
+class TestMonthlyAttendanceSheet(FrappeTestCase):
+ def setUp(self):
+ self.employee = make_employee("test_employee@example.com")
+ frappe.db.delete('Attendance', {'employee': self.employee})
+
+ def test_monthly_attendance_sheet_report(self):
+ now = now_datetime()
+ previous_month = now.month - 1
+ previous_month_first = now.replace(day=1).replace(month=previous_month).date()
+
+ company = frappe.db.get_value('Employee', self.employee, 'company')
+
+ # mark different attendance status on first 3 days of previous month
+ mark_attendance(self.employee, previous_month_first, 'Absent')
+ mark_attendance(self.employee, previous_month_first + relativedelta(days=1), 'Present')
+ mark_attendance(self.employee, previous_month_first + relativedelta(days=2), 'On Leave')
+
+ filters = frappe._dict({
+ 'month': previous_month,
+ 'year': now.year,
+ 'company': company,
+ })
+ report = execute(filters=filters)
+ employees = report[1][0]
+ datasets = report[3]['data']['datasets']
+ absent = datasets[0]['values']
+ present = datasets[1]['values']
+ leaves = datasets[2]['values']
+
+ # ensure correct attendance is reflect on the report
+ self.assertIn(self.employee, employees)
+ self.assertEqual(absent[0], 1)
+ self.assertEqual(present[1], 1)
+ self.assertEqual(leaves[2], 1)
diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json
index 196f36f..ef78a64 100644
--- a/erpnext/loan_management/doctype/loan/loan.json
+++ b/erpnext/loan_management/doctype/loan/loan.json
@@ -32,6 +32,8 @@
"monthly_repayment_amount",
"repayment_start_date",
"is_term_loan",
+ "accounting_dimensions_section",
+ "cost_center",
"account_info",
"mode_of_payment",
"disbursement_account",
@@ -366,12 +368,23 @@
"options": "Account",
"read_only": 1,
"reqd": 1
+ },
+ {
+ "fieldname": "accounting_dimensions_section",
+ "fieldtype": "Section Break",
+ "label": "Accounting Dimensions"
+ },
+ {
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "label": "Cost Center",
+ "options": "Cost Center"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-01-25 16:29:16.325501",
+ "modified": "2022-03-10 11:50:31.957360",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan",
diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py
index b798e08..0fe9947 100644
--- a/erpnext/loan_management/doctype/loan/loan.py
+++ b/erpnext/loan_management/doctype/loan/loan.py
@@ -25,6 +25,7 @@
self.set_loan_amount()
self.validate_loan_amount()
self.set_missing_fields()
+ self.validate_cost_center()
self.validate_accounts()
self.check_sanctioned_amount_limit()
self.validate_repay_from_salary()
@@ -45,6 +46,13 @@
frappe.throw(_("Account {0} does not belongs to company {1}").format(frappe.bold(self.get(fieldname)),
frappe.bold(self.company)))
+ def validate_cost_center(self):
+ if not self.cost_center and self.rate_of_interest != 0:
+ self.cost_center = frappe.db.get_value('Company', self.company, 'cost_center')
+
+ if not self.cost_center:
+ frappe.throw(_('Cost center is mandatory for loans having rate of interest greater than 0'))
+
def on_submit(self):
self.link_loan_security_pledge()
diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
index 1c800a0..f6a3ede 100644
--- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
+++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
@@ -6,7 +6,6 @@
from frappe import _
from frappe.utils import add_days, cint, date_diff, flt, get_datetime, getdate, nowdate
-import erpnext
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.controllers.accounts_controller import AccountsController
@@ -41,6 +40,8 @@
def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = []
+ cost_center = frappe.db.get_value('Loan', self.loan, 'cost_center')
+
if self.interest_amount:
gle_map.append(
self.get_gl_dict({
@@ -54,7 +55,7 @@
"against_voucher": self.loan,
"remarks": _("Interest accrued from {0} to {1} against loan: {2}").format(
self.last_accrual_date, self.posting_date, self.loan),
- "cost_center": erpnext.get_default_cost_center(self.company),
+ "cost_center": cost_center,
"posting_date": self.posting_date
})
)
@@ -69,7 +70,7 @@
"against_voucher": self.loan,
"remarks": ("Interest accrued from {0} to {1} against loan: {2}").format(
self.last_accrual_date, self.posting_date, self.loan),
- "cost_center": erpnext.get_default_cost_center(self.company),
+ "cost_center": cost_center,
"posting_date": self.posting_date
})
)
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 37d2b9f..a025ff7 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt
import functools
+import re
from collections import deque
from operator import itemgetter
from typing import List
@@ -103,25 +104,33 @@
)
def autoname(self):
- names = frappe.db.sql_list("""select name from `tabBOM` where item=%s""", self.item)
+ # ignore amended documents while calculating current index
+ existing_boms = frappe.get_all(
+ "BOM",
+ filters={"item": self.item, "amended_from": ["is", "not set"]},
+ pluck="name"
+ )
- if names:
- # name can be BOM/ITEM/001, BOM/ITEM/001-1, BOM-ITEM-001, BOM-ITEM-001-1
-
- # split by item
- names = [name.split(self.item, 1) for name in names]
- names = [d[-1][1:] for d in filter(lambda x: len(x) > 1 and x[-1], names)]
-
- # split by (-) if cancelled
- if names:
- names = [cint(name.split('-')[-1]) for name in names]
- idx = max(names) + 1
- else:
- idx = 1
+ if existing_boms:
+ index = self.get_next_version_index(existing_boms)
else:
- idx = 1
+ index = 1
- name = 'BOM-' + self.item + ('-%.3i' % idx)
+ prefix = self.doctype
+ suffix = "%.3i" % index # convert index to string (1 -> "001")
+ bom_name = f"{prefix}-{self.item}-{suffix}"
+
+ if len(bom_name) <= 140:
+ name = bom_name
+ else:
+ # since max characters for name is 140, remove enough characters from the
+ # item name to fit the prefix, suffix and the separators
+ truncated_length = 140 - (len(prefix) + len(suffix) + 2)
+ truncated_item_name = self.item[:truncated_length]
+ # if a partial word is found after truncate, remove the extra characters
+ truncated_item_name = truncated_item_name.rsplit(" ", 1)[0]
+ name = f"{prefix}-{truncated_item_name}-{suffix}"
+
if frappe.db.exists("BOM", name):
conflicting_bom = frappe.get_doc("BOM", name)
@@ -134,6 +143,26 @@
self.name = name
+ @staticmethod
+ def get_next_version_index(existing_boms: List[str]) -> int:
+ # split by "/" and "-"
+ delimiters = ["/", "-"]
+ pattern = "|".join(map(re.escape, delimiters))
+ bom_parts = [re.split(pattern, bom_name) for bom_name in existing_boms]
+
+ # filter out BOMs that do not follow the following formats: BOM/ITEM/001, BOM-ITEM-001
+ valid_bom_parts = list(filter(lambda x: len(x) > 1 and x[-1], bom_parts))
+
+ # extract the current index from the BOM parts
+ if valid_bom_parts:
+ # handle cancelled and submitted documents
+ indexes = [cint(part[-1]) for part in valid_bom_parts]
+ index = max(indexes) + 1
+ else:
+ index = 1
+
+ return index
+
def validate(self):
self.route = frappe.scrub(self.name).replace('_', '-')
@@ -192,12 +221,13 @@
if self.routing:
self.set("operations", [])
fields = ["sequence_id", "operation", "workstation", "description",
- "time_in_mins", "batch_size", "operating_cost", "idx", "hour_rate"]
+ "time_in_mins", "batch_size", "operating_cost", "idx", "hour_rate",
+ "set_cost_based_on_bom_qty", "fixed_time"]
for row in frappe.get_all("BOM Operation", fields = fields,
filters = {'parenttype': 'Routing', 'parent': self.routing}, order_by="sequence_id, idx"):
child = self.append('operations', row)
- child.hour_rate = flt(row.hour_rate / self.conversion_rate, 2)
+ child.hour_rate = flt(row.hour_rate / self.conversion_rate, child.precision("hour_rate"))
def set_bom_material_details(self):
for item in self.get("items"):
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 3cc91b3..4417123 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -432,6 +432,69 @@
self.assertEqual(bom.transfer_material_against, "Work Order")
bom.delete()
+ def test_bom_name_length(self):
+ """ test >140 char names"""
+ bom_tree = {
+ "x" * 140 : {
+ " ".join(["abc"] * 35): {}
+ }
+ }
+ create_nested_bom(bom_tree, prefix="")
+
+ def test_version_index(self):
+
+ bom = frappe.new_doc("BOM")
+
+ version_index_test_cases = [
+ (1, []),
+ (1, ["BOM#XYZ"]),
+ (2, ["BOM/ITEM/001"]),
+ (2, ["BOM-ITEM-001"]),
+ (3, ["BOM-ITEM-001", "BOM-ITEM-002"]),
+ (4, ["BOM-ITEM-001", "BOM-ITEM-002", "BOM-ITEM-003"]),
+ ]
+
+ for expected_index, existing_boms in version_index_test_cases:
+ with self.subTest():
+ self.assertEqual(expected_index, bom.get_next_version_index(existing_boms),
+ msg=f"Incorrect index for {existing_boms}")
+
+ def test_bom_versioning(self):
+ bom_tree = {
+ frappe.generate_hash(length=10) : {
+ frappe.generate_hash(length=10): {}
+ }
+ }
+ bom = create_nested_bom(bom_tree, prefix="")
+ self.assertEqual(int(bom.name.split("-")[-1]), 1)
+ original_bom_name = bom.name
+
+ bom.cancel()
+ bom.reload()
+ self.assertEqual(bom.name, original_bom_name)
+
+ # create a new amendment
+ amendment = frappe.copy_doc(bom)
+ amendment.docstatus = 0
+ amendment.amended_from = bom.name
+
+ amendment.save()
+ amendment.submit()
+ amendment.reload()
+
+ self.assertNotEqual(amendment.name, bom.name)
+ # `origname-001-1` version
+ self.assertEqual(int(amendment.name.split("-")[-1]), 1)
+ self.assertEqual(int(amendment.name.split("-")[-2]), 1)
+
+ # create a new version
+ version = frappe.copy_doc(amendment)
+ version.docstatus = 0
+ version.amended_from = None
+ version.save()
+ self.assertNotEqual(amendment.name, version.name)
+ self.assertEqual(int(version.name.split("-")[-1]), 2)
+
def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
index c7be7ef..341f969 100644
--- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
+++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
@@ -66,7 +66,8 @@
"label": "Hour Rate",
"oldfieldname": "hour_rate",
"oldfieldtype": "Currency",
- "options": "currency"
+ "options": "currency",
+ "precision": "2"
},
{
"description": "In minutes",
@@ -186,7 +187,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-12-15 03:00:00.473173",
+ "modified": "2022-03-10 06:19:08.462027",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Operation",
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 13f0e7b..ebda805 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -357,4 +357,5 @@
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
-erpnext.patches.v14_0.update_employee_advance_status
\ No newline at end of file
+erpnext.patches.v14_0.update_employee_advance_status
+erpnext.patches.v13_0.add_cost_center_in_loans
\ No newline at end of file
diff --git a/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py b/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py
index b3ee340..7ae4c42 100644
--- a/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py
+++ b/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py
@@ -5,10 +5,13 @@
frappe.reload_doc('accounts', 'doctype', 'bank', force=1)
if frappe.db.table_exists('Bank') and frappe.db.table_exists('Bank Account') and frappe.db.has_column('Bank Account', 'swift_number'):
- frappe.db.sql("""
- UPDATE `tabBank` b, `tabBank Account` ba
- SET b.swift_number = ba.swift_number WHERE b.name = ba.bank
- """)
+ try:
+ frappe.db.sql("""
+ UPDATE `tabBank` b, `tabBank Account` ba
+ SET b.swift_number = ba.swift_number WHERE b.name = ba.bank
+ """)
+ except Exception as e:
+ frappe.log_error(e, title="Patch Migration Failed")
frappe.reload_doc('accounts', 'doctype', 'bank_account')
frappe.reload_doc('accounts', 'doctype', 'payment_request')
diff --git a/erpnext/patches/v13_0/add_cost_center_in_loans.py b/erpnext/patches/v13_0/add_cost_center_in_loans.py
new file mode 100644
index 0000000..25e1722
--- /dev/null
+++ b/erpnext/patches/v13_0/add_cost_center_in_loans.py
@@ -0,0 +1,16 @@
+import frappe
+
+
+def execute():
+ frappe.reload_doc('loan_management', 'doctype', 'loan')
+ loan = frappe.qb.DocType('Loan')
+
+ for company in frappe.get_all('Company', pluck='name'):
+ default_cost_center = frappe.db.get_value('Company', company, 'cost_center')
+ frappe.qb.update(
+ loan
+ ).set(
+ loan.cost_center, default_cost_center
+ ).where(
+ loan.company == company
+ ).run()
\ No newline at end of file
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/report/project_profitability/project_profitability.py b/erpnext/projects/report/project_profitability/project_profitability.py
index 9520cd1..23c3b82 100644
--- a/erpnext/projects/report/project_profitability/project_profitability.py
+++ b/erpnext/projects/report/project_profitability/project_profitability.py
@@ -1,14 +1,12 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-
import frappe
from frappe import _
from frappe.utils import flt
def execute(filters=None):
- columns, data = [], []
data = get_data(filters)
columns = get_columns()
charts = get_chart_data(data)
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/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index ae0e2a3..9fec43b 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -39,6 +39,8 @@
this._calculate_taxes_and_totals();
this.calculate_discount_amount();
+ this.calculate_shipping_charges();
+
// Advance calculation applicable to Sales /Purchase Invoice
if(in_list(["Sales Invoice", "POS Invoice", "Purchase Invoice"], this.frm.doc.doctype)
&& this.frm.doc.docstatus < 2 && !this.frm.doc.is_return) {
@@ -81,7 +83,6 @@
this.initialize_taxes();
this.determine_exclusive_rate();
this.calculate_net_total();
- this.calculate_shipping_charges();
this.calculate_taxes();
this.manipulate_grand_total_for_inclusive_tax();
this.calculate_totals();
@@ -275,6 +276,7 @@
frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]);
if (frappe.meta.get_docfield(this.frm.doc.doctype, "shipping_rule", this.frm.doc.name)) {
this.shipping_rule();
+ this._calculate_taxes_and_totals();
}
}
diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
index cb79cf8..d5ff88c 100644
--- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
+++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
@@ -127,7 +127,8 @@
def get_inward_nil_exempt(self, state):
inward_nil_exempt = frappe.db.sql("""
- SELECT p.place_of_supply, sum(i.base_amount) as base_amount, i.is_nil_exempt, i.is_non_gst
+ SELECT p.place_of_supply, p.supplier_address,
+ i.base_amount, i.is_nil_exempt, i.is_non_gst
FROM `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
WHERE p.docstatus = 1 and p.name = i.parent
and p.is_opening = 'No'
@@ -135,7 +136,7 @@
and (i.is_nil_exempt = 1 or i.is_non_gst = 1 or p.gst_category = 'Registered Composition') and
month(p.posting_date) = %s and year(p.posting_date) = %s
and p.company = %s and p.company_gstin = %s
- GROUP BY p.place_of_supply, i.is_nil_exempt, i.is_non_gst""",
+ """,
(self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
inward_nil_exempt_details = {
@@ -149,18 +150,24 @@
}
}
+ address_state_map = get_address_state_map()
+
for d in inward_nil_exempt:
- if d.place_of_supply:
- if (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \
- and state == d.place_of_supply.split("-")[1]:
- inward_nil_exempt_details["gst"]["intra"] += d.base_amount
- elif (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \
- and state != d.place_of_supply.split("-")[1]:
- inward_nil_exempt_details["gst"]["inter"] += d.base_amount
- elif d.is_non_gst == 1 and state == d.place_of_supply.split("-")[1]:
- inward_nil_exempt_details["non_gst"]["intra"] += d.base_amount
- elif d.is_non_gst == 1 and state != d.place_of_supply.split("-")[1]:
- inward_nil_exempt_details["non_gst"]["inter"] += d.base_amount
+ if not d.place_of_supply:
+ d.place_of_supply = "00-" + cstr(state)
+
+ supplier_state = address_state_map.get(d.supplier_address) or state
+
+ if (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \
+ and cstr(supplier_state) == cstr(d.place_of_supply.split("-")[1]):
+ inward_nil_exempt_details["gst"]["intra"] += d.base_amount
+ elif (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \
+ and cstr(supplier_state) != cstr(d.place_of_supply.split("-")[1]):
+ inward_nil_exempt_details["gst"]["inter"] += d.base_amount
+ elif d.is_non_gst == 1 and cstr(supplier_state) == cstr(d.place_of_supply.split("-")[1]):
+ inward_nil_exempt_details["non_gst"]["intra"] += d.base_amount
+ elif d.is_non_gst == 1 and cstr(supplier_state) != cstr(d.place_of_supply.split("-")[1]):
+ inward_nil_exempt_details["non_gst"]["inter"] += d.base_amount
return inward_nil_exempt_details
@@ -419,6 +426,11 @@
return ",".join(missing_field_invoices)
+def get_address_state_map():
+ return frappe._dict(
+ frappe.get_all('Address', fields=['name', 'gst_state'], as_list=1)
+ )
+
def get_json(template):
file_path = os.path.join(os.path.dirname(__file__), '{template}.json'.format(template=template))
with open(file_path, 'r') as f:
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/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py
index beac7ed..92a10c2 100644
--- a/erpnext/regional/report/datev/datev.py
+++ b/erpnext/regional/report/datev/datev.py
@@ -343,8 +343,7 @@
/* against number or, if empty, party against number */
%(temporary_against_account_number)s as 'Gegenkonto (ohne BU-Schlüssel)',
- /* disable automatic VAT deduction */
- '40' as 'BU-Schlüssel',
+ '' as 'BU-Schlüssel',
gl.posting_date as 'Belegdatum',
gl.voucher_no as 'Belegfeld 1',
diff --git a/erpnext/regional/report/irs_1099/irs_1099.py b/erpnext/regional/report/irs_1099/irs_1099.py
index b1a5d10..147a59f 100644
--- a/erpnext/regional/report/irs_1099/irs_1099.py
+++ b/erpnext/regional/report/irs_1099/irs_1099.py
@@ -30,7 +30,6 @@
if region != 'United States':
return [], []
- data = []
columns = get_columns()
conditions = ""
if filters.supplier_group:
diff --git a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py
index 4133687..464939f 100644
--- a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py
+++ b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py
@@ -118,8 +118,7 @@
"customer_type": "Company",
})
customer.insert()
- else:
- customer = frappe.get_doc("Customer", "_Test UAE Customer")
+
def make_supplier():
if not frappe.db.exists("Supplier", "_Test UAE Supplier"):
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index 7742f26..634c481 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -218,7 +218,9 @@
else:
company_record.append(limit.company)
- outstanding_amt = get_customer_outstanding(self.name, limit.company)
+ outstanding_amt = get_customer_outstanding(
+ self.name, limit.company, ignore_outstanding_sales_order=limit.bypass_credit_limit_check
+ )
if flt(limit.credit_limit) < outstanding_amt:
frappe.throw(_("""New credit limit is less than current outstanding amount for the customer. Credit limit has to be atleast {0}""").format(outstanding_amt))
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/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index b528479..86a0882 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -1477,6 +1477,28 @@
self.assertEqual(so.items[0].work_order_qty, wo.produced_qty)
self.assertEqual(mr.status, "Manufactured")
+ def test_sales_order_with_shipping_rule(self):
+ from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule
+ shipping_rule = create_shipping_rule(shipping_rule_type = "Selling", shipping_rule_name = "Shipping Rule - Sales Invoice Test")
+ sales_order = make_sales_order(do_not_save=True)
+ sales_order.shipping_rule = shipping_rule.name
+
+ sales_order.items[0].qty = 1
+ sales_order.save()
+ self.assertEqual(sales_order.taxes[0].tax_amount, 50)
+
+ sales_order.items[0].qty = 2
+ sales_order.save()
+ self.assertEqual(sales_order.taxes[0].tax_amount, 100)
+
+ sales_order.items[0].qty = 3
+ sales_order.save()
+ self.assertEqual(sales_order.taxes[0].tax_amount, 200)
+
+ sales_order.items[0].qty = 21
+ sales_order.save()
+ self.assertEqual(sales_order.taxes[0].tax_amount, 0)
+
def automatically_fetch_payment_terms(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings")
accounts_settings.automatically_fetch_payment_terms = enable
diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
index 4a245e1..56e1eb5 100644
--- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
+++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
@@ -156,24 +156,24 @@
customer_record = customer_details.get(record.customer)
item_record = item_details.get(record.item_code)
row = {
- "item_code": record.item_code,
- "item_name": item_record.item_name,
- "item_group": item_record.item_group,
- "description": record.description,
- "quantity": record.qty,
- "uom": record.uom,
- "rate": record.base_rate,
- "amount": record.base_amount,
- "sales_order": record.name,
- "transaction_date": record.transaction_date,
- "customer": record.customer,
- "customer_name": customer_record.customer_name,
- "customer_group": customer_record.customer_group,
- "territory": record.territory,
- "project": record.project,
- "delivered_quantity": flt(record.delivered_qty),
- "billed_amount": flt(record.billed_amt),
- "company": record.company
+ "item_code": record.get('item_code'),
+ "item_name": item_record.get('item_name'),
+ "item_group": item_record.get('item_group'),
+ "description": record.get('description'),
+ "quantity": record.get('qty'),
+ "uom": record.get('uom'),
+ "rate": record.get('base_rate'),
+ "amount": record.get('base_amount'),
+ "sales_order": record.get('name'),
+ "transaction_date": record.get('transaction_date'),
+ "customer": record.get('customer'),
+ "customer_name": customer_record.get('customer_name'),
+ "customer_group": customer_record.get('customer_group'),
+ "territory": record.get('territory'),
+ "project": record.get('project'),
+ "delivered_quantity": flt(record.get('delivered_qty')),
+ "billed_amount": flt(record.get('billed_amt')),
+ "company": record.get('company')
}
data.append(row)
diff --git a/erpnext/setup/doctype/item_group/item_group.js b/erpnext/setup/doctype/item_group/item_group.js
index 885d874..f570c2f 100644
--- a/erpnext/setup/doctype/item_group/item_group.js
+++ b/erpnext/setup/doctype/item_group/item_group.js
@@ -14,6 +14,16 @@
]
}
}
+ frm.fields_dict['item_group_defaults'].grid.get_field("default_discount_account").get_query = function(doc, cdt, cdn) {
+ const row = locals[cdt][cdn];
+ return {
+ filters: {
+ 'report_type': 'Profit and Loss',
+ 'company': row.company,
+ "is_group": 0
+ }
+ };
+ }
frm.fields_dict["item_group_defaults"].grid.get_field("expense_account").get_query = function(doc, cdt, cdn) {
const row = locals[cdt][cdn];
return {
diff --git a/erpnext/setup/doctype/item_group/item_group.json b/erpnext/setup/doctype/item_group/item_group.json
index 3e0680f..50f923d 100644
--- a/erpnext/setup/doctype/item_group/item_group.json
+++ b/erpnext/setup/doctype/item_group/item_group.json
@@ -20,12 +20,14 @@
"sec_break_taxes",
"taxes",
"sb9",
- "show_in_website",
"route",
- "weightage",
- "slideshow",
"website_title",
"description",
+ "show_in_website",
+ "include_descendants",
+ "column_break_16",
+ "weightage",
+ "slideshow",
"website_specifications",
"website_filters_section",
"filter_fields",
@@ -111,7 +113,7 @@
},
{
"default": "0",
- "description": "Check this if you want to show in website",
+ "description": "Make Item Group visible in website",
"fieldname": "show_in_website",
"fieldtype": "Check",
"label": "Show in Website"
@@ -124,6 +126,7 @@
"unique": 1
},
{
+ "depends_on": "show_in_website",
"fieldname": "weightage",
"fieldtype": "Int",
"label": "Weightage"
@@ -186,6 +189,8 @@
"report_hide": 1
},
{
+ "collapsible": 1,
+ "depends_on": "show_in_website",
"fieldname": "website_filters_section",
"fieldtype": "Section Break",
"label": "Website Filters"
@@ -203,9 +208,22 @@
"options": "Website Attribute"
},
{
+ "depends_on": "show_in_website",
"fieldname": "website_title",
"fieldtype": "Data",
"label": "Title"
+ },
+ {
+ "fieldname": "column_break_16",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "show_in_website",
+ "description": "Include Website Items belonging to child Item Groups",
+ "fieldname": "include_descendants",
+ "fieldtype": "Check",
+ "label": "Include Descendants"
}
],
"icon": "fa fa-sitemap",
@@ -214,11 +232,12 @@
"is_tree": 1,
"links": [],
"max_attachments": 3,
- "modified": "2021-02-18 13:40:30.049650",
+ "modified": "2022-03-09 12:27:11.055782",
"modified_by": "Administrator",
"module": "Setup",
"name": "Item Group",
"name_case": "Title Case",
+ "naming_rule": "By fieldname",
"nsm_parent_field": "parent_item_group",
"owner": "Administrator",
"permissions": [
@@ -285,5 +304,6 @@
"search_fields": "parent_item_group",
"show_name_in_global_search": 1,
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py
index 4f92240..5c7194b 100644
--- a/erpnext/setup/doctype/item_group/item_group.py
+++ b/erpnext/setup/doctype/item_group/item_group.py
@@ -111,7 +111,7 @@
from erpnext.stock.doctype.item.item import validate_item_default_company_links
validate_item_default_company_links(self.item_group_defaults)
-def get_child_groups_for_website(item_group_name, immediate=False):
+def get_child_groups_for_website(item_group_name, immediate=False, include_self=False):
"""Returns child item groups *excluding* passed group."""
item_group = frappe.get_cached_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1)
filters = {
@@ -123,6 +123,12 @@
if immediate:
filters["parent_item_group"] = item_group_name
+ if include_self:
+ filters.update({
+ "lft": [">=", item_group.lft],
+ "rgt": ["<=", item_group.rgt]
+ })
+
return frappe.get_all(
"Item Group",
filters=filters,
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json
index 55a4c95..7ebc4ee 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -1315,7 +1315,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
- "modified": "2021-10-09 14:29:13.428984",
+ "modified": "2022-03-10 14:29:13.428984",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 2a4d639..75ccd86 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -13,7 +13,10 @@
from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.selling_controller import SellingController
from erpnext.stock.doctype.batch.batch import set_batch_nos
-from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no
+from erpnext.stock.doctype.serial_no.serial_no import (
+ get_delivery_note_serial_no,
+ update_serial_nos_after_submit,
+)
from erpnext.stock.utils import calculate_mapped_packed_items_return
form_grid_templates = {
@@ -220,6 +223,9 @@
# Updating stock ledger should always be called after updating prevdoc status,
# because updating reserved qty in bin depends upon updated delivered qty in SO
self.update_stock_ledger()
+ if self.is_return:
+ update_serial_nos_after_submit(self, "items")
+
self.make_gl_entries()
self.repost_future_sle_and_gle()
@@ -342,25 +348,21 @@
from frappe.query_builder.functions import Sum
# Billed against Sales Order directly
- si = frappe.qb.DocType("Sales Invoice").as_("si")
si_item = frappe.qb.DocType("Sales Invoice Item").as_("si_item")
sum_amount = Sum(si_item.amount).as_("amount")
- billed_against_so = frappe.qb.from_(si).from_(si_item).select(sum_amount).where(
- (si_item.parent == si.name) &
+ billed_against_so = frappe.qb.from_(si_item).select(sum_amount).where(
(si_item.so_detail == so_detail) &
((si_item.dn_detail.isnull()) | (si_item.dn_detail == '')) &
- (si_item.docstatus == 1) &
- (si.update_stock == 0)
+ (si_item.docstatus == 1)
).run()
billed_against_so = billed_against_so and billed_against_so[0][0] or 0
# Get all Delivery Note Item rows against the Sales Order Item row
-
dn = frappe.qb.DocType("Delivery Note").as_("dn")
dn_item = frappe.qb.DocType("Delivery Note Item").as_("dn_item")
- dn_details = frappe.qb.from_(dn).from_(dn_item).select(dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent, dn_item.stock_qty, dn_item.returned_qty).where(
+ dn_details = frappe.qb.from_(dn).from_(dn_item).select(dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent).where(
(dn.name == dn_item.parent) &
(dn_item.so_detail == so_detail) &
(dn.docstatus == 1) &
@@ -385,11 +387,7 @@
# Distribute billed amount directly against SO between DNs based on FIFO
if billed_against_so and billed_amt_agianst_dn < dnd.amount:
- if dnd.returned_qty:
- pending_to_bill = flt(dnd.amount) * (dnd.stock_qty - dnd.returned_qty) / dnd.stock_qty
- else:
- pending_to_bill = flt(dnd.amount)
- pending_to_bill -= billed_amt_agianst_dn
+ pending_to_bill = flt(dnd.amount) - billed_amt_agianst_dn
if pending_to_bill <= billed_against_so:
billed_amt_agianst_dn += pending_to_bill
billed_against_so -= pending_to_bill
@@ -798,3 +796,6 @@
}, target_doc, set_missing_values)
return doclist
+
+def on_doctype_update():
+ frappe.db.add_index("Delivery Note", ["customer", "is_return", "return_against"])
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 16c8921..fc3dce1 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -822,6 +822,11 @@
automatically_fetch_payment_terms(enable=0)
+ def test_standalone_serial_no_return(self):
+ dn = create_delivery_note(item_code="_Test Serialized Item With Series", is_return=True, qty=-1)
+ dn.reload()
+ self.assertTrue(dn.items[0].serial_no)
+
def create_return_delivery_note(**args):
args = frappe._dict(args)
from erpnext.controllers.sales_and_purchase_return import make_return_doc
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index ffea9c2..9e8b3bd 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -165,21 +165,21 @@
frm.set_value('has_batch_no', 0);
frm.toggle_enable(['has_serial_no', 'serial_no_series'], !frm.doc.is_fixed_asset);
- frm.call({
- method: "set_asset_naming_series",
- doc: frm.doc,
- callback: function() {
+ frappe.call({
+ method: "erpnext.stock.doctype.item.item.get_asset_naming_series",
+ callback: function(r) {
frm.set_value("is_stock_item", frm.doc.is_fixed_asset ? 0 : 1);
- frm.trigger("set_asset_naming_series");
+ frm.events.set_asset_naming_series(frm, r.message);
}
});
frm.trigger('auto_create_assets');
},
- set_asset_naming_series: function(frm) {
- if (frm.doc.__onload && frm.doc.__onload.asset_naming_series) {
- frm.set_df_property("asset_naming_series", "options", frm.doc.__onload.asset_naming_series);
+ set_asset_naming_series: function(frm, asset_naming_series) {
+ if ((frm.doc.__onload && frm.doc.__onload.asset_naming_series) || asset_naming_series) {
+ let naming_series = (frm.doc.__onload && frm.doc.__onload.asset_naming_series) || asset_naming_series;
+ frm.set_df_property("asset_naming_series", "options", naming_series);
}
},
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 494fb3b..8ede955 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -50,15 +50,7 @@
class Item(Document):
def onload(self):
self.set_onload('stock_exists', self.stock_ledger_created())
- self.set_asset_naming_series()
-
- @frappe.whitelist()
- def set_asset_naming_series(self):
- if not hasattr(self, '_asset_naming_series'):
- from erpnext.assets.doctype.asset.asset import get_asset_naming_series
- self._asset_naming_series = get_asset_naming_series()
-
- self.set_onload('asset_naming_series', self._asset_naming_series)
+ self.set_onload('asset_naming_series', get_asset_naming_series())
def autoname(self):
if frappe.db.get_default("item_naming_by") == "Naming Series":
@@ -400,6 +392,7 @@
self.validate_properties_before_merge(new_name)
self.validate_duplicate_product_bundles_before_merge(old_name, new_name)
self.validate_duplicate_website_item_before_merge(old_name, new_name)
+ self.delete_old_bins(old_name)
def after_rename(self, old_name, new_name, merge):
if merge:
@@ -428,6 +421,9 @@
frappe.db.set_value(dt, d.name, "item_wise_tax_detail",
json.dumps(item_wise_tax_detail), update_modified=False)
+ def delete_old_bins(self, old_name):
+ frappe.db.delete("Bin", {"item_code": old_name})
+
def validate_duplicate_item_in_stock_reconciliation(self, old_name, new_name):
records = frappe.db.sql(""" SELECT parent, COUNT(*) as records
FROM `tabStock Reconciliation Item`
@@ -508,11 +504,11 @@
existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock")
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
- repost_stock_for_warehouses = frappe.db.sql_list("""select distinct warehouse
- from tabBin where item_code=%s""", new_name)
+ repost_stock_for_warehouses = frappe.get_all("Stock Ledger Entry",
+ "warehouse", filters={"item_code": new_name}, pluck="warehouse", distinct=True)
# Delete all existing bins to avoid duplicate bins for the same item and warehouse
- frappe.db.sql("delete from `tabBin` where item_code=%s", new_name)
+ frappe.db.delete("Bin", {"item_code": new_name})
for warehouse in repost_stock_for_warehouses:
repost_stock(new_name, warehouse)
@@ -999,7 +995,7 @@
if uom == stock_uom:
return 1.0
- from_uom, to_uom = uom, stock_uom # renaming for readability
+ from_uom, to_uom = uom, stock_uom # renaming for readability
exact_match = frappe.db.get_value("UOM Conversion Factor", {"to_uom": to_uom, "from_uom": from_uom}, ["value"], as_dict=1)
if exact_match:
@@ -1011,9 +1007,9 @@
# This attempts to try and get conversion from intermediate UOM.
# case:
- # g -> mg = 1000
- # g -> kg = 0.001
- # therefore kg -> mg = 1000 / 0.001 = 1,000,000
+ # g -> mg = 1000
+ # g -> kg = 0.001
+ # therefore kg -> mg = 1000 / 0.001 = 1,000,000
intermediate_match = frappe.db.sql("""
select (first.value / second.value) as value
from `tabUOM Conversion Factor` first
@@ -1072,3 +1068,11 @@
frappe.bold(item_default.company),
frappe.bold(frappe.unscrub(field))
), title=_("Invalid Item Defaults"))
+
+
+@frappe.whitelist()
+def get_asset_naming_series():
+ from erpnext.assets.doctype.asset.asset import get_asset_naming_series
+
+ return get_asset_naming_series()
+
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index d7671b1..d57308b 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -371,23 +371,24 @@
variant.save()
def test_item_merging(self):
- create_item("Test Item for Merging 1")
- create_item("Test Item for Merging 2")
+ old = create_item(frappe.generate_hash(length=20)).name
+ new = create_item(frappe.generate_hash(length=20)).name
- make_stock_entry(item_code="Test Item for Merging 1", target="_Test Warehouse - _TC",
+ make_stock_entry(item_code=old, target="_Test Warehouse - _TC",
qty=1, rate=100)
- make_stock_entry(item_code="Test Item for Merging 2", target="_Test Warehouse 1 - _TC",
+ make_stock_entry(item_code=old, target="_Test Warehouse 1 - _TC",
+ qty=1, rate=100)
+ make_stock_entry(item_code=new, target="_Test Warehouse 1 - _TC",
qty=1, rate=100)
- frappe.rename_doc("Item", "Test Item for Merging 1", "Test Item for Merging 2", merge=True)
+ frappe.rename_doc("Item", old, new, merge=True)
- self.assertFalse(frappe.db.exists("Item", "Test Item for Merging 1"))
+ self.assertFalse(frappe.db.exists("Item", old))
self.assertTrue(frappe.db.get_value("Bin",
- {"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse - _TC"}))
-
+ {"item_code": new, "warehouse": "_Test Warehouse - _TC"}))
self.assertTrue(frappe.db.get_value("Bin",
- {"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse 1 - _TC"}))
+ {"item_code": new, "warehouse": "_Test Warehouse 1 - _TC"}))
def test_item_merging_with_product_bundle(self):
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js
index 5f53be0..e68b0ab 100644
--- a/erpnext/stock/doctype/material_request/material_request.js
+++ b/erpnext/stock/doctype/material_request/material_request.js
@@ -214,6 +214,7 @@
material_request_type: frm.doc.material_request_type,
plc_conversion_rate: 1,
rate: item.rate,
+ uom: item.uom,
conversion_factor: item.conversion_factor
},
overwrite_warehouse: overwrite_warehouse
@@ -392,6 +393,7 @@
item_code: function(frm, doctype, name) {
const item = locals[doctype][name];
item.rate = 0;
+ item.uom = '';
set_schedule_date(frm);
frm.events.get_item_data(frm, item, 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/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/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
index b54a90e..6d4b4a1 100755
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
@@ -1165,7 +1165,7 @@
"idx": 261,
"is_submittable": 1,
"links": [],
- "modified": "2022-02-01 11:40:52.690984",
+ "modified": "2022-03-10 11:40:52.690984",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 33e40c8..52f10ea 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -926,3 +926,6 @@
account.base_amount * item.get(based_on_field) / total_item_cost
return item_account_wise_cost
+
+def on_doctype_update():
+ frappe.db.add_index("Purchase Receipt", ["supplier", "is_return", "return_against"])
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index fa28f22..0017fa7 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -769,8 +769,7 @@
update_purchase_receipt_status,
)
- pr = make_purchase_receipt(do_not_submit=True)
- pr.submit()
+ pr = make_purchase_receipt()
update_purchase_receipt_status(pr.name, "Closed")
self.assertEqual(
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
index 977d470..f4d52ad 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -118,7 +118,8 @@
doc.set_status('Failed')
raise
finally:
- frappe.db.commit()
+ if not frappe.flags.in_test:
+ frappe.db.commit()
def repost_sl_entries(doc):
if doc.based_on == 'Transaction':
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index ee08e38..bf62f50 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -394,7 +394,7 @@
if not sle.is_cancelled and not sle.serial_no and cint(sle.actual_qty) > 0 \
and item_det.has_serial_no == 1 and item_det.serial_no_series:
serial_nos = get_auto_serial_nos(item_det.serial_no_series, sle.actual_qty)
- frappe.db.set(sle, "serial_no", serial_nos)
+ sle.db_set("serial_no", serial_nos)
validate_serial_no(sle, item_det)
if sle.serial_no:
auto_make_serial_nos(sle)
@@ -516,13 +516,16 @@
if controller.doctype == "Stock Entry":
warehouse = d.t_warehouse
qty = d.transfer_qty
+ elif controller.doctype in ("Sales Invoice", "Delivery Note"):
+ warehouse = d.warehouse
+ qty = d.stock_qty
else:
warehouse = d.warehouse
qty = (d.qty if controller.doctype == "Stock Reconciliation"
else d.stock_qty)
for sle in stock_ledger_entries:
if sle.voucher_detail_no==d.name:
- if not accepted_serial_nos_updated and qty and abs(sle.actual_qty)==qty \
+ if not accepted_serial_nos_updated and qty and abs(sle.actual_qty) == abs(qty) \
and sle.warehouse == warehouse and sle.serial_no != d.serial_no:
d.serial_no = sle.serial_no
frappe.db.set_value(d.doctype, d.name, "serial_no", sle.serial_no)
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 9bec5f7..9bb41b9 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -367,7 +367,7 @@
if not out[d[1]]:
out[d[1]] = frappe.get_cached_value('Company', args.company, d[2]) if d[2] else None
- for fieldname in ("item_name", "item_group", "barcodes", "brand", "stock_uom"):
+ for fieldname in ("item_name", "item_group", "brand", "stock_uom"):
out[fieldname] = item.get(fieldname)
if args.get("manufacturer"):
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_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py
index b4f43a7..24f47c1 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.py
+++ b/erpnext/stock/report/stock_balance/stock_balance.py
@@ -18,7 +18,6 @@
is_reposting_item_valuation_in_progress()
if not filters: filters = {}
- from_date = filters.get('from_date')
to_date = filters.get('to_date')
if filters.get("company"):
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 6975552..353bfa4 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -829,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)
@@ -838,11 +838,13 @@
for warehouse, data in self.data.items():
bin_name = get_or_make_bin(self.item_code, warehouse)
- frappe.db.set_value('Bin', bin_name, {
- "valuation_rate": data.valuation_rate,
+ updated_values = {
"actual_qty": data.qty_after_transaction,
"stock_value": data.stock_value
- })
+ }
+ if data.valuation_rate is not None:
+ updated_values["valuation_rate"] = data.valuation_rate
+ frappe.db.set_value('Bin', bin_name, updated_values)
def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
@@ -1157,7 +1159,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:
@@ -1171,7 +1173,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/pages/non_profit/__init__.py b/erpnext/templates/pages/non_profit/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/templates/pages/non_profit/__init__.py
diff --git a/erpnext/www/lms/macros/hero.html b/erpnext/www/lms/macros/hero.html
index 95ba8f7..dd3c23a 100644
--- a/erpnext/www/lms/macros/hero.html
+++ b/erpnext/www/lms/macros/hero.html
@@ -39,16 +39,13 @@
frappe.call(opts).then(res => {
let success_dialog = new frappe.ui.Dialog({
title: __('Success'),
- primary_action_label: __('View Program Content'),
+ primary_action_label: __('OK'),
primary_action: function() {
window.location.reload();
- },
- secondary_action: function() {
- window.location.reload();
}
})
success_dialog.show();
- success_dialog.set_message(__('You have successfully enrolled for the program '));
+ success_dialog.set_message(__('You have successfully enrolled for the program.'));
})
}
</script>