Merge branch 'develop' of https://github.com/frappe/erpnext into project_filter_search_fields

This commit is contained in:
Deepesh Garg 2021-02-27 17:29:11 +05:30
commit b5819c74c8
35 changed files with 787 additions and 297 deletions

View File

@ -61,8 +61,10 @@ class AccountingDimension(Document):
def on_update(self):
frappe.flags.accounting_dimensions = None
def make_dimension_in_accounting_doctypes(doc):
doclist = get_doctypes_with_dimensions()
def make_dimension_in_accounting_doctypes(doc, doclist=None):
if not doclist:
doclist = get_doctypes_with_dimensions()
doc_count = len(get_accounting_dimensions())
count = 0
@ -82,13 +84,13 @@ def make_dimension_in_accounting_doctypes(doc):
"owner": "Administrator"
}
if doctype == "Budget":
add_dimension_to_budget_doctype(df, doc)
else:
meta = frappe.get_meta(doctype, cached=False)
fieldnames = [d.fieldname for d in meta.get("fields")]
meta = frappe.get_meta(doctype, cached=False)
fieldnames = [d.fieldname for d in meta.get("fields")]
if df['fieldname'] not in fieldnames:
if df['fieldname'] not in fieldnames:
if doctype == "Budget":
add_dimension_to_budget_doctype(df.copy(), doc)
else:
create_custom_field(doctype, df)
count += 1
@ -178,15 +180,7 @@ def toggle_disabling(doc):
frappe.clear_cache(doctype=doctype)
def get_doctypes_with_dimensions():
doclist = ["GL Entry", "Sales Invoice", "POS Invoice", "Purchase Invoice", "Payment Entry", "Asset",
"Expense Claim", "Expense Claim Detail", "Expense Taxes and Charges", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note",
"Sales Invoice Item", "POS Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item",
"Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule",
"Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation",
"Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", "Subscription",
"Subscription Plan"]
return doclist
return frappe.get_hooks("accounting_dimension_doctypes")
def get_accounting_dimensions(as_list=True):
if frappe.flags.accounting_dimensions is None:

View File

@ -22,9 +22,10 @@ def validate_company(company):
'allow_account_creation_against_child_company'])
if parent_company and (not allow_account_creation_against_child_company):
frappe.throw(_("""{0} is a child company. Please import accounts against parent company
or enable {1} in company master""").format(frappe.bold(company),
frappe.bold('Allow Account Creation Against Child Company')), title='Wrong Company')
msg = _("{} is a child company. ").format(frappe.bold(company))
msg += _("Please import accounts against parent company or enable {} in company master.").format(
frappe.bold('Allow Account Creation Against Child Company'))
frappe.throw(msg, title=_('Wrong Company'))
if frappe.db.get_all('GL Entry', {"company": company}, "name", limit=1):
return False
@ -74,7 +75,9 @@ def generate_data_from_csv(file_doc, as_dict=False):
if as_dict:
data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)})
else:
if not row[1]: row[1] = row[0]
if not row[1]:
row[1] = row[0]
row[3] = row[2]
data.append(row)
# convert csv data
@ -96,7 +99,9 @@ def generate_data_from_excel(file_doc, extension, as_dict=False):
if as_dict:
data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)})
else:
if not row[1]: row[1] = row[0]
if not row[1]:
row[1] = row[0]
row[3] = row[2]
data.append(row)
return data
@ -147,7 +152,13 @@ def build_forest(data):
from frappe import _
for row in data:
account_name, parent_account = row[0:2]
account_name, parent_account, account_number, parent_account_number = row[0:4]
if account_number:
account_name = "{} - {}".format(account_number, account_name)
if parent_account_number:
parent_account_number = cstr(parent_account_number).strip()
parent_account = "{} - {}".format(parent_account_number, parent_account)
if parent_account == account_name == child:
return [parent_account]
elif account_name == child:
@ -159,20 +170,23 @@ def build_forest(data):
charts_map, paths = {}, []
line_no = 3
line_no = 2
error_messages = []
for i in data:
account_name, dummy, account_number, is_group, account_type, root_type = i
account_name, parent_account, account_number, parent_account_number, is_group, account_type, root_type = i
if not account_name:
error_messages.append("Row {0}: Please enter Account Name".format(line_no))
if account_number:
account_number = cstr(account_number).strip()
account_name = "{} - {}".format(account_number, account_name)
charts_map[account_name] = {}
if cint(is_group) == 1: charts_map[account_name]["is_group"] = is_group
if account_type: charts_map[account_name]["account_type"] = account_type
if root_type: charts_map[account_name]["root_type"] = root_type
if account_number: charts_map[account_name]["account_number"] = account_number
path = return_parent(data, account_name)[::-1]
paths.append(path) # List of path is created
line_no += 1
@ -221,7 +235,7 @@ def download_template(file_type, template_type):
def get_template(template_type):
fields = ["Account Name", "Parent Account", "Account Number", "Is Group", "Account Type", "Root Type"]
fields = ["Account Name", "Parent Account", "Account Number", "Parent Account Number", "Is Group", "Account Type", "Root Type"]
writer = UnicodeWriter()
writer.writerow(fields)
@ -241,23 +255,23 @@ def get_template(template_type):
def get_sample_template(writer):
template = [
["Application Of Funds(Assets)", "", "", 1, "", "Asset"],
["Sources Of Funds(Liabilities)", "", "", 1, "", "Liability"],
["Equity", "", "", 1, "", "Equity"],
["Expenses", "", "", 1, "", "Expense"],
["Income", "", "", 1, "", "Income"],
["Bank Accounts", "Application Of Funds(Assets)", "", 1, "Bank", "Asset"],
["Cash In Hand", "Application Of Funds(Assets)", "", 1, "Cash", "Asset"],
["Stock Assets", "Application Of Funds(Assets)", "", 1, "Stock", "Asset"],
["Cost Of Goods Sold", "Expenses", "", 0, "Cost of Goods Sold", "Expense"],
["Asset Depreciation", "Expenses", "", 0, "Depreciation", "Expense"],
["Fixed Assets", "Application Of Funds(Assets)", "", 0, "Fixed Asset", "Asset"],
["Accounts Payable", "Sources Of Funds(Liabilities)", "", 0, "Payable", "Liability"],
["Accounts Receivable", "Application Of Funds(Assets)", "", 1, "Receivable", "Asset"],
["Stock Expenses", "Expenses", "", 0, "Stock Adjustment", "Expense"],
["Sample Bank", "Bank Accounts", "", 0, "Bank", "Asset"],
["Cash", "Cash In Hand", "", 0, "Cash", "Asset"],
["Stores", "Stock Assets", "", 0, "Stock", "Asset"],
["Application Of Funds(Assets)", "", "", "", 1, "", "Asset"],
["Sources Of Funds(Liabilities)", "", "", "", 1, "", "Liability"],
["Equity", "", "", "", 1, "", "Equity"],
["Expenses", "", "", "", 1, "", "Expense"],
["Income", "", "", "", 1, "", "Income"],
["Bank Accounts", "Application Of Funds(Assets)", "", "", 1, "Bank", "Asset"],
["Cash In Hand", "Application Of Funds(Assets)", "", "", 1, "Cash", "Asset"],
["Stock Assets", "Application Of Funds(Assets)", "", "", 1, "Stock", "Asset"],
["Cost Of Goods Sold", "Expenses", "", "", 0, "Cost of Goods Sold", "Expense"],
["Asset Depreciation", "Expenses", "", "", 0, "Depreciation", "Expense"],
["Fixed Assets", "Application Of Funds(Assets)", "", "", 0, "Fixed Asset", "Asset"],
["Accounts Payable", "Sources Of Funds(Liabilities)", "", "", 0, "Payable", "Liability"],
["Accounts Receivable", "Application Of Funds(Assets)", "", "", 1, "Receivable", "Asset"],
["Stock Expenses", "Expenses", "", "", 0, "Stock Adjustment", "Expense"],
["Sample Bank", "Bank Accounts", "", "", 0, "Bank", "Asset"],
["Cash", "Cash In Hand", "", "", 0, "Cash", "Asset"],
["Stores", "Stock Assets", "", "", 0, "Stock", "Asset"],
]
for row in template:

View File

@ -136,7 +136,7 @@ class PricingRule(Document):
for d in self.items:
max_discount = frappe.get_cached_value("Item", d.item_code, "max_discount")
if max_discount and flt(self.discount_percentage) > flt(max_discount):
throw(_("Max discount allowed for item: {0} is {1}%").format(self.item_code, max_discount))
throw(_("Max discount allowed for item: {0} is {1}%").format(d.item_code, max_discount))
def validate_price_list_with_currency(self):
if self.currency and self.for_price_list:

View File

@ -21,6 +21,7 @@ from erpnext.accounts.general_ledger import get_round_off_account_and_cost_cente
from erpnext.accounts.doctype.loyalty_program.loyalty_program import \
get_loyalty_program_details_with_points, get_loyalty_details, validate_loyalty_points
from erpnext.accounts.deferred_revenue import validate_service_stop_date
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
from frappe.model.utils import get_fetch_values
from frappe.contacts.doctype.address.address import get_address_display
@ -75,6 +76,8 @@ class SalesInvoice(SellingController):
if not self.is_pos:
self.so_dn_required()
self.set_tax_withholding()
self.validate_proj_cust()
self.validate_pos_return()
@ -153,6 +156,32 @@ class SalesInvoice(SellingController):
if cost_center_company != self.company:
frappe.throw(_("Row #{0}: Cost Center {1} does not belong to company {2}").format(frappe.bold(item.idx), frappe.bold(item.cost_center), frappe.bold(self.company)))
def set_tax_withholding(self):
tax_withholding_details = get_party_tax_withholding_details(self)
if not tax_withholding_details:
return
accounts = []
tax_withholding_account = tax_withholding_details.get("account_head")
for d in self.taxes:
if d.account_head == tax_withholding_account:
d.update(tax_withholding_details)
accounts.append(d.account_head)
if not accounts or tax_withholding_account not in accounts:
self.append("taxes", tax_withholding_details)
to_remove = [d for d in self.taxes
if not d.tax_amount and d.charge_type == "Actual" and d.account_head == tax_withholding_account]
for d in to_remove:
self.remove(d)
# calculate totals again after applying TDS
self.calculate_taxes_and_totals()
def before_save(self):
set_account_for_mode_of_payment(self)

View File

@ -10,6 +10,14 @@ frappe.ui.form.on('Subscription', {
}
}
});
frm.set_query('cost_center', function() {
return {
filters: {
company: frm.doc.company
}
};
});
},
refresh: function(frm) {

View File

@ -7,9 +7,10 @@
"engine": "InnoDB",
"field_order": [
"party_type",
"status",
"cb_1",
"party",
"cb_1",
"company",
"status",
"subscription_period",
"start_date",
"end_date",
@ -44,80 +45,107 @@
{
"allow_on_submit": 1,
"fieldname": "cb_1",
"fieldtype": "Column Break"
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"no_copy": 1,
"options": "\nTrialling\nActive\nPast Due Date\nCancelled\nUnpaid\nCompleted",
"read_only": 1
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "subscription_period",
"fieldtype": "Section Break",
"label": "Subscription Period"
"label": "Subscription Period",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "cancelation_date",
"fieldtype": "Date",
"label": "Cancelation Date",
"read_only": 1
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"allow_on_submit": 1,
"fieldname": "trial_period_start",
"fieldtype": "Date",
"label": "Trial Period Start Date",
"set_only_once": 1
"set_only_once": 1,
"show_days": 1,
"show_seconds": 1
},
{
"depends_on": "eval:doc.trial_period_start",
"fieldname": "trial_period_end",
"fieldtype": "Date",
"label": "Trial Period End Date",
"set_only_once": 1
"set_only_once": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "current_invoice_start",
"fieldtype": "Date",
"label": "Current Invoice Start Date",
"read_only": 1
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "current_invoice_end",
"fieldtype": "Date",
"label": "Current Invoice End Date",
"read_only": 1
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"description": "Number of days that the subscriber has to pay invoices generated by this subscription",
"fieldname": "days_until_due",
"fieldtype": "Int",
"label": "Days Until Due"
"label": "Days Until Due",
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"fieldname": "cancel_at_period_end",
"fieldtype": "Check",
"label": "Cancel At End Of Period"
"label": "Cancel At End Of Period",
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"fieldname": "generate_invoice_at_period_start",
"fieldtype": "Check",
"label": "Generate Invoice At Beginning Of Period"
"label": "Generate Invoice At Beginning Of Period",
"show_days": 1,
"show_seconds": 1
},
{
"allow_on_submit": 1,
"fieldname": "sb_4",
"fieldtype": "Section Break",
"label": "Plans"
"label": "Plans",
"show_days": 1,
"show_seconds": 1
},
{
"allow_on_submit": 1,
@ -125,62 +153,84 @@
"fieldtype": "Table",
"label": "Plans",
"options": "Subscription Plan Detail",
"reqd": 1
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"depends_on": "eval:['Customer', 'Supplier'].includes(doc.party_type)",
"fieldname": "sb_1",
"fieldtype": "Section Break",
"label": "Taxes"
"label": "Taxes",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "sb_2",
"fieldtype": "Section Break",
"label": "Discounts"
"label": "Discounts",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "apply_additional_discount",
"fieldtype": "Select",
"label": "Apply Additional Discount On",
"options": "\nGrand Total\nNet Total"
"options": "\nGrand Total\nNet Total",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "cb_2",
"fieldtype": "Column Break"
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "additional_discount_percentage",
"fieldtype": "Percent",
"label": "Additional DIscount Percentage"
"label": "Additional DIscount Percentage",
"show_days": 1,
"show_seconds": 1
},
{
"collapsible": 1,
"fieldname": "additional_discount_amount",
"fieldtype": "Currency",
"label": "Additional DIscount Amount"
"label": "Additional DIscount Amount",
"show_days": 1,
"show_seconds": 1
},
{
"depends_on": "eval:doc.invoices",
"fieldname": "sb_3",
"fieldtype": "Section Break",
"label": "Invoices"
"label": "Invoices",
"show_days": 1,
"show_seconds": 1
},
{
"collapsible": 1,
"fieldname": "invoices",
"fieldtype": "Table",
"label": "Invoices",
"options": "Subscription Invoice"
"options": "Subscription Invoice",
"show_days": 1,
"show_seconds": 1
},
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
"label": "Accounting Dimensions",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "party_type",
@ -188,7 +238,9 @@
"label": "Party Type",
"options": "DocType",
"reqd": 1,
"set_only_once": 1
"set_only_once": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "party",
@ -197,21 +249,27 @@
"label": "Party",
"options": "party_type",
"reqd": 1,
"set_only_once": 1
"set_only_once": 1,
"show_days": 1,
"show_seconds": 1
},
{
"depends_on": "eval:doc.party_type === 'Customer'",
"fieldname": "sales_tax_template",
"fieldtype": "Link",
"label": "Sales Taxes and Charges Template",
"options": "Sales Taxes and Charges Template"
"options": "Sales Taxes and Charges Template",
"show_days": 1,
"show_seconds": 1
},
{
"depends_on": "eval:doc.party_type === 'Supplier'",
"fieldname": "purchase_tax_template",
"fieldtype": "Link",
"label": "Purchase Taxes and Charges Template",
"options": "Purchase Taxes and Charges Template"
"options": "Purchase Taxes and Charges Template",
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
@ -219,36 +277,55 @@
"fieldname": "follow_calendar_months",
"fieldtype": "Check",
"label": "Follow Calendar Months",
"set_only_once": 1
"set_only_once": 1,
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"description": "New invoices will be generated as per schedule even if current invoices are unpaid or past due date",
"fieldname": "generate_new_invoices_past_due_date",
"fieldtype": "Check",
"label": "Generate New Invoices Past Due Date"
"label": "Generate New Invoices Past Due Date",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "end_date",
"fieldtype": "Date",
"label": "Subscription End Date",
"set_only_once": 1
"set_only_once": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "start_date",
"fieldtype": "Date",
"label": "Subscription Start Date",
"set_only_once": 1
"set_only_once": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
"options": "Cost Center",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"show_days": 1,
"show_seconds": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-06-25 10:52:52.265105",
"modified": "2021-02-09 15:44:20.024789",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Subscription",

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
@ -5,12 +6,13 @@
from __future__ import unicode_literals
import frappe
import erpnext
from frappe import _
from frappe.model.document import Document
from frappe.utils.data import nowdate, getdate, cstr, cint, add_days, date_diff, get_last_day, add_to_date, flt
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
from erpnext import get_default_company
class Subscription(Document):
def before_insert(self):
@ -243,6 +245,7 @@ class Subscription(Document):
self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval())
self.validate_end_date()
self.validate_to_follow_calendar_months()
self.cost_center = erpnext.get_default_cost_center(self.get('company'))
def validate_trial_period(self):
"""
@ -304,6 +307,14 @@ class Subscription(Document):
doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice'
invoice = frappe.new_doc(doctype)
# For backward compatibility
# Earlier subscription didn't had any company field
company = self.get('company') or get_default_company()
if not company:
frappe.throw(_("Company is mandatory was generating invoice. Please set default company in Global Defaults"))
invoice.company = company
invoice.set_posting_time = 1
invoice.posting_date = self.current_invoice_start if self.generate_invoice_at_period_start \
else self.current_invoice_end
@ -330,6 +341,7 @@ class Subscription(Document):
# for that reason
items_list = self.get_items_from_plans(self.plans, prorate)
for item in items_list:
item['cost_center'] = self.cost_center
invoice.append('items', item)
# Taxes
@ -380,7 +392,8 @@ class Subscription(Document):
Returns the `Item`s linked to `Subscription Plan`
"""
if prorate:
prorate_factor = get_prorata_factor(self.current_invoice_end, self.current_invoice_start)
prorate_factor = get_prorata_factor(self.current_invoice_end, self.current_invoice_start,
self.generate_invoice_at_period_start)
items = []
party = self.party
@ -583,10 +596,13 @@ def get_calendar_months(billing_interval):
return calendar_months
def get_prorata_factor(period_end, period_start):
diff = flt(date_diff(nowdate(), period_start) + 1)
plan_days = flt(date_diff(period_end, period_start) + 1)
prorate_factor = diff / plan_days
def get_prorata_factor(period_end, period_start, is_prepaid):
if is_prepaid:
prorate_factor = 1
else:
diff = flt(date_diff(nowdate(), period_start) + 1)
plan_days = flt(date_diff(period_end, period_start) + 1)
prorate_factor = diff / plan_days
return prorate_factor

View File

@ -321,7 +321,8 @@ class TestSubscription(unittest.TestCase):
self.assertEqual(
flt(
get_prorata_factor(subscription.current_invoice_end, subscription.current_invoice_start),
get_prorata_factor(subscription.current_invoice_end, subscription.current_invoice_start,
subscription.generate_invoice_at_period_start),
2),
flt(prorate_factor, 2)
)
@ -561,9 +562,7 @@ class TestSubscription(unittest.TestCase):
current_inv = subscription.get_current_invoice()
self.assertEqual(current_inv.status, "Unpaid")
diff = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1)
plan_days = flt(date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1)
prorate_factor = flt(diff / plan_days)
prorate_factor = 1
self.assertEqual(flt(current_inv.grand_total, 2), flt(prorate_factor * 900, 2))

View File

@ -13,21 +13,28 @@
"fieldname": "document_type",
"fieldtype": "Link",
"label": "Document Type ",
"no_copy": 1,
"options": "DocType",
"read_only": 1
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "invoice",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Invoice",
"no_copy": 1,
"options": "document_type",
"read_only": 1
"read_only": 1,
"show_days": 1,
"show_seconds": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-06-01 22:23:54.462718",
"modified": "2021-02-09 15:43:32.026233",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Subscription Invoice",

View File

@ -12,37 +12,62 @@ from erpnext.accounts.utils import get_fiscal_year
class TaxWithholdingCategory(Document):
pass
def get_party_tax_withholding_details(ref_doc, tax_withholding_category=None):
def get_party_details(inv):
party_type, party = '', ''
if inv.doctype == 'Sales Invoice':
party_type = 'Customer'
party = inv.customer
else:
party_type = 'Supplier'
party = inv.supplier
return party_type, party
def get_party_tax_withholding_details(inv, tax_withholding_category=None):
pan_no = ''
suppliers = []
parties = []
party_type, party = get_party_details(inv)
if not tax_withholding_category:
tax_withholding_category, pan_no = frappe.db.get_value('Supplier', ref_doc.supplier, ['tax_withholding_category', 'pan'])
tax_withholding_category, pan_no = frappe.db.get_value(party_type, party, ['tax_withholding_category', 'pan'])
if not tax_withholding_category:
return
# if tax_withholding_category passed as an argument but not pan_no
if not pan_no:
pan_no = frappe.db.get_value('Supplier', ref_doc.supplier, 'pan')
pan_no = frappe.db.get_value(party_type, party, 'pan')
# Get others suppliers with the same PAN No
if pan_no:
suppliers = [d.name for d in frappe.get_all('Supplier', fields=['name'], filters={'pan': pan_no})]
parties = frappe.get_all(party_type, filters={ 'pan': pan_no }, pluck='name')
if not suppliers:
suppliers.append(ref_doc.supplier)
if not parties:
parties.append(party)
fiscal_year = get_fiscal_year(inv.posting_date, company=inv.company)
tax_details = get_tax_withholding_details(tax_withholding_category, fiscal_year[0], inv.company)
fy = get_fiscal_year(ref_doc.posting_date, company=ref_doc.company)
tax_details = get_tax_withholding_details(tax_withholding_category, fy[0], ref_doc.company)
if not tax_details:
frappe.throw(_('Please set associated account in Tax Withholding Category {0} against Company {1}')
.format(tax_withholding_category, ref_doc.company))
.format(tax_withholding_category, inv.company))
tds_amount = get_tds_amount(suppliers, ref_doc.net_total, ref_doc.company,
tax_details, fy, ref_doc.posting_date, pan_no)
if party_type == 'Customer' and not tax_details.cumulative_threshold:
# TCS is only chargeable on sum of invoiced value
frappe.throw(_('Tax Withholding Category {} against Company {} for Customer {} should have Cumulative Threshold value.')
.format(tax_withholding_category, inv.company, party))
tax_row = get_tax_row(tax_details, tds_amount)
tax_amount, tax_deducted = get_tax_amount(
party_type, parties,
inv, tax_details,
fiscal_year, pan_no
)
if party_type == 'Supplier':
tax_row = get_tax_row_for_tds(tax_details, tax_amount)
else:
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
return tax_row
@ -69,147 +94,254 @@ def get_tax_withholding_rates(tax_withholding, fiscal_year):
frappe.throw(_("No Tax Withholding data found for the current Fiscal Year."))
def get_tax_row(tax_details, tds_amount):
return {
def get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted):
row = {
"category": "Total",
"add_deduct_tax": "Deduct",
"charge_type": "Actual",
"account_head": tax_details.account_head,
"tax_amount": tax_amount,
"description": tax_details.description,
"tax_amount": tds_amount
"account_head": tax_details.account_head
}
def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_details, posting_date, pan_no=None):
fiscal_year, year_start_date, year_end_date = fiscal_year_details
tds_amount = 0
tds_deducted = 0
if tax_deducted:
# TCS already deducted on previous invoices
# So, TCS will be calculated by 'Previous Row Total'
def _get_tds(amount, rate):
if amount <= 0:
return 0
return amount * rate / 100
ldc_name = frappe.db.get_value('Lower Deduction Certificate',
{
'pan_no': pan_no,
'fiscal_year': fiscal_year
}, 'name')
ldc = ''
if ldc_name:
ldc = frappe.get_doc('Lower Deduction Certificate', ldc_name)
entries = frappe.db.sql("""
select voucher_no, credit
from `tabGL Entry`
where company = %s and
party in %s and fiscal_year=%s and credit > 0
and is_opening = 'No'
""", (company, tuple(suppliers), fiscal_year), as_dict=1)
vouchers = [d.voucher_no for d in entries]
advance_vouchers = get_advance_vouchers(suppliers, fiscal_year=fiscal_year, company=company)
tds_vouchers = vouchers + advance_vouchers
if tds_vouchers:
tds_deducted = frappe.db.sql("""
SELECT sum(credit) FROM `tabGL Entry`
WHERE
account=%s and fiscal_year=%s and credit > 0
and voucher_no in ({0})""". format(','.join(['%s'] * len(tds_vouchers))),
((tax_details.account_head, fiscal_year) + tuple(tds_vouchers)))
tds_deducted = tds_deducted[0][0] if tds_deducted and tds_deducted[0][0] else 0
if tds_deducted:
if ldc:
limit_consumed = frappe.db.get_value('Purchase Invoice',
{
'supplier': ('in', suppliers),
'apply_tds': 1,
'docstatus': 1
}, 'sum(net_total)')
if ldc and is_valid_certificate(ldc.valid_from, ldc.valid_upto, posting_date, limit_consumed, net_total,
ldc.certificate_limit):
tds_amount = get_ltds_amount(net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details)
taxes_excluding_tcs = [d for d in inv.taxes if d.account_head != tax_details.account_head]
if taxes_excluding_tcs:
# chargeable amount is the total amount after other charges are applied
row.update({
"charge_type": "On Previous Row Total",
"row_id": len(taxes_excluding_tcs),
"rate": tax_details.rate
})
else:
tds_amount = _get_tds(net_total, tax_details.rate)
else:
supplier_credit_amount = frappe.get_all('Purchase Invoice',
fields = ['sum(net_total)'],
filters = {'name': ('in', vouchers), 'docstatus': 1, "apply_tds": 1}, as_list=1)
# if only TCS is to be charged, then net total is chargeable amount
row.update({
"charge_type": "On Net Total",
"rate": tax_details.rate
})
supplier_credit_amount = (supplier_credit_amount[0][0]
if supplier_credit_amount and supplier_credit_amount[0][0] else 0)
return row
jv_supplier_credit_amt = frappe.get_all('Journal Entry Account',
fields = ['sum(credit_in_account_currency)'],
filters = {
'parent': ('in', vouchers), 'docstatus': 1,
'party': ('in', suppliers),
'reference_type': ('not in', ['Purchase Invoice'])
}, as_list=1)
def get_tax_row_for_tds(tax_details, tax_amount):
return {
"category": "Total",
"charge_type": "Actual",
"tax_amount": tax_amount,
"add_deduct_tax": "Deduct",
"description": tax_details.description,
"account_head": tax_details.account_head
}
supplier_credit_amount += (jv_supplier_credit_amt[0][0]
if jv_supplier_credit_amt and jv_supplier_credit_amt[0][0] else 0)
def get_lower_deduction_certificate(fiscal_year, pan_no):
ldc_name = frappe.db.get_value('Lower Deduction Certificate', { 'pan_no': pan_no, 'fiscal_year': fiscal_year }, 'name')
if ldc_name:
return frappe.get_doc('Lower Deduction Certificate', ldc_name)
supplier_credit_amount += net_total
def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, pan_no=None):
fiscal_year = fiscal_year_details[0]
debit_note_amount = get_debit_note_amount(suppliers, year_start_date, year_end_date)
supplier_credit_amount -= debit_note_amount
vouchers = get_invoice_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
advance_vouchers = get_advance_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
taxable_vouchers = vouchers + advance_vouchers
if ((tax_details.get('threshold', 0) and supplier_credit_amount >= tax_details.threshold)
or (tax_details.get('cumulative_threshold', 0) and supplier_credit_amount >= tax_details.cumulative_threshold)):
tax_deducted = 0
if taxable_vouchers:
tax_deducted = get_deducted_tax(taxable_vouchers, fiscal_year, tax_details)
if ldc and is_valid_certificate(ldc.valid_from, ldc.valid_upto, posting_date, tds_deducted, net_total,
ldc.certificate_limit):
tds_amount = get_ltds_amount(supplier_credit_amount, 0, ldc.certificate_limit, ldc.rate,
tax_details)
tax_amount = 0
posting_date = inv.posting_date
if party_type == 'Supplier':
ldc = get_lower_deduction_certificate(fiscal_year, pan_no)
if tax_deducted:
net_total = inv.net_total
if ldc:
tax_amount = get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total)
else:
tds_amount = _get_tds(supplier_credit_amount, tax_details.rate)
tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
else:
tax_amount = get_tds_amount(
ldc, parties, inv, tax_details,
fiscal_year_details, tax_deducted, vouchers
)
elif party_type == 'Customer':
if tax_deducted:
# if already TCS is charged, then amount will be calculated based on 'Previous Row Total'
tax_amount = 0
else:
# if no TCS has been charged in FY,
# then chargeable value is "prev invoices + advances" value which cross the threshold
tax_amount = get_tcs_amount(
parties, inv, tax_details,
fiscal_year_details, vouchers, advance_vouchers
)
return tax_amount, tax_deducted
def get_invoice_vouchers(parties, fiscal_year, company, party_type='Supplier'):
dr_or_cr = 'credit' if party_type == 'Supplier' else 'debit'
filters = {
dr_or_cr: ['>', 0],
'company': company,
'party_type': party_type,
'party': ['in', parties],
'fiscal_year': fiscal_year,
'is_opening': 'No',
'is_cancelled': 0
}
return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck="voucher_no") or [""]
def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None, to_date=None, party_type='Supplier'):
# for advance vouchers, debit and credit is reversed
dr_or_cr = 'debit' if party_type == 'Supplier' else 'credit'
filters = {
dr_or_cr: ['>', 0],
'is_opening': 'No',
'is_cancelled': 0,
'party_type': party_type,
'party': ['in', parties],
'against_voucher': ['is', 'not set']
}
if fiscal_year:
filters['fiscal_year'] = fiscal_year
if company:
filters['company'] = company
if from_date and to_date:
filters['posting_date'] = ['between', (from_date, to_date)]
return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck='voucher_no') or [""]
def get_deducted_tax(taxable_vouchers, fiscal_year, tax_details):
# check if TDS / TCS account is already charged on taxable vouchers
filters = {
'is_cancelled': 0,
'credit': ['>', 0],
'fiscal_year': fiscal_year,
'account': tax_details.account_head,
'voucher_no': ['in', taxable_vouchers],
}
field = "sum(credit)"
return frappe.db.get_value('GL Entry', filters, field) or 0.0
def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers):
tds_amount = 0
supp_credit_amt = frappe.db.get_value('Purchase Invoice', {
'name': ('in', vouchers), 'docstatus': 1, 'apply_tds': 1
}, 'sum(net_total)') or 0.0
supp_jv_credit_amt = frappe.db.get_value('Journal Entry Account', {
'parent': ('in', vouchers), 'docstatus': 1,
'party': ('in', parties), 'reference_type': ('!=', 'Purchase Invoice')
}, 'sum(credit_in_account_currency)') or 0.0
supp_credit_amt += supp_jv_credit_amt
supp_credit_amt += inv.net_total
debit_note_amount = get_debit_note_amount(parties, fiscal_year_details, inv.company)
supp_credit_amt -= debit_note_amount
threshold = tax_details.get('threshold', 0)
cumulative_threshold = tax_details.get('cumulative_threshold', 0)
if ((threshold and supp_credit_amt >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)):
if ldc and is_valid_certificate(
ldc.valid_from, ldc.valid_upto,
inv.posting_date, tax_deducted,
inv.net_total, ldc.certificate_limit
):
tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details)
else:
tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0
return tds_amount
def get_advance_vouchers(suppliers, fiscal_year=None, company=None, from_date=None, to_date=None):
condition = "fiscal_year=%s" % fiscal_year
def get_tcs_amount(parties, inv, tax_details, fiscal_year_details, vouchers, adv_vouchers):
tcs_amount = 0
fiscal_year, _, _ = fiscal_year_details
# sum of debit entries made from sales invoices
invoiced_amt = frappe.db.get_value('GL Entry', {
'is_cancelled': 0,
'party': ['in', parties],
'company': inv.company,
'voucher_no': ['in', vouchers],
}, 'sum(debit)') or 0.0
# sum of credit entries made from PE / JV with unset 'against voucher'
advance_amt = frappe.db.get_value('GL Entry', {
'is_cancelled': 0,
'party': ['in', parties],
'company': inv.company,
'voucher_no': ['in', adv_vouchers],
}, 'sum(credit)') or 0.0
# sum of credit entries made from sales invoice
credit_note_amt = frappe.db.get_value('GL Entry', {
'is_cancelled': 0,
'credit': ['>', 0],
'party': ['in', parties],
'fiscal_year': fiscal_year,
'company': inv.company,
'voucher_type': 'Sales Invoice',
}, 'sum(credit)') or 0.0
cumulative_threshold = tax_details.get('cumulative_threshold', 0)
current_invoice_total = get_invoice_total_without_tcs(inv, tax_details)
total_invoiced_amt = current_invoice_total + invoiced_amt + advance_amt - credit_note_amt
if ((cumulative_threshold and total_invoiced_amt >= cumulative_threshold)):
chargeable_amt = total_invoiced_amt - cumulative_threshold
tcs_amount = chargeable_amt * tax_details.rate / 100 if chargeable_amt > 0 else 0
return tcs_amount
def get_invoice_total_without_tcs(inv, tax_details):
tcs_tax_row = [d for d in inv.taxes if d.account_head == tax_details.account_head]
tcs_tax_row_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0
return inv.grand_total - tcs_tax_row_amount
def get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total):
tds_amount = 0
limit_consumed = frappe.db.get_value('Purchase Invoice', {
'supplier': ('in', parties),
'apply_tds': 1,
'docstatus': 1
}, 'sum(net_total)')
if is_valid_certificate(
ldc.valid_from, ldc.valid_upto,
posting_date, limit_consumed,
net_total, ldc.certificate_limit
):
tds_amount = get_ltds_amount(net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details)
return tds_amount
def get_debit_note_amount(suppliers, fiscal_year_details, company=None):
_, year_start_date, year_end_date = fiscal_year_details
filters = {
'supplier': ['in', suppliers],
'is_return': 1,
'docstatus': 1,
'posting_date': ['between', (year_start_date, year_end_date)]
}
fields = ['abs(sum(net_total)) as net_total']
if company:
condition += "and company =%s" % (company)
if from_date and to_date:
condition += "and posting_date between %s and %s" % (from_date, to_date)
filters['company'] = company
## Appending the same supplier again if length of suppliers list is 1
## since tuple of single element list contains None, For example ('Test Supplier 1', )
## and the below query fails
if len(suppliers) == 1:
suppliers.append(suppliers[0])
return frappe.db.sql_list("""
select distinct voucher_no
from `tabGL Entry`
where party in %s and %s and debit > 0
and is_opening = 'No'
""", (tuple(suppliers), condition)) or []
def get_debit_note_amount(suppliers, year_start_date, year_end_date, company=None):
condition = "and 1=1"
if company:
condition = " and company=%s " % company
if len(suppliers) == 1:
suppliers.append(suppliers[0])
return flt(frappe.db.sql("""
select abs(sum(net_total))
from `tabPurchase Invoice`
where supplier in %s and is_return=1 and docstatus=1
and posting_date between %s and %s %s
""", (tuple(suppliers), year_start_date, year_end_date, condition)))
return frappe.get_all('Purchase Invoice', filters, fields)[0].get('net_total') or 0.0
def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details):
if current_amount < (certificate_limit - deducted_amount):
@ -227,4 +359,4 @@ def is_valid_certificate(valid_from, valid_upto, posting_date, deducted_amount,
certificate_limit > deducted_amount):
valid = True
return valid
return valid

View File

@ -9,7 +9,7 @@ from frappe.utils import today
from erpnext.accounts.utils import get_fiscal_year
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
test_dependencies = ["Supplier Group"]
test_dependencies = ["Supplier Group", "Customer Group"]
class TestTaxWithholdingCategory(unittest.TestCase):
@classmethod
@ -18,6 +18,9 @@ class TestTaxWithholdingCategory(unittest.TestCase):
create_records()
create_tax_with_holding_category()
def tearDown(self):
cancel_invoices()
def test_cumulative_threshold_tds(self):
frappe.db.set_value("Supplier", "Test TDS Supplier", "tax_withholding_category", "Cumulative Threshold TDS")
invoices = []
@ -128,9 +131,59 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in invoices:
d.cancel()
def test_cumulative_threshold_tcs(self):
frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS")
invoices = []
# create invoices for lower than single threshold tax rate
for _ in range(2):
si = create_sales_invoice(customer = "Test TCS Customer")
si.submit()
invoices.append(si)
# create another invoice whose total when added to previously created invoice,
# surpasses cumulative threshhold
si = create_sales_invoice(customer = "Test TCS Customer", rate=12000)
si.submit()
# assert tax collection on total invoice amount created until now
tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC'])
self.assertEqual(tcs_charged, 200)
self.assertEqual(si.grand_total, 12200)
invoices.append(si)
# TCS is already collected once, so going forward system will collect TCS on every invoice
si = create_sales_invoice(customer = "Test TCS Customer", rate=5000)
si.submit()
tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC'])
self.assertEqual(tcs_charged, 500)
invoices.append(si)
#delete invoices to avoid clashing
for d in invoices:
d.cancel()
def cancel_invoices():
purchase_invoices = frappe.get_all("Purchase Invoice", {
'supplier': ['in', ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']],
'docstatus': 1
}, pluck="name")
sales_invoices = frappe.get_all("Sales Invoice", {
'customer': 'Test TCS Customer',
'docstatus': 1
}, pluck="name")
for d in purchase_invoices:
frappe.get_doc('Purchase Invoice', d).cancel()
for d in sales_invoices:
frappe.get_doc('Sales Invoice', d).cancel()
def create_purchase_invoice(**args):
# return sales invoice doc object
item = frappe.get_doc('Item', {'item_name': 'TDS Item'})
item = frappe.db.get_value('Item', {'item_name': 'TDS Item'}, "name")
args = frappe._dict(args)
pi = frappe.get_doc({
@ -145,7 +198,7 @@ def create_purchase_invoice(**args):
"taxes": [],
"items": [{
'doctype': 'Purchase Invoice Item',
'item_code': item.name,
'item_code': item,
'qty': args.qty or 1,
'rate': args.rate or 10000,
'cost_center': 'Main - _TC',
@ -156,6 +209,33 @@ def create_purchase_invoice(**args):
pi.save()
return pi
def create_sales_invoice(**args):
# return sales invoice doc object
item = frappe.db.get_value('Item', {'item_name': 'TCS Item'}, "name")
args = frappe._dict(args)
si = frappe.get_doc({
"doctype": "Sales Invoice",
"posting_date": today(),
"customer": args.customer,
"company": '_Test Company',
"taxes_and_charges": "",
"currency": "INR",
"debit_to": "Debtors - _TC",
"taxes": [],
"items": [{
'doctype': 'Sales Invoice Item',
'item_code': item,
'qty': args.qty or 1,
'rate': args.rate or 10000,
'cost_center': 'Main - _TC',
'expense_account': 'Cost of Goods Sold - _TC'
}]
})
si.save()
return si
def create_records():
# create a new suppliers
for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']:
@ -168,7 +248,17 @@ def create_records():
"doctype": "Supplier",
}).insert()
# create an item
for name in ['Test TCS Customer']:
if frappe.db.exists('Customer', name):
continue
frappe.get_doc({
"customer_group": "_Test Customer Group",
"customer_name": name,
"doctype": "Customer"
}).insert()
# create item
if not frappe.db.exists('Item', "TDS Item"):
frappe.get_doc({
"doctype": "Item",
@ -178,7 +268,16 @@ def create_records():
"is_stock_item": 0,
}).insert()
# create an account
if not frappe.db.exists('Item', "TCS Item"):
frappe.get_doc({
"doctype": "Item",
"item_code": "TCS Item",
"item_name": "TCS Item",
"item_group": "All Item Groups",
"is_stock_item": 1
}).insert()
# create tds account
if not frappe.db.exists("Account", "TDS - _TC"):
frappe.get_doc({
'doctype': 'Account',
@ -189,6 +288,17 @@ def create_records():
'root_type': 'Asset'
}).insert()
# create tcs account
if not frappe.db.exists("Account", "TCS - _TC"):
frappe.get_doc({
'doctype': 'Account',
'company': '_Test Company',
'account_name': 'TCS',
'parent_account': 'Duties and Taxes - _TC',
'report_type': 'Balance Sheet',
'root_type': 'Liability'
}).insert()
def create_tax_with_holding_category():
fiscal_year = get_fiscal_year(today(), company="_Test Company")[0]
@ -210,6 +320,23 @@ def create_tax_with_holding_category():
}]
}).insert()
if not frappe.db.exists("Tax Withholding Category", "Cumulative Threshold TCS"):
frappe.get_doc({
"doctype": "Tax Withholding Category",
"name": "Cumulative Threshold TCS",
"category_name": "10% TCS",
"rates": [{
'fiscal_year': fiscal_year,
'tax_withholding_rate': 10,
'single_threshold': 0,
'cumulative_threshold': 30000.00
}],
"accounts": [{
'company': '_Test Company',
'account': 'TCS - _TC'
}]
}).insert()
# Single thresold
if not frappe.db.exists("Tax Withholding Category", "Single Threshold TDS"):
frappe.get_doc({

View File

@ -196,7 +196,7 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
if not round_off_gle:
for k in ["voucher_type", "voucher_no", "company",
"posting_date", "remarks", "is_opening"]:
"posting_date", "remarks"]:
round_off_gle[k] = gl_map[0][k]
round_off_gle.update({
@ -208,6 +208,7 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
"cost_center": round_off_cost_center,
"party_type": None,
"party": None,
"is_opening": "No",
"against_voucher_type": None,
"against_voucher": None
})

View File

@ -897,17 +897,18 @@ def repost_gle_for_stock_vouchers(stock_vouchers, posting_date, company=None, wa
frappe.db.sql("""delete from `tabGL Entry`
where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no))
if not warehouse_account:
warehouse_account = get_warehouse_account_map(company)
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit")) or 2
gle = get_voucherwise_gl_entries(stock_vouchers, posting_date)
for voucher_type, voucher_no in stock_vouchers:
existing_gle = gle.get((voucher_type, voucher_no), [])
voucher_obj = frappe.get_cached_doc(voucher_type, voucher_no)
expected_gle = voucher_obj.get_gl_entries(warehouse_account)
if expected_gle:
if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle):
if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
_delete_gl_entries(voucher_type, voucher_no)
voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
else:
@ -953,16 +954,17 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date):
return gl_entries
def compare_existing_and_expected_gle(existing_gle, expected_gle):
def compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
matched = True
for entry in expected_gle:
account_existed = False
for e in existing_gle:
if entry.account == e.account:
account_existed = True
if entry.account == e.account and entry.against_account == e.against_account \
and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) \
and (entry.debit != e.debit or entry.credit != e.credit):
if (entry.account == e.account and entry.against_account == e.against_account
and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center)
and ( flt(entry.debit, precision) != flt(e.debit, precision) or
flt(entry.credit, precision) != flt(e.credit, precision))):
matched = False
break
if not account_existed:

View File

@ -19,7 +19,6 @@
],
"fields": [
{
"depends_on": "eval:!doc.asset_category_name",
"fieldname": "asset_category_name",
"fieldtype": "Data",
"in_list_view": 1,
@ -67,7 +66,7 @@
}
],
"links": [],
"modified": "2021-01-22 12:31:14.425319",
"modified": "2021-02-24 15:05:38.621803",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Category",

View File

@ -117,7 +117,7 @@ def call_mws_method(mws_method, *args, **kwargs):
return response
except Exception as e:
delay = math.pow(4, x) * 125
frappe.log_error(message=e, title=str(mws_method))
frappe.log_error(message=e, title=f'Method "{mws_method.__name__}" failed')
time.sleep(delay)
continue

View File

@ -393,6 +393,15 @@ payment_gateway_enabled = "erpnext.accounts.utils.create_payment_gateway_account
communication_doctypes = ["Customer", "Supplier"]
accounting_dimension_doctypes = ["GL Entry", "Sales Invoice", "Purchase Invoice", "Payment Entry", "Asset",
"Expense Claim", "Expense Claim Detail", "Expense Taxes and Charges", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note",
"Sales Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item",
"Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule",
"Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation",
"Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", "Subscription",
"Subscription Plan"
]
regional_overrides = {
'France': {
'erpnext.tests.test_regional.test_method': 'erpnext.regional.france.utils.test_method'

View File

@ -138,7 +138,7 @@
"idx": 1,
"issingle": 1,
"links": [],
"modified": "2020-08-27 14:30:28.995324",
"modified": "2021-02-25 12:31:14.947865",
"modified_by": "Administrator",
"module": "HR",
"name": "HR Settings",
@ -155,5 +155,6 @@
}
],
"sort_field": "modified",
"sort_order": "ASC"
}
"sort_order": "ASC",
"track_changes": 1
}

View File

@ -3,7 +3,7 @@
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"allow_rename": 1,
"autoname": "field:skill_name",
"beta": 0,
"creation": "2019-04-16 09:54:39.486915",
@ -56,7 +56,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-04-16 09:55:00.536328",
"modified": "2021-02-24 09:55:00.536328",
"modified_by": "Administrator",
"module": "HR",
"name": "Skill",
@ -110,4 +110,4 @@
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}
}

View File

@ -1,12 +1,13 @@
import frappe
from frappe import _
from frappe.utils import getdate, get_time
from erpnext.stock.stock_ledger import update_entries_after
from erpnext.accounts.utils import update_gl_entries_after
def execute():
frappe.reload_doc('stock', 'doctype', 'repost_item_valuation')
reposting_project_deployed_on = frappe.db.get_value("DocType", "Repost Item Valuation", "creation")
reposting_project_deployed_on = get_creation_time()
data = frappe.db.sql('''
SELECT
@ -40,7 +41,14 @@ def execute():
print("Reposting General Ledger Entries...")
posting_date = getdate(reposting_project_deployed_on)
posting_time = get_time(reposting_project_deployed_on)
for row in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}):
update_gl_entries_after('2020-12-25', '01:58:55', company=row.name)
update_gl_entries_after(posting_date, posting_time, company=row.name)
frappe.db.auto_commit_on_many_writes = 0
def get_creation_time():
return frappe.db.sql(''' SELECT create_time FROM
INFORMATION_SCHEMA.TABLES where TABLE_NAME = "tabRepost Item Valuation" ''', as_list=1)[0][0]

View File

@ -75,24 +75,27 @@ frappe.ui.form.on("Project", {
frm.add_custom_button(__('Cancelled'), () => {
frm.events.set_status(frm, 'Cancelled');
}, __('Set Status'));
}
if (frappe.model.can_read("Task")) {
frm.add_custom_button(__("Gantt Chart"), function () {
frappe.route_options = {
"project": frm.doc.name
};
frappe.set_route("List", "Task", "Gantt");
});
frm.add_custom_button(__("Kanban Board"), () => {
frappe.call('erpnext.projects.doctype.project.project.create_kanban_board_if_not_exists', {
project: frm.doc.project_name
}).then(() => {
frappe.set_route('List', 'Task', 'Kanban', frm.doc.project_name);
if (frappe.model.can_read("Task")) {
frm.add_custom_button(__("Gantt Chart"), function () {
frappe.route_options = {
"project": frm.doc.name
};
frappe.set_route("List", "Task", "Gantt");
});
});
frm.add_custom_button(__("Kanban Board"), () => {
frappe.call('erpnext.projects.doctype.project.project.create_kanban_board_if_not_exists', {
project: frm.doc.project_name
}).then(() => {
frappe.set_route('List', 'Task', 'Kanban', frm.doc.project_name);
});
});
}
}
},
create_duplicate: function(frm) {
@ -135,4 +138,4 @@ function open_form(frm, doctype, child_doctype, parentfield) {
frappe.ui.form.make_quick_entry(doctype, null, null, new_doc);
});
}
}

View File

@ -37,7 +37,7 @@ class TestProject(unittest.TestCase):
task1 = task_exists("Test Template Task Parent")
if not task1:
task1 = create_task(subject="Test Template Task Parent", is_group=1, is_template=1, begin=1, duration=1)
task1 = create_task(subject="Test Template Task Parent", is_group=1, is_template=1, begin=1, duration=4)
task2 = task_exists("Test Template Task Child 1")
if not task2:
@ -52,7 +52,7 @@ class TestProject(unittest.TestCase):
tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name', 'parent_task'], dict(project=project.name), order_by='creation asc')
self.assertEqual(tasks[0].subject, 'Test Template Task Parent')
self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 1, 1))
self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 1, 4))
self.assertEqual(tasks[1].subject, 'Test Template Task Child 1')
self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 1, 3))

View File

@ -20,6 +20,7 @@
},
{
"columns": 6,
"fetch_from": "task.subject",
"fieldname": "subject",
"fieldtype": "Read Only",
"in_list_view": 1,
@ -28,7 +29,7 @@
],
"istable": 1,
"links": [],
"modified": "2021-01-07 15:13:40.995071",
"modified": "2021-02-24 15:18:49.095071",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project Template Task",

View File

@ -30,6 +30,7 @@ class Task(NestedSet):
def validate(self):
self.validate_dates()
self.validate_parent_expected_end_date()
self.validate_parent_project_dates()
self.validate_progress()
self.validate_status()
@ -45,6 +46,12 @@ class Task(NestedSet):
frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \
frappe.bold("Actual End Date")))
def validate_parent_expected_end_date(self):
if self.parent_task:
parent_exp_end_date = frappe.db.get_value("Task", self.parent_task, "exp_end_date")
if parent_exp_end_date and getdate(self.get("exp_end_date")) > getdate(parent_exp_end_date):
frappe.throw(_("Expected End Date should be less than or equal to parent task's Expected End Date {0}.").format(getdate(parent_exp_end_date)))
def validate_parent_project_dates(self):
if not self.project or frappe.flags.in_test:
return

View File

@ -513,6 +513,7 @@ erpnext.utils.update_child_items = function(opts) {
}, {
fieldtype:'Currency',
fieldname:"rate",
options: "currency",
default: 0,
read_only: 0,
in_list_view: 1,

View File

@ -10,6 +10,7 @@ import sys
import json
import base64
import frappe
import six
import traceback
import io
from frappe import _, bold
@ -108,11 +109,13 @@ def get_party_details(address_name):
pincode = 999999
return frappe._dict(dict(
gstin=d.gstin, legal_name=d.address_title,
location=d.city, pincode=d.pincode,
gstin=d.gstin,
legal_name=sanitize_for_json(d.address_title),
location=sanitize_for_json(d.city),
pincode=d.pincode,
state_code=d.gst_state_number,
address_line1=d.address_line1,
address_line2=d.address_line2
address_line1=sanitize_for_json(d.address_line1),
address_line2=sanitize_for_json(d.address_line2)
))
def get_gstin_details(gstin):
@ -146,8 +149,11 @@ def get_overseas_address_details(address_name):
)
return frappe._dict(dict(
gstin='URP', legal_name=address_title, location=city,
address_line1=address_line1, address_line2=address_line2,
gstin='URP',
legal_name=sanitize_for_json(address_title),
location=city,
address_line1=sanitize_for_json(address_line1),
address_line2=sanitize_for_json(address_line2),
pincode=999999, state_code=96, place_of_supply=96
))
@ -160,7 +166,7 @@ def get_item_list(invoice):
item.update(d.as_dict())
item.sr_no = d.idx
item.description = json.dumps(d.item_name)[1:-1]
item.description = sanitize_for_json(d.item_name)
item.qty = abs(item.qty)
item.discount_amount = 0
@ -326,7 +332,7 @@ def make_einvoice(invoice):
buyer_details = get_overseas_address_details(invoice.customer_address)
else:
buyer_details = get_party_details(invoice.customer_address)
place_of_supply = get_place_of_supply(invoice, invoice.doctype) or invoice.billing_address_gstin
place_of_supply = get_place_of_supply(invoice, invoice.doctype) or sanitize_for_json(invoice.billing_address_gstin)
place_of_supply = place_of_supply[:2]
buyer_details.update(dict(place_of_supply=place_of_supply))
@ -356,7 +362,7 @@ def make_einvoice(invoice):
period_details=period_details, prev_doc_details=prev_doc_details,
export_details=export_details, eway_bill_details=eway_bill_details
)
einvoice = json.loads(einvoice)
einvoice = safe_json_load(einvoice)
validations = json.loads(read_json('einv_validation'))
errors = validate_einvoice(validations, einvoice)
@ -371,6 +377,18 @@ def make_einvoice(invoice):
return einvoice
def safe_json_load(json_string):
JSONDecodeError = ValueError if six.PY2 else json.JSONDecodeError
try:
return json.loads(json_string)
except JSONDecodeError as e:
# print a snippet of 40 characters around the location where error occured
pos = e.pos
start, end = max(0, pos-20), min(len(json_string)-1, pos+20)
snippet = json_string[start:end]
frappe.throw(_("Error in input data. Please check for any special characters near following input: <br> {}").format(snippet))
def validate_einvoice(validations, einvoice, errors=[]):
for fieldname, field_validation in validations.items():
value = einvoice.get(fieldname, None)
@ -798,6 +816,13 @@ class GSPConnector():
self.invoice.flags.ignore_validate = True
self.invoice.save()
def sanitize_for_json(string):
"""Escape JSON specific characters from a string."""
# json.dumps adds double-quotes to the string. Indexing to remove them.
return json.dumps(string)[1:-1]
@frappe.whitelist()
def get_einvoice(doctype, docname):
invoice = frappe.get_doc(doctype, docname)

View File

@ -16,6 +16,8 @@
"customer_name",
"gender",
"customer_type",
"pan",
"tax_withholding_category",
"default_bank_account",
"lead_name",
"image",
@ -479,13 +481,25 @@
"fieldname": "dn_required",
"fieldtype": "Check",
"label": "Allow Sales Invoice Creation Without Delivery Note"
},
{
"fieldname": "pan",
"fieldtype": "Data",
"label": "PAN"
},
{
"fieldname": "tax_withholding_category",
"fieldtype": "Link",
"label": "Tax Withholding Category",
"options": "Tax Withholding Category"
}
],
"icon": "fa fa-user",
"idx": 363,
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-01-06 19:35:25.418017",
"modified": "2021-01-28 12:54:57.258959",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",

View File

@ -498,10 +498,11 @@ erpnext.PointOfSale.Controller = class {
async on_cart_update(args) {
frappe.dom.freeze();
let item_row = undefined;
try {
let { field, value, item } = args;
const { item_code, batch_no, serial_no, uom } = item;
let item_row = this.get_item_from_frm(item_code, batch_no, uom);
item_row = this.get_item_from_frm(item_code, batch_no, uom);
const item_selected_from_selector = field === 'qty' && value === "+1"
@ -553,10 +554,12 @@ erpnext.PointOfSale.Controller = class {
this.check_serial_batch_selection_needed(item_row) && this.edit_item_details_of(item_row);
this.update_cart_html(item_row);
}
} catch (error) {
console.log(error);
} finally {
frappe.dom.unfreeze();
return item_row;
}
}

View File

@ -472,7 +472,8 @@ erpnext.PointOfSale.ItemCart = class {
if (!frm) frm = this.events.get_frm();
this.render_net_total(frm.doc.net_total);
this.render_grand_total(frm.doc.grand_total);
const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? frm.doc.grand_total : frm.doc.rounded_total;
this.render_grand_total(grand_total);
const taxes = frm.doc.taxes.map(t => {
return {

View File

@ -152,6 +152,10 @@ erpnext.PointOfSale.ItemSelector = class {
this.item_group_field.toggle_label(false);
}
set_search_value(value) {
$(this.search_field.$input[0]).val(value).trigger("input");
}
bind_events() {
const me = this;
window.onScan = onScan;
@ -159,7 +163,7 @@ erpnext.PointOfSale.ItemSelector = class {
onScan: (sScancode) => {
if (this.search_field && this.$component.is(':visible')) {
this.search_field.set_focus();
$(this.search_field.$input[0]).val(sScancode).trigger("input");
this.set_search_value(sScancode);
this.barcode_scanned = true;
}
}
@ -178,6 +182,7 @@ erpnext.PointOfSale.ItemSelector = class {
uom = uom === "undefined" ? undefined : uom;
me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom }});
me.set_search_value('');
});
this.search_field.$input.on('input', (e) => {

View File

@ -223,7 +223,8 @@ erpnext.PointOfSale.Payment = class {
if (success) {
title = __("Payment Received");
if (amount >= doc.grand_total) {
const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? doc.grand_total : doc.rounded_total;
if (amount >= grand_total) {
frappe.dom.unfreeze();
message = __("Payment of {0} received successfully.", [format_currency(amount, doc.currency, 0)]);
this.events.submit_invoice();
@ -243,7 +244,8 @@ erpnext.PointOfSale.Payment = class {
auto_set_remaining_amount() {
const doc = this.events.get_frm().doc;
const remaining_amount = doc.grand_total - doc.paid_amount;
const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? doc.grand_total : doc.rounded_total;
const remaining_amount = grand_total - doc.paid_amount;
const current_value = this.selected_mode ? this.selected_mode.get_value() : undefined;
if (!current_value && remaining_amount > 0 && this.selected_mode) {
this.selected_mode.set_value(remaining_amount);
@ -389,7 +391,7 @@ erpnext.PointOfSale.Payment = class {
}
attach_cash_shortcuts(doc) {
const grand_total = doc.grand_total;
const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? doc.grand_total : doc.rounded_total;
const currency = doc.currency;
const shortcuts = this.get_cash_shortcuts(flt(grand_total));
@ -499,7 +501,8 @@ erpnext.PointOfSale.Payment = class {
update_totals_section(doc) {
if (!doc) doc = this.events.get_frm().doc;
const paid_amount = doc.paid_amount;
const remaining = doc.grand_total - doc.paid_amount;
const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? doc.grand_total : doc.rounded_total;
const remaining = grand_total - doc.paid_amount;
const change = doc.change_amount || remaining <= 0 ? -1 * remaining : undefined;
const currency = doc.currency;
const label = change ? __('Change') : __('To Be Paid');
@ -507,7 +510,7 @@ erpnext.PointOfSale.Payment = class {
this.$totals.html(
`<div class="col">
<div class="total-label">Grand Total</div>
<div class="value">${format_currency(doc.grand_total, currency)}</div>
<div class="value">${format_currency(grand_total, currency)}</div>
</div>
<div class="seperator-y"></div>
<div class="col">

View File

@ -38,7 +38,7 @@ def get_warehouse_account_map(company=None):
frappe.flags.warehouse_account_map[company] = warehouse_account
else:
frappe.flags.warehouse_account_map = warehouse_account
return frappe.flags.warehouse_account_map.get(company) or frappe.flags.warehouse_account_map
def get_warehouse_account(warehouse, warehouse_account=None):
@ -64,6 +64,10 @@ def get_warehouse_account(warehouse, warehouse_account=None):
if not account and warehouse.company:
account = get_company_default_inventory_account(warehouse.company)
if not account and warehouse.company:
account = frappe.db.get_value('Account',
{'account_type': 'Stock', 'is_group': 0, 'company': warehouse.company}, 'name')
if not account and warehouse.company and not warehouse.is_group:
frappe.throw(_("Please set Account in Warehouse {0} or Default Inventory Account in Company {1}")
.format(warehouse.name, warehouse.company))

View File

@ -521,8 +521,7 @@
"fieldname": "has_variants",
"fieldtype": "Check",
"in_standard_filter": 1,
"label": "Has Variants",
"no_copy": 1
"label": "Has Variants"
},
{
"default": "Item Attribute",
@ -538,7 +537,6 @@
"fieldtype": "Table",
"hidden": 1,
"label": "Attributes",
"no_copy": 1,
"options": "Item Variant Attribute"
},
{
@ -1068,7 +1066,7 @@
"index_web_pages_for_search": 1,
"links": [],
"max_attachments": 1,
"modified": "2021-01-25 20:49:50.222976",
"modified": "2021-02-18 14:00:19.668049",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",

View File

@ -295,7 +295,8 @@ class PurchaseReceipt(BuyingController):
"against": warehouse_account[d.warehouse]["account"],
"cost_center": d.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(amount["base_amount"]),
"credit": (flt(amount["base_amount"]) if (amount["base_amount"] or
account_currency!=self.company_currency) else flt(amount["amount"])),
"credit_in_account_currency": flt(amount["amount"]),
"project": d.project
}, item=d))

View File

@ -276,9 +276,10 @@ class StockEntry(StockController):
item_wise_qty.setdefault(d.item_code, []).append(d.qty)
for item_code, qty_list in iteritems(item_wise_qty):
if self.fg_completed_qty != sum(qty_list):
total = flt(sum(qty_list), frappe.get_precision("Stock Entry Detail", "qty"))
if self.fg_completed_qty != total:
frappe.throw(_("The finished product {0} quantity {1} and For Quantity {2} cannot be different")
.format(frappe.bold(item_code), frappe.bold(sum(qty_list)), frappe.bold(self.fg_completed_qty)))
.format(frappe.bold(item_code), frappe.bold(total), frappe.bold(self.fg_completed_qty)))
def validate_difference_account(self):
if not cint(erpnext.is_perpetual_inventory_enabled(self.company)):

View File

@ -49,8 +49,8 @@ frappe.ui.form.on("Issue", {
},
refresh: function (frm) {
if (frm.doc.status !== "Closed" && frm.doc.agreement_status === "Ongoing") {
if (frm.doc.service_level_agreement) {
if (frm.doc.status !== "Closed") {
if (frm.doc.service_level_agreement && frm.doc.agreement_status === "Ongoing") {
frappe.call({
"method": "frappe.client.get",
args: {