mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-22 10:39:41 +00:00
Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c15cff05b0 | ||
|
|
b80526f226 | ||
|
|
0569f8a857 | ||
|
|
63715bf229 | ||
|
|
63cd4349a6 | ||
|
|
42b395916d | ||
|
|
0057c10a7d | ||
|
|
3c8c5d01fb | ||
|
|
d820757359 | ||
|
|
14e59c86aa | ||
|
|
9e16f4e412 | ||
|
|
6d269a4d89 | ||
|
|
4e8b39ab3f | ||
|
|
9925eb9982 | ||
|
|
301d199ece | ||
|
|
6c574fbf33 | ||
|
|
80981d025f | ||
|
|
a420d0242c | ||
|
|
809d5caf80 | ||
|
|
6f442d14cf | ||
|
|
a17acfdaa2 | ||
|
|
3673dea03b | ||
|
|
fb3725752f | ||
|
|
16c94d292c | ||
|
|
0e46b33ee3 | ||
|
|
df716fbd0c | ||
|
|
0dbfb1589e | ||
|
|
473a43b6b1 | ||
|
|
1a3b3b96c2 | ||
|
|
e9e53a74c9 | ||
|
|
26aef4fb1c | ||
|
|
b145fe3b3e | ||
|
|
d83c869d73 | ||
|
|
08c69c7a76 | ||
|
|
84e6cead56 | ||
|
|
590e48fa1d | ||
|
|
25da4d28ff | ||
|
|
f57d2fadbc | ||
|
|
9e7edd677a | ||
|
|
bc7494278d | ||
|
|
c442b4aef1 | ||
|
|
ef9b25cf08 | ||
|
|
2f2d3de306 | ||
|
|
13639682ce | ||
|
|
c01adae8a5 | ||
|
|
8ae691d9dc | ||
|
|
a95c011a93 | ||
|
|
42f3592e95 | ||
|
|
0602848caa | ||
|
|
9985d28571 | ||
|
|
31824c2280 | ||
|
|
18d93f8398 | ||
|
|
c9443123f9 | ||
|
|
0fdec8fac8 | ||
|
|
6fbb06a878 | ||
|
|
89348c1bb4 | ||
|
|
598dbc93ac | ||
|
|
51a4ef3069 | ||
|
|
49fb177f86 | ||
|
|
b0aae8a297 | ||
|
|
e310347ca4 | ||
|
|
9b1544aa14 | ||
|
|
a8da5f4566 | ||
|
|
0e469f6d95 | ||
|
|
b393c230bd | ||
|
|
32c1bb61de | ||
|
|
4f023757de | ||
|
|
d2d25e17bb | ||
|
|
a843e784e6 | ||
|
|
8a6432ec3f | ||
|
|
b97d30aad0 | ||
|
|
bc7cfe6919 | ||
|
|
85802b0f97 | ||
|
|
dc67d39ce6 | ||
|
|
529a47bc88 | ||
|
|
2381b81aac | ||
|
|
dbf245c687 | ||
|
|
5cdc267aee | ||
|
|
95e1021caf | ||
|
|
7d50f8798a | ||
|
|
dd751c6e92 | ||
|
|
763787b0a5 | ||
|
|
1998223ee3 | ||
|
|
794fd0819f | ||
|
|
1d69ce1932 | ||
|
|
8e23c6ad69 | ||
|
|
01d6df45d0 |
@@ -10,7 +10,7 @@
|
||||
"@semantic-release/release-notes-generator",
|
||||
[
|
||||
"@semantic-release/exec", {
|
||||
"prepareCmd": 'sed -ir "s/[0-9]*\.[0-9]*\.[0-9]*/${nextRelease.version}/" erpnext/__init__.py'
|
||||
"prepareCmd": 'sed -ir -E "s/\"[0-9]+\.[0-9]+\.[0-9]+\"/\"${nextRelease.version}\"/" erpnext/__init__.py'
|
||||
}
|
||||
],
|
||||
[
|
||||
|
||||
@@ -4,7 +4,7 @@ import frappe
|
||||
|
||||
from erpnext.hooks import regional_overrides
|
||||
|
||||
__version__ = "13.36.1"
|
||||
__version__ = "13.36.3"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -305,7 +305,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def validate_reference_documents(self):
|
||||
if self.party_type == "Student":
|
||||
valid_reference_doctypes = "Fees"
|
||||
valid_reference_doctypes = ("Fees", "Journal Entry")
|
||||
elif self.party_type == "Customer":
|
||||
valid_reference_doctypes = ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning")
|
||||
elif self.party_type == "Supplier":
|
||||
|
||||
@@ -222,9 +222,6 @@ class POSInvoice(SalesInvoice):
|
||||
allow_negative_stock = frappe.db.get_single_value("Stock Settings", "allow_negative_stock")
|
||||
|
||||
for d in self.get("items"):
|
||||
is_service_item = not (frappe.db.get_value("Item", d.get("item_code"), "is_stock_item"))
|
||||
if is_service_item:
|
||||
return
|
||||
if d.serial_no:
|
||||
self.validate_pos_reserved_serial_nos(d)
|
||||
self.validate_delivered_serial_nos(d)
|
||||
|
||||
@@ -9,7 +9,7 @@ from frappe import _
|
||||
from frappe.core.page.background_jobs.background_jobs import get_info
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import map_child_doc, map_doc
|
||||
from frappe.utils import flt, getdate, nowdate
|
||||
from frappe.utils import cint, flt, getdate, nowdate
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
@@ -219,6 +219,9 @@ class POSInvoiceMergeLog(Document):
|
||||
invoice.taxes_and_charges = None
|
||||
invoice.ignore_pricing_rule = 1
|
||||
invoice.customer = self.customer
|
||||
invoice.disable_rounded_total = cint(
|
||||
frappe.db.get_value("POS Profile", invoice.pos_profile, "disable_rounded_total")
|
||||
)
|
||||
|
||||
if self.merge_invoices_based_on == "Customer Group":
|
||||
invoice.flags.ignore_pos_profile = True
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"write_off_account",
|
||||
"write_off_cost_center",
|
||||
"account_for_change_amount",
|
||||
"disable_rounded_total",
|
||||
"column_break_23",
|
||||
"income_account",
|
||||
"expense_account",
|
||||
@@ -358,6 +359,13 @@
|
||||
"fieldname": "validate_stock_on_save",
|
||||
"fieldtype": "Check",
|
||||
"label": "Validate Stock on Save"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If enabled, the consolidated invoices will have rounded total disabled",
|
||||
"fieldname": "disable_rounded_total",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable Rounded Total"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -385,7 +393,7 @@
|
||||
"link_fieldname": "pos_profile"
|
||||
}
|
||||
],
|
||||
"modified": "2022-03-21 13:29:28.480533",
|
||||
"modified": "2022-07-21 11:16:46.911173",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile",
|
||||
|
||||
@@ -158,6 +158,7 @@ class PurchaseInvoice(BuyingController):
|
||||
if tds_category and not for_validate:
|
||||
self.apply_tds = 1
|
||||
self.tax_withholding_category = tds_category
|
||||
self.set_onload("supplier_tds", tds_category)
|
||||
|
||||
super(PurchaseInvoice, self).set_missing_values(for_validate)
|
||||
|
||||
|
||||
@@ -414,7 +414,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.is_return && doc.return_against",
|
||||
"depends_on": "eval: doc.is_return",
|
||||
"fieldname": "update_billed_amount_in_sales_order",
|
||||
"fieldtype": "Check",
|
||||
"hide_days": 1,
|
||||
@@ -2046,7 +2046,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2022-06-16 16:22:44.870575",
|
||||
"modified": "2022-07-11 17:43:56.435382",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -2173,13 +2173,13 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
target_detail_field = "sales_invoice_item" if doctype == "Sales Invoice" else "sales_order_item"
|
||||
source_document_warehouse_field = "target_warehouse"
|
||||
target_document_warehouse_field = "from_warehouse"
|
||||
received_items = get_received_items(source_name, target_doctype, target_detail_field)
|
||||
else:
|
||||
source_doc = frappe.get_doc(doctype, source_name)
|
||||
target_doctype = "Sales Invoice" if doctype == "Purchase Invoice" else "Sales Order"
|
||||
source_document_warehouse_field = "from_warehouse"
|
||||
target_document_warehouse_field = "target_warehouse"
|
||||
|
||||
received_items = get_received_items(source_name, target_doctype, target_detail_field)
|
||||
received_items = {}
|
||||
|
||||
validate_inter_company_transaction(source_doc, doctype)
|
||||
details = get_inter_company_details(source_doc, doctype)
|
||||
|
||||
@@ -2712,6 +2712,19 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 20)
|
||||
self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 10)
|
||||
|
||||
si = get_sales_invoice_for_e_invoice()
|
||||
si.apply_discount_on = ""
|
||||
si.items[1].price_list_rate = 15
|
||||
si.items[1].discount_amount = -5
|
||||
si.items[1].rate = 20
|
||||
si.save()
|
||||
|
||||
einvoice = make_einvoice(si)
|
||||
validate_totals(einvoice)
|
||||
|
||||
self.assertEqual(einvoice["ItemList"][1]["Discount"], 0)
|
||||
self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 20)
|
||||
|
||||
def test_einvoice_without_discounts(self):
|
||||
from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals
|
||||
|
||||
@@ -2804,6 +2817,19 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 18)
|
||||
self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 5)
|
||||
|
||||
si = get_sales_invoice_for_e_invoice()
|
||||
si.apply_discount_on = ""
|
||||
si.items[1].price_list_rate = 15
|
||||
si.items[1].discount_amount = -5
|
||||
si.items[1].rate = 20
|
||||
si.save()
|
||||
|
||||
einvoice = make_einvoice(si)
|
||||
validate_totals(einvoice)
|
||||
|
||||
self.assertEqual(einvoice["ItemList"][1]["Discount"], 0)
|
||||
self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 20)
|
||||
|
||||
def test_item_tax_net_range(self):
|
||||
item = create_item("T Shirt")
|
||||
|
||||
|
||||
@@ -44,14 +44,14 @@ frappe.query_reports["Gross Profit"] = {
|
||||
"parent_field": "parent_invoice",
|
||||
"initial_depth": 3,
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
if (column.fieldname == "sales_invoice" && column.options == "Item" && data.indent == 0) {
|
||||
if (column.fieldname == "sales_invoice" && column.options == "Item" && data && data.indent == 0) {
|
||||
column._options = "Sales Invoice";
|
||||
} else {
|
||||
column._options = "Item";
|
||||
}
|
||||
value = default_formatter(value, row, column, data);
|
||||
|
||||
if (data && (data.indent == 0.0 || row[1].content == "Total")) {
|
||||
if (data && (data.indent == 0.0 || (row[1] && row[1].content == "Total"))) {
|
||||
value = $(`<span>${value}</span>`);
|
||||
var $value = $(value).css("font-weight", "bold");
|
||||
value = $value.wrap("<p></p>").parent().html();
|
||||
|
||||
@@ -14,9 +14,9 @@ def execute(filters=None):
|
||||
filters.naming_series = frappe.db.get_single_value("Buying Settings", "supp_master_name")
|
||||
|
||||
columns = get_columns(filters)
|
||||
tds_docs, tds_accounts, tax_category_map = get_tds_docs(filters)
|
||||
tds_docs, tds_accounts, tax_category_map, journal_entry_party_map = get_tds_docs(filters)
|
||||
|
||||
res = get_result(filters, tds_docs, tds_accounts, tax_category_map)
|
||||
res = get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map)
|
||||
final_result = group_by_supplier_and_category(res)
|
||||
|
||||
return columns, final_result
|
||||
|
||||
@@ -26,7 +26,6 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
supplier_map = get_supplier_pan_map()
|
||||
tax_rate_map = get_tax_rate_map(filters)
|
||||
gle_map = get_gle_map(tds_docs)
|
||||
print(journal_entry_party_map)
|
||||
|
||||
out = []
|
||||
for name, details in gle_map.items():
|
||||
|
||||
@@ -113,7 +113,7 @@ class RequestforQuotation(BuyingController):
|
||||
|
||||
def get_link(self):
|
||||
# RFQ link for supplier portal
|
||||
return get_url("/rfq/" + self.name)
|
||||
return get_url("/app/request-for-quotation/" + self.name)
|
||||
|
||||
def update_supplier_part_no(self, supplier):
|
||||
self.vendor = supplier
|
||||
|
||||
@@ -301,7 +301,8 @@ class BuyingController(StockController, Subcontracting):
|
||||
|
||||
rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
|
||||
else:
|
||||
rate = frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), "rate")
|
||||
field = "incoming_rate" if self.get("is_internal_supplier") else "rate"
|
||||
rate = frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
|
||||
|
||||
if self.is_internal_transfer():
|
||||
if rate != d.rate:
|
||||
|
||||
@@ -18,8 +18,9 @@ from erpnext.stock.get_item_details import _get_item_tax_template
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def employee_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "Employee"
|
||||
conditions = []
|
||||
fields = get_fields("Employee", ["name", "employee_name"])
|
||||
fields = get_fields(doctype, ["name", "employee_name"])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select {fields} from `tabEmployee`
|
||||
@@ -49,7 +50,8 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def lead_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
fields = get_fields("Lead", ["name", "lead_name", "company_name"])
|
||||
doctype = "Lead"
|
||||
fields = get_fields(doctype, ["name", "lead_name", "company_name"])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select {fields} from `tabLead`
|
||||
@@ -77,6 +79,7 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def customer_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "Customer"
|
||||
conditions = []
|
||||
cust_master_name = frappe.defaults.get_user_default("cust_master_name")
|
||||
|
||||
@@ -85,9 +88,9 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
else:
|
||||
fields = ["name", "customer_name", "customer_group", "territory"]
|
||||
|
||||
fields = get_fields("Customer", fields)
|
||||
fields = get_fields(doctype, fields)
|
||||
|
||||
searchfields = frappe.get_meta("Customer").get_search_fields()
|
||||
searchfields = frappe.get_meta(doctype).get_search_fields()
|
||||
searchfields = " or ".join(field + " like %(txt)s" for field in searchfields)
|
||||
|
||||
return frappe.db.sql(
|
||||
@@ -116,6 +119,7 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def supplier_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "Supplier"
|
||||
supp_master_name = frappe.defaults.get_user_default("supp_master_name")
|
||||
|
||||
if supp_master_name == "Supplier Name":
|
||||
@@ -123,7 +127,7 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
else:
|
||||
fields = ["name", "supplier_name", "supplier_group"]
|
||||
|
||||
fields = get_fields("Supplier", fields)
|
||||
fields = get_fields(doctype, fields)
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select {field} from `tabSupplier`
|
||||
@@ -147,6 +151,7 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "Account"
|
||||
company_currency = erpnext.get_company_currency(filters.get("company"))
|
||||
|
||||
def get_accounts(with_account_type_filter):
|
||||
@@ -197,13 +202,14 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
|
||||
doctype = "Item"
|
||||
conditions = []
|
||||
|
||||
if isinstance(filters, str):
|
||||
filters = json.loads(filters)
|
||||
|
||||
# Get searchfields from meta and use in Item Link field query
|
||||
meta = frappe.get_meta("Item", cached=True)
|
||||
meta = frappe.get_meta(doctype, cached=True)
|
||||
searchfields = meta.get_search_fields()
|
||||
|
||||
# these are handled separately
|
||||
@@ -257,7 +263,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
|
||||
filters.pop("supplier", None)
|
||||
|
||||
description_cond = ""
|
||||
if frappe.db.count("Item", cache=True) < 50000:
|
||||
if frappe.db.count(doctype, cache=True) < 50000:
|
||||
# scan description only if items are less than 50000
|
||||
description_cond = "or tabItem.description LIKE %(txt)s"
|
||||
return frappe.db.sql(
|
||||
@@ -300,8 +306,9 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def bom(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "BOM"
|
||||
conditions = []
|
||||
fields = get_fields("BOM", ["name", "item"])
|
||||
fields = get_fields(doctype, ["name", "item"])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select {fields}
|
||||
@@ -331,6 +338,7 @@ def bom(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "Project"
|
||||
cond = ""
|
||||
if filters and filters.get("customer"):
|
||||
cond = """(`tabProject`.customer = %s or
|
||||
@@ -338,9 +346,9 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
||||
frappe.db.escape(filters.get("customer"))
|
||||
)
|
||||
|
||||
fields = get_fields("Project", ["name", "project_name"])
|
||||
searchfields = frappe.get_meta("Project").get_search_fields()
|
||||
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
|
||||
fields = get_fields(doctype, ["name", "project_name"])
|
||||
searchfields = frappe.get_meta(doctype).get_search_fields()
|
||||
searchfields = " or ".join(["`tabProject`." + field + " like %(txt)s" for field in searchfields])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select {fields} from `tabProject`
|
||||
@@ -366,7 +374,8 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, filters, as_dict):
|
||||
fields = get_fields("Delivery Note", ["name", "customer", "posting_date"])
|
||||
doctype = "Delivery Note"
|
||||
fields = get_fields(doctype, ["name", "customer", "posting_date"])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
@@ -402,6 +411,7 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len,
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "Batch"
|
||||
cond = ""
|
||||
if filters.get("posting_date"):
|
||||
cond = "and (batch.expiry_date is null or batch.expiry_date >= %(posting_date)s)"
|
||||
@@ -420,7 +430,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
||||
if filters.get("is_return"):
|
||||
having_clause = ""
|
||||
|
||||
meta = frappe.get_meta("Batch", cached=True)
|
||||
meta = frappe.get_meta(doctype, cached=True)
|
||||
searchfields = meta.get_search_fields()
|
||||
|
||||
search_columns = ""
|
||||
@@ -496,6 +506,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_account_list(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "Account"
|
||||
filter_list = []
|
||||
|
||||
if isinstance(filters, dict):
|
||||
@@ -514,7 +525,7 @@ def get_account_list(doctype, txt, searchfield, start, page_len, filters):
|
||||
filter_list.append([doctype, searchfield, "like", "%%%s%%" % txt])
|
||||
|
||||
return frappe.desk.reportview.execute(
|
||||
"Account",
|
||||
doctype,
|
||||
filters=filter_list,
|
||||
fields=["name", "parent_account"],
|
||||
limit_start=start,
|
||||
@@ -553,6 +564,7 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters):
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
doctype = "Account"
|
||||
condition = ""
|
||||
if filters.get("company"):
|
||||
condition += "and tabAccount.company = %(company)s"
|
||||
@@ -628,6 +640,7 @@ def get_expense_account(doctype, txt, searchfield, start, page_len, filters):
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
doctype = "Account"
|
||||
condition = ""
|
||||
if filters.get("company"):
|
||||
condition += "and tabAccount.company = %(company)s"
|
||||
@@ -650,6 +663,7 @@ def get_expense_account(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def warehouse_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
# Should be used when item code is passed in filters.
|
||||
doctype = "Warehouse"
|
||||
conditions, bin_conditions = [], []
|
||||
filter_dict = get_doctype_wise_filters(filters)
|
||||
|
||||
|
||||
@@ -614,13 +614,13 @@ class SellingController(StockController):
|
||||
stock_items = [d.item_code, d.description, d.warehouse, ""]
|
||||
non_stock_items = [d.item_code, d.description]
|
||||
|
||||
duplicate_items_msg = _("Item {0} entered multiple times.").format(frappe.bold(d.item_code))
|
||||
duplicate_items_msg += "<br><br>"
|
||||
duplicate_items_msg += _("Please enable {} in {} to allow same item in multiple rows").format(
|
||||
frappe.bold("Allow Item to Be Added Multiple Times in a Transaction"),
|
||||
get_link_to_form("Selling Settings", "Selling Settings"),
|
||||
)
|
||||
if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1:
|
||||
duplicate_items_msg = _("Item {0} entered multiple times.").format(frappe.bold(d.item_code))
|
||||
duplicate_items_msg += "<br><br>"
|
||||
duplicate_items_msg += _("Please enable {} in {} to allow same item in multiple rows").format(
|
||||
frappe.bold("Allow Item to Be Added Multiple Times in a Transaction"),
|
||||
get_link_to_form("Selling Settings", "Selling Settings"),
|
||||
)
|
||||
if stock_items in check_list:
|
||||
frappe.throw(duplicate_items_msg)
|
||||
else:
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "website_item.image",
|
||||
"fetch_from": "website_item.website_image",
|
||||
"fieldname": "website_item_image",
|
||||
"fieldtype": "Attach",
|
||||
"label": "Website Item Image",
|
||||
@@ -75,7 +75,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-07-13 21:02:19.031652",
|
||||
"modified": "2022-06-28 16:44:24.718728",
|
||||
"modified_by": "Administrator",
|
||||
"module": "E-commerce",
|
||||
"name": "Recommended Items",
|
||||
@@ -83,5 +83,6 @@
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -30,10 +30,6 @@ frappe.ui.form.on('Website Item', {
|
||||
}, __("View"));
|
||||
},
|
||||
|
||||
image: () => {
|
||||
refresh_field("image_view");
|
||||
},
|
||||
|
||||
copy_from_item_group: (frm) => {
|
||||
return frm.call({
|
||||
doc: frm.doc,
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
"column_break_11",
|
||||
"description",
|
||||
"brand",
|
||||
"image",
|
||||
"display_section",
|
||||
"website_image",
|
||||
"website_image_alt",
|
||||
@@ -113,8 +112,11 @@
|
||||
{
|
||||
"description": "Item Image (if not slideshow)",
|
||||
"fieldname": "website_image",
|
||||
"fieldtype": "Attach",
|
||||
"label": "Website Image"
|
||||
"fieldtype": "Attach Image",
|
||||
"hidden": 1,
|
||||
"in_preview": 1,
|
||||
"label": "Website Image",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"description": "Image Alternative Text",
|
||||
@@ -188,14 +190,6 @@
|
||||
"options": "Item Group",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Attach Image",
|
||||
"hidden": 1,
|
||||
"in_preview": 1,
|
||||
"label": "Image",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "published",
|
||||
@@ -348,13 +342,14 @@
|
||||
}
|
||||
],
|
||||
"has_web_view": 1,
|
||||
"image_field": "image",
|
||||
"image_field": "website_image",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-02 13:08:41.942726",
|
||||
"modified": "2022-06-28 17:10:30.613251",
|
||||
"modified_by": "Administrator",
|
||||
"module": "E-commerce",
|
||||
"name": "Website Item",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -410,6 +405,7 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "web_item_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
from typing import TYPE_CHECKING, List, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from erpnext.stock.doctype.item.item import Item
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
@@ -116,11 +120,6 @@ class WebsiteItem(WebsiteGenerator):
|
||||
if frappe.flags.in_import:
|
||||
return
|
||||
|
||||
auto_set_website_image = False
|
||||
if not self.website_image and self.image:
|
||||
auto_set_website_image = True
|
||||
self.website_image = self.image
|
||||
|
||||
if not self.website_image:
|
||||
return
|
||||
|
||||
@@ -137,18 +136,16 @@ class WebsiteItem(WebsiteGenerator):
|
||||
file_doc = file_doc[0]
|
||||
|
||||
if not file_doc:
|
||||
if not auto_set_website_image:
|
||||
frappe.msgprint(
|
||||
_("Website Image {0} attached to Item {1} cannot be found").format(
|
||||
self.website_image, self.name
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("Website Image {0} attached to Item {1} cannot be found").format(
|
||||
self.website_image, self.name
|
||||
)
|
||||
)
|
||||
|
||||
self.website_image = None
|
||||
|
||||
elif file_doc.is_private:
|
||||
if not auto_set_website_image:
|
||||
frappe.msgprint(_("Website Image should be a public file or website URL"))
|
||||
frappe.msgprint(_("Website Image should be a public file or website URL"))
|
||||
|
||||
self.website_image = None
|
||||
|
||||
@@ -159,9 +156,8 @@ class WebsiteItem(WebsiteGenerator):
|
||||
|
||||
import requests.exceptions
|
||||
|
||||
if not self.is_new() and self.website_image != frappe.db.get_value(
|
||||
self.doctype, self.name, "website_image"
|
||||
):
|
||||
db_website_image = frappe.db.get_value(self.doctype, self.name, "website_image")
|
||||
if not self.is_new() and self.website_image != db_website_image:
|
||||
self.thumbnail = None
|
||||
|
||||
if self.website_image and not self.thumbnail:
|
||||
@@ -437,7 +433,9 @@ def check_if_user_is_customer(user=None):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_website_item(doc, save=True):
|
||||
def make_website_item(doc: "Item", save: bool = True) -> Union["WebsiteItem", List[str]]:
|
||||
"Make Website Item from Item. Used via Form UI or patch."
|
||||
|
||||
if not doc:
|
||||
return
|
||||
|
||||
@@ -457,7 +455,6 @@ def make_website_item(doc, save=True):
|
||||
"item_group",
|
||||
"stock_uom",
|
||||
"brand",
|
||||
"image",
|
||||
"has_variants",
|
||||
"variant_of",
|
||||
"description",
|
||||
@@ -465,6 +462,10 @@ def make_website_item(doc, save=True):
|
||||
for field in fields_to_map:
|
||||
website_item.update({field: doc.get(field)})
|
||||
|
||||
# Needed for publishing/mapping via Form UI only
|
||||
if not frappe.flags.in_migrate and (doc.get("image") and not website_item.website_image):
|
||||
website_item.website_image = doc.get("image")
|
||||
|
||||
if not save:
|
||||
return website_item
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
frappe.listview_settings['Website Item'] = {
|
||||
add_fields: ["item_name", "web_item_name", "published", "image", "has_variants", "variant_of"],
|
||||
add_fields: ["item_name", "web_item_name", "published", "website_image", "has_variants", "variant_of"],
|
||||
filters: [["published", "=", "1"]],
|
||||
|
||||
get_indicator: function(doc) {
|
||||
|
||||
@@ -20,7 +20,15 @@ def add_to_wishlist(item_code):
|
||||
web_item_data = frappe.db.get_value(
|
||||
"Website Item",
|
||||
{"item_code": item_code},
|
||||
["image", "website_warehouse", "name", "web_item_name", "item_name", "item_group", "route"],
|
||||
[
|
||||
"website_image",
|
||||
"website_warehouse",
|
||||
"name",
|
||||
"web_item_name",
|
||||
"item_name",
|
||||
"item_group",
|
||||
"route",
|
||||
],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -30,7 +38,7 @@ def add_to_wishlist(item_code):
|
||||
"item_group": web_item_data.get("item_group"),
|
||||
"website_item": web_item_data.get("name"),
|
||||
"web_item_name": web_item_data.get("web_item_name"),
|
||||
"image": web_item_data.get("image"),
|
||||
"image": web_item_data.get("website_image"),
|
||||
"warehouse": web_item_data.get("website_warehouse"),
|
||||
"route": web_item_data.get("route"),
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ class ProductQuery:
|
||||
"variant_of",
|
||||
"has_variants",
|
||||
"item_group",
|
||||
"image",
|
||||
"web_long_description",
|
||||
"short_description",
|
||||
"route",
|
||||
|
||||
@@ -35,7 +35,7 @@ erpnext.ProductGrid = class {
|
||||
}
|
||||
|
||||
get_image_html(item, title) {
|
||||
let image = item.website_image || item.image;
|
||||
let image = item.website_image;
|
||||
|
||||
if (image) {
|
||||
return `
|
||||
|
||||
@@ -35,7 +35,7 @@ erpnext.ProductList = class {
|
||||
}
|
||||
|
||||
get_image_html(item, title, settings) {
|
||||
let image = item.website_image || item.image;
|
||||
let image = item.website_image;
|
||||
let wishlist_enabled = !item.has_variants && settings.enable_wishlist;
|
||||
let image_html = ``;
|
||||
|
||||
|
||||
@@ -199,16 +199,32 @@ def get_fee_components(fee_structure):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_fee_schedule(program, student_category=None):
|
||||
def get_fee_schedule(program, student_category=None, academic_year=None):
|
||||
"""Returns Fee Schedule.
|
||||
|
||||
:param program: Program.
|
||||
:param student_category: Student Category
|
||||
:param student_category: Student Category.
|
||||
:param academic_year: Academic Year.
|
||||
"""
|
||||
fs = frappe.get_all(
|
||||
"Program Fee",
|
||||
fields=["academic_term", "fee_structure", "due_date", "amount"],
|
||||
filters={"parent": program, "student_category": student_category},
|
||||
filters = {}
|
||||
if program:
|
||||
filters = {"program": program}
|
||||
|
||||
if student_category:
|
||||
filters["student_category"] = student_category
|
||||
|
||||
if academic_year:
|
||||
filters["academic_year"] = academic_year
|
||||
|
||||
fs = frappe.db.get_list(
|
||||
"Fee Schedule",
|
||||
filters=filters,
|
||||
fields=[
|
||||
"academic_term",
|
||||
"fee_structure",
|
||||
"student_category",
|
||||
"due_date",
|
||||
"total_amount as amount",
|
||||
],
|
||||
order_by="idx",
|
||||
)
|
||||
return fs
|
||||
|
||||
@@ -60,12 +60,15 @@ frappe.ui.form.on('Program Enrollment', {
|
||||
method: 'erpnext.education.api.get_fee_schedule',
|
||||
args: {
|
||||
'program': frm.doc.program,
|
||||
'student_category': frm.doc.student_category
|
||||
'student_category': frm.doc.student_category,
|
||||
'academic_year': frm.doc.academic_year
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
cur_frm.clear_table("fees");
|
||||
frm.refresh_fields('fees');
|
||||
frm.set_value('fees' ,r.message);
|
||||
frm.events.get_courses(frm);
|
||||
frm.refresh_fields('fees');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -76,6 +79,10 @@ frappe.ui.form.on('Program Enrollment', {
|
||||
frappe.ui.form.trigger('Program Enrollment', 'program');
|
||||
},
|
||||
|
||||
academic_year: function() {
|
||||
frappe.ui.form.trigger('Program Enrollment', 'program');
|
||||
},
|
||||
|
||||
get_courses: function(frm) {
|
||||
frm.set_value('courses',[]);
|
||||
frappe.call({
|
||||
|
||||
@@ -105,6 +105,8 @@ class ProgramEnrollment(Document):
|
||||
"academic_term": d.academic_term,
|
||||
"fee_structure": d.fee_structure,
|
||||
"program": self.program,
|
||||
"student_batch": self.student_batch_name,
|
||||
"student_category": self.student_category,
|
||||
"due_date": d.due_date,
|
||||
"student_name": self.student_name,
|
||||
"program_enrollment": self.name,
|
||||
|
||||
@@ -589,6 +589,7 @@ accounting_dimension_doctypes = [
|
||||
"Shipping Rule",
|
||||
"Landed Cost Item",
|
||||
"Asset Value Adjustment",
|
||||
"Asset Repair",
|
||||
"Loyalty Program",
|
||||
"Fee Schedule",
|
||||
"Fee Structure",
|
||||
|
||||
@@ -10,7 +10,7 @@ from frappe.permissions import (
|
||||
remove_user_permission,
|
||||
set_user_permission_if_allowed,
|
||||
)
|
||||
from frappe.utils import add_years, cstr, getdate, today, validate_email_address
|
||||
from frappe.utils import add_days, add_years, cstr, getdate, today, validate_email_address
|
||||
from frappe.utils.nestedset import NestedSet
|
||||
|
||||
from erpnext.utilities.transaction_base import delete_events
|
||||
@@ -64,6 +64,8 @@ class Employee(NestedSet):
|
||||
if existing_user_id:
|
||||
remove_user_permission("Employee", self.name, existing_user_id)
|
||||
|
||||
self.update_to_date_in_work_history()
|
||||
|
||||
def after_rename(self, old, new, merge):
|
||||
self.db_set("employee", new)
|
||||
|
||||
@@ -166,6 +168,18 @@ class Employee(NestedSet):
|
||||
user.flags.ignore_permissions = True
|
||||
user.add_roles("Expense Approver")
|
||||
|
||||
def update_to_date_in_work_history(self):
|
||||
if not self.internal_work_history:
|
||||
return
|
||||
|
||||
for idx, row in enumerate(self.internal_work_history):
|
||||
if not row.from_date or idx == 0:
|
||||
continue
|
||||
|
||||
self.internal_work_history[idx - 1].to_date = add_days(row.from_date, -1)
|
||||
|
||||
self.internal_work_history[-1].to_date = None
|
||||
|
||||
def validate_date(self):
|
||||
if self.date_of_birth and getdate(self.date_of_birth) > getdate(today()):
|
||||
throw(_("Date of Birth cannot be greater than today."))
|
||||
|
||||
@@ -80,12 +80,14 @@ class TestEmployeeTransfer(unittest.TestCase):
|
||||
department = ["Accounts - TC", "Management - TC"]
|
||||
designation = ["Accountant", "Manager"]
|
||||
dt = [getdate("01-10-2021"), getdate()]
|
||||
to_date = [add_days(dt[1], -1), None]
|
||||
|
||||
employee = frappe.get_doc("Employee", employee)
|
||||
for data in employee.internal_work_history:
|
||||
self.assertEqual(data.department, department[count])
|
||||
self.assertEqual(data.designation, designation[count])
|
||||
self.assertEqual(data.from_date, dt[count])
|
||||
self.assertEqual(data.to_date, to_date[count])
|
||||
count = count + 1
|
||||
|
||||
transfer.cancel()
|
||||
@@ -95,6 +97,7 @@ class TestEmployeeTransfer(unittest.TestCase):
|
||||
self.assertEqual(data.designation, designation[0])
|
||||
self.assertEqual(data.department, department[0])
|
||||
self.assertEqual(data.from_date, dt[0])
|
||||
self.assertEqual(data.to_date, None)
|
||||
|
||||
|
||||
def create_company():
|
||||
|
||||
@@ -254,9 +254,11 @@ frappe.ui.form.on("Expense Claim", {
|
||||
}, __("View"));
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus===1 && !cint(frm.doc.is_paid) && cint(frm.doc.grand_total) > 0
|
||||
&& (cint(frm.doc.total_amount_reimbursed) < cint(frm.doc.total_sanctioned_amount))
|
||||
&& frappe.model.can_create("Payment Entry")) {
|
||||
if (
|
||||
frm.doc.docstatus === 1
|
||||
&& frm.doc.status !== "Paid"
|
||||
&& frappe.model.can_create("Payment Entry")
|
||||
) {
|
||||
frm.add_custom_button(__('Payment'),
|
||||
function() { frm.events.make_payment_entry(frm); }, __('Create'));
|
||||
}
|
||||
|
||||
@@ -305,12 +305,12 @@ class ExpenseClaim(AccountsController):
|
||||
|
||||
if self.total_advance_amount:
|
||||
precision = self.precision("total_advance_amount")
|
||||
if flt(self.total_advance_amount, precision) > flt(self.total_claimed_amount, precision):
|
||||
frappe.throw(_("Total advance amount cannot be greater than total claimed amount"))
|
||||
amount_with_taxes = flt(
|
||||
(flt(self.total_sanctioned_amount, precision) + flt(self.total_taxes_and_charges, precision)),
|
||||
precision,
|
||||
)
|
||||
|
||||
if self.total_sanctioned_amount and flt(self.total_advance_amount, precision) > flt(
|
||||
self.total_sanctioned_amount, precision
|
||||
):
|
||||
if flt(self.total_advance_amount, precision) > amount_with_taxes:
|
||||
frappe.throw(_("Total advance amount cannot be greater than total sanctioned amount"))
|
||||
|
||||
def validate_sanctioned_amount(self):
|
||||
|
||||
@@ -114,6 +114,40 @@ class TestExpenseClaim(FrappeTestCase):
|
||||
self.assertEqual(claim.grand_total, 0)
|
||||
self.assertEqual(claim.status, "Paid")
|
||||
|
||||
def test_advance_amount_allocation_against_claim_with_taxes(self):
|
||||
from erpnext.hr.doctype.employee_advance.test_employee_advance import (
|
||||
get_advances_for_claim,
|
||||
make_employee_advance,
|
||||
make_payment_entry,
|
||||
)
|
||||
|
||||
frappe.db.delete("Employee Advance")
|
||||
|
||||
payable_account = get_payable_account("_Test Company")
|
||||
taxes = generate_taxes("_Test Company")
|
||||
claim = make_expense_claim(
|
||||
payable_account,
|
||||
700,
|
||||
700,
|
||||
"_Test Company",
|
||||
"Travel Expenses - _TC",
|
||||
do_not_submit=True,
|
||||
taxes=taxes,
|
||||
)
|
||||
claim.save()
|
||||
|
||||
advance = make_employee_advance(claim.employee)
|
||||
pe = make_payment_entry(advance)
|
||||
pe.submit()
|
||||
|
||||
# claim for already paid out advances
|
||||
claim = get_advances_for_claim(claim, advance.name, 763)
|
||||
claim.save()
|
||||
claim.submit()
|
||||
|
||||
self.assertEqual(claim.grand_total, 0)
|
||||
self.assertEqual(claim.status, "Paid")
|
||||
|
||||
def test_expense_claim_partially_paid_via_advance(self):
|
||||
from erpnext.hr.doctype.employee_advance.test_employee_advance import (
|
||||
get_advances_for_claim,
|
||||
@@ -300,12 +334,13 @@ def get_payable_account(company):
|
||||
return frappe.get_cached_value("Company", company, "default_payable_account")
|
||||
|
||||
|
||||
def generate_taxes():
|
||||
def generate_taxes(company=None):
|
||||
company = company or company_name
|
||||
parent_account = frappe.db.get_value(
|
||||
"Account", {"company": company_name, "is_group": 1, "account_type": "Tax"}, "name"
|
||||
"Account", filters={"account_name": "Duties and Taxes", "company": company}
|
||||
)
|
||||
account = create_account(
|
||||
company=company_name,
|
||||
company=company,
|
||||
account_name="Output Tax CGST",
|
||||
account_type="Tax",
|
||||
parent_account=parent_account,
|
||||
|
||||
@@ -224,6 +224,7 @@ def delete_employee_work_history(details, employee, date):
|
||||
filters["from_date"] = date
|
||||
if filters:
|
||||
frappe.db.delete("Employee Internal Work History", filters)
|
||||
employee.reload()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -99,7 +99,7 @@ class LoanBalanceAdjustment(AccountsController):
|
||||
loan_account = frappe.db.get_value("Loan", self.loan, "loan_account")
|
||||
remarks = "{} against loan {}".format(self.adjustment_type.capitalize(), self.loan)
|
||||
if self.reference_number:
|
||||
remarks += "with reference no. {}".format(self.reference_number)
|
||||
remarks += " with reference no. {}".format(self.reference_number)
|
||||
|
||||
loan_entry = {
|
||||
"account": loan_account,
|
||||
|
||||
@@ -163,11 +163,11 @@
|
||||
},
|
||||
{
|
||||
"fetch_from": "against_loan.disbursement_account",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "disbursement_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Disbursement Account",
|
||||
"options": "Account",
|
||||
"read_only": 1
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_16",
|
||||
@@ -185,7 +185,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-17 18:23:44.157598",
|
||||
"modified": "2022-08-04 17:16:04.922444",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Disbursement",
|
||||
|
||||
@@ -209,6 +209,9 @@ def get_disbursal_amount(loan, on_current_security_price=0):
|
||||
"loan_amount",
|
||||
"disbursed_amount",
|
||||
"total_payment",
|
||||
"debit_adjustment_amount",
|
||||
"credit_adjustment_amount",
|
||||
"refund_amount",
|
||||
"total_principal_paid",
|
||||
"total_interest_payable",
|
||||
"status",
|
||||
|
||||
@@ -147,6 +147,9 @@ def make_accrual_interest_entry_for_demand_loans(
|
||||
"name",
|
||||
"total_payment",
|
||||
"total_amount_paid",
|
||||
"debit_adjustment_amount",
|
||||
"credit_adjustment_amount",
|
||||
"refund_amount",
|
||||
"loan_account",
|
||||
"interest_income_account",
|
||||
"loan_amount",
|
||||
|
||||
@@ -281,11 +281,11 @@
|
||||
},
|
||||
{
|
||||
"fetch_from": "against_loan.payment_account",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "payment_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Repayment Account",
|
||||
"options": "Account",
|
||||
"read_only": 1
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_36",
|
||||
@@ -311,7 +311,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-18 19:10:07.742298",
|
||||
"modified": "2022-08-04 17:13:51.964203",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Repayment",
|
||||
@@ -353,4 +353,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +150,9 @@ class LoanRepayment(AccountsController):
|
||||
"status",
|
||||
"is_secured_loan",
|
||||
"total_payment",
|
||||
"debit_adjustment_amount",
|
||||
"credit_adjustment_amount",
|
||||
"refund_amount",
|
||||
"loan_amount",
|
||||
"disbursed_amount",
|
||||
"total_interest_payable",
|
||||
@@ -399,7 +402,7 @@ class LoanRepayment(AccountsController):
|
||||
remarks = "Repayment against loan " + self.against_loan
|
||||
|
||||
if self.reference_number:
|
||||
remarks += "with reference no. {}".format(self.reference_number)
|
||||
remarks += " with reference no. {}".format(self.reference_number)
|
||||
|
||||
if self.repay_from_salary:
|
||||
payment_account = self.payroll_payable_account
|
||||
|
||||
@@ -58,6 +58,9 @@ class LoanSecurityUnpledge(Document):
|
||||
self.loan,
|
||||
[
|
||||
"total_payment",
|
||||
"debit_adjustment_amount",
|
||||
"credit_adjustment_amount",
|
||||
"refund_amount",
|
||||
"total_principal_paid",
|
||||
"loan_amount",
|
||||
"total_interest_payable",
|
||||
|
||||
@@ -9,6 +9,9 @@ from frappe.utils import cint, flt, getdate
|
||||
import erpnext
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import (
|
||||
get_pending_principal_amount,
|
||||
)
|
||||
|
||||
|
||||
class LoanWriteOff(AccountsController):
|
||||
@@ -22,16 +25,26 @@ class LoanWriteOff(AccountsController):
|
||||
|
||||
def validate_write_off_amount(self):
|
||||
precision = cint(frappe.db.get_default("currency_precision")) or 2
|
||||
total_payment, principal_paid, interest_payable, written_off_amount = frappe.get_value(
|
||||
|
||||
loan_details = frappe.get_value(
|
||||
"Loan",
|
||||
self.loan,
|
||||
["total_payment", "total_principal_paid", "total_interest_payable", "written_off_amount"],
|
||||
[
|
||||
"total_payment",
|
||||
"debit_adjustment_amount",
|
||||
"credit_adjustment_amount",
|
||||
"refund_amount",
|
||||
"total_principal_paid",
|
||||
"loan_amount",
|
||||
"total_interest_payable",
|
||||
"written_off_amount",
|
||||
"disbursed_amount",
|
||||
"status",
|
||||
],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
pending_principal_amount = flt(
|
||||
flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount),
|
||||
precision,
|
||||
)
|
||||
pending_principal_amount = flt(get_pending_principal_amount(loan_details), precision)
|
||||
|
||||
if self.write_off_amount > pending_principal_amount:
|
||||
frappe.throw(_("Write off amount cannot be greater than pending principal amount"))
|
||||
|
||||
@@ -21,13 +21,18 @@ frappe.ui.form.on('Member', {
|
||||
|
||||
// custom buttons
|
||||
frm.add_custom_button(__('Accounting Ledger'), function() {
|
||||
frappe.set_route('query-report', 'General Ledger',
|
||||
{party_type:'Member', party:frm.doc.name});
|
||||
if (frm.doc.customer) {
|
||||
frappe.set_route('query-report', 'General Ledger', {party_type: 'Customer', party: frm.doc.customer});
|
||||
} else {
|
||||
frappe.set_route('query-report', 'General Ledger', {party_type: 'Member', party: frm.doc.name});
|
||||
}
|
||||
});
|
||||
|
||||
frm.add_custom_button(__('Accounts Receivable'), function() {
|
||||
frappe.set_route('query-report', 'Accounts Receivable', {member:frm.doc.name});
|
||||
});
|
||||
if (frm.doc.customer) {
|
||||
frm.add_custom_button(__('Accounts Receivable'), function() {
|
||||
frappe.set_route('query-report', 'Accounts Receivable', {customer: frm.doc.customer});
|
||||
});
|
||||
}
|
||||
|
||||
if (!frm.doc.customer) {
|
||||
frm.add_custom_button(__('Create Customer'), () => {
|
||||
|
||||
@@ -371,3 +371,5 @@ erpnext.patches.v13_0.add_cost_center_in_loans
|
||||
erpnext.patches.v13_0.show_india_localisation_deprecation_warning
|
||||
erpnext.patches.v13_0.fix_number_and_frequency_for_monthly_depreciation
|
||||
erpnext.patches.v13_0.reset_corrupt_defaults
|
||||
erpnext.patches.v13_0.show_hr_payroll_deprecation_warning
|
||||
erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
|
||||
|
||||
def execute():
|
||||
accounting_dimensions = frappe.db.get_all(
|
||||
"Accounting Dimension", fields=["fieldname", "label", "document_type", "disabled"]
|
||||
)
|
||||
|
||||
if not accounting_dimensions:
|
||||
return
|
||||
|
||||
for d in accounting_dimensions:
|
||||
doctype = "Asset Repair"
|
||||
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname})
|
||||
|
||||
if field:
|
||||
continue
|
||||
|
||||
df = {
|
||||
"fieldname": d.fieldname,
|
||||
"label": d.label,
|
||||
"fieldtype": "Link",
|
||||
"options": d.document_type,
|
||||
"insert_after": "accounting_dimensions_section",
|
||||
}
|
||||
|
||||
create_custom_field(doctype, df, ignore_validate=True)
|
||||
frappe.clear_cache(doctype=doctype)
|
||||
@@ -17,7 +17,6 @@ def execute():
|
||||
"item_group",
|
||||
"stock_uom",
|
||||
"brand",
|
||||
"image",
|
||||
"has_variants",
|
||||
"variant_of",
|
||||
"description",
|
||||
@@ -30,6 +29,7 @@ def execute():
|
||||
"website_warehouse",
|
||||
"web_long_description",
|
||||
"website_content",
|
||||
"website_image",
|
||||
"thumbnail",
|
||||
]
|
||||
|
||||
|
||||
16
erpnext/patches/v13_0/show_hr_payroll_deprecation_warning.py
Normal file
16
erpnext/patches/v13_0/show_hr_payroll_deprecation_warning.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import click
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
if "hrms" in frappe.get_installed_apps():
|
||||
return
|
||||
|
||||
click.secho(
|
||||
"HR and Payroll modules have been moved to a separate app"
|
||||
" and will be removed from ERPNext in Version 14."
|
||||
" Please install the HRMS app when upgrading to Version 14"
|
||||
" to continue using the HR and Payroll modules:\n"
|
||||
"https://github.com/frappe/hrms",
|
||||
fg="yellow",
|
||||
)
|
||||
@@ -622,11 +622,23 @@ class SalarySlip(TransactionBase):
|
||||
self.add_tax_components(payroll_period)
|
||||
|
||||
def add_structure_components(self, component_type):
|
||||
data = self.get_data_for_eval()
|
||||
data, default_data = self.get_data_for_eval()
|
||||
timesheet_component = frappe.db.get_value(
|
||||
"Salary Structure", self.salary_structure, "salary_component"
|
||||
)
|
||||
|
||||
for struct_row in self._salary_structure_doc.get(component_type):
|
||||
if self.salary_slip_based_on_timesheet and struct_row.salary_component == timesheet_component:
|
||||
continue
|
||||
|
||||
amount = self.eval_condition_and_formula(struct_row, data)
|
||||
if amount is not None and struct_row.statistical_component == 0:
|
||||
self.update_component_row(struct_row, amount, component_type, data=data)
|
||||
if (
|
||||
amount or (struct_row.amount_based_on_formula and amount is not None)
|
||||
) and struct_row.statistical_component == 0:
|
||||
default_amount = self.eval_condition_and_formula(struct_row, default_data)
|
||||
self.update_component_row(
|
||||
struct_row, amount, component_type, data=data, default_amount=default_amount
|
||||
)
|
||||
|
||||
def get_data_for_eval(self):
|
||||
"""Returns data for evaluating formula"""
|
||||
@@ -670,11 +682,15 @@ class SalarySlip(TransactionBase):
|
||||
for sc in salary_components:
|
||||
data.setdefault(sc.salary_component_abbr, 0)
|
||||
|
||||
# shallow copy of data to store default amounts (without payment days) for tax calculation
|
||||
default_data = data.copy()
|
||||
|
||||
for key in ("earnings", "deductions"):
|
||||
for d in self.get(key):
|
||||
default_data[d.abbr] = d.default_amount
|
||||
data[d.abbr] = d.amount
|
||||
|
||||
return data
|
||||
return data, default_data
|
||||
|
||||
def eval_condition_and_formula(self, d, data):
|
||||
try:
|
||||
@@ -780,7 +796,14 @@ class SalarySlip(TransactionBase):
|
||||
self.update_component_row(tax_row, tax_amount, "deductions")
|
||||
|
||||
def update_component_row(
|
||||
self, component_data, amount, component_type, additional_salary=None, is_recurring=0, data=None
|
||||
self,
|
||||
component_data,
|
||||
amount,
|
||||
component_type,
|
||||
additional_salary=None,
|
||||
is_recurring=0,
|
||||
data=None,
|
||||
default_amount=None,
|
||||
):
|
||||
component_row = None
|
||||
for d in self.get(component_type):
|
||||
@@ -841,7 +864,7 @@ class SalarySlip(TransactionBase):
|
||||
additional_salary.deduct_full_tax_on_selected_payroll_date
|
||||
)
|
||||
else:
|
||||
component_row.default_amount = amount
|
||||
component_row.default_amount = default_amount or amount
|
||||
component_row.additional_amount = 0
|
||||
component_row.deduct_full_tax_on_selected_payroll_date = (
|
||||
component_data.deduct_full_tax_on_selected_payroll_date
|
||||
@@ -1274,7 +1297,7 @@ class SalarySlip(TransactionBase):
|
||||
)[0].total_amount
|
||||
|
||||
def calculate_tax_by_tax_slab(self, annual_taxable_earning, tax_slab):
|
||||
data = self.get_data_for_eval()
|
||||
data, default_data = self.get_data_for_eval()
|
||||
data.update({"annual_taxable_earning": annual_taxable_earning})
|
||||
tax_amount = 0
|
||||
for slab in tax_slab.slabs:
|
||||
@@ -1352,23 +1375,22 @@ class SalarySlip(TransactionBase):
|
||||
self.total_interest_amount = 0
|
||||
self.total_principal_amount = 0
|
||||
|
||||
if not self.get("loans"):
|
||||
for loan in self.get_loan_details():
|
||||
self.set("loans", [])
|
||||
for loan in self.get_loan_details():
|
||||
amounts = calculate_amounts(loan.name, self.posting_date, "Regular Payment")
|
||||
|
||||
amounts = calculate_amounts(loan.name, self.posting_date, "Regular Payment")
|
||||
|
||||
if amounts["interest_amount"] or amounts["payable_principal_amount"]:
|
||||
self.append(
|
||||
"loans",
|
||||
{
|
||||
"loan": loan.name,
|
||||
"total_payment": amounts["interest_amount"] + amounts["payable_principal_amount"],
|
||||
"interest_amount": amounts["interest_amount"],
|
||||
"principal_amount": amounts["payable_principal_amount"],
|
||||
"loan_account": loan.loan_account,
|
||||
"interest_income_account": loan.interest_income_account,
|
||||
},
|
||||
)
|
||||
if amounts["interest_amount"] or amounts["payable_principal_amount"]:
|
||||
self.append(
|
||||
"loans",
|
||||
{
|
||||
"loan": loan.name,
|
||||
"total_payment": amounts["interest_amount"] + amounts["payable_principal_amount"],
|
||||
"interest_amount": amounts["interest_amount"],
|
||||
"principal_amount": amounts["payable_principal_amount"],
|
||||
"loan_account": loan.loan_account,
|
||||
"interest_income_account": loan.interest_income_account,
|
||||
},
|
||||
)
|
||||
|
||||
for payment in self.get("loans"):
|
||||
amounts = calculate_amounts(payment.loan, self.posting_date, "Regular Payment")
|
||||
|
||||
@@ -7,7 +7,7 @@ import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.tests.utils import change_settings
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
@@ -35,13 +35,12 @@ from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_month_detail
|
||||
from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
|
||||
|
||||
|
||||
class TestSalarySlip(unittest.TestCase):
|
||||
class TestSalarySlip(FrappeTestCase):
|
||||
def setUp(self):
|
||||
setup_test()
|
||||
frappe.flags.pop("via_payroll_entry", None)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0)
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@@ -372,13 +371,19 @@ class TestSalarySlip(unittest.TestCase):
|
||||
|
||||
self.assertEqual(salary_slip.payment_days, days_in_month - no_of_holidays - 1)
|
||||
|
||||
# gross pay calculation based on attendance (payment days)
|
||||
gross_pay = 78100 - (
|
||||
(78000 / (days_in_month - no_of_holidays))
|
||||
* flt(salary_slip.leave_without_pay + salary_slip.absent_days)
|
||||
# component calculation based on attendance (payment days)
|
||||
amount, precision = None, None
|
||||
|
||||
for row in salary_slip.earnings:
|
||||
if row.salary_component == "Basic Salary":
|
||||
amount = row.amount
|
||||
precision = row.precision("amount")
|
||||
break
|
||||
expected_amount = flt(
|
||||
(50000 * salary_slip.payment_days / salary_slip.total_working_days), precision
|
||||
)
|
||||
|
||||
self.assertEqual(salary_slip.gross_pay, flt(gross_pay, 2))
|
||||
self.assertEqual(amount, expected_amount)
|
||||
|
||||
@change_settings("Payroll Settings", {"payroll_based_on": "Attendance"})
|
||||
def test_component_amount_dependent_on_another_payment_days_based_component(self):
|
||||
@@ -919,6 +924,41 @@ class TestSalarySlip(unittest.TestCase):
|
||||
# undelete fixture data
|
||||
frappe.db.rollback()
|
||||
|
||||
@change_settings(
|
||||
"Payroll Settings",
|
||||
{
|
||||
"payroll_based_on": "Attendance",
|
||||
"consider_unmarked_attendance_as": "Present",
|
||||
"include_holidays_in_total_working_days": True,
|
||||
},
|
||||
)
|
||||
def test_default_amount(self):
|
||||
# Special Allowance (SA) uses another component Basic (BS) in it's formula : BD * .5
|
||||
# Basic has "Depends on Payment Days" enabled
|
||||
# Test default amount for SA is based on default amount for BS (irrespective of PD)
|
||||
# Test amount for SA is based on amount for BS (based on PD)
|
||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||
|
||||
month_start_date = get_first_day(nowdate())
|
||||
joining_date = add_days(month_start_date, 3)
|
||||
employee = make_employee("test_tax_for_mid_joinee@salary.com", date_of_joining=joining_date)
|
||||
|
||||
salary_structure = make_salary_structure(
|
||||
"Stucture to test tax",
|
||||
"Monthly",
|
||||
test_tax=True,
|
||||
from_date=joining_date,
|
||||
employee=employee,
|
||||
)
|
||||
|
||||
ss = make_salary_slip(salary_structure.name, employee=employee)
|
||||
|
||||
# default amount for SA (special allowance = BS*0.5) should be based on default amount for basic
|
||||
self.assertEqual(ss.earnings[2].default_amount, 25000)
|
||||
self.assertEqual(
|
||||
ss.earnings[2].amount, flt(ss.earnings[0].amount * 0.5, ss.earnings[0].precision("amount"))
|
||||
)
|
||||
|
||||
def test_tax_for_recurring_additional_salary(self):
|
||||
frappe.db.sql("""delete from `tabPayroll Period`""")
|
||||
frappe.db.sql("""delete from `tabSalary Component`""")
|
||||
@@ -981,6 +1021,16 @@ class TestSalarySlip(unittest.TestCase):
|
||||
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_do_not_show_statistical_component_in_slip(self):
|
||||
make_employee("test_statistical_component@salary.com")
|
||||
new_ss = make_employee_salary_slip(
|
||||
"test_statistical_component@salary.com",
|
||||
"Monthly",
|
||||
"Test Payment Based On Attendence",
|
||||
)
|
||||
components = [row.salary_component for row in new_ss.get("earnings")]
|
||||
self.assertNotIn("Statistical Component", components)
|
||||
|
||||
def make_activity_for_employee(self):
|
||||
activity_type = frappe.get_doc("Activity Type", "_Test Activity Type")
|
||||
activity_type.billing_rate = 50
|
||||
@@ -1038,7 +1088,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None, po
|
||||
def make_salary_component(salary_components, test_tax, company_list=None):
|
||||
for salary_component in salary_components:
|
||||
if frappe.db.exists("Salary Component", salary_component["salary_component"]):
|
||||
continue
|
||||
frappe.delete_doc("Salary Component", salary_component["salary_component"], force=True)
|
||||
|
||||
if test_tax:
|
||||
if salary_component["type"] == "Earning":
|
||||
@@ -1122,6 +1172,13 @@ def make_earning_salary_component(
|
||||
"depends_on_payment_days": 0,
|
||||
},
|
||||
{"salary_component": "Leave Encashment", "abbr": "LE", "type": "Earning"},
|
||||
{
|
||||
"salary_component": "Statistical Component",
|
||||
"abbr": "SC",
|
||||
"type": "Earning",
|
||||
"statistical_component": 1,
|
||||
"amount": 500,
|
||||
},
|
||||
]
|
||||
if include_flexi_benefits:
|
||||
data.extend(
|
||||
@@ -1419,6 +1476,10 @@ def setup_test():
|
||||
"Salary Slip",
|
||||
"Attendance",
|
||||
"Additional Salary",
|
||||
"Employee Tax Exemption Declaration",
|
||||
"Employee Tax Exemption Proof Submission",
|
||||
"Employee Benefit Claim",
|
||||
"Salary Structure Assignment",
|
||||
]:
|
||||
frappe.db.sql("delete from `tab%s`" % dt)
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ class SalaryStructure(Document):
|
||||
self.validate_max_benefits_with_flexi()
|
||||
self.validate_component_based_on_tax_slab()
|
||||
self.validate_payment_days_based_dependent_component()
|
||||
self.validate_timesheet_component()
|
||||
|
||||
def set_missing_values(self):
|
||||
overwritten_fields = [
|
||||
@@ -89,6 +90,21 @@ class SalaryStructure(Document):
|
||||
|
||||
return abbr
|
||||
|
||||
def validate_timesheet_component(self):
|
||||
if not self.salary_slip_based_on_timesheet:
|
||||
return
|
||||
|
||||
for component in self.earnings:
|
||||
if component.salary_component == self.salary_component:
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Row #{0}: Timesheet amount will overwrite the Earning component amount for the Salary Component {1}"
|
||||
).format(self.idx, frappe.bold(self.salary_component)),
|
||||
title=_("Warning"),
|
||||
indicator="orange",
|
||||
)
|
||||
break
|
||||
|
||||
def strip_condition_and_formula_fields(self):
|
||||
# remove whitespaces from condition and formula fields
|
||||
for row in self.earnings:
|
||||
|
||||
@@ -150,6 +150,7 @@ def make_salary_structure(
|
||||
currency=erpnext.get_default_currency(),
|
||||
payroll_period=None,
|
||||
include_flexi_benefits=False,
|
||||
base=None,
|
||||
):
|
||||
if test_tax:
|
||||
frappe.db.sql("""delete from `tabSalary Structure` where name=%s""", (salary_structure))
|
||||
@@ -200,6 +201,7 @@ def make_salary_structure(
|
||||
company=company,
|
||||
currency=currency,
|
||||
payroll_period=payroll_period,
|
||||
base=base,
|
||||
)
|
||||
|
||||
return salary_structure_doc
|
||||
|
||||
@@ -16,7 +16,7 @@ class Homepage(Document):
|
||||
def setup_items(self):
|
||||
for d in frappe.get_all(
|
||||
"Website Item",
|
||||
fields=["name", "item_name", "description", "image", "route"],
|
||||
fields=["name", "item_name", "description", "website_image", "route"],
|
||||
filters={"published": 1},
|
||||
limit=3,
|
||||
):
|
||||
@@ -31,7 +31,7 @@ class Homepage(Document):
|
||||
item_code=d.name,
|
||||
item_name=d.item_name,
|
||||
description=d.description,
|
||||
image=d.image,
|
||||
image=d.website_image,
|
||||
route=d.route,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -2122,7 +2122,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
"qty": item.qty,
|
||||
"description": item.description,
|
||||
"serial_no": item.serial_no,
|
||||
"batch_no": item.batch_no
|
||||
"batch_no": item.batch_no,
|
||||
"sample_size": item.sample_quantity
|
||||
});
|
||||
dialog_items.grid.refresh();
|
||||
}
|
||||
|
||||
@@ -265,6 +265,10 @@ def get_overseas_address_details(address_name):
|
||||
def get_item_list(invoice):
|
||||
item_list = []
|
||||
|
||||
hide_discount_in_einvoice = cint(
|
||||
frappe.db.get_single_value("E Invoice Settings", "dont_show_discounts_in_e_invoice")
|
||||
)
|
||||
|
||||
for d in invoice.items:
|
||||
einvoice_item_schema = read_json("einv_item_template")
|
||||
item = frappe._dict({})
|
||||
@@ -276,17 +280,12 @@ def get_item_list(invoice):
|
||||
item.qty = abs(item.qty)
|
||||
item_qty = item.qty
|
||||
|
||||
item.discount_amount = abs(item.discount_amount)
|
||||
item.taxable_value = abs(item.taxable_value)
|
||||
|
||||
if invoice.get("is_return") or invoice.get("is_debit_note"):
|
||||
item_qty = item_qty or 1
|
||||
|
||||
hide_discount_in_einvoice = cint(
|
||||
frappe.db.get_single_value("E Invoice Settings", "dont_show_discounts_in_e_invoice")
|
||||
)
|
||||
|
||||
if hide_discount_in_einvoice:
|
||||
if hide_discount_in_einvoice or invoice.is_internal_customer or item.discount_amount < 0:
|
||||
item.unit_rate = item.taxable_value / item_qty
|
||||
item.gross_amount = item.taxable_value
|
||||
item.discount_amount = 0
|
||||
|
||||
@@ -580,7 +580,7 @@ def get_ewb_data(dt, dn):
|
||||
|
||||
if dt == "Delivery Note":
|
||||
data.subSupplyType = 1
|
||||
elif doc.gst_category in ["Registered Regular", "SEZ"]:
|
||||
elif doc.gst_category in ["Unregistered", "Registered Regular", "SEZ"]:
|
||||
data.subSupplyType = 1
|
||||
elif doc.gst_category in ["Overseas", "Deemed Export"]:
|
||||
data.subSupplyType = 3
|
||||
|
||||
@@ -23,24 +23,31 @@ def _execute(filters=None):
|
||||
if not filters:
|
||||
filters = {}
|
||||
columns = get_columns()
|
||||
output_gst_accounts = get_output_gst_accounts(filters.company)
|
||||
|
||||
company_currency = erpnext.get_company_currency(filters.company)
|
||||
item_list = get_items(filters)
|
||||
if item_list:
|
||||
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
|
||||
itemised_tax, tax_columns = get_tax_accounts(
|
||||
item_list, columns, company_currency, output_gst_accounts
|
||||
)
|
||||
|
||||
data = []
|
||||
added_item = []
|
||||
for d in item_list:
|
||||
if (d.parent, d.item_code) not in added_item:
|
||||
row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty, d.tax_rate]
|
||||
row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty]
|
||||
total_tax = 0
|
||||
tax_rate = 0
|
||||
for tax in tax_columns:
|
||||
item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {})
|
||||
if item_tax.get("is_gst_tax"):
|
||||
tax_rate += flt(item_tax.get("tax_rate", 0))
|
||||
total_tax += flt(item_tax.get("tax_amount", 0))
|
||||
|
||||
row += [d.base_net_amount + total_tax]
|
||||
row += [d.base_net_amount]
|
||||
row += [tax_rate]
|
||||
row += [d.taxable_value + total_tax]
|
||||
row += [d.taxable_value]
|
||||
for tax in tax_columns:
|
||||
item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {})
|
||||
row += [item_tax.get("tax_amount", 0)]
|
||||
@@ -51,6 +58,40 @@ def _execute(filters=None):
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_output_gst_accounts(company):
|
||||
|
||||
accounts = frappe.qb.DocType("Account")
|
||||
gst_accounts = frappe.qb.DocType("GST Account")
|
||||
|
||||
accounts_query = (
|
||||
frappe.qb.from_(accounts)
|
||||
.select(accounts.name)
|
||||
.where((accounts.account_type == "Tax") & (accounts.root_type == "Liability"))
|
||||
)
|
||||
|
||||
gst_accounts_query = (
|
||||
frappe.qb.from_(gst_accounts)
|
||||
.select(
|
||||
gst_accounts.cgst_account,
|
||||
gst_accounts.sgst_account,
|
||||
gst_accounts.igst_account,
|
||||
gst_accounts.utgst_account,
|
||||
gst_accounts.cess_account,
|
||||
)
|
||||
.where((gst_accounts.is_reverse_charge_account == 0) & (gst_accounts.company == company))
|
||||
)
|
||||
|
||||
gst_accounts_list = [
|
||||
account for sublist in gst_accounts_query.run() for account in sublist if account
|
||||
]
|
||||
|
||||
tax_accounts_list = [account[0] for account in accounts_query.run() if account]
|
||||
|
||||
output_tax_list = [account for account in gst_accounts_list if account in tax_accounts_list]
|
||||
|
||||
return output_tax_list
|
||||
|
||||
|
||||
def get_columns():
|
||||
columns = [
|
||||
{
|
||||
@@ -99,34 +140,35 @@ def get_items(filters):
|
||||
match_conditions = " and {0} ".format(match_conditions)
|
||||
|
||||
items = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
f"""
|
||||
SELECT
|
||||
`tabSales Invoice Item`.gst_hsn_code,
|
||||
`tabSales Invoice Item`.stock_uom,
|
||||
sum(`tabSales Invoice Item`.stock_qty) as stock_qty,
|
||||
sum(`tabSales Invoice Item`.base_net_amount) as base_net_amount,
|
||||
sum(`tabSales Invoice Item`.base_price_list_rate) as base_price_list_rate,
|
||||
sum(`tabSales Invoice Item`.stock_qty) AS stock_qty,
|
||||
sum(`tabSales Invoice Item`.taxable_value) AS taxable_value,
|
||||
sum(`tabSales Invoice Item`.base_price_list_rate) AS base_price_list_rate,
|
||||
`tabSales Invoice Item`.parent,
|
||||
`tabSales Invoice Item`.item_code,
|
||||
`tabGST HSN Code`.description,
|
||||
json_extract(`tabSales Taxes and Charges`.item_wise_tax_detail,
|
||||
concat('$."' , `tabSales Invoice Item`.item_code, '"[0]')) * count(distinct `tabSales Taxes and Charges`.name) as tax_rate
|
||||
from
|
||||
`tabSales Invoice`,
|
||||
`tabSales Invoice Item`,
|
||||
`tabGST HSN Code`,
|
||||
`tabSales Taxes and Charges`
|
||||
where
|
||||
`tabSales Invoice`.name = `tabSales Invoice Item`.parent
|
||||
and `tabSales Taxes and Charges`.parent = `tabSales Invoice`.name
|
||||
and `tabSales Invoice`.docstatus = 1
|
||||
and `tabSales Invoice Item`.gst_hsn_code is not NULL
|
||||
and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s
|
||||
group by
|
||||
`tabGST HSN Code`.description
|
||||
FROM
|
||||
`tabSales Invoice`
|
||||
INNER JOIN `tabSales Invoice Item` ON `tabSales Invoice`.name = `tabSales Invoice Item`.parent
|
||||
INNER JOIN `tabGST HSN Code` ON `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name
|
||||
WHERE
|
||||
`tabSales Invoice`.docstatus = 1
|
||||
AND `tabSales Invoice Item`.gst_hsn_code IS NOT NULL
|
||||
{conditions}
|
||||
GROUP BY
|
||||
`tabSales Invoice Item`.parent,
|
||||
`tabSales Invoice Item`.item_code
|
||||
"""
|
||||
% (conditions, match_conditions),
|
||||
`tabSales Invoice Item`.item_code,
|
||||
`tabSales Invoice Item`.gst_hsn_code,
|
||||
`tabSales Invoice Item`.uom
|
||||
ORDER BY
|
||||
`tabSales Invoice Item`.gst_hsn_code,
|
||||
`tabSales Invoice Item`.uom
|
||||
""".format(
|
||||
conditions=conditions
|
||||
),
|
||||
filters,
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -138,6 +180,7 @@ def get_tax_accounts(
|
||||
item_list,
|
||||
columns,
|
||||
company_currency,
|
||||
output_gst_accounts,
|
||||
doctype="Sales Invoice",
|
||||
tax_doctype="Sales Taxes and Charges",
|
||||
):
|
||||
@@ -177,7 +220,7 @@ def get_tax_accounts(
|
||||
|
||||
for parent, account_head, item_wise_tax_detail, tax_amount in tax_details:
|
||||
|
||||
if account_head not in tax_columns and tax_amount:
|
||||
if account_head in output_gst_accounts and account_head not in tax_columns and tax_amount:
|
||||
# as description is text editor earlier and markup can break the column convention in reports
|
||||
tax_columns.append(account_head)
|
||||
|
||||
@@ -190,29 +233,40 @@ def get_tax_accounts(
|
||||
continue
|
||||
itemised_tax.setdefault(item_code, frappe._dict())
|
||||
if isinstance(tax_data, list):
|
||||
tax_rate = 0
|
||||
is_gst_tax = 0
|
||||
if account_head in output_gst_accounts:
|
||||
is_gst_tax = 1
|
||||
tax_rate = tax_data[0]
|
||||
tax_amount = tax_data[1]
|
||||
else:
|
||||
tax_rate = 0
|
||||
tax_amount = 0
|
||||
|
||||
for d in item_row_map.get(parent, {}).get(item_code, []):
|
||||
item_tax_amount = tax_amount
|
||||
if item_tax_amount:
|
||||
itemised_tax.setdefault((parent, item_code), {})[account_head] = frappe._dict(
|
||||
{"tax_amount": flt(item_tax_amount, tax_amount_precision)}
|
||||
{
|
||||
"tax_rate": flt(tax_rate, 2),
|
||||
"is_gst_tax": is_gst_tax,
|
||||
"tax_amount": flt(item_tax_amount, tax_amount_precision),
|
||||
}
|
||||
)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
tax_columns.sort()
|
||||
for account_head in tax_columns:
|
||||
columns.append(
|
||||
{
|
||||
"label": account_head,
|
||||
"fieldname": frappe.scrub(account_head),
|
||||
"fieldtype": "Float",
|
||||
"width": 110,
|
||||
}
|
||||
)
|
||||
if account_head in output_gst_accounts:
|
||||
columns.append(
|
||||
{
|
||||
"label": account_head,
|
||||
"fieldname": frappe.scrub(account_head),
|
||||
"fieldtype": "Float",
|
||||
"width": 110,
|
||||
}
|
||||
)
|
||||
|
||||
return itemised_tax, tax_columns
|
||||
|
||||
|
||||
@@ -1548,6 +1548,65 @@ class TestSalesOrder(FrappeTestCase):
|
||||
so.load_from_db()
|
||||
self.assertEqual(so.billing_status, "Fully Billed")
|
||||
|
||||
def test_so_billing_status_with_crnote_against_sales_return(self):
|
||||
"""
|
||||
| Step | Document creation | |
|
||||
|------+--------------------------------------+-------------------------------|
|
||||
| 1 | SO -> DN -> SI | SO Fully Billed and Completed |
|
||||
| 2 | DN -> Sales Return(Partial) | SO 50% Delivered, 100% billed |
|
||||
| 3 | Sales Return(Partial) -> Credit Note | SO 50% Delivered, 50% billed |
|
||||
|
||||
"""
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
so = make_sales_order(uom="Nos", do_not_save=1)
|
||||
so.save()
|
||||
so.submit()
|
||||
|
||||
self.assertEqual(so.billing_status, "Not Billed")
|
||||
|
||||
dn1 = make_delivery_note(so.name)
|
||||
dn1.taxes_and_charges = ""
|
||||
dn1.taxes.clear()
|
||||
dn1.save().submit()
|
||||
|
||||
si = create_sales_invoice(qty=10, do_not_save=1)
|
||||
si.items[0].sales_order = so.name
|
||||
si.items[0].so_detail = so.items[0].name
|
||||
si.items[0].delivery_note = dn1.name
|
||||
si.items[0].dn_detail = dn1.items[0].name
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
so.reload()
|
||||
self.assertEqual(so.billing_status, "Fully Billed")
|
||||
self.assertEqual(so.status, "Completed")
|
||||
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
dn1.reload()
|
||||
dn_ret = create_delivery_note(is_return=1, return_against=dn1.name, qty=-5, do_not_submit=True)
|
||||
dn_ret.items[0].against_sales_order = so.name
|
||||
dn_ret.items[0].so_detail = so.items[0].name
|
||||
dn_ret.submit()
|
||||
|
||||
so.reload()
|
||||
self.assertEqual(so.per_billed, 100)
|
||||
self.assertEqual(so.per_delivered, 50)
|
||||
|
||||
cr_note = create_sales_invoice(is_return=1, qty=-1, do_not_submit=True)
|
||||
cr_note.items[0].qty = -5
|
||||
cr_note.items[0].sales_order = so.name
|
||||
cr_note.items[0].so_detail = so.items[0].name
|
||||
cr_note.items[0].delivery_note = dn_ret.name
|
||||
cr_note.items[0].dn_detail = dn_ret.items[0].name
|
||||
cr_note.update_billed_amount_in_sales_order = True
|
||||
cr_note.submit()
|
||||
|
||||
so.reload()
|
||||
self.assertEqual(so.per_billed, 50)
|
||||
self.assertEqual(so.per_delivered, 50)
|
||||
|
||||
def test_so_back_updated_from_wo_via_mr(self):
|
||||
"SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO."
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
|
||||
@@ -497,7 +497,10 @@ erpnext.PointOfSale.Controller = class {
|
||||
|
||||
set_pos_profile_data() {
|
||||
if (this.company && !this.frm.doc.company) this.frm.doc.company = this.company;
|
||||
if (this.pos_profile && !this.frm.doc.pos_profile) this.frm.doc.pos_profile = this.pos_profile;
|
||||
if ((this.pos_profile && !this.frm.doc.pos_profile) | (this.frm.doc.is_return && this.pos_profile != this.frm.doc.pos_profile)) {
|
||||
this.frm.doc.pos_profile = this.pos_profile;
|
||||
}
|
||||
|
||||
if (!this.frm.doc.company) return;
|
||||
|
||||
return this.frm.trigger("set_pos_data");
|
||||
|
||||
@@ -161,13 +161,14 @@ erpnext.PointOfSale.Payment = class {
|
||||
|
||||
frappe.ui.form.on('POS Invoice', 'contact_mobile', (frm) => {
|
||||
const contact = frm.doc.contact_mobile;
|
||||
if (!this.request_for_payment_field) return;
|
||||
const request_button = $(this.request_for_payment_field.$input[0]);
|
||||
if (contact) {
|
||||
request_button.removeClass('btn-default').addClass('btn-primary');
|
||||
} else {
|
||||
request_button.removeClass('btn-primary').addClass('btn-default');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => {
|
||||
if (frm.doc.coupon_code && !frm.applying_pos_coupon_code) {
|
||||
|
||||
@@ -143,10 +143,6 @@ def get_item_for_list_in_html(context):
|
||||
if (context.get("website_image") or "").startswith("files/"):
|
||||
context["website_image"] = "/" + quote(context["website_image"])
|
||||
|
||||
context["show_availability_status"] = cint(
|
||||
frappe.db.get_single_value("E Commerce Settings", "show_availability_status")
|
||||
)
|
||||
|
||||
products_template = "templates/includes/products_as_list.html"
|
||||
|
||||
return frappe.get_template(products_template).render(context)
|
||||
|
||||
@@ -6,8 +6,6 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.utils.dashboard import cache_source
|
||||
|
||||
from erpnext.stock.utils import get_stock_value_from_bin
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@cache_source
|
||||
@@ -30,26 +28,24 @@ def get(
|
||||
warehouse_filters.append(["company", "=", filters.get("company")])
|
||||
|
||||
warehouses = frappe.get_list(
|
||||
"Warehouse", fields=["name"], filters=warehouse_filters, order_by="name"
|
||||
"Warehouse", pluck="name", filters=warehouse_filters, order_by="name"
|
||||
)
|
||||
|
||||
for wh in warehouses:
|
||||
balance = get_stock_value_from_bin(warehouse=wh.name)
|
||||
wh["balance"] = balance[0][0]
|
||||
|
||||
warehouses = [x for x in warehouses if not (x.get("balance") == None)]
|
||||
warehouses = frappe.get_list(
|
||||
"Bin",
|
||||
fields=["warehouse", "sum(stock_value) stock_value"],
|
||||
filters={"warehouse": ["IN", warehouses], "stock_value": [">", 0]},
|
||||
group_by="warehouse",
|
||||
order_by="stock_value DESC",
|
||||
limit_page_length=10,
|
||||
)
|
||||
|
||||
if not warehouses:
|
||||
return []
|
||||
|
||||
sorted_warehouse_map = sorted(warehouses, key=lambda i: i["balance"], reverse=True)
|
||||
|
||||
if len(sorted_warehouse_map) > 10:
|
||||
sorted_warehouse_map = sorted_warehouse_map[:10]
|
||||
|
||||
for warehouse in sorted_warehouse_map:
|
||||
labels.append(_(warehouse.get("name")))
|
||||
datapoints.append(warehouse.get("balance"))
|
||||
for warehouse in warehouses:
|
||||
labels.append(_(warehouse.get("warehouse")))
|
||||
datapoints.append(warehouse.get("stock_value"))
|
||||
|
||||
return {
|
||||
"labels": labels,
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"company",
|
||||
"purpose",
|
||||
"customer",
|
||||
"customer_name",
|
||||
"work_order",
|
||||
"material_request",
|
||||
"for_qty",
|
||||
@@ -126,11 +127,19 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Group Same Items",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.purpose==='Delivery' && doc.customer",
|
||||
"fetch_from": "customer.customer_name",
|
||||
"fieldname": "customer_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Customer Name",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-21 07:56:40.646473",
|
||||
"modified": "2022-07-19 11:03:04.442174",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Pick List",
|
||||
@@ -202,4 +211,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -635,6 +635,24 @@ class update_entries_after(object):
|
||||
voucher_detail_no=sle.voucher_detail_no,
|
||||
sle=sle,
|
||||
)
|
||||
|
||||
elif (
|
||||
sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
|
||||
and sle.actual_qty > 0
|
||||
and frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_internal_supplier")
|
||||
):
|
||||
sle_details = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{
|
||||
"voucher_type": sle.voucher_type,
|
||||
"voucher_no": sle.voucher_no,
|
||||
"dependant_sle_voucher_detail_no": sle.voucher_detail_no,
|
||||
},
|
||||
["stock_value_difference", "actual_qty"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
rate = abs(sle_details.stock_value_difference / sle.actual_qty)
|
||||
else:
|
||||
if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
|
||||
rate_field = "valuation_rate"
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
})
|
||||
</script>
|
||||
{% else %}
|
||||
{{ product_image(doc.website_image or doc.image, alt=doc.website_image_alt or doc.item_name) }}
|
||||
{{ product_image(doc.website_image, alt=doc.website_image_alt or doc.item_name) }}
|
||||
{% endif %}
|
||||
|
||||
<!-- Simple image preview -->
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
{%- set col_size = 3 if is_full_width else 4 -%}
|
||||
{%- set title = item.web_item_name or item.item_name or item.item_code -%}
|
||||
{%- set title = title[:50] + "..." if title|len > 50 else title -%}
|
||||
{%- set image = item.website_image or item.image -%}
|
||||
{%- set image = item.website_image -%}
|
||||
{%- set description = item.website_description or item.description-%}
|
||||
|
||||
{% if is_featured %}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* csslint ignore:start */
|
||||
{% if homepage.hero_image %}
|
||||
.hero-image {
|
||||
background-image: url("{{ homepage.hero_image }}");
|
||||
background-size: cover;
|
||||
padding: 10rem 0;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
{% block content %}
|
||||
<main>
|
||||
{% if homepage.hero_section_based_on == 'Default' %}
|
||||
<section class="hero-section border-bottom {%if homepage.hero_image%}hero-image{%endif%}">
|
||||
<section class="hero-section border-bottom {%if homepage.hero_image%}hero-image{%endif%}"
|
||||
{% if homepage.hero_image %}
|
||||
style="background-image: url('{{ homepage.hero_image }}');"
|
||||
{%- endif %}
|
||||
>
|
||||
<div class="container py-5">
|
||||
<h1 class="d-none d-sm-block display-4">{{ homepage.tag_line }}</h1>
|
||||
<h1 class="d-block d-sm-none">{{ homepage.tag_line }}</h1>
|
||||
|
||||
@@ -5997,7 +5997,7 @@ CN,CN,
|
||||
DE,DE,
|
||||
ES,ES,
|
||||
FR,FR,
|
||||
IN,IM,
|
||||
IN,Ein,
|
||||
JP,JP,
|
||||
IT,ES,
|
||||
MX,MX,
|
||||
|
||||
|
Can't render this file because it is too large.
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user