Merge branch 'develop' of https://github.com/frappe/erpnext into project_filter_search_fields
This commit is contained in:
commit
b5819c74c8
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
||||
|
@ -10,6 +10,14 @@ frappe.ui.form.on('Subscription', {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
frm.set_query('cost_center', function() {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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({
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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:
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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]
|
@ -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);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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) => {
|
||||
|
@ -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">
|
||||
|
@ -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))
|
||||
|
@ -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",
|
||||
|
@ -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))
|
||||
|
@ -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)):
|
||||
|
@ -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: {
|
||||
|
Loading…
x
Reference in New Issue
Block a user