Merge pull request #31812 from frappe/version-13-hotfix

chore: weekly version 13 release
This commit is contained in:
Deepesh Garg
2022-08-09 19:37:13 +05:30
committed by GitHub
33 changed files with 342 additions and 134 deletions

View File

@@ -10,7 +10,7 @@
"@semantic-release/release-notes-generator", "@semantic-release/release-notes-generator",
[ [
"@semantic-release/exec", { "@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'
} }
], ],
[ [

View File

@@ -222,9 +222,6 @@ class POSInvoice(SalesInvoice):
allow_negative_stock = frappe.db.get_single_value("Stock Settings", "allow_negative_stock") allow_negative_stock = frappe.db.get_single_value("Stock Settings", "allow_negative_stock")
for d in self.get("items"): 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: if d.serial_no:
self.validate_pos_reserved_serial_nos(d) self.validate_pos_reserved_serial_nos(d)
self.validate_delivered_serial_nos(d) self.validate_delivered_serial_nos(d)

View File

@@ -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" target_detail_field = "sales_invoice_item" if doctype == "Sales Invoice" else "sales_order_item"
source_document_warehouse_field = "target_warehouse" source_document_warehouse_field = "target_warehouse"
target_document_warehouse_field = "from_warehouse" target_document_warehouse_field = "from_warehouse"
received_items = get_received_items(source_name, target_doctype, target_detail_field)
else: else:
source_doc = frappe.get_doc(doctype, source_name) source_doc = frappe.get_doc(doctype, source_name)
target_doctype = "Sales Invoice" if doctype == "Purchase Invoice" else "Sales Order" target_doctype = "Sales Invoice" if doctype == "Purchase Invoice" else "Sales Order"
source_document_warehouse_field = "from_warehouse" source_document_warehouse_field = "from_warehouse"
target_document_warehouse_field = "target_warehouse" target_document_warehouse_field = "target_warehouse"
received_items = {}
received_items = get_received_items(source_name, target_doctype, target_detail_field)
validate_inter_company_transaction(source_doc, doctype) validate_inter_company_transaction(source_doc, doctype)
details = get_inter_company_details(source_doc, doctype) details = get_inter_company_details(source_doc, doctype)

View File

@@ -44,14 +44,14 @@ frappe.query_reports["Gross Profit"] = {
"parent_field": "parent_invoice", "parent_field": "parent_invoice",
"initial_depth": 3, "initial_depth": 3,
"formatter": function(value, row, column, data, default_formatter) { "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"; column._options = "Sales Invoice";
} else { } else {
column._options = "Item"; column._options = "Item";
} }
value = default_formatter(value, row, column, data); 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>`); value = $(`<span>${value}</span>`);
var $value = $(value).css("font-weight", "bold"); var $value = $(value).css("font-weight", "bold");
value = $value.wrap("<p></p>").parent().html(); value = $value.wrap("<p></p>").parent().html();

View File

@@ -14,9 +14,9 @@ def execute(filters=None):
filters.naming_series = frappe.db.get_single_value("Buying Settings", "supp_master_name") filters.naming_series = frappe.db.get_single_value("Buying Settings", "supp_master_name")
columns = get_columns(filters) 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) final_result = group_by_supplier_and_category(res)
return columns, final_result return columns, final_result

View File

@@ -26,7 +26,6 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
supplier_map = get_supplier_pan_map() supplier_map = get_supplier_pan_map()
tax_rate_map = get_tax_rate_map(filters) tax_rate_map = get_tax_rate_map(filters)
gle_map = get_gle_map(tds_docs) gle_map = get_gle_map(tds_docs)
print(journal_entry_party_map)
out = [] out = []
for name, details in gle_map.items(): for name, details in gle_map.items():

View File

@@ -113,7 +113,7 @@ class RequestforQuotation(BuyingController):
def get_link(self): def get_link(self):
# RFQ link for supplier portal # 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): def update_supplier_part_no(self, supplier):
self.vendor = supplier self.vendor = supplier

View File

@@ -301,7 +301,8 @@ class BuyingController(StockController, Subcontracting):
rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate")) rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
else: 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 self.is_internal_transfer():
if rate != d.rate: if rate != d.rate:

View File

@@ -18,8 +18,9 @@ from erpnext.stock.get_item_details import _get_item_tax_template
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def employee_query(doctype, txt, searchfield, start, page_len, filters): def employee_query(doctype, txt, searchfield, start, page_len, filters):
doctype = "Employee"
conditions = [] conditions = []
fields = get_fields("Employee", ["name", "employee_name"]) fields = get_fields(doctype, ["name", "employee_name"])
return frappe.db.sql( return frappe.db.sql(
"""select {fields} from `tabEmployee` """select {fields} from `tabEmployee`
@@ -49,7 +50,8 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def lead_query(doctype, txt, searchfield, start, page_len, filters): 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( return frappe.db.sql(
"""select {fields} from `tabLead` """select {fields} from `tabLead`
@@ -77,6 +79,7 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def customer_query(doctype, txt, searchfield, start, page_len, filters): def customer_query(doctype, txt, searchfield, start, page_len, filters):
doctype = "Customer"
conditions = [] conditions = []
cust_master_name = frappe.defaults.get_user_default("cust_master_name") 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: else:
fields = ["name", "customer_name", "customer_group", "territory"] 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) searchfields = " or ".join(field + " like %(txt)s" for field in searchfields)
return frappe.db.sql( return frappe.db.sql(
@@ -116,6 +119,7 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def supplier_query(doctype, txt, searchfield, start, page_len, filters): def supplier_query(doctype, txt, searchfield, start, page_len, filters):
doctype = "Supplier"
supp_master_name = frappe.defaults.get_user_default("supp_master_name") supp_master_name = frappe.defaults.get_user_default("supp_master_name")
if supp_master_name == "Supplier Name": if supp_master_name == "Supplier Name":
@@ -123,7 +127,7 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters):
else: else:
fields = ["name", "supplier_name", "supplier_group"] fields = ["name", "supplier_name", "supplier_group"]
fields = get_fields("Supplier", fields) fields = get_fields(doctype, fields)
return frappe.db.sql( return frappe.db.sql(
"""select {field} from `tabSupplier` """select {field} from `tabSupplier`
@@ -147,6 +151,7 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def tax_account_query(doctype, txt, searchfield, start, page_len, filters): def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
doctype = "Account"
company_currency = erpnext.get_company_currency(filters.get("company")) company_currency = erpnext.get_company_currency(filters.get("company"))
def get_accounts(with_account_type_filter): 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.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False): def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
doctype = "Item"
conditions = [] conditions = []
if isinstance(filters, str): if isinstance(filters, str):
filters = json.loads(filters) filters = json.loads(filters)
# Get searchfields from meta and use in Item Link field query # 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() searchfields = meta.get_search_fields()
# these are handled separately # 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) filters.pop("supplier", None)
description_cond = "" 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 # scan description only if items are less than 50000
description_cond = "or tabItem.description LIKE %(txt)s" description_cond = "or tabItem.description LIKE %(txt)s"
return frappe.db.sql( return frappe.db.sql(
@@ -300,8 +306,9 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def bom(doctype, txt, searchfield, start, page_len, filters): def bom(doctype, txt, searchfield, start, page_len, filters):
doctype = "BOM"
conditions = [] conditions = []
fields = get_fields("BOM", ["name", "item"]) fields = get_fields(doctype, ["name", "item"])
return frappe.db.sql( return frappe.db.sql(
"""select {fields} """select {fields}
@@ -331,6 +338,7 @@ def bom(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_project_name(doctype, txt, searchfield, start, page_len, filters): def get_project_name(doctype, txt, searchfield, start, page_len, filters):
doctype = "Project"
cond = "" cond = ""
if filters and filters.get("customer"): if filters and filters.get("customer"):
cond = """(`tabProject`.customer = %s or 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")) frappe.db.escape(filters.get("customer"))
) )
fields = get_fields("Project", ["name", "project_name"]) fields = get_fields(doctype, ["name", "project_name"])
searchfields = frappe.get_meta("Project").get_search_fields() searchfields = frappe.get_meta(doctype).get_search_fields()
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields]) searchfields = " or ".join(["`tabProject`." + field + " like %(txt)s" for field in searchfields])
return frappe.db.sql( return frappe.db.sql(
"""select {fields} from `tabProject` """select {fields} from `tabProject`
@@ -366,7 +374,8 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, filters, as_dict): 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( return frappe.db.sql(
""" """
@@ -402,6 +411,7 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len,
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_batch_no(doctype, txt, searchfield, start, page_len, filters): def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
doctype = "Batch"
cond = "" cond = ""
if filters.get("posting_date"): if filters.get("posting_date"):
cond = "and (batch.expiry_date is null or batch.expiry_date >= %(posting_date)s)" 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"): if filters.get("is_return"):
having_clause = "" having_clause = ""
meta = frappe.get_meta("Batch", cached=True) meta = frappe.get_meta(doctype, cached=True)
searchfields = meta.get_search_fields() searchfields = meta.get_search_fields()
search_columns = "" search_columns = ""
@@ -496,6 +506,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_account_list(doctype, txt, searchfield, start, page_len, filters): def get_account_list(doctype, txt, searchfield, start, page_len, filters):
doctype = "Account"
filter_list = [] filter_list = []
if isinstance(filters, dict): 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]) filter_list.append([doctype, searchfield, "like", "%%%s%%" % txt])
return frappe.desk.reportview.execute( return frappe.desk.reportview.execute(
"Account", doctype,
filters=filter_list, filters=filter_list,
fields=["name", "parent_account"], fields=["name", "parent_account"],
limit_start=start, limit_start=start,
@@ -553,6 +564,7 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters):
if not filters: if not filters:
filters = {} filters = {}
doctype = "Account"
condition = "" condition = ""
if filters.get("company"): if filters.get("company"):
condition += "and tabAccount.company = %(company)s" condition += "and tabAccount.company = %(company)s"
@@ -628,6 +640,7 @@ def get_expense_account(doctype, txt, searchfield, start, page_len, filters):
if not filters: if not filters:
filters = {} filters = {}
doctype = "Account"
condition = "" condition = ""
if filters.get("company"): if filters.get("company"):
condition += "and tabAccount.company = %(company)s" 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 @frappe.validate_and_sanitize_search_inputs
def warehouse_query(doctype, txt, searchfield, start, page_len, filters): def warehouse_query(doctype, txt, searchfield, start, page_len, filters):
# Should be used when item code is passed in filters. # Should be used when item code is passed in filters.
doctype = "Warehouse"
conditions, bin_conditions = [], [] conditions, bin_conditions = [], []
filter_dict = get_doctype_wise_filters(filters) filter_dict = get_doctype_wise_filters(filters)

View File

@@ -589,6 +589,7 @@ accounting_dimension_doctypes = [
"Shipping Rule", "Shipping Rule",
"Landed Cost Item", "Landed Cost Item",
"Asset Value Adjustment", "Asset Value Adjustment",
"Asset Repair",
"Loyalty Program", "Loyalty Program",
"Fee Schedule", "Fee Schedule",
"Fee Structure", "Fee Structure",

View File

@@ -10,7 +10,7 @@ from frappe.permissions import (
remove_user_permission, remove_user_permission,
set_user_permission_if_allowed, 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 frappe.utils.nestedset import NestedSet
from erpnext.utilities.transaction_base import delete_events from erpnext.utilities.transaction_base import delete_events
@@ -64,6 +64,8 @@ class Employee(NestedSet):
if existing_user_id: if existing_user_id:
remove_user_permission("Employee", self.name, 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): def after_rename(self, old, new, merge):
self.db_set("employee", new) self.db_set("employee", new)
@@ -166,6 +168,18 @@ class Employee(NestedSet):
user.flags.ignore_permissions = True user.flags.ignore_permissions = True
user.add_roles("Expense Approver") 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): def validate_date(self):
if self.date_of_birth and getdate(self.date_of_birth) > getdate(today()): if self.date_of_birth and getdate(self.date_of_birth) > getdate(today()):
throw(_("Date of Birth cannot be greater than today.")) throw(_("Date of Birth cannot be greater than today."))

View File

@@ -80,12 +80,14 @@ class TestEmployeeTransfer(unittest.TestCase):
department = ["Accounts - TC", "Management - TC"] department = ["Accounts - TC", "Management - TC"]
designation = ["Accountant", "Manager"] designation = ["Accountant", "Manager"]
dt = [getdate("01-10-2021"), getdate()] dt = [getdate("01-10-2021"), getdate()]
to_date = [add_days(dt[1], -1), None]
employee = frappe.get_doc("Employee", employee) employee = frappe.get_doc("Employee", employee)
for data in employee.internal_work_history: for data in employee.internal_work_history:
self.assertEqual(data.department, department[count]) self.assertEqual(data.department, department[count])
self.assertEqual(data.designation, designation[count]) self.assertEqual(data.designation, designation[count])
self.assertEqual(data.from_date, dt[count]) self.assertEqual(data.from_date, dt[count])
self.assertEqual(data.to_date, to_date[count])
count = count + 1 count = count + 1
transfer.cancel() transfer.cancel()
@@ -95,6 +97,7 @@ class TestEmployeeTransfer(unittest.TestCase):
self.assertEqual(data.designation, designation[0]) self.assertEqual(data.designation, designation[0])
self.assertEqual(data.department, department[0]) self.assertEqual(data.department, department[0])
self.assertEqual(data.from_date, dt[0]) self.assertEqual(data.from_date, dt[0])
self.assertEqual(data.to_date, None)
def create_company(): def create_company():

View File

@@ -254,9 +254,11 @@ frappe.ui.form.on("Expense Claim", {
}, __("View")); }, __("View"));
} }
if (frm.doc.docstatus===1 && !cint(frm.doc.is_paid) && cint(frm.doc.grand_total) > 0 if (
&& (cint(frm.doc.total_amount_reimbursed) < cint(frm.doc.total_sanctioned_amount)) frm.doc.docstatus === 1
&& frappe.model.can_create("Payment Entry")) { && frm.doc.status !== "Paid"
&& frappe.model.can_create("Payment Entry")
) {
frm.add_custom_button(__('Payment'), frm.add_custom_button(__('Payment'),
function() { frm.events.make_payment_entry(frm); }, __('Create')); function() { frm.events.make_payment_entry(frm); }, __('Create'));
} }

View File

@@ -305,8 +305,9 @@ class ExpenseClaim(AccountsController):
if self.total_advance_amount: if self.total_advance_amount:
precision = self.precision("total_advance_amount") precision = self.precision("total_advance_amount")
amount_with_taxes = flt(self.total_sanctioned_amount, precision) + flt( amount_with_taxes = flt(
self.total_taxes_and_charges, precision (flt(self.total_sanctioned_amount, precision) + flt(self.total_taxes_and_charges, precision)),
precision,
) )
if flt(self.total_advance_amount, precision) > amount_with_taxes: if flt(self.total_advance_amount, precision) > amount_with_taxes:

View File

@@ -224,6 +224,7 @@ def delete_employee_work_history(details, employee, date):
filters["from_date"] = date filters["from_date"] = date
if filters: if filters:
frappe.db.delete("Employee Internal Work History", filters) frappe.db.delete("Employee Internal Work History", filters)
employee.reload()
@frappe.whitelist() @frappe.whitelist()

View File

@@ -99,7 +99,7 @@ class LoanBalanceAdjustment(AccountsController):
loan_account = frappe.db.get_value("Loan", self.loan, "loan_account") loan_account = frappe.db.get_value("Loan", self.loan, "loan_account")
remarks = "{} against loan {}".format(self.adjustment_type.capitalize(), self.loan) remarks = "{} against loan {}".format(self.adjustment_type.capitalize(), self.loan)
if self.reference_number: if self.reference_number:
remarks += "with reference no. {}".format(self.reference_number) remarks += " with reference no. {}".format(self.reference_number)
loan_entry = { loan_entry = {
"account": loan_account, "account": loan_account,

View File

@@ -163,11 +163,11 @@
}, },
{ {
"fetch_from": "against_loan.disbursement_account", "fetch_from": "against_loan.disbursement_account",
"fetch_if_empty": 1,
"fieldname": "disbursement_account", "fieldname": "disbursement_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Disbursement Account", "label": "Disbursement Account",
"options": "Account", "options": "Account"
"read_only": 1
}, },
{ {
"fieldname": "column_break_16", "fieldname": "column_break_16",
@@ -185,7 +185,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-02-17 18:23:44.157598", "modified": "2022-08-04 17:16:04.922444",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Disbursement", "name": "Loan Disbursement",

View File

@@ -209,6 +209,9 @@ def get_disbursal_amount(loan, on_current_security_price=0):
"loan_amount", "loan_amount",
"disbursed_amount", "disbursed_amount",
"total_payment", "total_payment",
"debit_adjustment_amount",
"credit_adjustment_amount",
"refund_amount",
"total_principal_paid", "total_principal_paid",
"total_interest_payable", "total_interest_payable",
"status", "status",

View File

@@ -147,6 +147,9 @@ def make_accrual_interest_entry_for_demand_loans(
"name", "name",
"total_payment", "total_payment",
"total_amount_paid", "total_amount_paid",
"debit_adjustment_amount",
"credit_adjustment_amount",
"refund_amount",
"loan_account", "loan_account",
"interest_income_account", "interest_income_account",
"loan_amount", "loan_amount",

View File

@@ -281,11 +281,11 @@
}, },
{ {
"fetch_from": "against_loan.payment_account", "fetch_from": "against_loan.payment_account",
"fetch_if_empty": 1,
"fieldname": "payment_account", "fieldname": "payment_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Repayment Account", "label": "Repayment Account",
"options": "Account", "options": "Account"
"read_only": 1
}, },
{ {
"fieldname": "column_break_36", "fieldname": "column_break_36",
@@ -311,7 +311,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-02-18 19:10:07.742298", "modified": "2022-08-04 17:13:51.964203",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Repayment", "name": "Loan Repayment",

View File

@@ -150,6 +150,9 @@ class LoanRepayment(AccountsController):
"status", "status",
"is_secured_loan", "is_secured_loan",
"total_payment", "total_payment",
"debit_adjustment_amount",
"credit_adjustment_amount",
"refund_amount",
"loan_amount", "loan_amount",
"disbursed_amount", "disbursed_amount",
"total_interest_payable", "total_interest_payable",
@@ -399,7 +402,7 @@ class LoanRepayment(AccountsController):
remarks = "Repayment against loan " + self.against_loan remarks = "Repayment against loan " + self.against_loan
if self.reference_number: 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: if self.repay_from_salary:
payment_account = self.payroll_payable_account payment_account = self.payroll_payable_account

View File

@@ -58,6 +58,9 @@ class LoanSecurityUnpledge(Document):
self.loan, self.loan,
[ [
"total_payment", "total_payment",
"debit_adjustment_amount",
"credit_adjustment_amount",
"refund_amount",
"total_principal_paid", "total_principal_paid",
"loan_amount", "loan_amount",
"total_interest_payable", "total_interest_payable",

View File

@@ -9,6 +9,9 @@ from frappe.utils import cint, flt, getdate
import erpnext import erpnext
from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import (
get_pending_principal_amount,
)
class LoanWriteOff(AccountsController): class LoanWriteOff(AccountsController):
@@ -22,16 +25,26 @@ class LoanWriteOff(AccountsController):
def validate_write_off_amount(self): def validate_write_off_amount(self):
precision = cint(frappe.db.get_default("currency_precision")) or 2 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", "Loan",
self.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( pending_principal_amount = flt(get_pending_principal_amount(loan_details), precision)
flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount),
precision,
)
if self.write_off_amount > pending_principal_amount: if self.write_off_amount > pending_principal_amount:
frappe.throw(_("Write off amount cannot be greater than pending principal amount")) frappe.throw(_("Write off amount cannot be greater than pending principal amount"))

View File

@@ -372,3 +372,4 @@ erpnext.patches.v13_0.show_india_localisation_deprecation_warning
erpnext.patches.v13_0.fix_number_and_frequency_for_monthly_depreciation erpnext.patches.v13_0.fix_number_and_frequency_for_monthly_depreciation
erpnext.patches.v13_0.reset_corrupt_defaults erpnext.patches.v13_0.reset_corrupt_defaults
erpnext.patches.v13_0.show_hr_payroll_deprecation_warning erpnext.patches.v13_0.show_hr_payroll_deprecation_warning
erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair

View File

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

View File

@@ -622,7 +622,7 @@ class SalarySlip(TransactionBase):
self.add_tax_components(payroll_period) self.add_tax_components(payroll_period)
def add_structure_components(self, component_type): 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( timesheet_component = frappe.db.get_value(
"Salary Structure", self.salary_structure, "salary_component" "Salary Structure", self.salary_structure, "salary_component"
) )
@@ -633,11 +633,12 @@ class SalarySlip(TransactionBase):
amount = self.eval_condition_and_formula(struct_row, data) amount = self.eval_condition_and_formula(struct_row, data)
if ( if (
amount amount or (struct_row.amount_based_on_formula and amount is not None)
or (struct_row.amount_based_on_formula and amount is not None) ) and struct_row.statistical_component == 0:
and struct_row.statistical_component == 0 default_amount = self.eval_condition_and_formula(struct_row, default_data)
): self.update_component_row(
self.update_component_row(struct_row, amount, component_type, data=data) struct_row, amount, component_type, data=data, default_amount=default_amount
)
def get_data_for_eval(self): def get_data_for_eval(self):
"""Returns data for evaluating formula""" """Returns data for evaluating formula"""
@@ -681,11 +682,15 @@ class SalarySlip(TransactionBase):
for sc in salary_components: for sc in salary_components:
data.setdefault(sc.salary_component_abbr, 0) 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 key in ("earnings", "deductions"):
for d in self.get(key): for d in self.get(key):
default_data[d.abbr] = d.default_amount
data[d.abbr] = d.amount data[d.abbr] = d.amount
return data return data, default_data
def eval_condition_and_formula(self, d, data): def eval_condition_and_formula(self, d, data):
try: try:
@@ -791,7 +796,14 @@ class SalarySlip(TransactionBase):
self.update_component_row(tax_row, tax_amount, "deductions") self.update_component_row(tax_row, tax_amount, "deductions")
def update_component_row( 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 component_row = None
for d in self.get(component_type): for d in self.get(component_type):
@@ -852,7 +864,7 @@ class SalarySlip(TransactionBase):
additional_salary.deduct_full_tax_on_selected_payroll_date additional_salary.deduct_full_tax_on_selected_payroll_date
) )
else: else:
component_row.default_amount = amount component_row.default_amount = default_amount or amount
component_row.additional_amount = 0 component_row.additional_amount = 0
component_row.deduct_full_tax_on_selected_payroll_date = ( component_row.deduct_full_tax_on_selected_payroll_date = (
component_data.deduct_full_tax_on_selected_payroll_date component_data.deduct_full_tax_on_selected_payroll_date
@@ -1285,7 +1297,7 @@ class SalarySlip(TransactionBase):
)[0].total_amount )[0].total_amount
def calculate_tax_by_tax_slab(self, annual_taxable_earning, tax_slab): 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}) data.update({"annual_taxable_earning": annual_taxable_earning})
tax_amount = 0 tax_amount = 0
for slab in tax_slab.slabs: for slab in tax_slab.slabs:

View File

@@ -7,7 +7,7 @@ import unittest
import frappe import frappe
from frappe.model.document import Document 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 ( from frappe.utils import (
add_days, add_days,
add_months, 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 from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
class TestSalarySlip(unittest.TestCase): class TestSalarySlip(FrappeTestCase):
def setUp(self): def setUp(self):
setup_test() setup_test()
frappe.flags.pop("via_payroll_entry", None) frappe.flags.pop("via_payroll_entry", None)
def tearDown(self): def tearDown(self):
frappe.db.rollback()
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0) frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0)
frappe.set_user("Administrator") 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) self.assertEqual(salary_slip.payment_days, days_in_month - no_of_holidays - 1)
# gross pay calculation based on attendance (payment days) # component calculation based on attendance (payment days)
gross_pay = 78100 - ( amount, precision = None, None
(78000 / (days_in_month - no_of_holidays))
* flt(salary_slip.leave_without_pay + salary_slip.absent_days) 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"}) @change_settings("Payroll Settings", {"payroll_based_on": "Attendance"})
def test_component_amount_dependent_on_another_payment_days_based_component(self): def test_component_amount_dependent_on_another_payment_days_based_component(self):
@@ -919,6 +924,41 @@ class TestSalarySlip(unittest.TestCase):
# undelete fixture data # undelete fixture data
frappe.db.rollback() 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): def test_tax_for_recurring_additional_salary(self):
frappe.db.sql("""delete from `tabPayroll Period`""") frappe.db.sql("""delete from `tabPayroll Period`""")
frappe.db.sql("""delete from `tabSalary Component`""") frappe.db.sql("""delete from `tabSalary Component`""")
@@ -981,6 +1021,16 @@ class TestSalarySlip(unittest.TestCase):
frappe.db.rollback() 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): def make_activity_for_employee(self):
activity_type = frappe.get_doc("Activity Type", "_Test Activity Type") activity_type = frappe.get_doc("Activity Type", "_Test Activity Type")
activity_type.billing_rate = 50 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): def make_salary_component(salary_components, test_tax, company_list=None):
for salary_component in salary_components: for salary_component in salary_components:
if frappe.db.exists("Salary Component", salary_component["salary_component"]): 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 test_tax:
if salary_component["type"] == "Earning": if salary_component["type"] == "Earning":
@@ -1122,6 +1172,13 @@ def make_earning_salary_component(
"depends_on_payment_days": 0, "depends_on_payment_days": 0,
}, },
{"salary_component": "Leave Encashment", "abbr": "LE", "type": "Earning"}, {"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: if include_flexi_benefits:
data.extend( data.extend(
@@ -1419,6 +1476,10 @@ def setup_test():
"Salary Slip", "Salary Slip",
"Attendance", "Attendance",
"Additional Salary", "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) frappe.db.sql("delete from `tab%s`" % dt)

View File

@@ -150,6 +150,7 @@ def make_salary_structure(
currency=erpnext.get_default_currency(), currency=erpnext.get_default_currency(),
payroll_period=None, payroll_period=None,
include_flexi_benefits=False, include_flexi_benefits=False,
base=None,
): ):
if test_tax: if test_tax:
frappe.db.sql("""delete from `tabSalary Structure` where name=%s""", (salary_structure)) frappe.db.sql("""delete from `tabSalary Structure` where name=%s""", (salary_structure))
@@ -200,6 +201,7 @@ def make_salary_structure(
company=company, company=company,
currency=currency, currency=currency,
payroll_period=payroll_period, payroll_period=payroll_period,
base=base,
) )
return salary_structure_doc return salary_structure_doc

View File

@@ -23,24 +23,31 @@ def _execute(filters=None):
if not filters: if not filters:
filters = {} filters = {}
columns = get_columns() columns = get_columns()
output_gst_accounts = get_output_gst_accounts(filters.company)
company_currency = erpnext.get_company_currency(filters.company) company_currency = erpnext.get_company_currency(filters.company)
item_list = get_items(filters) item_list = get_items(filters)
if item_list: 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 = [] data = []
added_item = [] added_item = []
for d in item_list: for d in item_list:
if (d.parent, d.item_code) not in added_item: 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 or 0] row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty]
total_tax = 0 total_tax = 0
tax_rate = 0
for tax in tax_columns: for tax in tax_columns:
item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {}) 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)) total_tax += flt(item_tax.get("tax_amount", 0))
row += [d.base_net_amount + total_tax] row += [tax_rate]
row += [d.base_net_amount] row += [d.taxable_value + total_tax]
row += [d.taxable_value]
for tax in tax_columns: for tax in tax_columns:
item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {}) item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {})
row += [item_tax.get("tax_amount", 0)] row += [item_tax.get("tax_amount", 0)]
@@ -51,6 +58,40 @@ def _execute(filters=None):
return columns, data 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(): def get_columns():
columns = [ columns = [
{ {
@@ -99,44 +140,24 @@ def get_items(filters):
match_conditions = " and {0} ".format(match_conditions) match_conditions = " and {0} ".format(match_conditions)
items = frappe.db.sql( items = frappe.db.sql(
""" f"""
SELECT SELECT
`tabSales Invoice Item`.gst_hsn_code, `tabSales Invoice Item`.gst_hsn_code,
`tabSales Invoice Item`.stock_uom, `tabSales Invoice Item`.stock_uom,
sum( sum(`tabSales Invoice Item`.stock_qty) AS stock_qty,
`tabSales Invoice Item`.stock_qty sum(`tabSales Invoice Item`.taxable_value) AS taxable_value,
) as stock_qty, sum(`tabSales Invoice Item`.base_price_list_rate) AS base_price_list_rate,
sum(
`tabSales Invoice Item`.base_net_amount
) as base_net_amount,
sum(
`tabSales Invoice Item`.base_price_list_rate
) as base_price_list_rate,
`tabSales Invoice Item`.parent, `tabSales Invoice Item`.parent,
`tabSales Invoice Item`.item_code, `tabSales Invoice Item`.item_code,
`tabGST HSN Code`.description, `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 FROM
`tabSales Invoice` `tabSales Invoice`
INNER JOIN INNER JOIN `tabSales Invoice Item` ON `tabSales Invoice`.name = `tabSales Invoice Item`.parent
`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
INNER JOIN
`tabGST HSN Code` ON `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name % s % s
LEFT JOIN
`tabSales Taxes and Charges` ON `tabSales Taxes and Charges`.parent = `tabSales Invoice`.name
WHERE WHERE
`tabSales Invoice`.docstatus = 1 `tabSales Invoice`.docstatus = 1
AND AND `tabSales Invoice Item`.gst_hsn_code IS NOT NULL
`tabSales Invoice Item`.gst_hsn_code is not NULL {conditions}
GROUP BY GROUP BY
`tabSales Invoice Item`.parent, `tabSales Invoice Item`.parent,
`tabSales Invoice Item`.item_code, `tabSales Invoice Item`.item_code,
@@ -145,8 +166,9 @@ def get_items(filters):
ORDER BY ORDER BY
`tabSales Invoice Item`.gst_hsn_code, `tabSales Invoice Item`.gst_hsn_code,
`tabSales Invoice Item`.uom `tabSales Invoice Item`.uom
""" """.format(
% (conditions, match_conditions), conditions=conditions
),
filters, filters,
as_dict=1, as_dict=1,
) )
@@ -158,6 +180,7 @@ def get_tax_accounts(
item_list, item_list,
columns, columns,
company_currency, company_currency,
output_gst_accounts,
doctype="Sales Invoice", doctype="Sales Invoice",
tax_doctype="Sales Taxes and Charges", tax_doctype="Sales Taxes and Charges",
): ):
@@ -197,7 +220,7 @@ def get_tax_accounts(
for parent, account_head, item_wise_tax_detail, tax_amount in tax_details: 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 # as description is text editor earlier and markup can break the column convention in reports
tax_columns.append(account_head) tax_columns.append(account_head)
@@ -210,29 +233,40 @@ def get_tax_accounts(
continue continue
itemised_tax.setdefault(item_code, frappe._dict()) itemised_tax.setdefault(item_code, frappe._dict())
if isinstance(tax_data, list): 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] tax_amount = tax_data[1]
else: else:
tax_rate = 0
tax_amount = 0 tax_amount = 0
for d in item_row_map.get(parent, {}).get(item_code, []): for d in item_row_map.get(parent, {}).get(item_code, []):
item_tax_amount = tax_amount item_tax_amount = tax_amount
if item_tax_amount: if item_tax_amount:
itemised_tax.setdefault((parent, item_code), {})[account_head] = frappe._dict( 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: except ValueError:
continue continue
tax_columns.sort() tax_columns.sort()
for account_head in tax_columns: for account_head in tax_columns:
columns.append( if account_head in output_gst_accounts:
{ columns.append(
"label": account_head, {
"fieldname": frappe.scrub(account_head), "label": account_head,
"fieldtype": "Float", "fieldname": frappe.scrub(account_head),
"width": 110, "fieldtype": "Float",
} "width": 110,
) }
)
return itemised_tax, tax_columns return itemised_tax, tax_columns

View File

@@ -161,13 +161,14 @@ erpnext.PointOfSale.Payment = class {
frappe.ui.form.on('POS Invoice', 'contact_mobile', (frm) => { frappe.ui.form.on('POS Invoice', 'contact_mobile', (frm) => {
const contact = frm.doc.contact_mobile; const contact = frm.doc.contact_mobile;
if (!this.request_for_payment_field) return;
const request_button = $(this.request_for_payment_field.$input[0]); const request_button = $(this.request_for_payment_field.$input[0]);
if (contact) { if (contact) {
request_button.removeClass('btn-default').addClass('btn-primary'); request_button.removeClass('btn-default').addClass('btn-primary');
} else { } else {
request_button.removeClass('btn-primary').addClass('btn-default'); request_button.removeClass('btn-primary').addClass('btn-default');
} }
}); });
frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => { frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => {
if (frm.doc.coupon_code && !frm.applying_pos_coupon_code) { if (frm.doc.coupon_code && !frm.applying_pos_coupon_code) {

View File

@@ -143,10 +143,6 @@ def get_item_for_list_in_html(context):
if (context.get("website_image") or "").startswith("files/"): if (context.get("website_image") or "").startswith("files/"):
context["website_image"] = "/" + quote(context["website_image"]) 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" products_template = "templates/includes/products_as_list.html"
return frappe.get_template(products_template).render(context) return frappe.get_template(products_template).render(context)

View File

@@ -6,8 +6,6 @@ import frappe
from frappe import _ from frappe import _
from frappe.utils.dashboard import cache_source from frappe.utils.dashboard import cache_source
from erpnext.stock.utils import get_stock_value_from_bin
@frappe.whitelist() @frappe.whitelist()
@cache_source @cache_source
@@ -30,26 +28,24 @@ def get(
warehouse_filters.append(["company", "=", filters.get("company")]) warehouse_filters.append(["company", "=", filters.get("company")])
warehouses = frappe.get_list( 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: warehouses = frappe.get_list(
balance = get_stock_value_from_bin(warehouse=wh.name) "Bin",
wh["balance"] = balance[0][0] fields=["warehouse", "sum(stock_value) stock_value"],
filters={"warehouse": ["IN", warehouses], "stock_value": [">", 0]},
warehouses = [x for x in warehouses if not (x.get("balance") == None)] group_by="warehouse",
order_by="stock_value DESC",
limit_page_length=10,
)
if not warehouses: if not warehouses:
return [] return []
sorted_warehouse_map = sorted(warehouses, key=lambda i: i["balance"], reverse=True) for warehouse in warehouses:
labels.append(_(warehouse.get("warehouse")))
if len(sorted_warehouse_map) > 10: datapoints.append(warehouse.get("stock_value"))
sorted_warehouse_map = sorted_warehouse_map[:10]
for warehouse in sorted_warehouse_map:
labels.append(_(warehouse.get("name")))
datapoints.append(warehouse.get("balance"))
return { return {
"labels": labels, "labels": labels,

View File

@@ -635,6 +635,24 @@ class update_entries_after(object):
voucher_detail_no=sle.voucher_detail_no, voucher_detail_no=sle.voucher_detail_no,
sle=sle, 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: else:
if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"): if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
rate_field = "valuation_rate" rate_field = "valuation_rate"