Merge branch 'version-13-hotfix' into email-digest

This commit is contained in:
Anupam Kumar
2021-07-28 18:24:29 +05:30
committed by GitHub
1203 changed files with 31372 additions and 27391 deletions

View File

@@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides
from frappe.utils import getdate
__version__ = '13.0.0-dev'
__version__ = '13.2.0'
def get_default_company(user=None):
'''Get default company for user'''

View File

@@ -33,6 +33,8 @@ def get_shipping_address(company, address = None):
if address and frappe.db.get_value('Dynamic Link',
{'parent': address, 'link_name': company}):
filters.append(["Address", "name", "=", address])
if not address:
filters.append(["Address", "is_shipping_address", "=", 1])
address = frappe.get_all("Address", filters=filters, fields=fields) or {}

View File

@@ -41,7 +41,7 @@ def build_conditions(process_type, account, company):
if account:
conditions += "AND %s='%s'"%(deferred_account, account)
elif company:
conditions += "AND p.company='%s'"%(company)
conditions += f"AND p.company = {frappe.db.escape(company)}"
return conditions
@@ -263,6 +263,9 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
amount, base_amount = calculate_amount(doc, item, last_gl_entry,
total_days, total_booking_days, account_currency)
if not amount:
return
if via_journal_entry:
book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount,
base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry)
@@ -298,17 +301,21 @@ def process_deferred_accounting(posting_date=None):
start_date = add_months(today(), -1)
end_date = add_days(today(), -1)
for record_type in ('Income', 'Expense'):
doc = frappe.get_doc(dict(
doctype='Process Deferred Accounting',
posting_date=posting_date,
start_date=start_date,
end_date=end_date,
type=record_type
))
companies = frappe.get_all('Company')
doc.insert()
doc.submit()
for company in companies:
for record_type in ('Income', 'Expense'):
doc = frappe.get_doc(dict(
doctype='Process Deferred Accounting',
company=company.name,
posting_date=posting_date,
start_date=start_date,
end_date=end_date,
type=record_type
))
doc.insert()
doc.submit()
def make_gl_entries(doc, credit_account, debit_account, against,
amount, base_amount, posting_date, project, account_currency, cost_center, item, deferred_process=None):
@@ -360,12 +367,10 @@ def make_gl_entries(doc, credit_account, debit_account, against,
frappe.flags.deferred_accounting_error = True
def send_mail(deferred_process):
title = _("Error while processing deferred accounting for {0}".format(deferred_process))
content = _("""
Deferred accounting failed for some invoices:
Please check Process Deferred Accounting {0}
and submit manually after resolving errors
""").format(get_link_to_form('Process Deferred Accounting', deferred_process))
title = _("Error while processing deferred accounting for {0}").format(deferred_process)
link = get_link_to_form('Process Deferred Accounting', deferred_process)
content = _("Deferred accounting failed for some invoices:") + "\n"
content += _("Please check Process Deferred Accounting {0} and submit manually after resolving errors.").format(link)
sendmail_to_system_managers(title, content)
def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,

View File

@@ -13,7 +13,7 @@ class BalanceMismatchError(frappe.ValidationError): pass
class Account(NestedSet):
nsm_parent_field = 'parent_account'
def on_update(self):
if frappe.local.flags.ignore_on_update:
if frappe.local.flags.ignore_update_nsm:
return
else:
super(Account, self).on_update()

View File

@@ -57,10 +57,10 @@ def create_charts(company, chart_template=None, existing_company=None, custom_ch
# Rebuild NestedSet HSM tree for Account Doctype
# after all accounts are already inserted.
frappe.local.flags.ignore_on_update = True
frappe.local.flags.ignore_update_nsm = True
_import_accounts(chart, None, None, root_account=True)
rebuild_tree("Account", "parent_account")
frappe.local.flags.ignore_on_update = False
frappe.local.flags.ignore_update_nsm = False
def add_suffix_if_duplicate(account_name, account_number, accounts):
if account_number:

View File

@@ -19,7 +19,7 @@ class AccountingDimension(Document):
def validate(self):
if self.document_type in core_doctypes_list + ('Accounting Dimension', 'Project',
'Cost Center', 'Accounting Dimension Detail', 'Company') :
'Cost Center', 'Accounting Dimension Detail', 'Company', 'Account') :
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
frappe.throw(msg)
@@ -27,7 +27,7 @@ class AccountingDimension(Document):
exists = frappe.db.get_value("Accounting Dimension", {'document_type': self.document_type}, ['name'])
if exists and self.is_new():
frappe.throw("Document Type already used as a dimension")
frappe.throw(_("Document Type already used as a dimension"))
if not self.is_new():
self.validate_document_type_change()

View File

@@ -7,7 +7,8 @@ import frappe
import unittest
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import delete_accounting_dimension
test_dependencies = ['Cost Center', 'Location', 'Warehouse', 'Department']
class TestAccountingDimension(unittest.TestCase):
def setUp(self):

View File

@@ -9,6 +9,8 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import create_dimension, disable_dimension
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
test_dependencies = ['Location', 'Cost Center', 'Department']
class TestAccountingDimensionFilter(unittest.TestCase):
def setUp(self):
create_dimension()

View File

@@ -10,6 +10,8 @@ from erpnext.accounts.general_ledger import ClosedAccountingPeriod
from erpnext.accounts.doctype.accounting_period.accounting_period import OverlapError
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
test_dependencies = ['Item']
class TestAccountingPeriod(unittest.TestCase):
def test_overlap(self):
ap1 = create_accounting_period(start_date = "2018-04-01",
@@ -38,7 +40,7 @@ def create_accounting_period(**args):
accounting_period.start_date = args.start_date or nowdate()
accounting_period.end_date = args.end_date or add_months(nowdate(), 1)
accounting_period.company = args.company or "_Test Company"
accounting_period.period_name =args.period_name or "_Test_Period_Name_1"
accounting_period.period_name = args.period_name or "_Test_Period_Name_1"
accounting_period.append("closed_documents", {
"document_type": 'Sales Invoice', "closed": 1
})

View File

@@ -7,25 +7,31 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"auto_accounting_for_stock",
"acc_frozen_upto",
"frozen_accounts_modifier",
"determine_address_tax_category_from",
"accounts_transactions_settings_section",
"over_billing_allowance",
"column_break_4",
"credit_controller",
"check_supplier_invoice_uniqueness",
"role_allowed_to_over_bill",
"make_payment_via_journal_entry",
"column_break_11",
"check_supplier_invoice_uniqueness",
"unlink_payment_on_cancellation_of_invoice",
"unlink_advance_payment_on_cancelation_of_order",
"book_asset_depreciation_entry_automatically",
"add_taxes_from_item_tax_template",
"automatically_fetch_payment_terms",
"delete_linked_ledger_entries",
"book_asset_depreciation_entry_automatically",
"unlink_advance_payment_on_cancelation_of_order",
"post_change_gl_entries",
"tax_settings_section",
"determine_address_tax_category_from",
"column_break_19",
"add_taxes_from_item_tax_template",
"period_closing_settings_section",
"acc_frozen_upto",
"frozen_accounts_modifier",
"column_break_4",
"credit_controller",
"deferred_accounting_settings_section",
"automatically_process_deferred_accounting_entry",
"book_deferred_entries_based_on",
"column_break_18",
"automatically_process_deferred_accounting_entry",
"book_deferred_entries_via_journal_entry",
"submit_journal_entries",
"print_settings",
@@ -39,15 +45,6 @@
"use_custom_cash_flow"
],
"fields": [
{
"default": "1",
"description": "If enabled, the system will post accounting entries for inventory automatically",
"fieldname": "auto_accounting_for_stock",
"fieldtype": "Check",
"hidden": 1,
"in_list_view": 1,
"label": "Make Accounting Entry For Every Stock Movement"
},
{
"description": "Accounting entries are frozen up to this date. Nobody can create or modify entries except users with the role specified below",
"fieldname": "acc_frozen_upto",
@@ -93,6 +90,7 @@
"default": "0",
"fieldname": "make_payment_via_journal_entry",
"fieldtype": "Check",
"hidden": 1,
"label": "Make Payment via Journal Entry"
},
{
@@ -226,6 +224,43 @@
"fieldname": "delete_linked_ledger_entries",
"fieldtype": "Check",
"label": "Delete Accounting and Stock Ledger Entries on deletion of Transaction"
},
{
"description": "Users with this role are allowed to over bill above the allowance percentage",
"fieldname": "role_allowed_to_over_bill",
"fieldtype": "Link",
"label": "Role Allowed to Over Bill ",
"options": "Role"
},
{
"fieldname": "period_closing_settings_section",
"fieldtype": "Section Break",
"label": "Period Closing Settings"
},
{
"fieldname": "accounts_transactions_settings_section",
"fieldtype": "Section Break",
"label": "Transactions Settings"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "tax_settings_section",
"fieldtype": "Section Break",
"label": "Tax Settings"
},
{
"fieldname": "column_break_19",
"fieldtype": "Column Break"
},
{
"default": "1",
"description": "If enabled, ledger entries will be posted for change amount in POS transactions",
"fieldname": "post_change_gl_entries",
"fieldtype": "Check",
"label": "Create Ledger Entries for Change Amount"
}
],
"icon": "icon-cog",
@@ -233,7 +268,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-01-05 13:04:00.118892",
"modified": "2021-06-17 20:26:03.721202",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -5,6 +5,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import cint
from frappe.model.document import Document
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
@@ -24,11 +25,11 @@ class AccountsSettings(Document):
def validate_stale_days(self):
if not self.allow_stale and cint(self.stale_days) <= 0:
frappe.msgprint(
"Stale Days should start from 1.", title='Error', indicator='red',
_("Stale Days should start from 1."), title='Error', indicator='red',
raise_exception=1)
def enable_payment_schedule_in_print(self):
show_in_print = cint(self.show_payment_schedule_in_print)
for doctype in ("Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"):
make_property_setter(doctype, "due_date", "print_hide", show_in_print, "Check")
make_property_setter(doctype, "payment_schedule", "print_hide", 0 if show_in_print else 1, "Check")
make_property_setter(doctype, "due_date", "print_hide", show_in_print, "Check", validate_fields_for_doctype=False)
make_property_setter(doctype, "payment_schedule", "print_hide", 0 if show_in_print else 1, "Check", validate_fields_for_doctype=False)

View File

@@ -0,0 +1,197 @@
{
"actions": [],
"creation": "2020-09-12 22:26:19.594367",
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"add_deduct_tax",
"charge_type",
"row_id",
"account_head",
"col_break_1",
"description",
"included_in_paid_amount",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"section_break_8",
"rate",
"section_break_9",
"currency",
"tax_amount",
"total",
"allocated_amount",
"column_break_13",
"base_tax_amount",
"base_total",
"base_allocated_amount"
],
"fields": [
{
"columns": 2,
"fieldname": "charge_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type",
"oldfieldname": "charge_type",
"oldfieldtype": "Select",
"options": "\nActual\nOn Paid Amount\nOn Previous Row Amount\nOn Previous Row Total",
"reqd": 1
},
{
"depends_on": "eval:[\"On Previous Row Amount\", \"On Previous Row Total\"].indexOf(doc.charge_type)!==-1",
"fieldname": "row_id",
"fieldtype": "Data",
"label": "Reference Row #",
"oldfieldname": "row_id",
"oldfieldtype": "Data"
},
{
"columns": 2,
"fieldname": "account_head",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Account Head",
"oldfieldname": "account_head",
"oldfieldtype": "Link",
"options": "Account",
"reqd": 1,
"search_index": 1
},
{
"fieldname": "col_break_1",
"fieldtype": "Column Break",
"width": "50%"
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description",
"oldfieldname": "description",
"oldfieldtype": "Small Text",
"print_width": "300px",
"reqd": 1,
"width": "300px"
},
{
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"default": ":Company",
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"oldfieldname": "cost_center_other_charges",
"oldfieldtype": "Link",
"options": "Cost Center"
},
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_8",
"fieldtype": "Section Break"
},
{
"columns": 2,
"fieldname": "rate",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Rate",
"oldfieldname": "rate",
"oldfieldtype": "Currency"
},
{
"fieldname": "section_break_9",
"fieldtype": "Section Break"
},
{
"columns": 2,
"fieldname": "tax_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount",
"options": "currency"
},
{
"columns": 2,
"fieldname": "total",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Total",
"options": "currency",
"read_only": 1
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
},
{
"fieldname": "base_tax_amount",
"fieldtype": "Currency",
"label": "Amount (Company Currency)",
"oldfieldname": "tax_amount",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "base_total",
"fieldtype": "Currency",
"label": "Total (Company Currency)",
"oldfieldname": "total",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "add_deduct_tax",
"fieldtype": "Select",
"label": "Add Or Deduct",
"options": "Add\nDeduct",
"reqd": 1
},
{
"default": "0",
"fieldname": "included_in_paid_amount",
"fieldtype": "Check",
"label": "Considered In Paid Amount"
},
{
"fieldname": "allocated_amount",
"fieldtype": "Currency",
"label": "Allocated Amount",
"options": "currency"
},
{
"fieldname": "base_allocated_amount",
"fieldtype": "Currency",
"label": "Allocated Amount (Company Currency)",
"options": "Company:company:default_currency"
},
{
"fetch_from": "account_head.account_currency",
"fieldname": "currency",
"fieldtype": "Link",
"label": "Account Currency",
"options": "Currency",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-06-09 11:46:58.373170",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Advance Taxes and Charges",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "ASC"
}

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class AdvanceTaxesandCharges(Document):
pass

View File

@@ -42,10 +42,9 @@ let add_fields_to_mapping_table = function (frm) {
});
});
frappe.meta.get_docfield("Bank Transaction Mapping", "bank_transaction_field",
frm.doc.name).options = options;
frm.fields_dict.bank_transaction_mapping.grid.refresh();
frm.fields_dict.bank_transaction_mapping.grid.update_docfield_property(
'bank_transaction_field', 'options', options
);
};
erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
@@ -121,4 +120,4 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
plaid_success(token, response) {
frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' });
}
};
};

View File

@@ -78,8 +78,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
if (
frm.doc.bank_account &&
frm.doc.bank_statement_from_date &&
frm.doc.bank_statement_to_date &&
frm.doc.bank_statement_closing_balance
frm.doc.bank_statement_to_date
) {
frm.trigger("render_chart");
frm.trigger("render");

View File

@@ -39,13 +39,13 @@
"depends_on": "eval: doc.bank_account",
"fieldname": "bank_statement_from_date",
"fieldtype": "Date",
"label": "Bank Statement From Date"
"label": "From Date"
},
{
"depends_on": "eval: doc.bank_statement_from_date",
"fieldname": "bank_statement_to_date",
"fieldtype": "Date",
"label": "Bank Statement To Date"
"label": "To Date"
},
{
"fieldname": "column_break_2",
@@ -63,11 +63,10 @@
"depends_on": "eval: doc.bank_statement_to_date",
"fieldname": "bank_statement_closing_balance",
"fieldtype": "Currency",
"label": "Bank Statement Closing Balance",
"label": "Closing Balance",
"options": "Currency"
},
{
"depends_on": "eval: doc.bank_statement_closing_balance",
"fieldname": "section_break_1",
"fieldtype": "Section Break",
"label": "Reconcile"
@@ -90,7 +89,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-02-02 01:35:53.043578",
"modified": "2021-04-21 11:13:49.831769",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Reconciliation Tool",

View File

@@ -239,6 +239,7 @@ frappe.ui.form.on("Bank Statement Import", {
"withdrawal",
"description",
"reference_number",
"bank_account"
],
},
});

View File

@@ -146,7 +146,7 @@
},
{
"depends_on": "eval:!doc.__islocal && !doc.import_file\n",
"description": "Must be a publicly accessible Google Sheets URL",
"description": "Must be a publicly accessible Google Sheets URL and adding Bank Account column is necessary for importing via Google Sheets",
"fieldname": "google_sheets_url",
"fieldtype": "Data",
"label": "Import from Google Sheets"
@@ -202,7 +202,7 @@
],
"hide_toolbar": 1,
"links": [],
"modified": "2021-02-10 19:29:59.027325",
"modified": "2021-05-12 14:17:37.777246",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Statement Import",
@@ -224,4 +224,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View File

@@ -47,6 +47,13 @@ class BankStatementImport(DataImport):
def start_import(self):
preview = frappe.get_doc("Bank Statement Import", self.name).get_preview_from_template(
self.import_file, self.google_sheets_url
)
if 'Bank Account' not in json.dumps(preview['columns']):
frappe.throw(_("Please add the Bank Account column"))
from frappe.core.page.background_jobs.background_jobs import get_info
from frappe.utils.scheduler import is_scheduler_inactive
@@ -67,6 +74,7 @@ class BankStatementImport(DataImport):
data_import=self.name,
bank_account=self.bank_account,
import_file_path=self.import_file,
google_sheets_url=self.google_sheets_url,
bank=self.bank,
template_options=self.template_options,
now=frappe.conf.developer_mode or frappe.flags.in_test,
@@ -90,18 +98,20 @@ def download_errored_template(data_import_name):
data_import = frappe.get_doc("Bank Statement Import", data_import_name)
data_import.export_errored_rows()
def start_import(data_import, bank_account, import_file_path, bank, template_options):
def start_import(data_import, bank_account, import_file_path, google_sheets_url, bank, template_options):
"""This method runs in background job"""
update_mapping_db(bank, template_options)
data_import = frappe.get_doc("Bank Statement Import", data_import)
file = import_file_path if import_file_path else google_sheets_url
import_file = ImportFile("Bank Transaction", file = import_file_path, import_type="Insert New Records")
import_file = ImportFile("Bank Transaction", file = file, import_type="Insert New Records")
data = import_file.raw_data
add_bank_account(data, bank_account)
write_files(import_file, data)
if import_file_path:
add_bank_account(data, bank_account)
write_files(import_file, data)
try:
i = Importer(data_import.reference_doctype, data_import=data_import)

View File

@@ -175,22 +175,24 @@
},
{
"fieldname": "deposit",
"oldfieldname": "debit",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Deposit"
"label": "Deposit",
"oldfieldname": "debit",
"options": "currency"
},
{
"fieldname": "withdrawal",
"oldfieldname": "credit",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Withdrawal"
"label": "Withdrawal",
"oldfieldname": "credit",
"options": "currency"
}
],
"is_submittable": 1,
"links": [],
"modified": "2020-12-30 19:40:54.221070",
"modified": "2021-04-14 17:31:58.963529",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",

View File

@@ -11,6 +11,8 @@ from erpnext.buying.doctype.purchase_order.test_purchase_order import create_pur
from erpnext.accounts.doctype.budget.budget import get_actual_expense, BudgetError
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
test_dependencies = ['Monthly Distribution']
class TestBudget(unittest.TestCase):
def test_monthly_budget_crossed_ignore(self):
set_total_expense_zero(nowdate(), "cost_center")

View File

@@ -54,7 +54,7 @@ class CForm(Document):
frappe.throw(_("Please enter atleast 1 invoice in the table"))
def set_total_invoiced_amount(self):
total = sum([flt(d.grand_total) for d in self.get('invoices')])
total = sum(flt(d.grand_total) for d in self.get('invoices'))
frappe.db.set(self, 'total_invoiced_amount', total)
@frappe.whitelist()

View File

@@ -13,7 +13,8 @@ from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import
from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file, read_xls_file_from_attached_file
class ChartofAccountsImporter(Document):
pass
def validate(self):
validate_accounts(self.import_file)
@frappe.whitelist()
def validate_company(company):
@@ -22,7 +23,7 @@ def validate_company(company):
'allow_account_creation_against_child_company'])
if parent_company and (not allow_account_creation_against_child_company):
msg = _("{} is a child company. ").format(frappe.bold(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'))
@@ -56,7 +57,7 @@ def get_file(file_name):
extension = extension.lstrip(".")
if extension not in ('csv', 'xlsx', 'xls'):
frappe.throw("Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload")
frappe.throw(_("Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload"))
return file_doc, extension
@@ -293,7 +294,7 @@ def validate_accounts(file_name):
accounts_dict = {}
for account in accounts:
accounts_dict.setdefault(account["account_name"], account)
if not hasattr(account, "parent_account"):
if "parent_account" not in account:
msg = _("Please make sure the file you are using has 'Parent Account' column present in the header.")
msg += "<br><br>"
msg += _("Alternatively, you can download the template and fill your data in.")
@@ -301,28 +302,27 @@ def validate_accounts(file_name):
if account["parent_account"] and accounts_dict.get(account["parent_account"]):
accounts_dict[account["parent_account"]]["is_group"] = 1
message = validate_root(accounts_dict)
if message: return message
message = validate_account_types(accounts_dict)
if message: return message
validate_root(accounts_dict)
validate_account_types(accounts_dict)
return [True, len(accounts)]
def validate_root(accounts):
roots = [accounts[d] for d in accounts if not accounts[d].get('parent_account')]
if len(roots) < 4:
return _("Number of root accounts cannot be less than 4")
frappe.throw(_("Number of root accounts cannot be less than 4"))
error_messages = []
for account in roots:
if not account.get("root_type") and account.get("account_name"):
error_messages.append("Please enter Root Type for account- {0}".format(account.get("account_name")))
error_messages.append(_("Please enter Root Type for account- {0}").format(account.get("account_name")))
elif account.get("root_type") not in get_root_types() and account.get("account_name"):
error_messages.append("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity".format(account.get("account_name")))
error_messages.append(_("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity").format(account.get("account_name")))
if error_messages:
return "<br>".join(error_messages)
frappe.throw("<br>".join(error_messages))
def get_root_types():
return ('Asset', 'Liability', 'Expense', 'Income', 'Equity')
@@ -356,7 +356,7 @@ def validate_account_types(accounts):
missing = list(set(account_types_for_ledger) - set(account_types))
if missing:
return _("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing))
frappe.throw(_("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing)))
account_types_for_group = ["Bank", "Cash", "Stock"]
# fix logic bug
@@ -364,7 +364,7 @@ def validate_account_types(accounts):
missing = list(set(account_types_for_group) - set(account_groups))
if missing:
return _("Please identify/create Account (Group) for type - {0}").format(' , '.join(missing))
frappe.throw(_("Please identify/create Account (Group) for type - {0}").format(' , '.join(missing)))
def unset_existing_data(company):
linked = frappe.db.sql('''select fieldname from tabDocField
@@ -391,5 +391,5 @@ def set_default_accounts(company):
})
company.save()
install_country_fixtures(company.name)
install_country_fixtures(company.name, company.country)
company.create_default_tax_template()

View File

@@ -14,7 +14,7 @@ class CouponCode(Document):
if not self.coupon_code:
if self.coupon_type == "Promotional":
self.coupon_code =''.join([i for i in self.coupon_name if not i.isdigit()])[0:8].upper()
self.coupon_code =''.join(i for i in self.coupon_name if not i.isdigit())[0:8].upper()
elif self.coupon_type == "Gift Card":
self.coupon_code = frappe.generate_hash()[:10].upper()

View File

@@ -25,7 +25,7 @@ class Dunning(AccountsController):
def validate_amount(self):
amounts = calculate_interest_and_amount(
self.posting_date, self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days)
self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days)
if self.interest_amount != amounts.get('interest_amount'):
self.interest_amount = flt(amounts.get('interest_amount'), self.precision('interest_amount'))
if self.dunning_amount != amounts.get('dunning_amount'):
@@ -86,18 +86,18 @@ def resolve_dunning(doc, state):
for reference in doc.references:
if reference.reference_doctype == 'Sales Invoice' and reference.outstanding_amount <= 0:
dunnings = frappe.get_list('Dunning', filters={
'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')})
'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')}, ignore_permissions=True)
for dunning in dunnings:
frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved')
def calculate_interest_and_amount(posting_date, outstanding_amount, rate_of_interest, dunning_fee, overdue_days):
def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days):
interest_amount = 0
grand_total = 0
grand_total = flt(outstanding_amount) + flt(dunning_fee)
if rate_of_interest:
interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100
interest_amount = (interest_per_year * cint(overdue_days)) / 365
grand_total = flt(outstanding_amount) + flt(interest_amount) + flt(dunning_fee)
interest_amount = (interest_per_year * cint(overdue_days)) / 365
grand_total += flt(interest_amount)
dunning_amount = flt(interest_amount) + flt(dunning_fee)
return {
'interest_amount': interest_amount,

View File

@@ -16,6 +16,7 @@ class TestDunning(unittest.TestCase):
@classmethod
def setUpClass(self):
create_dunning_type()
create_dunning_type_with_zero_interest_rate()
unlink_payment_on_cancel_of_invoice()
@classmethod
@@ -25,11 +26,20 @@ class TestDunning(unittest.TestCase):
def test_dunning(self):
dunning = create_dunning()
amounts = calculate_interest_and_amount(
dunning.posting_date, dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days)
dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days)
self.assertEqual(round(amounts.get('interest_amount'), 2), 0.44)
self.assertEqual(round(amounts.get('dunning_amount'), 2), 20.44)
self.assertEqual(round(amounts.get('grand_total'), 2), 120.44)
def test_dunning_with_zero_interest_rate(self):
dunning = create_dunning_with_zero_interest_rate()
amounts = calculate_interest_and_amount(
dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days)
self.assertEqual(round(amounts.get('interest_amount'), 2), 0)
self.assertEqual(round(amounts.get('dunning_amount'), 2), 20)
self.assertEqual(round(amounts.get('grand_total'), 2), 120)
def test_gl_entries(self):
dunning = create_dunning()
dunning.submit()
@@ -42,9 +52,9 @@ class TestDunning(unittest.TestCase):
['Sales - _TC', 0.0, 20.44]
])
for gle in gl_entries:
self.assertEquals(expected_values[gle.account][0], gle.account)
self.assertEquals(expected_values[gle.account][1], gle.debit)
self.assertEquals(expected_values[gle.account][2], gle.credit)
self.assertEqual(expected_values[gle.account][0], gle.account)
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
def test_payment_entry(self):
dunning = create_dunning()
@@ -83,6 +93,27 @@ def create_dunning():
dunning.save()
return dunning
def create_dunning_with_zero_interest_rate():
posting_date = add_days(today(), -20)
due_date = add_days(today(), -15)
sales_invoice = create_sales_invoice_against_cost_center(
posting_date=posting_date, due_date=due_date, status='Overdue')
dunning_type = frappe.get_doc("Dunning Type", 'First Notice with 0% Rate of Interest')
dunning = frappe.new_doc("Dunning")
dunning.sales_invoice = sales_invoice.name
dunning.customer_name = sales_invoice.customer_name
dunning.outstanding_amount = sales_invoice.outstanding_amount
dunning.debit_to = sales_invoice.debit_to
dunning.currency = sales_invoice.currency
dunning.company = sales_invoice.company
dunning.posting_date = nowdate()
dunning.due_date = sales_invoice.due_date
dunning.dunning_type = 'First Notice with 0% Rate of Interest'
dunning.rate_of_interest = dunning_type.rate_of_interest
dunning.dunning_fee = dunning_type.dunning_fee
dunning.save()
return dunning
def create_dunning_type():
dunning_type = frappe.new_doc("Dunning Type")
dunning_type.dunning_type = 'First Notice'
@@ -98,3 +129,19 @@ def create_dunning_type():
}
)
dunning_type.save()
def create_dunning_type_with_zero_interest_rate():
dunning_type = frappe.new_doc("Dunning Type")
dunning_type.dunning_type = 'First Notice with 0% Rate of Interest'
dunning_type.start_day = 10
dunning_type.end_day = 20
dunning_type.dunning_fee = 20
dunning_type.rate_of_interest = 0
dunning_type.append(
"dunning_letter_text", {
'language': 'en',
'body_text': 'We have still not received payment for our invoice ',
'closing_text': 'We kindly request that you pay the outstanding amount immediately, and late fees.'
}
)
dunning_type.save()

View File

@@ -21,21 +21,17 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
refresh: function(frm) {
if(frm.doc.docstatus==1) {
frappe.db.get_value("Journal Entry Account", {
'reference_type': 'Exchange Rate Revaluation',
'reference_name': frm.doc.name,
'docstatus': 1
}, "sum(debit) as sum", (r) =>{
let total_amt = 0;
frm.doc.accounts.forEach(d=> {
total_amt = total_amt + d['new_balance_in_base_currency'];
});
if(total_amt !== r.sum) {
frm.add_custom_button(__('Journal Entry'), function() {
return frm.events.make_jv(frm);
}, __('Create'));
frappe.call({
method: 'check_journal_entry_condition',
doc: frm.doc,
callback: function(r) {
if (r.message) {
frm.add_custom_button(__('Journal Entry'), function() {
return frm.events.make_jv(frm);
}, __('Create'));
}
}
}, 'Journal Entry');
});
}
},

View File

@@ -27,6 +27,26 @@ class ExchangeRateRevaluation(Document):
if not (self.company and self.posting_date):
frappe.throw(_("Please select Company and Posting Date to getting entries"))
def on_cancel(self):
self.ignore_linked_doctypes = ('GL Entry')
@frappe.whitelist()
def check_journal_entry_condition(self):
total_debit = frappe.db.get_value("Journal Entry Account", {
'reference_type': 'Exchange Rate Revaluation',
'reference_name': self.name,
'docstatus': 1
}, "sum(debit) as sum")
total_amt = 0
for d in self.accounts:
total_amt = total_amt + d.new_balance_in_base_currency
if total_amt != total_debit:
return True
return False
@frappe.whitelist()
def get_accounts_data(self, account=None):
accounts = []
@@ -82,10 +102,12 @@ class ExchangeRateRevaluation(Document):
sum(debit) - sum(credit) as balance
from `tabGL Entry`
where account in (%s)
group by account, party_type, party
and posting_date <= %s
and is_cancelled = 0
group by account, NULLIF(party_type,''), NULLIF(party,'')
having sum(debit) != sum(credit)
order by account
""" % ', '.join(['%s']*len(accounts)), tuple(accounts), as_dict=1)
""" % (', '.join(['%s']*len(accounts)), '%s'), tuple(accounts + [self.posting_date]), as_dict=1)
return account_details
@@ -126,9 +148,9 @@ class ExchangeRateRevaluation(Document):
"party_type": d.get("party_type"),
"party": d.get("party"),
"account_currency": d.get("account_currency"),
"balance": d.get("balance_in_account_currency"),
dr_or_cr: abs(d.get("balance_in_account_currency")),
"exchange_rate":d.get("new_exchange_rate"),
"balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")),
dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")),
"exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")),
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
})
@@ -137,9 +159,9 @@ class ExchangeRateRevaluation(Document):
"party_type": d.get("party_type"),
"party": d.get("party"),
"account_currency": d.get("account_currency"),
"balance": d.get("balance_in_account_currency"),
reverse_dr_or_cr: abs(d.get("balance_in_account_currency")),
"exchange_rate": d.get("current_exchange_rate"),
"balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")),
reverse_dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")),
"exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")),
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name
})
@@ -168,9 +190,9 @@ def get_account_details(account, company, posting_date, party_type=None, party=N
account_details = {}
company_currency = erpnext.get_company_currency(company)
balance = get_balance_on(account, party_type=party_type, party=party, in_account_currency=False)
balance = get_balance_on(account, date=posting_date, party_type=party_type, party=party, in_account_currency=False)
if balance:
balance_in_account_currency = get_balance_on(account, party_type=party_type, party=party)
balance_in_account_currency = get_balance_on(account, date=posting_date, party_type=party_type, party=party)
current_exchange_rate = balance / balance_in_account_currency if balance_in_account_currency else 0
new_exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
new_balance_in_base_currency = balance_in_account_currency * new_exchange_rate

View File

@@ -75,8 +75,13 @@ class GLEntry(Document):
def pl_must_have_cost_center(self):
if frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss":
if not self.cost_center and self.voucher_type != 'Period Closing Voucher':
frappe.throw(_("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}. Please set up a default Cost Center for the Company.")
.format(self.voucher_type, self.voucher_no, self.account))
msg = _("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}.").format(
self.voucher_type, self.voucher_no, self.account)
msg += " "
msg += _("Please set the cost center field in {0} or setup a default Cost Center for the Company.").format(
self.voucher_type)
frappe.throw(msg, title=_("Missing Cost Center"))
def validate_dimensions_for_pl_and_bs(self):
account_type = frappe.db.get_value("Account", self.account, "report_type")
@@ -116,8 +121,7 @@ class GLEntry(Document):
def check_pl_account(self):
if self.is_opening=='Yes' and \
frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss" and \
self.voucher_type not in ['Purchase Invoice', 'Sales Invoice']:
frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss":
frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry")
.format(self.voucher_type, self.voucher_no, self.account))

View File

@@ -54,4 +54,4 @@ class TestGLEntry(unittest.TestCase):
self.assertTrue(all(new.name != old.name for new, old in zip(gl_entries, new_gl_entries)))
new_naming_series_current_value = frappe.db.sql("SELECT current from tabSeries where name = %s", naming_series)[0][0]
self.assertEquals(old_naming_series_current_value + 2, new_naming_series_current_value)
self.assertEqual(old_naming_series_current_value + 2, new_naming_series_current_value)

View File

@@ -1,196 +1,82 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2018-01-02 15:48:58.768352",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"creation": "2018-01-02 15:48:58.768352",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company",
"cgst_account",
"sgst_account",
"igst_account",
"cess_account",
"is_reverse_charge_account"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "company",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Company",
"length": 0,
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"columns": 1,
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "cgst_account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "CGST Account",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"columns": 2,
"fieldname": "cgst_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "CGST Account",
"options": "Account",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "sgst_account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "SGST Account",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"columns": 2,
"fieldname": "sgst_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "SGST Account",
"options": "Account",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "igst_account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "IGST Account",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"columns": 2,
"fieldname": "igst_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "IGST Account",
"options": "Account",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "cess_account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "CESS Account",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"columns": 2,
"fieldname": "cess_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "CESS Account",
"options": "Account"
},
{
"columns": 1,
"default": "0",
"fieldname": "is_reverse_charge_account",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Is Reverse Charge Account"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-01-02 15:52:22.335988",
"modified_by": "Administrator",
"module": "Accounts",
"name": "GST Account",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-04-09 12:30:25.889993",
"modified_by": "Administrator",
"module": "Accounts",
"name": "GST Account",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -42,18 +42,18 @@ class InvoiceDiscounting(AccountsController):
record.idx, frappe.bold(actual_outstanding), frappe.bold(record.sales_invoice)))
def calculate_total_amount(self):
self.total_amount = sum([flt(d.outstanding_amount) for d in self.invoices])
self.total_amount = sum(flt(d.outstanding_amount) for d in self.invoices)
def on_submit(self):
self.update_sales_invoice()
self.make_gl_entries()
def on_cancel(self):
self.set_status()
self.set_status(cancel=1)
self.update_sales_invoice()
self.make_gl_entries()
def set_status(self, status=None):
def set_status(self, status=None, cancel=0):
if status:
self.status = status
self.db_set("status", status)
@@ -66,6 +66,9 @@ class InvoiceDiscounting(AccountsController):
elif self.docstatus == 2:
self.status = "Cancelled"
if cancel:
self.db_set('status', self.status, update_modified = True)
def update_sales_invoice(self):
for d in self.invoices:
if self.docstatus == 1:

View File

@@ -327,18 +327,16 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({
},
setup_balance_formatter: function() {
var me = this;
$.each(["balance", "party_balance"], function(i, field) {
var df = frappe.meta.get_docfield("Journal Entry Account", field, me.frm.doc.name);
df.formatter = function(value, df, options, doc) {
var currency = frappe.meta.get_field_currency(df, doc);
var dr_or_cr = value ? ('<label>' + (value > 0.0 ? __("Dr") : __("Cr")) + '</label>') : "";
return "<div style='text-align: right'>"
+ ((value==null || value==="") ? "" : format_currency(Math.abs(value), currency))
+ " " + dr_or_cr
+ "</div>";
}
})
const formatter = function(value, df, options, doc) {
var currency = frappe.meta.get_field_currency(df, doc);
var dr_or_cr = value ? ('<label>' + (value > 0.0 ? __("Dr") : __("Cr")) + '</label>') : "";
return "<div style='text-align: right'>"
+ ((value==null || value==="") ? "" : format_currency(Math.abs(value), currency))
+ " " + dr_or_cr
+ "</div>";
};
this.frm.fields_dict.accounts.grid.update_docfield_property('balance', 'formatter', formatter);
this.frm.fields_dict.accounts.grid.update_docfield_property('party_balance', 'formatter', formatter);
},
reference_name: function(doc, cdt, cdn) {
@@ -431,15 +429,6 @@ cur_frm.cscript.validate = function(doc,cdt,cdn) {
cur_frm.cscript.update_totals(doc);
}
cur_frm.cscript.select_print_heading = function(doc,cdt,cdn){
if(doc.select_print_heading){
// print heading
cur_frm.pformat.print_heading = doc.select_print_heading;
}
else
cur_frm.pformat.print_heading = __("Journal Entry");
}
frappe.ui.form.on("Journal Entry Account", {
party: function(frm, cdt, cdn) {
var d = frappe.get_doc(cdt, cdn);
@@ -511,8 +500,11 @@ $.extend(erpnext.journal_entry, {
};
$.each(field_label_map, function (fieldname, label) {
var df = frappe.meta.get_docfield("Journal Entry Account", fieldname, frm.doc.name);
df.label = frm.doc.multi_currency ? (label + " in Account Currency") : label;
frm.fields_dict.accounts.grid.update_docfield_property(
fieldname,
'label',
frm.doc.multi_currency ? (label + " in Account Currency") : label
);
})
},

View File

@@ -39,7 +39,11 @@ class JournalEntry(AccountsController):
self.validate_multi_currency()
self.set_amounts_in_company_currency()
self.validate_debit_credit_amount()
self.validate_total_debit_and_credit()
# Do not validate while importing via data import
if not frappe.flags.in_import:
self.validate_total_debit_and_credit()
self.validate_against_jv()
self.validate_reference_doc()
self.set_against_account()
@@ -192,8 +196,8 @@ class JournalEntry(AccountsController):
frappe.throw(_("Row {0}: Party Type and Party is required for Receivable / Payable account {1}").format(d.idx, d.account))
def check_credit_limit(self):
customers = list(set([d.party for d in self.get("accounts")
if d.party_type=="Customer" and d.party and flt(d.debit) > 0]))
customers = list(set(d.party for d in self.get("accounts")
if d.party_type=="Customer" and d.party and flt(d.debit) > 0))
if customers:
from erpnext.selling.doctype.customer.customer import check_credit_limit
for customer in customers:
@@ -592,6 +596,7 @@ class JournalEntry(AccountsController):
self.validate_total_debit_and_credit()
@frappe.whitelist()
def get_outstanding_invoices(self):
self.set('accounts', [])
total = 0

View File

@@ -0,0 +1,17 @@
frappe.ui.form.on("Journal Entry", {
refresh: function(frm) {
frm.set_query('company_address', function(doc) {
if(!doc.company) {
frappe.throw(__('Please set Company'));
}
return {
query: 'frappe.contacts.doctype.address.address.address_query',
filters: {
link_doctype: 'Company',
link_name: doc.company
}
};
});
}
});

View File

@@ -280,7 +280,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2020-06-24 14:06:54.833738",
"modified": "2020-06-26 14:06:54.833738",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry Account",

View File

@@ -21,7 +21,7 @@ class MonthlyDistribution(Document):
idx += 1
def validate(self):
total = sum([flt(d.percentage_allocation) for d in self.get("percentages")])
total = sum(flt(d.percentage_allocation) for d in self.get("percentages"))
if flt(total, 2) != 100.0:
frappe.throw(_("Percentage Allocation should be equal to 100%") + \

View File

@@ -1,87 +1,39 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2014-08-29 16:02:39.740505",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"actions": [],
"creation": "2014-08-29 16:02:39.740505",
"doctype": "DocType",
"editable_grid": 1,
"field_order": [
"company",
"account"
],
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "company",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Company",
"length": 0,
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Account",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"fieldname": "account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Account",
"options": "Account"
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2016-07-11 03:28:03.348246",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Party Account",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_seen": 0
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-04-07 18:13:08.833822",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Party Account",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@@ -3,6 +3,8 @@
{% include "erpnext/public/js/controllers/accounts.js" %}
frappe.provide("erpnext.accounts.dimensions");
cur_frm.cscript.tax_table = "Advance Taxes and Charges";
frappe.ui.form.on('Payment Entry', {
onload: function(frm) {
if(frm.doc.__islocal) {
@@ -91,6 +93,16 @@ frappe.ui.form.on('Payment Entry', {
}
});
frm.set_query("advance_tax_account", function() {
return {
filters: {
"company": frm.doc.company,
"root_type": ["in", ["Asset", "Liability"]],
"is_group": 0
}
}
});
frm.set_query("reference_doctype", "references", function() {
if (frm.doc.party_type == "Customer") {
var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"];
@@ -182,6 +194,8 @@ frappe.ui.form.on('Payment Entry', {
frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency));
frm.toggle_display("base_paid_amount", frm.doc.paid_from_account_currency != company_currency);
frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges &&
(frm.doc.paid_from_account_currency != company_currency));
frm.toggle_display("base_received_amount", (
frm.doc.paid_to_account_currency != company_currency
@@ -216,7 +230,7 @@ frappe.ui.form.on('Payment Entry', {
var company_currency = frm.doc.company? frappe.get_doc(":Company", frm.doc.company).default_currency: "";
frm.set_currency_labels(["base_paid_amount", "base_received_amount", "base_total_allocated_amount",
"difference_amount"], company_currency);
"difference_amount", "base_paid_amount_after_tax", "base_received_amount_after_tax"], company_currency);
frm.set_currency_labels(["paid_amount"], frm.doc.paid_from_account_currency);
frm.set_currency_labels(["received_amount"], frm.doc.paid_to_account_currency);
@@ -224,11 +238,13 @@ frappe.ui.form.on('Payment Entry', {
var party_account_currency = frm.doc.payment_type=="Receive" ?
frm.doc.paid_from_account_currency : frm.doc.paid_to_account_currency;
frm.set_currency_labels(["total_allocated_amount", "unallocated_amount"], party_account_currency);
frm.set_currency_labels(["total_allocated_amount", "unallocated_amount",
"total_taxes_and_charges"], party_account_currency);
var currency_field = (frm.doc.payment_type=="Receive") ? "paid_from_account_currency" : "paid_to_account_currency"
frm.set_df_property("total_allocated_amount", "options", currency_field);
frm.set_df_property("unallocated_amount", "options", currency_field);
frm.set_df_property("total_taxes_and_charges", "options", currency_field);
frm.set_df_property("party_balance", "options", currency_field);
frm.set_currency_labels(["total_amount", "outstanding_amount", "allocated_amount"],
@@ -364,6 +380,16 @@ frappe.ui.form.on('Payment Entry', {
}
},
apply_tax_withholding_amount: function(frm) {
if (!frm.doc.apply_tax_withholding_amount) {
frm.set_value("tax_withholding_category", '');
} else {
frappe.db.get_value('Supplier', frm.doc.party, 'tax_withholding_category', (values) => {
frm.set_value("tax_withholding_category", values.tax_withholding_category);
});
}
},
paid_from: function(frm) {
if(frm.set_party_account_based_on_party) return;
@@ -561,7 +587,7 @@ frappe.ui.form.on('Payment Entry', {
flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate));
if(frm.doc.payment_type == "Pay")
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount);
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, 1);
else
frm.events.set_unallocated_amount(frm);
@@ -582,7 +608,7 @@ frappe.ui.form.on('Payment Entry', {
}
if(frm.doc.payment_type == "Receive")
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount);
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, 1);
else
frm.events.set_unallocated_amount(frm);
},
@@ -606,9 +632,9 @@ frappe.ui.form.on('Payment Entry', {
{fieldtype:"Float", label: __("Less Than Amount"), fieldname:"outstanding_amt_less_than"},
{fieldtype:"Section Break"},
{fieldtype:"Link", label:__("Cost Center"), fieldname:"cost_center", options:"Cost Center",
"get_query": function() {
return {
"filters": {"company": frm.doc.company}
"get_query": function() {
return {
"filters": {"company": frm.doc.company}
}
}
},
@@ -743,7 +769,7 @@ frappe.ui.form.on('Payment Entry', {
});
},
allocate_party_amount_against_ref_docs: function(frm, paid_amount) {
allocate_party_amount_against_ref_docs: function(frm, paid_amount, paid_amount_change) {
var total_positive_outstanding_including_order = 0;
var total_negative_outstanding = 0;
var total_deductions = frappe.utils.sum($.map(frm.doc.deductions || [],
@@ -800,22 +826,15 @@ frappe.ui.form.on('Payment Entry', {
//If allocate payment amount checkbox is unchecked, set zero to allocate amount
row.allocated_amount = 0;
} else if (frappe.flags.allocate_payment_amount != 0 && !row.allocated_amount) {
if (row.outstanding_amount > 0 && allocated_positive_outstanding > 0) {
if (row.outstanding_amount >= allocated_positive_outstanding) {
row.allocated_amount = allocated_positive_outstanding;
} else {
row.allocated_amount = row.outstanding_amount;
}
} else if (frappe.flags.allocate_payment_amount != 0 && (!row.allocated_amount || paid_amount_change)) {
if (row.outstanding_amount > 0 && allocated_positive_outstanding >= 0) {
row.allocated_amount = (row.outstanding_amount >= allocated_positive_outstanding) ?
allocated_positive_outstanding : row.outstanding_amount;
allocated_positive_outstanding -= flt(row.allocated_amount);
} else if (row.outstanding_amount < 0 && allocated_negative_outstanding) {
if (Math.abs(row.outstanding_amount) >= allocated_negative_outstanding) {
row.allocated_amount = -1*allocated_negative_outstanding;
} else {
row.allocated_amount = row.outstanding_amount;
};
} else if (row.outstanding_amount < 0 && allocated_negative_outstanding) {
row.allocated_amount = (Math.abs(row.outstanding_amount) >= allocated_negative_outstanding) ?
-1*allocated_negative_outstanding : row.outstanding_amount;
allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount));
}
}
@@ -850,12 +869,12 @@ frappe.ui.form.on('Payment Entry', {
if(frm.doc.payment_type == "Receive"
&& frm.doc.base_total_allocated_amount < frm.doc.base_received_amount + total_deductions
&& frm.doc.total_allocated_amount < frm.doc.paid_amount + (total_deductions / frm.doc.source_exchange_rate)) {
unallocated_amount = (frm.doc.base_received_amount + total_deductions
- frm.doc.base_total_allocated_amount) / frm.doc.source_exchange_rate;
unallocated_amount = (frm.doc.base_received_amount + total_deductions + frm.doc.base_total_taxes_and_charges
+ frm.doc.base_total_allocated_amount) / frm.doc.source_exchange_rate;
} else if (frm.doc.payment_type == "Pay"
&& frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount - total_deductions
&& frm.doc.total_allocated_amount < frm.doc.received_amount + (total_deductions / frm.doc.target_exchange_rate)) {
unallocated_amount = (frm.doc.base_paid_amount - (total_deductions
unallocated_amount = (frm.doc.base_paid_amount + frm.doc.base_total_taxes_and_charges - (total_deductions
+ frm.doc.base_total_allocated_amount)) / frm.doc.target_exchange_rate;
}
}
@@ -881,7 +900,8 @@ frappe.ui.form.on('Payment Entry', {
var total_deductions = frappe.utils.sum($.map(frm.doc.deductions || [],
function(d) { return flt(d.amount) }));
frm.set_value("difference_amount", difference_amount - total_deductions);
frm.set_value("difference_amount", difference_amount - total_deductions +
frm.doc.base_total_taxes_and_charges);
frm.events.hide_unhide_fields(frm);
},
@@ -1009,7 +1029,266 @@ frappe.ui.form.on('Payment Entry', {
}
});
}
}
},
sales_taxes_and_charges_template: function(frm) {
frm.trigger('fetch_taxes_from_template');
},
purchase_taxes_and_charges_template: function(frm) {
frm.trigger('fetch_taxes_from_template');
},
fetch_taxes_from_template: function(frm) {
let master_doctype = '';
let taxes_and_charges = '';
if (frm.doc.party_type == 'Supplier') {
master_doctype = 'Purchase Taxes and Charges Template';
taxes_and_charges = frm.doc.purchase_taxes_and_charges_template;
} else if (frm.doc.party_type == 'Customer') {
master_doctype = 'Sales Taxes and Charges Template';
taxes_and_charges = frm.doc.sales_taxes_and_charges_template;
}
if (!taxes_and_charges) {
return;
}
frappe.call({
method: "erpnext.controllers.accounts_controller.get_taxes_and_charges",
args: {
"master_doctype": master_doctype,
"master_name": taxes_and_charges
},
callback: function(r) {
if(!r.exc && r.message) {
// set taxes table
if(r.message) {
for (let tax of r.message) {
if (tax.charge_type === 'On Net Total') {
tax.charge_type = 'On Paid Amount';
}
me.frm.add_child("taxes", tax);
}
frm.events.apply_taxes(frm);
frm.events.set_unallocated_amount(frm);
}
}
}
});
},
apply_taxes: function(frm) {
frm.events.initialize_taxes(frm);
frm.events.determine_exclusive_rate(frm);
frm.events.calculate_taxes(frm);
},
initialize_taxes: function(frm) {
$.each(frm.doc["taxes"] || [], function(i, tax) {
frm.events.validate_taxes_and_charges(tax);
frm.events.validate_inclusive_tax(tax);
tax.item_wise_tax_detail = {};
let tax_fields = ["total", "tax_fraction_for_current_item",
"grand_total_fraction_for_current_item"];
if (cstr(tax.charge_type) != "Actual") {
tax_fields.push("tax_amount");
}
$.each(tax_fields, function(i, fieldname) { tax[fieldname] = 0.0; });
frm.doc.paid_amount_after_tax = frm.doc.paid_amount;
});
},
validate_taxes_and_charges: function(d) {
let msg = "";
if (d.account_head && !d.description) {
// set description from account head
d.description = d.account_head.split(' - ').slice(0, -1).join(' - ');
}
if (!d.charge_type && (d.row_id || d.rate || d.tax_amount)) {
msg = __("Please select Charge Type first");
d.row_id = "";
d.rate = d.tax_amount = 0.0;
} else if ((d.charge_type == 'Actual' || d.charge_type == 'On Net Total' || d.charge_type == 'On Paid Amount') && d.row_id) {
msg = __("Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'");
d.row_id = "";
} else if ((d.charge_type == 'On Previous Row Amount' || d.charge_type == 'On Previous Row Total') && d.row_id) {
if (d.idx == 1) {
msg = __("Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row");
d.charge_type = '';
} else if (!d.row_id) {
msg = __("Please specify a valid Row ID for row {0} in table {1}", [d.idx, __(d.doctype)]);
d.row_id = "";
} else if (d.row_id && d.row_id >= d.idx) {
msg = __("Cannot refer row number greater than or equal to current row number for this Charge type");
d.row_id = "";
}
}
if (msg) {
frappe.validated = false;
refresh_field("taxes");
frappe.throw(msg);
}
},
validate_inclusive_tax: function(tax) {
let actual_type_error = function() {
let msg = __("Actual type tax cannot be included in Item rate in row {0}", [tax.idx])
frappe.throw(msg);
};
let on_previous_row_error = function(row_range) {
let msg = __("For row {0} in {1}. To include {2} in Item rate, rows {3} must also be included",
[tax.idx, __(tax.doctype), tax.charge_type, row_range])
frappe.throw(msg);
};
if(cint(tax.included_in_paid_amount)) {
if(tax.charge_type == "Actual") {
// inclusive tax cannot be of type Actual
actual_type_error();
} else if(tax.charge_type == "On Previous Row Amount" &&
!cint(this.frm.doc["taxes"][tax.row_id - 1].included_in_paid_amount)
) {
// referred row should also be an inclusive tax
on_previous_row_error(tax.row_id);
} else if(tax.charge_type == "On Previous Row Total") {
let taxes_not_included = $.map(this.frm.doc["taxes"].slice(0, tax.row_id),
function(t) { return cint(t.included_in_paid_amount) ? null : t; });
if(taxes_not_included.length > 0) {
// all rows above this tax should be inclusive
on_previous_row_error(tax.row_id == 1 ? "1" : "1 - " + tax.row_id);
}
}
}
},
determine_exclusive_rate: function(frm) {
let has_inclusive_tax = false;
$.each(frm.doc["taxes"] || [], function(i, row) {
if(cint(row.included_in_paid_amount)) has_inclusive_tax = true;
});
if(has_inclusive_tax==false) return;
let cumulated_tax_fraction = 0.0;
$.each(frm.doc["taxes"] || [], function(i, tax) {
tax.tax_fraction_for_current_item = frm.events.get_current_tax_fraction(frm, tax);
if(i==0) {
tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item;
} else {
tax.grand_total_fraction_for_current_item =
me.frm.doc["taxes"][i-1].grand_total_fraction_for_current_item +
tax.tax_fraction_for_current_item;
}
cumulated_tax_fraction += tax.tax_fraction_for_current_item;
frm.doc.paid_amount_after_tax = flt(frm.doc.paid_amount/(1+cumulated_tax_fraction))
});
},
get_current_tax_fraction: function(frm, tax) {
let current_tax_fraction = 0.0;
if(cint(tax.included_in_paid_amount)) {
let tax_rate = tax.rate;
if(tax.charge_type == "On Paid Amount") {
current_tax_fraction = (tax_rate / 100.0);
} else if(tax.charge_type == "On Previous Row Amount") {
current_tax_fraction = (tax_rate / 100.0) *
frm.doc["taxes"][cint(tax.row_id) - 1].tax_fraction_for_current_item;
} else if(tax.charge_type == "On Previous Row Total") {
current_tax_fraction = (tax_rate / 100.0) *
frm.doc["taxes"][cint(tax.row_id) - 1].grand_total_fraction_for_current_item;
}
}
if(tax.add_deduct_tax && tax.add_deduct_tax == "Deduct") {
current_tax_fraction *= -1;
}
return current_tax_fraction;
},
calculate_taxes: function(frm) {
frm.doc.total_taxes_and_charges = 0.0;
frm.doc.base_total_taxes_and_charges = 0.0;
let actual_tax_dict = {};
// maintain actual tax rate based on idx
$.each(frm.doc["taxes"] || [], function(i, tax) {
if (tax.charge_type == "Actual") {
actual_tax_dict[tax.idx] = flt(tax.tax_amount, precision("tax_amount", tax));
}
});
$.each(me.frm.doc["taxes"] || [], function(i, tax) {
let current_tax_amount = frm.events.get_current_tax_amount(frm, tax);
// Adjust divisional loss to the last item
if (tax.charge_type == "Actual") {
actual_tax_dict[tax.idx] -= current_tax_amount;
if (i == frm.doc["taxes"].length - 1) {
current_tax_amount += actual_tax_dict[tax.idx];
}
}
tax.tax_amount = current_tax_amount;
tax.base_tax_amount = tax.tax_amount * frm.doc.source_exchange_rate;
current_tax_amount *= (tax.add_deduct_tax == "Deduct") ? -1.0 : 1.0;
if(i==0) {
tax.total = flt(frm.doc.paid_amount_after_tax + current_tax_amount, precision("total", tax));
} else {
tax.total = flt(frm.doc["taxes"][i-1].total + current_tax_amount, precision("total", tax));
}
tax.base_total = tax.total * frm.doc.source_exchange_rate;
frm.doc.total_taxes_and_charges += current_tax_amount;
frm.doc.base_total_taxes_and_charges += current_tax_amount * frm.doc.source_exchange_rate;
frm.refresh_field('taxes');
frm.refresh_field('total_taxes_and_charges');
frm.refresh_field('base_total_taxes_and_charges');
});
},
get_current_tax_amount: function(frm, tax) {
let tax_rate = tax.rate;
let current_tax_amount = 0.0;
// To set row_id by default as previous row.
if(["On Previous Row Amount", "On Previous Row Total"].includes(tax.charge_type)) {
if (tax.idx === 1) {
frappe.throw(
__("Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row"));
}
}
if(tax.charge_type == "Actual") {
current_tax_amount = flt(tax.tax_amount, precision("tax_amount", tax))
} else if(tax.charge_type == "On Paid Amount") {
current_tax_amount = flt((tax_rate / 100.0) * frm.doc.paid_amount_after_tax);
} else if(tax.charge_type == "On Previous Row Amount") {
current_tax_amount = flt((tax_rate / 100.0) *
frm.doc["taxes"][cint(tax.row_id) - 1].tax_amount);
} else if(tax.charge_type == "On Previous Row Total") {
current_tax_amount = flt((tax_rate / 100.0) *
frm.doc["taxes"][cint(tax.row_id) - 1].total);
}
return current_tax_amount;
},
});
@@ -1056,6 +1335,38 @@ frappe.ui.form.on('Payment Entry Reference', {
}
})
frappe.ui.form.on('Advance Taxes and Charges', {
rate: function(frm) {
frm.events.apply_taxes(frm);
frm.events.set_unallocated_amount(frm);
},
tax_amount : function(frm) {
frm.events.apply_taxes(frm);
frm.events.set_unallocated_amount(frm);
},
row_id: function(frm) {
frm.events.apply_taxes(frm);
frm.events.set_unallocated_amount(frm);
},
taxes_remove: function(frm) {
frm.events.apply_taxes(frm);
frm.events.set_unallocated_amount(frm);
},
included_in_paid_amount: function(frm) {
frm.events.apply_taxes(frm);
frm.events.set_unallocated_amount(frm);
},
charge_type: function(frm) {
frm.events.apply_taxes(frm);
frm.events.set_unallocated_amount(frm);
}
})
frappe.ui.form.on('Payment Entry Deduction', {
amount: function(frm) {
frm.events.set_unallocated_amount(frm);

View File

@@ -35,12 +35,16 @@
"paid_to_account_balance",
"payment_amounts_section",
"paid_amount",
"paid_amount_after_tax",
"source_exchange_rate",
"base_paid_amount",
"base_paid_amount_after_tax",
"column_break_21",
"received_amount",
"received_amount_after_tax",
"target_exchange_rate",
"base_received_amount",
"base_received_amount_after_tax",
"section_break_14",
"get_outstanding_invoice",
"references",
@@ -52,6 +56,17 @@
"unallocated_amount",
"difference_amount",
"write_off_difference_amount",
"taxes_and_charges_section",
"purchase_taxes_and_charges_template",
"sales_taxes_and_charges_template",
"advance_tax_account",
"column_break_55",
"apply_tax_withholding_amount",
"tax_withholding_category",
"section_break_56",
"taxes",
"base_total_taxes_and_charges",
"total_taxes_and_charges",
"deductions_or_loss_section",
"deductions",
"transaction_references",
@@ -320,6 +335,7 @@
"reqd": 1
},
{
"depends_on": "doc.received_amount",
"fieldname": "base_received_amount",
"fieldtype": "Currency",
"label": "Received Amount (Company Currency)",
@@ -584,12 +600,119 @@
"fieldname": "custom_remarks",
"fieldtype": "Check",
"label": "Custom Remarks"
},
{
"depends_on": "eval:doc.apply_tax_withholding_amount",
"fieldname": "tax_withholding_category",
"fieldtype": "Link",
"label": "Tax Withholding Category",
"mandatory_depends_on": "eval:doc.apply_tax_withholding_amount",
"options": "Tax Withholding Category"
},
{
"default": "0",
"depends_on": "eval:doc.party_type == 'Supplier'",
"fieldname": "apply_tax_withholding_amount",
"fieldtype": "Check",
"label": "Apply Tax Withholding Amount"
},
{
"collapsible": 1,
"fieldname": "taxes_and_charges_section",
"fieldtype": "Section Break",
"label": "Taxes and Charges"
},
{
"depends_on": "eval:doc.party_type == 'Supplier'",
"fieldname": "purchase_taxes_and_charges_template",
"fieldtype": "Link",
"label": "Taxes and Charges Template",
"options": "Purchase Taxes and Charges Template"
},
{
"depends_on": "eval: doc.party_type == 'Customer'",
"fieldname": "sales_taxes_and_charges_template",
"fieldtype": "Link",
"label": "Taxes and Charges Template",
"options": "Sales Taxes and Charges Template"
},
{
"depends_on": "eval: doc.party_type == 'Supplier' || doc.party_type == 'Customer'",
"fieldname": "taxes",
"fieldtype": "Table",
"label": "Advance Taxes and Charges",
"options": "Advance Taxes and Charges"
},
{
"fieldname": "base_total_taxes_and_charges",
"fieldtype": "Currency",
"label": "Total Taxes and Charges (Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "total_taxes_and_charges",
"fieldtype": "Currency",
"label": "Total Taxes and Charges",
"read_only": 1
},
{
"fieldname": "paid_amount_after_tax",
"fieldtype": "Currency",
"hidden": 1,
"label": "Paid Amount After Tax",
"options": "paid_from_account_currency",
"read_only": 1
},
{
"fieldname": "base_paid_amount_after_tax",
"fieldtype": "Currency",
"hidden": 1,
"label": "Paid Amount After Tax (Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "column_break_55",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_56",
"fieldtype": "Section Break",
"hide_border": 1
},
{
"depends_on": "eval:doc.apply_tax_withholding_amount",
"description": "Provisional tax account for advance tax. Taxes are parked in this account until payments are allocated to invoices",
"fieldname": "advance_tax_account",
"fieldtype": "Link",
"label": "Advance Tax Account",
"mandatory_depends_on": "eval:doc.apply_tax_withholding_amount",
"options": "Account"
},
{
"depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'",
"fieldname": "received_amount_after_tax",
"fieldtype": "Currency",
"hidden": 1,
"label": "Received Amount After Tax",
"options": "paid_to_account_currency",
"read_only": 1
},
{
"depends_on": "doc.received_amount",
"fieldname": "base_received_amount_after_tax",
"fieldtype": "Currency",
"hidden": 1,
"label": "Received Amount After Tax (Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-03-08 13:05:16.958866",
"modified": "2021-07-09 08:58:15.008761",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",
@@ -633,4 +756,4 @@
"sort_order": "DESC",
"title_field": "title",
"track_changes": 1
}
}

View File

@@ -4,8 +4,8 @@
from __future__ import unicode_literals
import frappe, erpnext, json
from frappe import _, scrub, ValidationError
from frappe.utils import flt, comma_or, nowdate, getdate
from frappe import _, scrub, ValidationError, throw
from frappe.utils import flt, comma_or, nowdate, getdate, cint
from erpnext.accounts.utils import get_outstanding_invoices, get_account_currency, get_balance_on
from erpnext.accounts.party import get_party_account
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
@@ -15,9 +15,11 @@ from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amo
from erpnext.accounts.doctype.bank_account.bank_account import get_party_bank_account, get_bank_account_details
from erpnext.controllers.accounts_controller import AccountsController, get_supplier_block_status
from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import get_party_account_based_on_invoice_discounting
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
from six import string_types, iteritems
from erpnext.controllers.accounts_controller import validate_taxes_and_charges
class InvalidPaymentEntry(ValidationError):
pass
@@ -52,6 +54,8 @@ class PaymentEntry(AccountsController):
self.set_exchange_rate()
self.validate_mandatory()
self.validate_reference_documents()
self.set_tax_withholding()
self.apply_taxes()
self.set_amounts()
self.clear_unallocated_reference_document_rows()
self.validate_payment_against_negative_invoice()
@@ -65,7 +69,6 @@ class PaymentEntry(AccountsController):
self.set_status()
def on_submit(self):
self.setup_party_account_field()
if self.difference_amount:
frappe.throw(_("Difference Amount must be zero"))
self.make_gl_entries()
@@ -78,7 +81,6 @@ class PaymentEntry(AccountsController):
def on_cancel(self):
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
self.setup_party_account_field()
self.make_gl_entries(cancel=1)
self.update_outstanding_amounts()
self.update_advance_paid()
@@ -122,6 +124,11 @@ class PaymentEntry(AccountsController):
if flt(d.allocated_amount) > flt(d.outstanding_amount):
frappe.throw(_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx))
# Check for negative outstanding invoices as well
if flt(d.allocated_amount) < 0:
if flt(d.allocated_amount) < flt(d.outstanding_amount):
frappe.throw(_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx))
def delink_advance_entry_references(self):
for reference in self.references:
if reference.reference_doctype in ("Sales Invoice", "Purchase Invoice"):
@@ -176,8 +183,15 @@ class PaymentEntry(AccountsController):
d.reference_name, self.party_account_currency)
for field, value in iteritems(ref_details):
if d.exchange_gain_loss:
# for cases where gain/loss is booked into invoice
# exchange_gain_loss is calculated from invoice & populated
# and row.exchange_rate is already set to payment entry's exchange rate
# refer -> `update_reference_in_payment_entry()` in utils.py
continue
if field == 'exchange_rate' or not d.get(field) or force:
d.set(field, value)
d.db_set(field, value)
def validate_payment_type(self):
if self.payment_type not in ("Receive", "Pay", "Internal Transfer"):
@@ -303,11 +317,10 @@ class PaymentEntry(AccountsController):
for k, v in no_oustanding_refs.items():
frappe.msgprint(
_("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.")
.format(k, frappe.bold(", ".join([d.reference_name for d in v])), frappe.bold("negative outstanding amount"))
.format(k, frappe.bold(", ".join(d.reference_name for d in v)), frappe.bold("negative outstanding amount"))
+ "<br><br>" + _("If this is undesirable please cancel the corresponding Payment Entry."),
title=_("Warning"), indicator="orange")
def validate_journal_entry(self):
for d in self.get("references"):
if d.allocated_amount and d.reference_doctype == "Journal Entry":
@@ -386,12 +399,104 @@ class PaymentEntry(AccountsController):
else:
self.status = 'Draft'
self.db_set('status', self.status, update_modified = True)
def set_tax_withholding(self):
if not self.party_type == 'Supplier':
return
if not self.apply_tax_withholding_amount:
return
if not self.advance_tax_account:
frappe.throw(_("Advance TDS account is mandatory for advance TDS deduction"))
net_total = self.paid_amount
for reference in self.get("references"):
net_total_for_tds = 0
if reference.reference_doctype == 'Purchase Order':
net_total_for_tds += flt(frappe.db.get_value('Purchase Order', reference.reference_name, 'net_total'))
if net_total_for_tds:
net_total = net_total_for_tds
# Adding args as purchase invoice to get TDS amount
args = frappe._dict({
'company': self.company,
'doctype': 'Purchase Invoice',
'supplier': self.party,
'posting_date': self.posting_date,
'net_total': net_total
})
tax_withholding_details = get_party_tax_withholding_details(args, self.tax_withholding_category)
if not tax_withholding_details:
return
tax_withholding_details.update({
'add_deduct_tax': 'Add',
'cost_center': self.cost_center or erpnext.get_default_cost_center(self.company)
})
accounts = []
for d in self.taxes:
if d.account_head == tax_withholding_details.get("account_head"):
# Preserve user updated included in paid amount
if d.included_in_paid_amount:
tax_withholding_details.update({'included_in_paid_amount': d.included_in_paid_amount})
d.update(tax_withholding_details)
accounts.append(d.account_head)
if not accounts or tax_withholding_details.get("account_head") not in accounts:
self.append("taxes", tax_withholding_details)
to_remove = [d for d in self.taxes
if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head")]
for d in to_remove:
self.remove(d)
def apply_taxes(self):
self.initialize_taxes()
self.determine_exclusive_rate()
self.calculate_taxes()
def set_amounts(self):
self.set_received_amount()
self.set_amounts_in_company_currency()
self.set_amounts_after_tax()
self.set_total_allocated_amount()
self.set_unallocated_amount()
self.set_difference_amount()
def set_received_amount(self):
self.base_received_amount = self.base_paid_amount
def set_amounts_after_tax(self):
applicable_tax = 0
base_applicable_tax = 0
for tax in self.get('taxes'):
if not tax.included_in_paid_amount:
amount = -1 * tax.tax_amount if tax.add_deduct_tax == 'Deduct' else tax.tax_amount
base_amount = -1 * tax.base_tax_amount if tax.add_deduct_tax == 'Deduct' else tax.base_tax_amount
applicable_tax += amount
base_applicable_tax += base_amount
self.paid_amount_after_tax = flt(flt(self.paid_amount) + flt(applicable_tax),
self.precision("paid_amount_after_tax"))
self.base_paid_amount_after_tax = flt(flt(self.paid_amount_after_tax) * flt(self.source_exchange_rate),
self.precision("base_paid_amount_after_tax"))
self.received_amount_after_tax = flt(flt(self.received_amount) + flt(applicable_tax),
self.precision("paid_amount_after_tax"))
self.base_received_amount_after_tax = flt(flt(self.received_amount_after_tax) * flt(self.target_exchange_rate),
self.precision("base_paid_amount_after_tax"))
def set_amounts_in_company_currency(self):
self.base_paid_amount, self.base_received_amount, self.difference_amount = 0, 0, 0
if self.paid_amount:
@@ -419,17 +524,20 @@ class PaymentEntry(AccountsController):
def set_unallocated_amount(self):
self.unallocated_amount = 0
if self.party:
total_deductions = sum([flt(d.amount) for d in self.get("deductions")])
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
included_taxes = self.get_included_taxes()
if self.payment_type == "Receive" \
and self.base_total_allocated_amount < self.base_received_amount + total_deductions \
and self.total_allocated_amount < self.paid_amount + (total_deductions / self.source_exchange_rate):
self.unallocated_amount = (self.base_received_amount + total_deductions -
self.base_total_allocated_amount) / self.source_exchange_rate
self.unallocated_amount = (self.received_amount + total_deductions -
self.base_total_allocated_amount) / self.source_exchange_rate
self.unallocated_amount -= included_taxes
elif self.payment_type == "Pay" \
and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) \
and self.total_allocated_amount < self.received_amount + (total_deductions / self.target_exchange_rate):
self.unallocated_amount = (self.base_paid_amount - (total_deductions +
self.base_total_allocated_amount)) / self.target_exchange_rate
self.unallocated_amount = (self.base_paid_amount - (total_deductions +
self.base_total_allocated_amount)) / self.target_exchange_rate
self.unallocated_amount -= included_taxes
def set_difference_amount(self):
base_unallocated_amount = flt(self.unallocated_amount) * (flt(self.source_exchange_rate)
@@ -444,11 +552,23 @@ class PaymentEntry(AccountsController):
else:
self.difference_amount = self.base_paid_amount - flt(self.base_received_amount)
total_deductions = sum([flt(d.amount) for d in self.get("deductions")])
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
included_taxes = self.get_included_taxes()
self.difference_amount = flt(self.difference_amount - total_deductions,
self.difference_amount = flt(self.difference_amount - total_deductions - included_taxes,
self.precision("difference_amount"))
def get_included_taxes(self):
included_taxes = 0
for tax in self.get('taxes'):
if tax.included_in_paid_amount:
if tax.add_deduct_tax == 'Add':
included_taxes += tax.base_tax_amount
else:
included_taxes -= tax.base_tax_amount
return included_taxes
# Paid amount is auto allocated in the reference document by default.
# Clear the reference document which doesn't have allocated amount on validate so that form can be loaded fast
def clear_unallocated_reference_document_rows(self):
@@ -460,8 +580,8 @@ class PaymentEntry(AccountsController):
if ((self.payment_type=="Pay" and self.party_type=="Customer")
or (self.payment_type=="Receive" and self.party_type=="Supplier")):
total_negative_outstanding = sum([abs(flt(d.outstanding_amount))
for d in self.get("references") if flt(d.outstanding_amount) < 0])
total_negative_outstanding = sum(abs(flt(d.outstanding_amount))
for d in self.get("references") if flt(d.outstanding_amount) < 0)
paid_amount = self.paid_amount if self.payment_type=="Receive" else self.received_amount
additional_charges = sum([flt(d.amount) for d in self.deductions])
@@ -532,6 +652,7 @@ class PaymentEntry(AccountsController):
self.add_party_gl_entries(gl_entries)
self.add_bank_gl_entries(gl_entries)
self.add_deductions_gl_entries(gl_entries)
self.add_tax_gl_entries(gl_entries)
make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj)
@@ -571,8 +692,8 @@ class PaymentEntry(AccountsController):
gl_entries.append(gle)
if self.unallocated_amount:
base_unallocated_amount = base_unallocated_amount = self.unallocated_amount * \
(self.source_exchange_rate if self.payment_type=="Receive" else self.target_exchange_rate)
exchange_rate = self.get_exchange_rate()
base_unallocated_amount = (self.unallocated_amount * exchange_rate)
gle = party_gl_dict.copy()
@@ -607,6 +728,51 @@ class PaymentEntry(AccountsController):
}, item=self)
)
def add_tax_gl_entries(self, gl_entries):
for d in self.get('taxes'):
account_currency = get_account_currency(d.account_head)
if account_currency != self.company_currency:
frappe.throw(_("Currency for {0} must be {1}").format(d.account_head, self.company_currency))
if self.payment_type in ('Pay', 'Internal Transfer'):
dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit"
against = self.party or self.paid_from
elif self.payment_type == 'Receive':
dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit"
against = self.party or self.paid_to
payment_or_advance_account = self.get_party_account_for_taxes()
tax_amount = d.tax_amount
base_tax_amount = d.base_tax_amount
if self.advance_tax_account:
tax_amount = -1 * tax_amount
base_tax_amount = -1 * base_tax_amount
gl_entries.append(
self.get_gl_dict({
"account": d.account_head,
"against": against,
dr_or_cr: tax_amount,
dr_or_cr + "_in_account_currency": base_tax_amount
if account_currency==self.company_currency
else d.tax_amount,
"cost_center": d.cost_center
}, account_currency, item=d))
#Intentionally use -1 to get net values in party account
if not d.included_in_paid_amount or self.advance_tax_account:
gl_entries.append(
self.get_gl_dict({
"account": payment_or_advance_account,
"against": against,
dr_or_cr: -1 * tax_amount,
dr_or_cr + "_in_account_currency": -1 * base_tax_amount
if account_currency==self.company_currency
else d.tax_amount,
"cost_center": self.cost_center,
}, account_currency, item=d))
def add_deductions_gl_entries(self, gl_entries):
for d in self.get("deductions"):
if d.amount:
@@ -625,6 +791,14 @@ class PaymentEntry(AccountsController):
}, item=d)
)
def get_party_account_for_taxes(self):
if self.advance_tax_account:
return self.advance_tax_account
elif self.payment_type == 'Receive':
return self.paid_to
elif self.payment_type in ('Pay', 'Internal Transfer'):
return self.paid_from
def update_advance_paid(self):
if self.payment_type in ("Receive", "Pay") and self.party:
for d in self.get("references"):
@@ -667,10 +841,150 @@ class PaymentEntry(AccountsController):
if account_details:
row.update(account_details)
if not row.get('amount'):
# if no difference amount
return
self.append('deductions', row)
self.set_unallocated_amount()
def get_exchange_rate(self):
return self.source_exchange_rate if self.payment_type=="Receive" else self.target_exchange_rate
def initialize_taxes(self):
for tax in self.get("taxes"):
validate_taxes_and_charges(tax)
validate_inclusive_tax(tax, self)
tax_fields = ["total", "tax_fraction_for_current_item", "grand_total_fraction_for_current_item"]
if tax.charge_type != "Actual":
tax_fields.append("tax_amount")
for fieldname in tax_fields:
tax.set(fieldname, 0.0)
self.paid_amount_after_tax = self.paid_amount
def determine_exclusive_rate(self):
if not any((cint(tax.included_in_paid_amount) for tax in self.get("taxes"))):
return
cumulated_tax_fraction = 0
for i, tax in enumerate(self.get("taxes")):
tax.tax_fraction_for_current_item = self.get_current_tax_fraction(tax)
if i==0:
tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item
else:
tax.grand_total_fraction_for_current_item = \
self.get("taxes")[i-1].grand_total_fraction_for_current_item \
+ tax.tax_fraction_for_current_item
cumulated_tax_fraction += tax.tax_fraction_for_current_item
self.paid_amount_after_tax = flt(self.paid_amount/(1+cumulated_tax_fraction))
def calculate_taxes(self):
self.total_taxes_and_charges = 0.0
self.base_total_taxes_and_charges = 0.0
actual_tax_dict = dict([[tax.idx, flt(tax.tax_amount, tax.precision("tax_amount"))]
for tax in self.get("taxes") if tax.charge_type == "Actual"])
for i, tax in enumerate(self.get('taxes')):
current_tax_amount = self.get_current_tax_amount(tax)
if tax.charge_type == "Actual":
actual_tax_dict[tax.idx] -= current_tax_amount
if i == len(self.get("taxes")) - 1:
current_tax_amount += actual_tax_dict[tax.idx]
tax.tax_amount = current_tax_amount
tax.base_tax_amount = tax.tax_amount * self.source_exchange_rate
if tax.add_deduct_tax == "Deduct":
current_tax_amount *= -1.0
else:
current_tax_amount *= 1.0
if i == 0:
tax.total = flt(self.paid_amount_after_tax + current_tax_amount, self.precision("total", tax))
else:
tax.total = flt(self.get('taxes')[i-1].total + current_tax_amount, self.precision("total", tax))
tax.base_total = tax.total * self.source_exchange_rate
self.total_taxes_and_charges += current_tax_amount
self.base_total_taxes_and_charges += current_tax_amount * self.source_exchange_rate
if self.get('taxes'):
self.paid_amount_after_tax = self.get('taxes')[-1].base_total
def get_current_tax_amount(self, tax):
tax_rate = tax.rate
# To set row_id by default as previous row.
if tax.charge_type in ["On Previous Row Amount", "On Previous Row Total"]:
if tax.idx == 1:
frappe.throw(_("Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row"))
if not tax.row_id:
tax.row_id = tax.idx - 1
if tax.charge_type == "Actual":
current_tax_amount = flt(tax.tax_amount, self.precision("tax_amount", tax))
elif tax.charge_type == "On Paid Amount":
current_tax_amount = (tax_rate / 100.0) * self.paid_amount_after_tax
elif tax.charge_type == "On Previous Row Amount":
current_tax_amount = (tax_rate / 100.0) * \
self.get('taxes')[cint(tax.row_id) - 1].tax_amount
elif tax.charge_type == "On Previous Row Total":
current_tax_amount = (tax_rate / 100.0) * \
self.get('taxes')[cint(tax.row_id) - 1].total
return current_tax_amount
def get_current_tax_fraction(self, tax):
current_tax_fraction = 0
if cint(tax.included_in_paid_amount):
tax_rate = tax.rate
if tax.charge_type == "On Paid Amount":
current_tax_fraction = tax_rate / 100.0
elif tax.charge_type == "On Previous Row Amount":
current_tax_fraction = (tax_rate / 100.0) * \
self.get("taxes")[cint(tax.row_id) - 1].tax_fraction_for_current_item
elif tax.charge_type == "On Previous Row Total":
current_tax_fraction = (tax_rate / 100.0) * \
self.get("taxes")[cint(tax.row_id) - 1].grand_total_fraction_for_current_item
if getattr(tax, "add_deduct_tax", None) and tax.add_deduct_tax == "Deduct":
current_tax_fraction *= -1.0
return current_tax_fraction
def validate_inclusive_tax(tax, doc):
def _on_previous_row_error(row_range):
throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx, row_range))
if cint(getattr(tax, "included_in_paid_amount", None)):
if tax.charge_type == "Actual":
# inclusive tax cannot be of type Actual
throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format(tax.idx))
elif tax.charge_type == "On Previous Row Amount" and \
not cint(doc.get("taxes")[cint(tax.row_id) - 1].included_in_paid_amount):
# referred row should also be inclusive
_on_previous_row_error(tax.row_id)
elif tax.charge_type == "On Previous Row Total" and \
not all([cint(t.included_in_paid_amount for t in doc.get("taxes")[:cint(tax.row_id) - 1])]):
# all rows about the referred tax should be inclusive
_on_previous_row_error("1 - %d" % (cint(tax.row_id),))
elif tax.get("category") == "Valuation":
frappe.throw(_("Valuation type charges can not be marked as Inclusive"))
@frappe.whitelist()
def get_outstanding_reference_documents(args):
@@ -791,7 +1105,7 @@ def split_invoices_based_on_payment_terms(outstanding_invoices):
outstanding_invoices.pop(idx - 1)
outstanding_invoices += invoice_ref_based_on_payment_terms[idx]
return outstanding_invoices
def get_orders_to_be_billed(posting_date, party_type, party,
@@ -989,6 +1303,7 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
outstanding_amount = ref_doc.get("outstanding_amount")
elif reference_doctype == "Donation":
total_amount = ref_doc.get("amount")
outstanding_amount = total_amount
exchange_rate = 1
elif reference_doctype == "Dunning":
total_amount = ref_doc.get("dunning_amount")
@@ -1045,9 +1360,9 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
return frappe._dict({
"due_date": ref_doc.get("due_date"),
"total_amount": total_amount,
"outstanding_amount": outstanding_amount,
"exchange_rate": exchange_rate,
"total_amount": flt(total_amount),
"outstanding_amount": flt(outstanding_amount),
"exchange_rate": flt(exchange_rate),
"bill_no": bill_no
})
@@ -1235,6 +1550,13 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
})
pe.set_difference_amount()
if doc.doctype == 'Purchase Order' and doc.apply_tds:
pe.apply_tax_withholding_amount = 1
pe.tax_withholding_category = doc.tax_withholding_category
if not pe.advance_tax_account:
pe.advance_tax_account = frappe.db.get_value('Company', pe.company, 'unrealized_profit_loss_account')
return pe
def get_bank_cash_account(doc, bank_account):
@@ -1353,6 +1675,7 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta
paid_amount = received_amount * doc.get('conversion_rate', 1)
if dt == "Employee Advance":
paid_amount = received_amount * doc.get('exchange_rate', 1)
return paid_amount, received_amount
def apply_early_payment_discount(paid_amount, received_amount, doc):

View File

@@ -589,9 +589,9 @@ class TestPaymentEntry(unittest.TestCase):
party_account_balance = get_balance_on(account=pe.paid_from, cost_center=pe.cost_center)
self.assertEqual(pe.cost_center, si.cost_center)
self.assertEqual(expected_account_balance, account_balance)
self.assertEqual(expected_party_balance, party_balance)
self.assertEqual(expected_party_account_balance, party_account_balance)
self.assertEqual(flt(expected_account_balance), account_balance)
self.assertEqual(flt(expected_party_balance), party_balance)
self.assertEqual(flt(expected_party_account_balance), party_account_balance)
def create_payment_terms_template():

View File

@@ -1,140 +1,70 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2016-06-15 15:56:30.815503",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"actions": [],
"creation": "2016-06-15 15:56:30.815503",
"doctype": "DocType",
"editable_grid": 1,
"field_order": [
"account",
"cost_center",
"amount",
"column_break_2",
"description"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Account",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Account",
"options": "Account",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "cost_center",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Cost Center",
"length": 0,
"no_copy": 0,
"options": "Cost Center",
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "cost_center",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Cost Center",
"options": "Cost Center",
"print_hide": 1,
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Amount",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description",
"show_days": 1,
"show_seconds": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2019-01-07 16:52:07.040146",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Deduction",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0,
"track_views": 0
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-09-12 20:38:08.110674",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Deduction",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@@ -14,7 +14,8 @@
"total_amount",
"outstanding_amount",
"allocated_amount",
"exchange_rate"
"exchange_rate",
"exchange_gain_loss"
],
"fields": [
{
@@ -90,12 +91,19 @@
"fieldtype": "Link",
"label": "Payment Term",
"options": "Payment Term"
},
{
"fieldname": "exchange_gain_loss",
"fieldtype": "Currency",
"label": "Exchange Gain/Loss",
"options": "Company:company:default_currency",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-02-10 11:25:47.144392",
"modified": "2021-04-21 13:30:11.605388",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Reference",

View File

@@ -31,10 +31,10 @@ class TestPaymentOrder(unittest.TestCase):
doc = create_payment_order_against_payment_entry(payment_entry, "Payment Entry")
reference_doc = doc.get("references")[0]
self.assertEquals(reference_doc.reference_name, payment_entry.name)
self.assertEquals(reference_doc.reference_doctype, "Payment Entry")
self.assertEquals(reference_doc.supplier, "_Test Supplier")
self.assertEquals(reference_doc.amount, 250)
self.assertEqual(reference_doc.reference_name, payment_entry.name)
self.assertEqual(reference_doc.reference_doctype, "Payment Entry")
self.assertEqual(reference_doc.supplier, "_Test Supplier")
self.assertEqual(reference_doc.amount, 250)
def create_payment_order_against_payment_entry(ref_doc, order_type):
payment_order = frappe.get_doc(dict(

View File

@@ -234,8 +234,9 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
});
if (invoices) {
frappe.meta.get_docfield("Payment Reconciliation Payment", "invoice_number",
me.frm.doc.name).options = "\n" + invoices.join("\n");
this.frm.fields_dict.payments.grid.update_docfield_property(
'invoice_number', 'options', "\n" + invoices.join("\n")
);
$.each(me.frm.doc.payments || [], function(i, p) {
if(!in_list(invoices, cstr(p.invoice_number))) p.invoice_number = null;

View File

@@ -114,7 +114,7 @@ class PaymentReconciliation(Document):
'party_type': self.party_type,
'voucher_type': voucher_type,
'account': self.receivable_payable_account
}, as_dict=1, debug=1)
}, as_dict=1)
def add_payment_entries(self, entries):
self.set('payments', [])
@@ -306,5 +306,5 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
}
]
})
jv.flags.ignore_mandatory = True
jv.submit()

View File

@@ -101,7 +101,7 @@ class PaymentRequest(Document):
controller.validate_transaction_currency(self.currency)
controller.request_for_payment(**payment_record)
def get_request_amount(self):
data_of_completed_requests = frappe.get_all("Integration Request", filters={
'reference_doctype': self.doctype,
@@ -112,7 +112,7 @@ class PaymentRequest(Document):
if not data_of_completed_requests:
return self.grand_total
request_amounts = sum([json.loads(d).get('request_amount') for d in data_of_completed_requests])
request_amounts = sum(json.loads(d).get('request_amount') for d in data_of_completed_requests)
return request_amounts
def on_cancel(self):
@@ -492,7 +492,6 @@ def update_payment_req_status(doc, method):
status = 'Requested'
pay_req_doc.db_set('status', status)
frappe.db.commit()
def get_dummy_message(doc):
return frappe.render_template("""{% if doc.contact_person -%}

View File

@@ -20,10 +20,11 @@
"discount",
"section_break_9",
"payment_amount",
"outstanding",
"paid_amount",
"discounted_amount",
"column_break_3",
"outstanding",
"paid_amount"
"base_payment_amount"
],
"fields": [
{
@@ -78,7 +79,8 @@
"depends_on": "paid_amount",
"fieldname": "paid_amount",
"fieldtype": "Currency",
"label": "Paid Amount"
"label": "Paid Amount",
"options": "currency"
},
{
"fieldname": "column_break_3",
@@ -97,6 +99,7 @@
"fieldname": "outstanding",
"fieldtype": "Currency",
"label": "Outstanding",
"options": "currency",
"read_only": 1
},
{
@@ -145,12 +148,18 @@
{
"fieldname": "section_break_4",
"fieldtype": "Section Break"
},
{
"fieldname": "base_payment_amount",
"fieldtype": "Currency",
"label": "Payment Amount (Company Currency)",
"options": "Company:company:default_currency"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-02-15 21:03:12.540546",
"modified": "2021-04-28 05:41:35.084233",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Schedule",

View File

@@ -26,7 +26,7 @@ class PaymentTermsTemplate(Document):
def check_duplicate_terms(self):
terms = []
for term in self.terms:
term_info = (term.credit_days, term.credit_months, term.due_date_based_on)
term_info = (term.payment_term, term.credit_days, term.credit_months, term.due_date_based_on)
if term_info in terms:
frappe.msgprint(
_('The Payment Term at row {0} is possibly a duplicate.').format(term.idx),

View File

@@ -1,350 +1,138 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "ACC-PCV-.YYYY.-.#####",
"beta": 0,
"creation": "2013-01-10 16:34:07",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"editable_grid": 0,
"engine": "InnoDB",
"actions": [],
"autoname": "ACC-PCV-.YYYY.-.#####",
"creation": "2013-01-10 16:34:07",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"transaction_date",
"posting_date",
"fiscal_year",
"amended_from",
"company",
"cost_center_wise_pnl",
"column_break1",
"closing_account_head",
"remarks"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "transaction_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Transaction Date",
"length": 0,
"no_copy": 0,
"oldfieldname": "transaction_date",
"oldfieldtype": "Date",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "transaction_date",
"fieldtype": "Date",
"label": "Transaction Date",
"oldfieldname": "transaction_date",
"oldfieldtype": "Date"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "posting_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Posting Date",
"length": 0,
"no_copy": 0,
"oldfieldname": "posting_date",
"oldfieldtype": "Date",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "posting_date",
"fieldtype": "Date",
"label": "Posting Date",
"oldfieldname": "posting_date",
"oldfieldtype": "Date",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "fiscal_year",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Closing Fiscal Year",
"length": 0,
"no_copy": 0,
"oldfieldname": "fiscal_year",
"oldfieldtype": "Select",
"options": "Fiscal Year",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "fiscal_year",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Closing Fiscal Year",
"oldfieldname": "fiscal_year",
"oldfieldtype": "Select",
"options": "Fiscal Year",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "amended_from",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 1,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Amended From",
"length": 0,
"no_copy": 1,
"oldfieldname": "amended_from",
"oldfieldtype": "Data",
"options": "Period Closing Voucher",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "amended_from",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Amended From",
"no_copy": 1,
"oldfieldname": "amended_from",
"oldfieldtype": "Data",
"options": "Period Closing Voucher",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "company",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Company",
"length": 0,
"no_copy": 0,
"oldfieldname": "company",
"oldfieldtype": "Select",
"options": "Company",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"oldfieldname": "company",
"oldfieldtype": "Select",
"options": "Company",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break1",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"oldfieldtype": "Column Break",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "column_break1",
"fieldtype": "Column Break",
"oldfieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "The account head under Liability or Equity, in which Profit/Loss will be booked",
"fieldname": "closing_account_head",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Closing Account Head",
"length": 0,
"no_copy": 0,
"oldfieldname": "closing_account_head",
"oldfieldtype": "Link",
"options": "Account",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"description": "The account head under Liability or Equity, in which Profit/Loss will be booked",
"fieldname": "closing_account_head",
"fieldtype": "Link",
"label": "Closing Account Head",
"oldfieldname": "closing_account_head",
"oldfieldtype": "Link",
"options": "Account",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "remarks",
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Remarks",
"length": 0,
"no_copy": 0,
"oldfieldname": "remarks",
"oldfieldtype": "Small Text",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldname": "remarks",
"fieldtype": "Small Text",
"label": "Remarks",
"oldfieldname": "remarks",
"oldfieldtype": "Small Text",
"reqd": 1
},
{
"default": "0",
"fieldname": "cost_center_wise_pnl",
"fieldtype": "Check",
"label": "Book Cost Center Wise Profit/Loss"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-file-text",
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 1,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Period Closing Voucher",
"owner": "Administrator",
],
"icon": "fa fa-file-text",
"idx": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-05-20 15:27:37.210458",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Period Closing Voucher",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"search_fields": "posting_date, fiscal_year",
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "closing_account_head",
"track_changes": 0,
"track_seen": 0,
"track_views": 0
],
"search_fields": "posting_date, fiscal_year",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "closing_account_head"
}

View File

@@ -51,63 +51,96 @@ class PeriodClosingVoucher(AccountsController):
def make_gl_entries(self):
gl_entries = []
net_pl_balance = 0
dimension_fields = ['t1.cost_center']
net_pl_balance = 0
accounting_dimensions = get_accounting_dimensions()
for dimension in accounting_dimensions:
dimension_fields.append('t1.{0}'.format(dimension))
dimension_filters, default_dimensions = get_dimensions()
pl_accounts = self.get_pl_balances(dimension_fields)
pl_accounts = self.get_pl_balances()
for acc in pl_accounts:
if flt(acc.balance_in_company_currency):
if flt(acc.bal_in_company_currency):
gl_entries.append(self.get_gl_dict({
"account": acc.account,
"cost_center": acc.cost_center,
"account_currency": acc.account_currency,
"debit_in_account_currency": abs(flt(acc.balance_in_account_currency)) \
if flt(acc.balance_in_account_currency) < 0 else 0,
"debit": abs(flt(acc.balance_in_company_currency)) \
if flt(acc.balance_in_company_currency) < 0 else 0,
"credit_in_account_currency": abs(flt(acc.balance_in_account_currency)) \
if flt(acc.balance_in_account_currency) > 0 else 0,
"credit": abs(flt(acc.balance_in_company_currency)) \
if flt(acc.balance_in_company_currency) > 0 else 0
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) < 0 else 0,
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0,
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0,
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0
}, item=acc))
net_pl_balance += flt(acc.balance_in_company_currency)
net_pl_balance += flt(acc.bal_in_company_currency)
if net_pl_balance:
cost_center = frappe.db.get_value("Company", self.company, "cost_center")
gl_entry = self.get_gl_dict({
"account": self.closing_account_head,
"debit_in_account_currency": abs(net_pl_balance) if net_pl_balance > 0 else 0,
"debit": abs(net_pl_balance) if net_pl_balance > 0 else 0,
"credit_in_account_currency": abs(net_pl_balance) if net_pl_balance < 0 else 0,
"credit": abs(net_pl_balance) if net_pl_balance < 0 else 0,
"cost_center": cost_center
})
for dimension in accounting_dimensions:
gl_entry.update({
dimension: default_dimensions.get(self.company, {}).get(dimension)
})
gl_entries.append(gl_entry)
if self.cost_center_wise_pnl:
costcenter_wise_gl_entries = self.get_costcenter_wise_pnl_gl_entries(pl_accounts)
gl_entries += costcenter_wise_gl_entries
else:
gl_entry = self.get_pnl_gl_entry(net_pl_balance)
gl_entries.append(gl_entry)
from erpnext.accounts.general_ledger import make_gl_entries
make_gl_entries(gl_entries)
def get_pnl_gl_entry(self, net_pl_balance):
cost_center = frappe.db.get_value("Company", self.company, "cost_center")
gl_entry = self.get_gl_dict({
"account": self.closing_account_head,
"debit_in_account_currency": abs(net_pl_balance) if net_pl_balance > 0 else 0,
"debit": abs(net_pl_balance) if net_pl_balance > 0 else 0,
"credit_in_account_currency": abs(net_pl_balance) if net_pl_balance < 0 else 0,
"credit": abs(net_pl_balance) if net_pl_balance < 0 else 0,
"cost_center": cost_center
})
self.update_default_dimensions(gl_entry)
return gl_entry
def get_costcenter_wise_pnl_gl_entries(self, pl_accounts):
company_cost_center = frappe.db.get_value("Company", self.company, "cost_center")
gl_entries = []
for acc in pl_accounts:
if flt(acc.bal_in_company_currency):
gl_entry = self.get_gl_dict({
"account": self.closing_account_head,
"cost_center": acc.cost_center or company_cost_center,
"account_currency": acc.account_currency,
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0,
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) < 0 else 0,
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0
}, item=acc)
self.update_default_dimensions(gl_entry)
gl_entries.append(gl_entry)
return gl_entries
def update_default_dimensions(self, gl_entry):
if not self.accounting_dimensions:
self.accounting_dimensions = get_accounting_dimensions()
_, default_dimensions = get_dimensions()
for dimension in self.accounting_dimensions:
gl_entry.update({
dimension: default_dimensions.get(self.company, {}).get(dimension)
})
def get_pl_balances(self):
"""Get balance for dimension-wise pl accounts"""
dimension_fields = ['t1.cost_center']
self.accounting_dimensions = get_accounting_dimensions()
for dimension in self.accounting_dimensions:
dimension_fields.append('t1.{0}'.format(dimension))
def get_pl_balances(self, dimension_fields):
"""Get balance for pl accounts"""
return frappe.db.sql("""
select
t1.account, t2.account_currency, {dimension_fields},
sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as balance_in_account_currency,
sum(t1.debit) - sum(t1.credit) as balance_in_company_currency
sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency,
sum(t1.debit) - sum(t1.credit) as bal_in_company_currency
from `tabGL Entry` t1, `tabAccount` t2
where t1.account = t2.name and t2.report_type = 'Profit and Loss'
and t2.docstatus < 2 and t2.company = %s

View File

@@ -8,6 +8,7 @@ import frappe
from frappe.utils import flt, today
from erpnext.accounts.utils import get_fiscal_year, now
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
class TestPeriodClosingVoucher(unittest.TestCase):
def test_closing_entry(self):
@@ -65,6 +66,58 @@ class TestPeriodClosingVoucher(unittest.TestCase):
self.assertEqual(gle_for_random_expense_account[0].amount_in_account_currency,
-1*random_expense_account[0].balance_in_account_currency)
def test_cost_center_wise_posting(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
company = create_company()
surplus_account = create_account()
cost_center1 = create_cost_center("Test Cost Center 1")
cost_center2 = create_cost_center("Test Cost Center 2")
create_sales_invoice(
company=company,
cost_center=cost_center1,
income_account="Sales - TPC",
expense_account="Cost of Goods Sold - TPC",
rate=400,
debit_to="Debtors - TPC"
)
create_sales_invoice(
company=company,
cost_center=cost_center2,
income_account="Sales - TPC",
expense_account="Cost of Goods Sold - TPC",
rate=200,
debit_to="Debtors - TPC"
)
pcv = frappe.get_doc({
"transaction_date": today(),
"posting_date": today(),
"fiscal_year": get_fiscal_year(today())[0],
"company": "Test PCV Company",
"cost_center_wise_pnl": 1,
"closing_account_head": surplus_account,
"remarks": "Test",
"doctype": "Period Closing Voucher"
})
pcv.insert()
pcv.submit()
expected_gle = (
('Sales - TPC', 200.0, 0.0, cost_center2),
(surplus_account, 0.0, 200.0, cost_center2),
('Sales - TPC', 400.0, 0.0, cost_center1),
(surplus_account, 0.0, 400.0, cost_center1)
)
pcv_gle = frappe.db.sql("""
select account, debit, credit, cost_center from `tabGL Entry` where voucher_no=%s
""", (pcv.name))
self.assertTrue(pcv_gle, expected_gle)
def make_period_closing_voucher(self):
pcv = frappe.get_doc({
"doctype": "Period Closing Voucher",
@@ -80,6 +133,38 @@ class TestPeriodClosingVoucher(unittest.TestCase):
return pcv
def create_company():
company = frappe.get_doc({
'doctype': 'Company',
'company_name': "Test PCV Company",
'country': 'United States',
'default_currency': 'USD'
})
company.insert(ignore_if_duplicate = True)
return company.name
def create_account():
account = frappe.get_doc({
"account_name": "Reserve and Surplus",
"is_group": 0,
"company": "Test PCV Company",
"root_type": "Liability",
"report_type": "Balance Sheet",
"account_currency": "USD",
"parent_account": "Current Liabilities - TPC",
"doctype": "Account"
}).insert(ignore_if_duplicate = True)
return account.name
def create_cost_center(cc_name):
costcenter = frappe.get_doc({
"company": "Test PCV Company",
"cost_center_name": cc_name,
"doctype": "Cost Center",
"parent_cost_center": "Test PCV Company - TPC"
})
costcenter.insert(ignore_if_duplicate = True)
return costcenter.name
test_dependencies = ["Customer", "Cost Center"]
test_records = frappe.get_test_records("Period Closing Voucher")

View File

@@ -22,7 +22,43 @@ frappe.ui.form.on('POS Closing Entry', {
});
if (frm.doc.docstatus === 0 && !frm.doc.amended_from) frm.set_value("period_end_date", frappe.datetime.now_datetime());
if (frm.doc.docstatus === 1) set_html_data(frm);
frappe.realtime.on('closing_process_complete', async function(data) {
await frm.reload_doc();
if (frm.doc.status == 'Failed' && frm.doc.error_message && data.user == frappe.session.user) {
frappe.msgprint({
title: __('POS Closing Failed'),
message: frm.doc.error_message,
indicator: 'orange',
clear: true
});
}
});
set_html_data(frm);
},
refresh: function(frm) {
if (frm.doc.docstatus == 1 && frm.doc.status == 'Failed') {
const issue = '<a id="jump_to_error" style="text-decoration: underline;">issue</a>';
frm.dashboard.set_headline(
__('POS Closing failed while running in a background process. You can resolve the {0} and retry the process again.', [issue]));
$('#jump_to_error').on('click', (e) => {
e.preventDefault();
frappe.utils.scroll_to(
cur_frm.get_field("error_message").$wrapper,
true,
30
);
});
frm.add_custom_button(__('Retry'), function () {
frm.call('retry', {}, () => {
frm.reload_doc();
});
});
}
},
pos_opening_entry(frm) {
@@ -61,48 +97,37 @@ frappe.ui.form.on('POS Closing Entry', {
refresh_fields(frm);
set_html_data(frm);
}
})
});
},
before_save: function(frm) {
frm.set_value("grand_total", 0);
frm.set_value("net_total", 0);
frm.set_value("total_quantity", 0);
frm.set_value("taxes", []);
for (let row of frm.doc.payment_reconciliation) {
row.expected_amount = row.opening_amount;
}
for (let row of frm.doc.pos_transactions) {
frappe.db.get_doc("POS Invoice", row.pos_invoice).then(doc => {
frm.doc.grand_total += flt(doc.grand_total);
frm.doc.net_total += flt(doc.net_total);
frm.doc.total_quantity += flt(doc.total_qty);
refresh_payments(doc, frm);
refresh_taxes(doc, frm);
refresh_fields(frm);
set_html_data(frm);
});
}
}
});
cur_frm.cscript.before_pos_transactions_remove = function(doc, cdt, cdn) {
const removed_row = locals[cdt][cdn];
if (!removed_row.pos_invoice) return;
frappe.db.get_doc("POS Invoice", removed_row.pos_invoice).then(doc => {
cur_frm.doc.grand_total -= flt(doc.grand_total);
cur_frm.doc.net_total -= flt(doc.net_total);
cur_frm.doc.total_quantity -= flt(doc.total_qty);
refresh_payments(doc, cur_frm, 1);
refresh_taxes(doc, cur_frm, 1);
refresh_fields(cur_frm);
set_html_data(cur_frm);
});
}
frappe.ui.form.on('POS Invoice Reference', {
pos_invoice(frm, cdt, cdn) {
const added_row = locals[cdt][cdn];
if (!added_row.pos_invoice) return;
frappe.db.get_doc("POS Invoice", added_row.pos_invoice).then(doc => {
frm.doc.grand_total += flt(doc.grand_total);
frm.doc.net_total += flt(doc.net_total);
frm.doc.total_quantity += flt(doc.total_qty);
refresh_payments(doc, frm);
refresh_taxes(doc, frm);
refresh_fields(frm);
set_html_data(frm);
});
}
})
frappe.ui.form.on('POS Closing Entry Detail', {
closing_amount: (frm, cdt, cdn) => {
const row = locals[cdt][cdn];
frappe.model.set_value(cdt, cdn, "difference", flt(row.expected_amount - row.closing_amount))
frappe.model.set_value(cdt, cdn, "difference", flt(row.expected_amount - row.closing_amount));
}
})
@@ -126,28 +151,31 @@ function add_to_pos_transaction(d, frm) {
})
}
function refresh_payments(d, frm, remove) {
function refresh_payments(d, frm) {
d.payments.forEach(p => {
const payment = frm.doc.payment_reconciliation.find(pay => pay.mode_of_payment === p.mode_of_payment);
if (p.account == d.account_for_change_amount) {
p.amount -= flt(d.change_amount);
}
if (payment) {
if (!remove) payment.expected_amount += flt(p.amount);
else payment.expected_amount -= flt(p.amount);
payment.expected_amount += flt(p.amount);
payment.difference = payment.closing_amount - payment.expected_amount;
} else {
frm.add_child("payment_reconciliation", {
mode_of_payment: p.mode_of_payment,
opening_amount: 0,
expected_amount: p.amount
expected_amount: p.amount,
closing_amount: 0
})
}
})
}
function refresh_taxes(d, frm, remove) {
function refresh_taxes(d, frm) {
d.taxes.forEach(t => {
const tax = frm.doc.taxes.find(tx => tx.account_head === t.account_head && tx.rate === t.rate);
if (tax) {
if (!remove) tax.amount += flt(t.tax_amount);
else tax.amount -= flt(t.tax_amount);
tax.amount += flt(t.tax_amount);
} else {
frm.add_child("taxes", {
account_head: t.account_head,
@@ -177,11 +205,13 @@ function refresh_fields(frm) {
}
function set_html_data(frm) {
frappe.call({
method: "get_payment_reconciliation_details",
doc: frm.doc,
callback: (r) => {
frm.get_field("payment_reconciliation_details").$wrapper.html(r.message);
}
})
if (frm.doc.docstatus === 1 && frm.doc.status == 'Submitted') {
frappe.call({
method: "get_payment_reconciliation_details",
doc: frm.doc,
callback: (r) => {
frm.get_field("payment_reconciliation_details").$wrapper.html(r.message);
}
});
}
}

View File

@@ -30,6 +30,8 @@
"total_quantity",
"column_break_16",
"taxes",
"failure_description_section",
"error_message",
"section_break_14",
"amended_from"
],
@@ -195,7 +197,7 @@
"fieldtype": "Select",
"hidden": 1,
"label": "Status",
"options": "Draft\nSubmitted\nQueued\nCancelled",
"options": "Draft\nSubmitted\nQueued\nFailed\nCancelled",
"print_hide": 1,
"read_only": 1
},
@@ -203,6 +205,21 @@
"fieldname": "period_details_section",
"fieldtype": "Section Break",
"label": "Period Details"
},
{
"collapsible": 1,
"collapsible_depends_on": "error_message",
"depends_on": "error_message",
"fieldname": "failure_description_section",
"fieldtype": "Section Break",
"label": "Failure Description"
},
{
"depends_on": "error_message",
"fieldname": "error_message",
"fieldtype": "Small Text",
"label": "Error",
"read_only": 1
}
],
"is_submittable": 1,
@@ -212,7 +229,7 @@
"link_fieldname": "pos_closing_entry"
}
],
"modified": "2021-02-01 13:47:20.722104",
"modified": "2021-05-05 16:59:49.723261",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Closing Entry",

View File

@@ -16,28 +16,8 @@ class POSClosingEntry(StatusUpdater):
if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
self.validate_pos_closing()
self.validate_pos_invoices()
def validate_pos_closing(self):
user = frappe.db.sql("""
SELECT name FROM `tabPOS Closing Entry`
WHERE
user = %(user)s AND docstatus = 1 AND pos_profile = %(profile)s AND
(period_start_date between %(start)s and %(end)s OR period_end_date between %(start)s and %(end)s)
""", {
'user': self.user,
'profile': self.pos_profile,
'start': self.period_start_date,
'end': self.period_end_date
})
if user:
bold_already_exists = frappe.bold(_("already exists"))
bold_user = frappe.bold(self.user)
frappe.throw(_("POS Closing Entry {} against {} between selected period")
.format(bold_already_exists, bold_user), title=_("Invalid Period"))
def validate_pos_invoices(self):
invalid_rows = []
for d in self.pos_transactions:
@@ -80,6 +60,10 @@ class POSClosingEntry(StatusUpdater):
def on_cancel(self):
unconsolidate_pos_invoices(closing_entry=self)
@frappe.whitelist()
def retry(self):
consolidate_pos_invoices(closing_entry=self)
def update_opening_entry(self, for_cancel=False):
opening_entry = frappe.get_doc("POS Opening Entry", self.pos_opening_entry)
opening_entry.pos_closing_entry = self.name if not for_cancel else None
@@ -89,8 +73,8 @@ class POSClosingEntry(StatusUpdater):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_cashiers(doctype, txt, searchfield, start, page_len, filters):
cashiers_list = frappe.get_all("POS Profile User", filters=filters, fields=['user'])
return [c['user'] for c in cashiers_list]
cashiers_list = frappe.get_all("POS Profile User", filters=filters, fields=['user'], as_list=1)
return [c for c in cashiers_list]
@frappe.whitelist()
def get_pos_invoices(start, end, pos_profile, user):

View File

@@ -8,6 +8,7 @@ frappe.listview_settings['POS Closing Entry'] = {
"Draft": "red",
"Submitted": "blue",
"Queued": "orange",
"Failed": "red",
"Cancelled": "red"
};

View File

@@ -46,6 +46,7 @@
"reqd": 1
},
{
"default": "0",
"fieldname": "closing_amount",
"fieldtype": "Currency",
"in_list_view": 1,
@@ -57,7 +58,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-10-23 16:45:43.662034",
"modified": "2021-05-19 20:08:44.523861",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Closing Entry Detail",

View File

@@ -96,47 +96,58 @@ class POSInvoice(SalesInvoice):
if paid_amt and pay.amount != paid_amt:
return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment))
def validate_pos_reserved_serial_nos(self, item):
serial_nos = get_serial_nos(item.serial_no)
filters = {"item_code": item.item_code, "warehouse": item.warehouse}
if item.batch_no:
filters["batch_no"] = item.batch_no
reserved_serial_nos = get_pos_reserved_serial_nos(filters)
invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos]
bold_invalid_serial_nos = frappe.bold(', '.join(invalid_serial_nos))
if len(invalid_serial_nos) == 1:
frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.")
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
elif invalid_serial_nos:
frappe.throw(_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.")
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
def validate_delivered_serial_nos(self, item):
serial_nos = get_serial_nos(item.serial_no)
delivered_serial_nos = frappe.db.get_list('Serial No', {
'item_code': item.item_code,
'name': ['in', serial_nos],
'sales_invoice': ['is', 'set']
}, pluck='name')
if delivered_serial_nos:
bold_delivered_serial_nos = frappe.bold(', '.join(delivered_serial_nos))
frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another Sales Invoice. Please select valid serial no.")
.format(item.idx, bold_delivered_serial_nos), title=_("Item Unavailable"))
def validate_stock_availablility(self):
if self.is_return:
return
allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock')
error_msg = []
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
for d in self.get('items'):
msg = ""
if d.serial_no:
filters = { "item_code": d.item_code, "warehouse": d.warehouse }
if d.batch_no:
filters["batch_no"] = d.batch_no
reserved_serial_nos = get_pos_reserved_serial_nos(filters)
serial_nos = get_serial_nos(d.serial_no)
invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos]
bold_invalid_serial_nos = frappe.bold(', '.join(invalid_serial_nos))
if len(invalid_serial_nos) == 1:
msg = (_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.")
.format(d.idx, bold_invalid_serial_nos))
elif invalid_serial_nos:
msg = (_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.")
.format(d.idx, bold_invalid_serial_nos))
self.validate_pos_reserved_serial_nos(d)
self.validate_delivered_serial_nos(d)
else:
if allow_negative_stock:
return
available_stock = get_stock_availability(d.item_code, d.warehouse)
item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)
if flt(available_stock) <= 0:
msg = (_('Row #{}: Item Code: {} is not available under warehouse {}.').format(d.idx, item_code, warehouse))
frappe.throw(_('Row #{}: Item Code: {} is not available under warehouse {}.')
.format(d.idx, item_code, warehouse), title=_("Item Unavailable"))
elif flt(available_stock) < flt(d.qty):
msg = (_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.')
.format(d.idx, item_code, warehouse, qty))
if msg:
error_msg.append(msg)
if error_msg:
frappe.throw(error_msg, title=_("Item Unavailable"), as_list=True)
frappe.throw(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.')
.format(d.idx, item_code, warehouse, available_stock), title=_("Item Unavailable"))
def validate_serialised_or_batched_item(self):
error_msg = []
@@ -203,9 +214,9 @@ class POSInvoice(SalesInvoice):
for d in self.get("items"):
is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item")
if not is_stock_item:
frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice. ").format(
d.idx, frappe.bold(d.item_code)
), title=_("Invalid Item"))
if not frappe.db.exists('Product Bundle', d.item_code):
frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice.")
.format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
def validate_mode_of_payment(self):
if len(self.payments) == 0:
@@ -446,29 +457,48 @@ class POSInvoice(SalesInvoice):
@frappe.whitelist()
def get_stock_availability(item_code, warehouse):
latest_sle = frappe.db.sql("""select qty_after_transaction
from `tabStock Ledger Entry`
if frappe.db.get_value('Item', item_code, 'is_stock_item'):
bin_qty = get_bin_qty(item_code, warehouse)
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
return bin_qty - pos_sales_qty
else:
if frappe.db.exists('Product Bundle', item_code):
return get_bundle_availability(item_code, warehouse)
def get_bundle_availability(bundle_item_code, warehouse):
product_bundle = frappe.get_doc('Product Bundle', bundle_item_code)
bundle_bin_qty = 1000000
for item in product_bundle.items:
item_bin_qty = get_bin_qty(item.item_code, warehouse)
item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
available_qty = item_bin_qty - item_pos_reserved_qty
max_available_bundles = available_qty / item.qty
if bundle_bin_qty > max_available_bundles:
bundle_bin_qty = max_available_bundles
pos_sales_qty = get_pos_reserved_qty(bundle_item_code, warehouse)
return bundle_bin_qty - pos_sales_qty
def get_bin_qty(item_code, warehouse):
bin_qty = frappe.db.sql("""select actual_qty from `tabBin`
where item_code = %s and warehouse = %s
order by posting_date desc, posting_time desc
limit 1""", (item_code, warehouse), as_dict=1)
pos_sales_qty = frappe.db.sql("""select sum(p_item.qty) as qty
return bin_qty[0].actual_qty or 0 if bin_qty else 0
def get_pos_reserved_qty(item_code, warehouse):
reserved_qty = frappe.db.sql("""select sum(p_item.qty) as qty
from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item
where p.name = p_item.parent
and p.consolidated_invoice is NULL
and p.docstatus = 1
and ifnull(p.consolidated_invoice, '') = ''
and p_item.docstatus = 1
and p_item.item_code = %s
and p_item.warehouse = %s
""", (item_code, warehouse), as_dict=1)
sle_qty = latest_sle[0].qty_after_transaction or 0 if latest_sle else 0
pos_sales_qty = pos_sales_qty[0].qty or 0 if pos_sales_qty else 0
if sle_qty and pos_sales_qty:
return sle_qty - pos_sales_qty
else:
return sle_qty
return reserved_qty[0].qty or 0 if reserved_qty else 0
@frappe.whitelist()
def make_sales_return(source_name, target_doc=None):
@@ -515,4 +545,4 @@ def add_return_modes(doc, pos_profile):
mode_of_payment = pos_payment_method.mode_of_payment
if pos_payment_method.allow_in_returns and not [d for d in doc.get('payments') if d.mode_of_payment == mode_of_payment]:
payment_mode = get_mode_of_payment_info(mode_of_payment, doc.company)
append_payment(payment_mode[0])
append_payment(payment_mode[0])

View File

@@ -10,10 +10,12 @@ from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
class TestPOSInvoice(unittest.TestCase):
@classmethod
def setUpClass(cls):
make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item", qty=800, basic_rate=100)
frappe.db.sql("delete from `tabTax Rule`")
def tearDown(self):
@@ -320,6 +322,34 @@ class TestPOSInvoice(unittest.TestCase):
self.assertRaises(frappe.ValidationError, pos2.insert)
def test_delivered_serialized_item_transaction(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
se = make_serialized_item(company='_Test Company',
target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC')
serial_nos = get_serial_nos(se.get("items")[0].serial_no)
si = create_sales_invoice(company='_Test Company', debit_to='Debtors - _TC',
account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
si.get("items")[0].serial_no = serial_nos[0]
si.insert()
si.submit()
pos2 = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
pos2.get("items")[0].serial_no = serial_nos[0]
pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000})
self.assertRaises(frappe.ValidationError, pos2.insert)
def test_loyalty_points(self):
from erpnext.accounts.doctype.loyalty_program.test_loyalty_program import create_records
from erpnext.accounts.doctype.loyalty_program.loyalty_program import get_loyalty_program_details_with_points

View File

@@ -12,8 +12,8 @@ from frappe.utils.background_jobs import enqueue
from frappe.model.mapper import map_doc, map_child_doc
from frappe.utils.scheduler import is_scheduler_inactive
from frappe.core.page.background_jobs.background_jobs import get_info
from six import iteritems
import json
import six
class POSInvoiceMergeLog(Document):
def validate(self):
@@ -42,8 +42,9 @@ class POSInvoiceMergeLog(Document):
if return_against_status != "Consolidated":
# if return entry is not getting merged in the current pos closing and if it is not consolidated
bold_unconsolidated = frappe.bold("not Consolidated")
msg = (_("Row #{}: Original Invoice {} of return invoice {} is {}. ")
msg = (_("Row #{}: Original Invoice {} of return invoice {} is {}.")
.format(d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated))
msg += " "
msg += _("Original invoice should be consolidated before or along with the return invoice.")
msg += "<br><br>"
msg += _("You can add original invoice {} manually to proceed.").format(bold_return_against)
@@ -56,12 +57,12 @@ class POSInvoiceMergeLog(Document):
sales = [d for d in pos_invoice_docs if d.get('is_return') == 0]
sales_invoice, credit_note = "", ""
if sales:
sales_invoice = self.process_merging_into_sales_invoice(sales)
if returns:
credit_note = self.process_merging_into_credit_note(returns)
if sales:
sales_invoice = self.process_merging_into_sales_invoice(sales)
self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log
self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note)
@@ -78,8 +79,11 @@ class POSInvoiceMergeLog(Document):
sales_invoice = self.merge_pos_invoice_into(sales_invoice, data)
sales_invoice.is_consolidated = 1
sales_invoice.set_posting_time = 1
sales_invoice.posting_date = getdate(self.posting_date)
sales_invoice.save()
sales_invoice.submit()
self.consolidated_invoice = sales_invoice.name
return sales_invoice.name
@@ -91,10 +95,13 @@ class POSInvoiceMergeLog(Document):
credit_note = self.merge_pos_invoice_into(credit_note, data)
credit_note.is_consolidated = 1
credit_note.set_posting_time = 1
credit_note.posting_date = getdate(self.posting_date)
# TODO: return could be against multiple sales invoice which could also have been consolidated?
# credit_note.return_against = self.consolidated_invoice
credit_note.save()
credit_note.submit()
self.consolidated_credit_note = credit_note.name
return credit_note.name
@@ -131,12 +138,14 @@ class POSInvoiceMergeLog(Document):
if t.account_head == tax.account_head and t.cost_center == tax.cost_center:
t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount)
t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount)
update_item_wise_tax_detail(t, tax)
found = True
if not found:
tax.charge_type = 'Actual'
tax.included_in_print_rate = 0
tax.tax_amount = tax.tax_amount_after_discount_amount
tax.base_tax_amount = tax.base_tax_amount_after_discount_amount
tax.item_wise_tax_detail = tax.item_wise_tax_detail
taxes.append(tax)
for payment in doc.get('payments'):
@@ -168,11 +177,9 @@ class POSInvoiceMergeLog(Document):
sales_invoice = frappe.new_doc('Sales Invoice')
sales_invoice.customer = self.customer
sales_invoice.is_pos = 1
# date can be pos closing date?
sales_invoice.posting_date = getdate(nowdate())
return sales_invoice
def update_pos_invoices(self, invoice_docs, sales_invoice='', credit_note=''):
for doc in invoice_docs:
doc.load_from_db()
@@ -187,6 +194,26 @@ class POSInvoiceMergeLog(Document):
si.flags.ignore_validate = True
si.cancel()
def update_item_wise_tax_detail(consolidate_tax_row, tax_row):
consolidated_tax_detail = json.loads(consolidate_tax_row.item_wise_tax_detail)
tax_row_detail = json.loads(tax_row.item_wise_tax_detail)
if not consolidated_tax_detail:
consolidated_tax_detail = {}
for item_code, tax_data in tax_row_detail.items():
if consolidated_tax_detail.get(item_code):
consolidated_tax_data = consolidated_tax_detail.get(item_code)
consolidated_tax_detail.update({
item_code: [consolidated_tax_data[0], consolidated_tax_data[1] + tax_data[1]]
})
else:
consolidated_tax_detail.update({
item_code: [tax_data[0], tax_data[1]]
})
consolidate_tax_row.item_wise_tax_detail = json.dumps(consolidated_tax_detail, separators=(',', ':'))
def get_all_unconsolidated_invoices():
filters = {
'consolidated_invoice': [ 'in', [ '', None ]],
@@ -208,13 +235,13 @@ def get_invoice_customer_map(pos_invoices):
return pos_invoice_customer_map
def consolidate_pos_invoices(pos_invoices=[], closing_entry={}):
invoices = pos_invoices or closing_entry.get('pos_transactions') or get_all_unconsolidated_invoices()
def consolidate_pos_invoices(pos_invoices=None, closing_entry=None):
invoices = pos_invoices or (closing_entry and closing_entry.get('pos_transactions')) or get_all_unconsolidated_invoices()
invoice_by_customer = get_invoice_customer_map(invoices)
if len(invoices) >= 5 and closing_entry:
if len(invoices) >= 10 and closing_entry:
closing_entry.set_status(update=True, status='Queued')
enqueue_job(create_merge_logs, invoice_by_customer, closing_entry)
enqueue_job(create_merge_logs, invoice_by_customer=invoice_by_customer, closing_entry=closing_entry)
else:
create_merge_logs(invoice_by_customer, closing_entry)
@@ -225,50 +252,83 @@ def unconsolidate_pos_invoices(closing_entry):
pluck='name'
)
if len(merge_logs) >= 5:
if len(merge_logs) >= 10:
closing_entry.set_status(update=True, status='Queued')
enqueue_job(cancel_merge_logs, merge_logs, closing_entry)
enqueue_job(cancel_merge_logs, merge_logs=merge_logs, closing_entry=closing_entry)
else:
cancel_merge_logs(merge_logs, closing_entry)
def create_merge_logs(invoice_by_customer, closing_entry={}):
for customer, invoices in iteritems(invoice_by_customer):
merge_log = frappe.new_doc('POS Invoice Merge Log')
merge_log.posting_date = getdate(nowdate())
merge_log.customer = customer
merge_log.pos_closing_entry = closing_entry.get('name', None)
def create_merge_logs(invoice_by_customer, closing_entry=None):
try:
for customer, invoices in six.iteritems(invoice_by_customer):
merge_log = frappe.new_doc('POS Invoice Merge Log')
merge_log.posting_date = getdate(closing_entry.get('posting_date')) if closing_entry else nowdate()
merge_log.customer = customer
merge_log.pos_closing_entry = closing_entry.get('name') if closing_entry else None
merge_log.set('pos_invoices', invoices)
merge_log.save(ignore_permissions=True)
merge_log.submit()
if closing_entry:
closing_entry.set_status(update=True, status='Submitted')
closing_entry.update_opening_entry()
merge_log.set('pos_invoices', invoices)
merge_log.save(ignore_permissions=True)
merge_log.submit()
def cancel_merge_logs(merge_logs, closing_entry={}):
for log in merge_logs:
merge_log = frappe.get_doc('POS Invoice Merge Log', log)
merge_log.flags.ignore_permissions = True
merge_log.cancel()
if closing_entry:
closing_entry.set_status(update=True, status='Submitted')
closing_entry.db_set('error_message', '')
closing_entry.update_opening_entry()
if closing_entry:
closing_entry.set_status(update=True, status='Cancelled')
closing_entry.update_opening_entry(for_cancel=True)
except Exception as e:
frappe.db.rollback()
message_log = frappe.message_log.pop() if frappe.message_log else str(e)
error_message = safe_load_json(message_log)
def enqueue_job(job, invoice_by_customer, closing_entry):
if closing_entry:
closing_entry.set_status(update=True, status='Failed')
closing_entry.db_set('error_message', error_message)
raise
finally:
frappe.db.commit()
frappe.publish_realtime('closing_process_complete', {'user': frappe.session.user})
def cancel_merge_logs(merge_logs, closing_entry=None):
try:
for log in merge_logs:
merge_log = frappe.get_doc('POS Invoice Merge Log', log)
merge_log.flags.ignore_permissions = True
merge_log.cancel()
if closing_entry:
closing_entry.set_status(update=True, status='Cancelled')
closing_entry.db_set('error_message', '')
closing_entry.update_opening_entry(for_cancel=True)
except Exception as e:
frappe.db.rollback()
message_log = frappe.message_log.pop() if frappe.message_log else str(e)
error_message = safe_load_json(message_log)
if closing_entry:
closing_entry.set_status(update=True, status='Submitted')
closing_entry.db_set('error_message', error_message)
raise
finally:
frappe.db.commit()
frappe.publish_realtime('closing_process_complete', {'user': frappe.session.user})
def enqueue_job(job, **kwargs):
check_scheduler_status()
closing_entry = kwargs.get('closing_entry') or {}
job_name = closing_entry.get("name")
if not job_already_enqueued(job_name):
enqueue(
job,
**kwargs,
queue="long",
timeout=10000,
event="processing_merge_logs",
job_name=job_name,
closing_entry=closing_entry,
invoice_by_customer=invoice_by_customer,
now=frappe.conf.developer_mode or frappe.flags.in_test
)
@@ -286,4 +346,12 @@ def check_scheduler_status():
def job_already_enqueued(job_name):
enqueued_jobs = [d.get("job_name") for d in get_info()]
if job_name in enqueued_jobs:
return True
return True
def safe_load_json(message):
try:
json_message = json.loads(message).get('message')
except Exception:
json_message = message
return json_message

View File

@@ -5,6 +5,7 @@ from __future__ import unicode_literals
import frappe
import unittest
import json
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices
@@ -99,4 +100,51 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")
def test_consolidated_invoice_item_taxes(self):
frappe.db.sql("delete from `tabPOS Invoice`")
try:
inv = create_pos_invoice(qty=1, rate=100, do_not_save=True)
inv.append("taxes", {
"account_head": "_Test Account VAT - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"rate": 9
})
inv.insert()
inv.submit()
inv2 = create_pos_invoice(qty=1, rate=100, do_not_save=True)
inv2.get('items')[0].item_code = '_Test Item 2'
inv2.append("taxes", {
"account_head": "_Test Account VAT - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"rate": 5
})
inv2.insert()
inv2.submit()
consolidate_pos_invoices()
inv.load_from_db()
consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
item_wise_tax_detail = json.loads(consolidated_invoice.get('taxes')[0].item_wise_tax_detail)
tax_rate, amount = item_wise_tax_detail.get('_Test Item')
self.assertEqual(tax_rate, 9)
self.assertEqual(amount, 9)
tax_rate2, amount2 = item_wise_tax_detail.get('_Test Item 2')
self.assertEqual(tax_rate2, 5)
self.assertEqual(amount2, 5)
finally:
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")

View File

@@ -62,14 +62,15 @@ class POSProfile(Document):
if len(default_mode) > 1:
frappe.throw(_("You can only select one mode of payment as default"))
invalid_modes = []
for d in self.payments:
account = frappe.db.get_value(
"Mode of Payment Account",
"Mode of Payment Account",
{"parent": d.mode_of_payment, "company": self.company},
"default_account"
)
if not account:
invalid_modes.append(get_link_to_form("Mode of Payment", d.mode_of_payment))

View File

@@ -92,11 +92,21 @@ def make_pos_profile(**args):
"write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC"
})
payments = [{
mode_of_payment = frappe.get_doc("Mode of Payment", "Cash")
company = args.company or "_Test Company"
default_account = args.income_account or "Sales - _TC"
if not frappe.db.get_value("Mode of Payment Account", {"company": company, "parent": "Cash"}):
mode_of_payment.append("accounts", {
"company": company,
"default_account": default_account
})
mode_of_payment.save()
pos_profile.append("payments", {
'mode_of_payment': 'Cash',
'default': 1
}]
pos_profile.set("payments", payments)
})
if not frappe.db.exists("POS Profile", args.name or "_Test POS Profile"):
pos_profile.insert()

View File

@@ -0,0 +1,37 @@
{
"actions": [],
"creation": "2021-04-19 14:56:06.652327",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"field",
"fieldname"
],
"fields": [
{
"fieldname": "fieldname",
"fieldtype": "Data",
"hidden": 1,
"label": "Fieldname"
},
{
"fieldname": "field",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Field"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-04-21 11:12:54.632093",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Search Fields",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class POSSearchFields(Document):
pass

View File

@@ -1,9 +1,17 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
let search_fields_datatypes = ['Data', 'Link', 'Dynamic Link', 'Long Text', 'Select', 'Small Text', 'Text', 'Text Editor'];
let do_not_include_fields = ["naming_series", "item_code", "item_name", "stock_uom", "hub_sync_id", "asset_naming_series",
"default_material_request_type", "valuation_method", "warranty_period", "weight_uom", "batch_number_series",
"serial_no_series", "purchase_uom", "customs_tariff_number", "sales_uom", "deferred_revenue_account",
"deferred_expense_account", "quality_inspection_template", "route", "slideshow", "website_image_alt", "thumbnail",
"web_long_description", "hub_sync_id"]
frappe.ui.form.on('POS Settings', {
onload: function(frm) {
frm.trigger("get_invoice_fields");
frm.trigger("add_search_options");
},
get_invoice_fields: function(frm) {
@@ -16,8 +24,43 @@ frappe.ui.form.on('POS Settings', {
}
});
frappe.meta.get_docfield("POS Field", "fieldname", frm.doc.name).options = [""].concat(fields);
frm.fields_dict.invoice_fields.grid.update_docfield_property(
'fieldname', 'options', [""].concat(fields)
);
});
},
add_search_options: function(frm) {
frappe.model.with_doctype("Item", () => {
var fields = $.map(frappe.get_doc("DocType", "Item").fields, function(d) {
if (search_fields_datatypes.includes(d.fieldtype) && !(do_not_include_fields.includes(d.fieldname))) {
return [d.label];
} else {
return null;
}
});
fields.unshift('');
frm.fields_dict.pos_search_fields.grid.update_docfield_property('field', 'options', fields);
});
}
});
frappe.ui.form.on("POS Search Fields", {
field: function(frm, doctype, name) {
var doc = frappe.get_doc(doctype, name);
var df = $.map(frappe.get_doc("DocType", "Item").fields, function(d) {
if (doc.field == d.label && search_fields_datatypes.includes(d.fieldtype)) {
return d;
} else {
return null;
}
})[0];
doc.fieldname = df.fieldname;
frm.refresh_field("fields");
}
});

View File

@@ -5,7 +5,8 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"invoice_fields"
"invoice_fields",
"pos_search_fields"
],
"fields": [
{
@@ -13,11 +14,17 @@
"fieldtype": "Table",
"label": "POS Field",
"options": "POS Field"
},
{
"fieldname": "pos_search_fields",
"fieldtype": "Table",
"label": "POS Search Fields",
"options": "POS Search Fields"
}
],
"issingle": 1,
"links": [],
"modified": "2020-06-01 15:46:41.478928",
"modified": "2021-04-19 14:56:24.465218",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Settings",

View File

@@ -152,7 +152,7 @@ class PricingRule(Document):
frappe.throw(_("Valid from date must be less than valid upto date"))
def validate_condition(self):
if self.condition and ("=" in self.condition) and re.match("""[\w\.:_]+\s*={1}\s*[\w\.@'"]+""", self.condition):
if self.condition and ("=" in self.condition) and re.match(r'[\w\.:_]+\s*={1}\s*[\w\.@\'"]+', self.condition):
frappe.throw(_("Invalid condition expression"))
#--------------------------------------------------------------------------------

View File

@@ -99,7 +99,7 @@ class TestPricingRule(unittest.TestCase):
args.item_code = "_Test Item 2"
details = get_item_details(args)
self.assertEquals(details.get("discount_percentage"), 15)
self.assertEqual(details.get("discount_percentage"), 15)
def test_pricing_rule_for_margin(self):
from erpnext.stock.get_item_details import get_item_details
@@ -145,8 +145,8 @@ class TestPricingRule(unittest.TestCase):
"name": None
})
details = get_item_details(args)
self.assertEquals(details.get("margin_type"), "Percentage")
self.assertEquals(details.get("margin_rate_or_amount"), 10)
self.assertEqual(details.get("margin_type"), "Percentage")
self.assertEqual(details.get("margin_rate_or_amount"), 10)
def test_mixed_conditions_for_item_group(self):
for item in ["Mixed Cond Item 1", "Mixed Cond Item 2"]:
@@ -192,7 +192,7 @@ class TestPricingRule(unittest.TestCase):
"name": None
})
details = get_item_details(args)
self.assertEquals(details.get("discount_percentage"), 10)
self.assertEqual(details.get("discount_percentage"), 10)
def test_pricing_rule_for_variants(self):
from erpnext.stock.get_item_details import get_item_details
@@ -322,11 +322,26 @@ class TestPricingRule(unittest.TestCase):
si.insert(ignore_permissions=True)
item = si.items[0]
self.assertEquals(item.margin_rate_or_amount, 10)
self.assertEquals(item.rate_with_margin, 1100)
self.assertEqual(item.margin_rate_or_amount, 10)
self.assertEqual(item.rate_with_margin, 1100)
self.assertEqual(item.discount_percentage, 10)
self.assertEquals(item.discount_amount, 110)
self.assertEquals(item.rate, 990)
self.assertEqual(item.discount_amount, 110)
self.assertEqual(item.rate, 990)
def test_pricing_rule_with_margin_and_discount_amount(self):
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
make_pricing_rule(selling=1, margin_type="Percentage", margin_rate_or_amount=10,
rate_or_discount="Discount Amount", discount_amount=110)
si = create_sales_invoice(do_not_save=True)
si.items[0].price_list_rate = 1000
si.payment_schedule = []
si.insert(ignore_permissions=True)
item = si.items[0]
self.assertEqual(item.margin_rate_or_amount, 10)
self.assertEqual(item.rate_with_margin, 1100)
self.assertEqual(item.discount_amount, 110)
self.assertEqual(item.rate, 990)
def test_pricing_rule_for_product_discount_on_same_item(self):
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
@@ -443,21 +458,21 @@ class TestPricingRule(unittest.TestCase):
si.items[0].price_list_rate = 1000
si.submit()
item = si.items[0]
self.assertEquals(item.rate, 100)
self.assertEqual(item.rate, 100)
# Correct Customer and Incorrect is_return value
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", is_return=1, qty=-1)
si.items[0].price_list_rate = 1000
si.submit()
item = si.items[0]
self.assertEquals(item.rate, 100)
self.assertEqual(item.rate, 100)
# Correct Customer and correct is_return value
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", is_return=0)
si.items[0].price_list_rate = 1000
si.submit()
item = si.items[0]
self.assertEquals(item.rate, 900)
self.assertEqual(item.rate, 900)
def test_multiple_pricing_rules(self):
make_pricing_rule(discount_percentage=20, selling=1, priority=1, apply_multiple_pricing_rules=1,
@@ -530,11 +545,11 @@ class TestPricingRule(unittest.TestCase):
apply_on="Transaction", free_item="Water Flask 1", free_qty=1, free_item_rate=10)
si = create_sales_invoice(qty=5, do_not_submit=True)
self.assertEquals(len(si.items), 2)
self.assertEquals(si.items[1].rate, 10)
self.assertEqual(len(si.items), 2)
self.assertEqual(si.items[1].rate, 10)
si1 = create_sales_invoice(qty=2, do_not_submit=True)
self.assertEquals(len(si1.items), 1)
self.assertEqual(len(si1.items), 1)
for doc in [si, si1]:
doc.delete()
@@ -560,6 +575,7 @@ def make_pricing_rule(**args):
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
"condition": args.condition or '',
"priority": 1,
"discount_amount": args.discount_amount or 0.0,
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0
})

View File

@@ -20,9 +20,9 @@ from frappe.utils import cint, flt, get_link_to_form, getdate, today, fmt_money
class MultiplePricingRuleConflict(frappe.ValidationError): pass
apply_on_table = {
'Item Code': 'items',
'Item Group': 'item_groups',
'Brand': 'brands'
'Item Code': 'items',
'Item Group': 'item_groups',
'Brand': 'brands'
}
def get_pricing_rules(args, doc=None):
@@ -173,7 +173,7 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
if parenttype in ["Customer Group", "Item Group", "Territory"]:
parent_field = "parent_{0}".format(frappe.scrub(parenttype))
root_name = frappe.db.get_list(parenttype,
{"is_group": 1, parent_field: ("is", "not set")}, "name", as_list=1)
{"is_group": 1, parent_field: ("is", "not set")}, "name", as_list=1, ignore_permissions=True)
if root_name and root_name[0][0]:
parent_groups.append(root_name[0][0])
@@ -183,7 +183,7 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
condition = "ifnull({table}.{field}, '') in ({parent_groups})".format(
table=table,
field=field,
parent_groups=", ".join([frappe.db.escape(d) for d in parent_groups])
parent_groups=", ".join(frappe.db.escape(d) for d in parent_groups)
)
frappe.flags.tree_conditions[key] = condition
@@ -264,7 +264,7 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
# find pricing rule with highest priority
if pricing_rules:
max_priority = max([cint(p.priority) for p in pricing_rules])
max_priority = max(cint(p.priority) for p in pricing_rules)
if max_priority:
pricing_rules = list(filter(lambda x: cint(x.priority)==max_priority, pricing_rules))
@@ -272,14 +272,14 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
pricing_rules = list(pricing_rules)
if len(pricing_rules) > 1:
rate_or_discount = list(set([d.rate_or_discount for d in pricing_rules]))
rate_or_discount = list(set(d.rate_or_discount for d in pricing_rules))
if len(rate_or_discount) == 1 and rate_or_discount[0] == "Discount Percentage":
pricing_rules = list(filter(lambda x: x.for_price_list==args.price_list, pricing_rules)) \
or pricing_rules
if len(pricing_rules) > 1 and not args.for_shopping_cart:
frappe.throw(_("Multiple Price Rules exists with same criteria, please resolve conflict by assigning priority. Price Rules: {0}")
.format("\n".join([d.name for d in pricing_rules])), MultiplePricingRuleConflict)
.format("\n".join(d.name for d in pricing_rules)), MultiplePricingRuleConflict)
elif pricing_rules:
return pricing_rules[0]
@@ -471,7 +471,7 @@ def apply_pricing_rule_on_transaction(doc):
if not d.get(pr_field): continue
if d.validate_applied_rule and doc.get(field) < d.get(pr_field):
if d.validate_applied_rule and doc.get(field) is not None and doc.get(field) < d.get(pr_field):
frappe.msgprint(_("User has not applied rule on the invoice {0}")
.format(doc.name))
else:
@@ -541,7 +541,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values=False):
if pricing_rule_args:
items = tuple([(d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item])
items = tuple((d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item)
for args in pricing_rule_args:
if not items or (args.get('item_code'), args.get('pricing_rules')) not in items:
@@ -589,4 +589,4 @@ def update_coupon_code_count(coupon_name,transaction_type):
elif transaction_type=='cancelled':
if coupon.used>0:
coupon.used=coupon.used-1
coupon.save(ignore_permissions=True)
coupon.save(ignore_permissions=True)

View File

@@ -1,25 +1,43 @@
<h1 class="text-center" style="page-break-before:always">{{ filters.party[0] }}</h1>
<h3 class="text-center">{{ _("Statement of Accounts") }}</h3>
<div class="page-break">
<div id="header-html" class="hidden-pdf">
{% if letter_head %}
<div class="letter-head text-center">{{ letter_head.content }}</div>
<hr style="height:2px;border-width:0;color:black;background-color:black;">
{% endif %}
</div>
<div id="footer-html" class="visible-pdf">
{% if letter_head.footer %}
<div class="letter-head-footer">
<hr style="border-width:0;color:black;background-color:black;padding-bottom:2px;">
{{ letter_head.footer }}
</div>
{% endif %}
</div>
<h2 class="text-center">{{ _("STATEMENTS OF ACCOUNTS") }}</h2>
<div>
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{filters.party[0] }}</b></h5>
<h5 style="float: right;">
{{ _("Date: ") }}
<b>{{ frappe.format(filters.from_date, 'Date')}}
{{ _("to") }}
{{ frappe.format(filters.to_date, 'Date')}}</b>
</h5>
</div>
<br>
<h5 class="text-center">
{{ frappe.format(filters.from_date, 'Date')}}
{{ _("to") }}
{{ frappe.format(filters.to_date, 'Date')}}
</h5>
<table class="table table-bordered">
<thead>
<tr>
<th style="width: 12%">{{ _("Date") }}</th>
<th style="width: 15%">{{ _("Ref") }}</th>
<th style="width: 25%">{{ _("Party") }}</th>
<th style="width: 15%">{{ _("Debit") }}</th>
<th style="width: 15%">{{ _("Credit") }}</th>
<th style="width: 18%">{{ _("Balance (Dr - Cr)") }}</th>
</tr>
</thead>
<tbody>
{% for row in data %}
<table class="table table-bordered">
<thead>
<tr>
<th style="width: 12%">{{ _("Date") }}</th>
<th style="width: 15%">{{ _("Reference") }}</th>
<th style="width: 25%">{{ _("Remarks") }}</th>
<th style="width: 15%">{{ _("Debit") }}</th>
<th style="width: 15%">{{ _("Credit") }}</th>
<th style="width: 18%">{{ _("Balance (Dr - Cr)") }}</th>
</tr>
</thead>
<tbody>
{% for row in data %}
<tr>
{% if(row.posting_date) %}
<td>{{ frappe.format(row.posting_date, 'Date') }}</td>
@@ -38,52 +56,54 @@
{% endif %}
</td>
<td style="text-align: right">
{{ frappe.utils.fmt_money(row.debit, filters.presentation_currency) }}</td>
{{ frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }}</td>
<td style="text-align: right">
{{ frappe.utils.fmt_money(row.credit, filters.presentation_currency) }}</td>
{{ frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }}</td>
{% else %}
<td></td>
<td></td>
<td><b>{{ frappe.format(row.account, {fieldtype: "Link"}) or "&nbsp;" }}</b></td>
<td style="text-align: right">
{{ row.account and frappe.utils.fmt_money(row.debit, filters.presentation_currency) }}
{{ row.account and frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }}
</td>
<td style="text-align: right">
{{ row.account and frappe.utils.fmt_money(row.credit, filters.presentation_currency) }}
{{ row.account and frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }}
</td>
{% endif %}
<td style="text-align: right">
{{ frappe.utils.fmt_money(row.balance, filters.presentation_currency) }}
{{ frappe.utils.fmt_money(row.balance, currency=filters.presentation_currency) }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<br><br>
{% if aging %}
<h3 class="text-center">{{ _("Ageing Report Based On ") }} {{ aging.ageing_based_on }}</h3>
<h5 class="text-center">
{{ _("Up to " ) }} {{ frappe.format(filters.to_date, 'Date')}}
</h5>
<br>
<table class="table table-bordered">
<thead>
<tr>
<th style="width: 12%">30 Days</th>
<th style="width: 15%">60 Days</th>
<th style="width: 25%">90 Days</th>
<th style="width: 15%">120 Days</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ aging.range1 }}</td>
<td>{{ aging.range2 }}</td>
<td>{{ aging.range3 }}</td>
<td>{{ aging.range4 }}</td>
</tr>
</tbody>
</table>
{% endif %}
<p class="text-right text-muted">Printed On {{ frappe.format(frappe.utils.get_datetime(), 'Datetime') }}</p>
</table>
<br>
{% if ageing %}
<h4 class="text-center">{{ _("Ageing Report based on ") }} {{ ageing.ageing_based_on }}
{{ _("up to " ) }} {{ frappe.format(filters.to_date, 'Date')}}
</h4>
<table class="table table-bordered">
<thead>
<tr>
<th style="width: 25%">30 Days</th>
<th style="width: 25%">60 Days</th>
<th style="width: 25%">90 Days</th>
<th style="width: 25%">120 Days</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ frappe.utils.fmt_money(ageing.range1, currency=filters.presentation_currency) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range2, currency=filters.presentation_currency) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range3, currency=filters.presentation_currency) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range4, currency=filters.presentation_currency) }}</td>
</tr>
</tbody>
</table>
{% endif %}
{% if terms_and_conditions %}
<div>
{{ terms_and_conditions }}
</div>
{% endif %}
</div>

View File

@@ -19,7 +19,7 @@ frappe.ui.form.on('Process Statement Of Accounts', {
frappe.show_alert({message: __('Emails Queued'), indicator: 'blue'});
}
else{
frappe.msgprint('No Records for these settings.')
frappe.msgprint(__('No Records for these settings.'))
}
}
});
@@ -33,7 +33,7 @@ frappe.ui.form.on('Process Statement Of Accounts', {
type: 'GET',
success: function(result) {
if(jQuery.isEmptyObject(result)){
frappe.msgprint('No Records for these settings.');
frappe.msgprint(__('No Records for these settings.'));
}
else{
window.location = url;
@@ -92,7 +92,7 @@ frappe.ui.form.on('Process Statement Of Accounts', {
frm.refresh_field('customers');
}
else{
frappe.msgprint('No Customers found with selected options.');
frappe.throw(__('No Customers found with selected options.'));
}
}
}
@@ -129,4 +129,4 @@ frappe.ui.form.on('Process Statement Of Accounts Customer', {
}
})
}
});
});

View File

@@ -1,6 +1,5 @@
{
"actions": [],
"allow_workflow": 1,
"autoname": "Prompt",
"creation": "2020-05-22 16:46:18.712954",
"doctype": "DocType",
@@ -28,9 +27,11 @@
"customers",
"preferences",
"orientation",
"section_break_14",
"include_ageing",
"ageing_based_on",
"section_break_14",
"letter_head",
"terms_and_conditions",
"section_break_1",
"enable_auto_email",
"section_break_18",
@@ -270,10 +271,22 @@
"fieldname": "body",
"fieldtype": "Text Editor",
"label": "Body"
},
{
"fieldname": "letter_head",
"fieldtype": "Link",
"label": "Letter Head",
"options": "Letter Head"
},
{
"fieldname": "terms_and_conditions",
"fieldtype": "Link",
"label": "Terms and Conditions",
"options": "Terms and Conditions"
}
],
"links": [],
"modified": "2020-08-08 08:47:09.185728",
"modified": "2021-05-21 10:14:22.426672",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",

View File

@@ -4,10 +4,12 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from erpnext.accounts.report.general_ledger.general_ledger import execute as get_soa
from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_summary import execute as get_ageing
from frappe.core.doctype.communication.email import make
from erpnext import get_company_currency
from erpnext.accounts.party import get_party_account_currency
from frappe.utils.print_format import report_to_pdf
from frappe.utils.pdf import get_pdf
@@ -29,7 +31,7 @@ class ProcessStatementOfAccounts(Document):
validate_template(self.body)
if not self.customers:
frappe.throw(frappe._('Customers not selected.'))
frappe.throw(_('Customers not selected.'))
if self.enable_auto_email:
self.to_date = self.start_date
@@ -38,7 +40,7 @@ class ProcessStatementOfAccounts(Document):
def get_report_pdf(doc, consolidated=True):
statement_dict = {}
aging = ''
ageing = ''
base_template_path = "frappe/www/printview.html"
template_path = "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
@@ -54,26 +56,33 @@ def get_report_pdf(doc, consolidated=True):
'range4': 120,
'customer': entry.customer
})
col1, aging = get_ageing(ageing_filters)
aging[0]['ageing_based_on'] = doc.ageing_based_on
col1, ageing = get_ageing(ageing_filters)
if ageing:
ageing[0]['ageing_based_on'] = doc.ageing_based_on
tax_id = frappe.get_doc('Customer', entry.customer).tax_id
presentation_currency = get_party_account_currency('Customer', entry.customer, doc.company) \
or doc.currency or get_company_currency(doc.company)
if doc.letter_head:
from frappe.www.printview import get_letter_head
letter_head = get_letter_head(doc, 0)
filters= frappe._dict({
'from_date': doc.from_date,
'to_date': doc.to_date,
'company': doc.company,
'finance_book': doc.finance_book if doc.finance_book else None,
"account": doc.account if doc.account else None,
'account': doc.account if doc.account else None,
'party_type': 'Customer',
'party': [entry.customer],
'presentation_currency': presentation_currency,
'group_by': doc.group_by,
'currency': doc.currency,
'cost_center': [cc.cost_center_name for cc in doc.cost_center],
'project': [p.project_name for p in doc.project],
'show_opening_entries': 0,
'include_default_book_entries': 0,
'show_cancelled_entries': 1,
'tax_id': tax_id if tax_id else None
})
col, res = get_soa(filters)
@@ -83,11 +92,17 @@ def get_report_pdf(doc, consolidated=True):
if len(res) == 3:
continue
html = frappe.render_template(template_path, \
{"filters": filters, "data": res, "aging": aging[0] if doc.include_ageing else None})
{"filters": filters, "data": res, "ageing": ageing[0] if (doc.include_ageing and ageing) else None,
"letter_head": letter_head if doc.letter_head else None,
"terms_and_conditions": frappe.db.get_value('Terms and Conditions', doc.terms_and_conditions, 'terms')
if doc.terms_and_conditions else None})
html = frappe.render_template(base_template_path, {"body": html, \
"css": get_print_style(), "title": "Statement For " + entry.customer})
statement_dict[entry.customer] = html
if not bool(statement_dict):
return False
elif consolidated:
@@ -126,9 +141,11 @@ def get_customers_based_on_sales_person(sales_person):
sales_person_records = frappe._dict()
for d in records:
sales_person_records.setdefault(d.parenttype, set()).add(d.parent)
customers = frappe.get_list('Customer', fields=['name', 'email_id'], \
if sales_person_records.get('Customer'):
return frappe.get_list('Customer', fields=['name', 'email_id'], \
filters=[['name', 'in', list(sales_person_records['Customer'])]])
return customers
else:
return []
def get_recipients_and_cc(customer, doc):
recipients = []
@@ -165,7 +182,7 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
if customer_collection == 'Sales Person':
customers = get_customers_based_on_sales_person(collection_name)
if not bool(customers):
frappe.throw('No Customers found with selected options.')
frappe.throw(_('No Customers found with selected options.'))
else:
if customer_collection == 'Sales Partner':
customers = frappe.get_list('Customer', fields=['name', 'email_id'], \
@@ -190,21 +207,20 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
@frappe.whitelist()
def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True):
billing_email = frappe.db.sql("""
SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent \
WHERE l.link_doctype='Customer' and l.link_name='""" + customer_name + """' and \
c.is_billing_contact=1 \
order by c.creation desc""")
SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent
WHERE l.link_doctype='Customer' and l.link_name=%s and c.is_billing_contact=1
order by c.creation desc""", customer_name)
if len(billing_email) == 0 or (billing_email[0][0] is None):
if billing_and_primary:
frappe.throw('No billing email found for customer: '+ customer_name)
frappe.throw(_("No billing email found for customer: {0}").format(customer_name))
else:
return ''
if billing_and_primary:
primary_email = frappe.get_value('Customer', customer_name, 'email_id')
if primary_email is None and int(primary_mandatory):
frappe.throw('No primary email found for customer: '+ customer_name)
frappe.throw(_("No primary email found for customer: {0}").format(customer_name))
return [primary_email or '', billing_email[0][0]]
else:
return billing_email[0][0] or ''

View File

@@ -9,7 +9,7 @@ from frappe.utils import cstr
from frappe.model.naming import make_autoname
from frappe.model.document import Document
pricing_rule_fields = ['apply_on', 'mixed_conditions', 'is_cumulative', 'other_item_code', 'other_item_group'
pricing_rule_fields = ['apply_on', 'mixed_conditions', 'is_cumulative', 'other_item_code', 'other_item_group',
'apply_rule_on_other', 'other_brand', 'selling', 'buying', 'applicable_for', 'valid_from',
'valid_upto', 'customer', 'customer_group', 'territory', 'sales_partner', 'campaign', 'supplier',
'supplier_group', 'company', 'currency', 'apply_multiple_pricing_rules']
@@ -111,4 +111,4 @@ def get_args_for_pricing_rule(doc):
for d in pricing_rule_fields:
args[d] = doc.get(d)
return args
return args

View File

@@ -27,10 +27,6 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
});
},
company: function() {
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
},
onload: function() {
this._super();
@@ -496,15 +492,6 @@ cur_frm.fields_dict['items'].grid.get_field('project').get_query = function(doc,
}
}
cur_frm.cscript.select_print_heading = function(doc,cdt,cdn){
if(doc.select_print_heading){
// print heading
cur_frm.pformat.print_heading = doc.select_print_heading;
}
else
cur_frm.pformat.print_heading = __("Purchase Invoice");
}
frappe.ui.form.on("Purchase Invoice", {
setup: function(frm) {
frm.custom_make_buttons = {
@@ -523,6 +510,28 @@ frappe.ui.form.on("Purchase Invoice", {
}
},
refresh: function(frm) {
frm.events.add_custom_buttons(frm);
},
add_custom_buttons: function(frm) {
if (frm.doc.per_received < 100) {
frm.add_custom_button(__('Purchase Receipt'), () => {
frm.events.make_purchase_receipt(frm);
}, __('Create'));
}
if (frm.doc.docstatus == 1 && frm.doc.per_received > 0) {
frm.add_custom_button(__('Purchase Receipt'), () => {
frappe.route_options = {
'purchase_invoice': frm.doc.name
}
frappe.set_route("List", "Purchase Receipt", "List")
}, __('View'));
}
},
onload: function(frm) {
if(frm.doc.__onload && frm.is_new()) {
if(frm.doc.supplier) {
@@ -548,5 +557,17 @@ frappe.ui.form.on("Purchase Invoice", {
update_stock: function(frm) {
hide_fields(frm.doc);
frm.fields_dict.items.grid.toggle_reqd("item_code", frm.doc.update_stock? true: false);
}
},
make_purchase_receipt: function(frm) {
frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_purchase_receipt",
frm: frm,
freeze_message: __("Creating Purchase Receipt ...")
})
},
company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
})

View File

@@ -127,7 +127,6 @@
"write_off_cost_center",
"advances_section",
"allocate_advances_automatically",
"adjust_advance_taxes",
"get_advances",
"advances",
"payment_schedule_section",
@@ -164,7 +163,8 @@
"to_date",
"column_break_114",
"auto_repeat",
"update_auto_repeat_reference"
"update_auto_repeat_reference",
"per_received"
],
"fields": [
{
@@ -592,11 +592,12 @@
"label": "Raw Materials Supplied"
},
{
"depends_on": "update_stock",
"fieldname": "supplied_items",
"fieldtype": "Table",
"label": "Supplied Items",
"options": "Purchase Receipt Item Supplied",
"read_only": 1
"no_copy": 1,
"options": "Purchase Receipt Item Supplied"
},
{
"fieldname": "section_break_26",
@@ -837,6 +838,7 @@
"read_only": 1
},
{
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "base_rounding_adjustment",
"fieldtype": "Currency",
"label": "Rounding Adjustment (Company Currency)",
@@ -883,6 +885,7 @@
"read_only": 1
},
{
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "rounding_adjustment",
"fieldtype": "Currency",
"label": "Rounding Adjustment",
@@ -1326,13 +1329,6 @@
"label": "Project",
"options": "Project"
},
{
"default": "0",
"description": "Taxes paid while advance payment will be adjusted against this invoice",
"fieldname": "adjust_advance_taxes",
"fieldtype": "Check",
"label": "Adjust Advance Taxes"
},
{
"depends_on": "eval:doc.is_internal_supplier",
"description": "Unrealized Profit / Loss account for intra-company transfers",
@@ -1372,13 +1368,22 @@
"print_hide": 1,
"print_width": "50px",
"width": "50px"
},
{
"fieldname": "per_received",
"fieldtype": "Percent",
"hidden": 1,
"label": "Per Received",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2021-03-09 21:12:30.422084",
"modified": "2021-06-15 18:20:56.806195",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -68,9 +68,6 @@ class PurchaseInvoice(BuyingController):
super(PurchaseInvoice, self).validate()
# apply tax withholding only if checked and applicable
self.set_tax_withholding()
if not self.is_return:
self.po_required()
self.pr_required()
@@ -251,11 +248,9 @@ class PurchaseInvoice(BuyingController):
if self.update_stock and (not item.from_warehouse):
if for_validate and item.expense_account and item.expense_account != warehouse_account[item.warehouse]["account"]:
msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]))
msg += _("because account {} is not linked to warehouse {} ").format(frappe.bold(item.expense_account), frappe.bold(item.warehouse))
msg += _("or it is not the default inventory account")
msg = _("Row {0}: Expense Head changed to {1} because account {2} is not linked to warehouse {3} or it is not the default inventory account").format(
item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]), frappe.bold(item.expense_account), frappe.bold(item.warehouse))
frappe.msgprint(msg, title=_("Expense Head Changed"))
item.expense_account = warehouse_account[item.warehouse]["account"]
else:
# check if 'Stock Received But Not Billed' account is credited in Purchase receipt or not
@@ -266,8 +261,8 @@ class PurchaseInvoice(BuyingController):
if negative_expense_booked_in_pr:
if for_validate and item.expense_account and item.expense_account != stock_not_billed_account:
msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(stock_not_billed_account))
msg += _("because expense is booked against this account in Purchase Receipt {}").format(frappe.bold(item.purchase_receipt))
msg = _("Row {0}: Expense Head changed to {1} because expense is booked against this account in Purchase Receipt {2}").format(
item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.purchase_receipt))
frappe.msgprint(msg, title=_("Expense Head Changed"))
item.expense_account = stock_not_billed_account
@@ -275,8 +270,9 @@ class PurchaseInvoice(BuyingController):
# If no purchase receipt present then book expense in 'Stock Received But Not Billed'
# This is done in cases when Purchase Invoice is created before Purchase Receipt
if for_validate and item.expense_account and item.expense_account != stock_not_billed_account:
msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(stock_not_billed_account))
msg += _("as no Purchase Receipt is created against Item {}. ").format(frappe.bold(item.item_code))
msg = _("Row {0}: Expense Head changed to {1} as no Purchase Receipt is created against Item {2}.").format(
item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.item_code))
msg += "<br>"
msg += _("This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice")
frappe.msgprint(msg, title=_("Expense Head Changed"))
@@ -308,8 +304,8 @@ class PurchaseInvoice(BuyingController):
if not d.purchase_order:
msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code))
msg += "<br><br>"
msg += _("To submit the invoice without purchase order please set {} ").format(frappe.bold(_('Purchase Order Required')))
msg += _("as {} in {}").format(frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))
msg += _("To submit the invoice without purchase order please set {0} as {1} in {2}").format(
frappe.bold(_('Purchase Order Required')), frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))
throw(msg, title=_("Mandatory Purchase Order"))
def pr_required(self):
@@ -323,8 +319,8 @@ class PurchaseInvoice(BuyingController):
if not d.purchase_receipt and d.item_code in stock_items:
msg = _("Purchase Receipt Required for item {}").format(frappe.bold(d.item_code))
msg += "<br><br>"
msg += _("To submit the invoice without purchase receipt please set {} ").format(frappe.bold(_('Purchase Receipt Required')))
msg += _("as {} in {}").format(frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))
msg += _("To submit the invoice without purchase receipt please set {0} as {1} in {2}").format(
frappe.bold(_('Purchase Receipt Required')), frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))
throw(msg, title=_("Mandatory Purchase Receipt"))
def validate_write_off_account(self):
@@ -404,6 +400,7 @@ class PurchaseInvoice(BuyingController):
# because updating ordered qty in bin depends upon updated ordered qty in PO
if self.update_stock == 1:
self.update_stock_ledger()
self.set_consumed_qty_in_po()
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
update_serial_nos_after_submit(self, "items")
@@ -454,8 +451,11 @@ class PurchaseInvoice(BuyingController):
self.get_asset_gl_entry(gl_entries)
self.make_tax_gl_entries(gl_entries)
self.make_exchange_gain_loss_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries)
self.allocate_advance_taxes(gl_entries)
gl_entries = make_regional_gl_entries(gl_entries, self)
gl_entries = merge_similar_entries(gl_entries)
@@ -1000,6 +1000,7 @@ class PurchaseInvoice(BuyingController):
if self.update_stock == 1:
self.update_stock_ledger()
self.delete_auto_created_batches()
self.set_consumed_qty_in_po()
self.make_gl_entries_on_cancel()
@@ -1090,6 +1091,7 @@ class PurchaseInvoice(BuyingController):
for d in self.taxes:
if d.account_head == tax_withholding_details.get("account_head"):
d.update(tax_withholding_details)
accounts.append(d.account_head)
if not accounts or tax_withholding_details.get("account_head") not in accounts:
@@ -1207,3 +1209,41 @@ def make_inter_company_sales_invoice(source_name, target_doc=None):
def on_doctype_update():
frappe.db.add_index("Purchase Invoice", ["supplier", "is_return", "return_against"])
@frappe.whitelist()
def make_purchase_receipt(source_name, target_doc=None):
def update_item(obj, target, source_parent):
target.qty = flt(obj.qty) - flt(obj.received_qty)
target.received_qty = flt(obj.qty) - flt(obj.received_qty)
target.stock_qty = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.conversion_factor)
target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate)
target.base_amount = (flt(obj.qty) - flt(obj.received_qty)) * \
flt(obj.rate) * flt(source_parent.conversion_rate)
doc = get_mapped_doc("Purchase Invoice", source_name, {
"Purchase Invoice": {
"doctype": "Purchase Receipt",
"validation": {
"docstatus": ["=", 1],
}
},
"Purchase Invoice Item": {
"doctype": "Purchase Receipt Item",
"field_map": {
"name": "purchase_invoice_item",
"parent": "purchase_invoice",
"bom": "bom",
"purchase_order": "purchase_order",
"po_detail": "purchase_order_item",
"material_request": "material_request",
"material_request_item": "material_request_item"
},
"postprocess": update_item,
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty)
},
"Purchase Taxes and Charges": {
"doctype": "Purchase Taxes and Charges"
}
}, target_doc)
return doc

View File

@@ -16,6 +16,7 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_tra
from erpnext.projects.doctype.project.test_project import make_project
from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Template"]
test_ignore = ["Serial No"]
@@ -397,7 +398,7 @@ class TestPurchaseInvoice(unittest.TestCase):
pi.update({
"payment_schedule": get_payment_terms("_Test Payment Term Template",
pi.posting_date, pi.grand_total)
pi.posting_date, pi.grand_total, pi.base_grand_total)
})
pi.save()
@@ -620,8 +621,10 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(actual_qty_0, get_qty_after_transaction())
def test_subcontracting_via_purchase_invoice(self):
from erpnext.buying.doctype.purchase_order.test_purchase_order import update_backflush_based_on
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
update_backflush_based_on('BOM')
make_stock_entry(item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100)
make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse 1 - _TC",
qty=100, basic_rate=100)
@@ -631,13 +634,13 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(len(pi.get("supplied_items")), 2)
rm_supp_cost = sum([d.amount for d in pi.get("supplied_items")])
rm_supp_cost = sum(d.amount for d in pi.get("supplied_items"))
self.assertEqual(flt(pi.get("items")[0].rm_supp_cost, 2), flt(rm_supp_cost, 2))
def test_rejected_serial_no(self):
pi = make_purchase_invoice(item_code="_Test Serialized Item With Series", received_qty=2, qty=1,
rejected_qty=1, rate=500, update_stock=1,
rejected_warehouse = "_Test Rejected Warehouse - _TC")
rejected_qty=1, rate=500, update_stock=1, rejected_warehouse = "_Test Rejected Warehouse - _TC",
allow_zero_valuation_rate=1)
self.assertEqual(frappe.db.get_value("Serial No", pi.get("items")[0].serial_no, "warehouse"),
pi.get("items")[0].warehouse)
@@ -950,6 +953,217 @@ class TestPurchaseInvoice(unittest.TestCase):
acc_settings.submit_journal_entriessubmit_journal_entries = 0
acc_settings.save()
def test_gain_loss_with_advance_entry(self):
unlink_enabled = frappe.db.get_value(
"Accounts Settings", "Accounts Settings",
"unlink_payment_on_cancel_of_invoice")
frappe.db.set_value(
"Accounts Settings", "Accounts Settings",
"unlink_payment_on_cancel_of_invoice", 1)
original_account = frappe.db.get_value("Company", "_Test Company", "exchange_gain_loss_account")
frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", "Exchange Gain/Loss - _TC")
pay = frappe.get_doc({
'doctype': 'Payment Entry',
'company': '_Test Company',
'payment_type': 'Pay',
'party_type': 'Supplier',
'party': '_Test Supplier USD',
'paid_to': '_Test Payable USD - _TC',
'paid_from': 'Cash - _TC',
'paid_amount': 70000,
'target_exchange_rate': 70,
'received_amount': 1000,
})
pay.insert()
pay.submit()
pi = make_purchase_invoice(supplier='_Test Supplier USD', currency="USD",
conversion_rate=75, rate=500, do_not_save=1, qty=1)
pi.cost_center = "_Test Cost Center - _TC"
pi.advances = []
pi.append("advances", {
"reference_type": "Payment Entry",
"reference_name": pay.name,
"advance_amount": 1000,
"remarks": pay.remarks,
"allocated_amount": 500,
"ref_exchange_rate": 70
})
pi.save()
pi.submit()
expected_gle = [
["_Test Account Cost for Goods Sold - _TC", 37500.0],
["_Test Payable USD - _TC", -40000.0],
["Exchange Gain/Loss - _TC", 2500.0]
]
gl_entries = frappe.db.sql("""
select account, sum(debit - credit) as balance from `tabGL Entry`
where voucher_no=%s
group by account
order by account asc""", (pi.name), as_dict=1)
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account)
self.assertEqual(expected_gle[i][1], gle.balance)
pi_2 = make_purchase_invoice(supplier='_Test Supplier USD', currency="USD",
conversion_rate=73, rate=500, do_not_save=1, qty=1)
pi_2.cost_center = "_Test Cost Center - _TC"
pi_2.advances = []
pi_2.append("advances", {
"reference_type": "Payment Entry",
"reference_name": pay.name,
"advance_amount": 500,
"remarks": pay.remarks,
"allocated_amount": 500,
"ref_exchange_rate": 70
})
pi_2.save()
pi_2.submit()
expected_gle = [
["_Test Account Cost for Goods Sold - _TC", 36500.0],
["_Test Payable USD - _TC", -38000.0],
["Exchange Gain/Loss - _TC", 1500.0]
]
gl_entries = frappe.db.sql("""
select account, sum(debit - credit) as balance from `tabGL Entry`
where voucher_no=%s
group by account order by account asc""", (pi_2.name), as_dict=1)
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account)
self.assertEqual(expected_gle[i][1], gle.balance)
expected_gle = [
["_Test Payable USD - _TC", 70000.0],
["Cash - _TC", -70000.0]
]
gl_entries = frappe.db.sql("""
select account, sum(debit - credit) as balance from `tabGL Entry`
where voucher_no=%s and is_cancelled=0
group by account order by account asc""", (pay.name), as_dict=1)
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account)
self.assertEqual(expected_gle[i][1], gle.balance)
pi.reload()
pi.cancel()
pi_2.reload()
pi_2.cancel()
pay.reload()
pay.cancel()
frappe.db.set_value("Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled)
frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account)
def test_purchase_invoice_advance_taxes(self):
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
# create a new supplier to test
supplier = create_supplier(supplier_name = '_Test TDS Advance Supplier',
tax_withholding_category = 'TDS - 194 - Dividends - Individual')
# Update tax withholding category with current fiscal year and rate details
update_tax_witholding_category('_Test Company', 'TDS Payable - _TC', nowdate())
# Create Purchase Order with TDS applied
po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item')
po.apply_tds = 1
po.tax_withholding_category = 'TDS - 194 - Dividends - Individual'
po.save()
po.submit()
# Update Unrealized Profit / Loss Account which is used as default advance tax account
frappe.db.set_value('Company', '_Test Company', 'unrealized_profit_loss_account', '_Test Account Excise Duty - _TC')
# Create Payment Entry Against the order
payment_entry = get_payment_entry(dt='Purchase Order', dn=po.name)
payment_entry.paid_from = 'Cash - _TC'
payment_entry.save()
payment_entry.submit()
# Check GLE for Payment Entry
expected_gle = [
['_Test Account Excise Duty - _TC', 3000, 0],
['Cash - _TC', 0, 27000],
['Creditors - _TC', 27000, 0],
['TDS Payable - _TC', 0, 3000],
]
gl_entries = frappe.db.sql("""select account, debit, credit
from `tabGL Entry`
where voucher_type='Payment Entry' and voucher_no=%s
order by account asc""", (payment_entry.name), as_dict=1)
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account)
self.assertEqual(expected_gle[i][1], gle.debit)
self.assertEqual(expected_gle[i][2], gle.credit)
# Create Purchase Invoice against Purchase Order
purchase_invoice = get_mapped_purchase_invoice(po.name)
purchase_invoice.allocate_advances_automatically = 1
purchase_invoice.items[0].item_code = '_Test Non Stock Item'
purchase_invoice.items[0].expense_account = '_Test Account Cost for Goods Sold - _TC'
purchase_invoice.save()
purchase_invoice.submit()
# Check GLE for Purchase Invoice
# Zero net effect on final TDS Payable on invoice
expected_gle = [
['_Test Account Cost for Goods Sold - _TC', 30000],
['_Test Account Excise Duty - _TC', -3000],
['Creditors - _TC', -27000],
['TDS Payable - _TC', 0]
]
gl_entries = frappe.db.sql("""select account, sum(debit - credit) as amount
from `tabGL Entry`
where voucher_type='Purchase Invoice' and voucher_no=%s
group by account
order by account asc""", (purchase_invoice.name), as_dict=1)
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account)
self.assertEqual(expected_gle[i][1], gle.amount)
def update_tax_witholding_category(company, account, date):
from erpnext.accounts.utils import get_fiscal_year
fiscal_year = get_fiscal_year(date=date, company=company)
if not frappe.db.get_value('Tax Withholding Rate',
{'parent': 'TDS - 194 - Dividends - Individual', 'fiscal_year': fiscal_year[0]}):
tds_category = frappe.get_doc('Tax Withholding Category', 'TDS - 194 - Dividends - Individual')
tds_category.append('rates', {
'fiscal_year': fiscal_year[0],
'tax_withholding_rate': 10,
'single_threshold': 2500,
'cumulative_threshold': 0
})
tds_category.save()
if not frappe.db.get_value('Tax Withholding Account',
{'parent': 'TDS - 194 - Dividends - Individual', 'account': account}):
tds_category = frappe.get_doc('Tax Withholding Category', 'TDS - 194 - Dividends - Individual')
tds_category.append('accounts', {
'company': company,
'account': account
})
tds_category.save()
def unlink_payment_on_cancel_of_invoice(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings")
@@ -994,7 +1208,8 @@ def make_purchase_invoice(**args):
"project": args.project,
"rejected_warehouse": args.rejected_warehouse or "",
"rejected_serial_no": args.rejected_serial_no or "",
"asset_location": args.location or ""
"asset_location": args.location or "",
"allow_zero_valuation_rate": args.get("allow_zero_valuation_rate") or 0
})
if args.get_taxes_and_charges:

View File

@@ -1,235 +1,127 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2013-03-08 15:36:46",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"actions": [],
"creation": "2013-03-08 15:36:46",
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"reference_type",
"reference_name",
"remarks",
"reference_row",
"col_break1",
"advance_amount",
"allocated_amount",
"exchange_gain_loss",
"ref_exchange_rate"
],
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "reference_type",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Reference Type",
"length": 0,
"no_copy": 1,
"oldfieldname": "journal_voucher",
"oldfieldtype": "Link",
"options": "DocType",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "180px",
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"fieldname": "reference_type",
"fieldtype": "Link",
"label": "Reference Type",
"no_copy": 1,
"oldfieldname": "journal_voucher",
"oldfieldtype": "Link",
"options": "DocType",
"print_width": "180px",
"read_only": 1,
"width": "180px"
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 3,
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Reference Name",
"length": 0,
"no_copy": 1,
"options": "reference_type",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"columns": 3,
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Reference Name",
"no_copy": 1,
"options": "reference_type",
"read_only": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 3,
"fieldname": "remarks",
"fieldtype": "Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Remarks",
"length": 0,
"no_copy": 1,
"oldfieldname": "remarks",
"oldfieldtype": "Small Text",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "150px",
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"columns": 3,
"fieldname": "remarks",
"fieldtype": "Text",
"in_list_view": 1,
"label": "Remarks",
"no_copy": 1,
"oldfieldname": "remarks",
"oldfieldtype": "Small Text",
"print_width": "150px",
"read_only": 1,
"width": "150px"
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "reference_row",
"fieldtype": "Data",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Reference Row",
"length": 0,
"no_copy": 1,
"oldfieldname": "jv_detail_no",
"oldfieldtype": "Date",
"permlevel": 0,
"print_hide": 1,
"print_hide_if_no_value": 0,
"print_width": "80px",
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"fieldname": "reference_row",
"fieldtype": "Data",
"hidden": 1,
"label": "Reference Row",
"no_copy": 1,
"oldfieldname": "jv_detail_no",
"oldfieldtype": "Date",
"print_hide": 1,
"print_width": "80px",
"read_only": 1,
"width": "80px"
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "col_break1",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "col_break1",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2,
"fieldname": "advance_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Advance Amount",
"length": 0,
"no_copy": 1,
"oldfieldname": "advance_amount",
"oldfieldtype": "Currency",
"options": "party_account_currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "100px",
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"columns": 2,
"fieldname": "advance_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Advance Amount",
"no_copy": 1,
"oldfieldname": "advance_amount",
"oldfieldtype": "Currency",
"options": "party_account_currency",
"print_width": "100px",
"read_only": 1,
"width": "100px"
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2,
"fieldname": "allocated_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Allocated Amount",
"length": 0,
"no_copy": 1,
"oldfieldname": "allocated_amount",
"oldfieldtype": "Currency",
"options": "party_account_currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "100px",
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"columns": 2,
"fieldname": "allocated_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Allocated Amount",
"no_copy": 1,
"oldfieldname": "allocated_amount",
"oldfieldtype": "Currency",
"options": "party_account_currency",
"print_width": "100px",
"width": "100px"
},
{
"fieldname": "exchange_gain_loss",
"fieldtype": "Currency",
"label": "Exchange Gain/Loss",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "ref_exchange_rate",
"fieldtype": "Float",
"label": "Reference Exchange Rate",
"non_negative": 1,
"read_only": 1
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"menu_index": 0,
"modified": "2016-08-26 02:30:54.407138",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Advance",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"sort_order": "DESC",
"track_seen": 0
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-04-20 16:26:53.820530",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Advance",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@@ -272,7 +272,7 @@
"fieldname": "rate",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Rate ",
"label": "Rate",
"oldfieldname": "import_rate",
"oldfieldtype": "Currency",
"options": "currency",
@@ -607,6 +607,7 @@
"oldfieldname": "purchase_order",
"oldfieldtype": "Link",
"options": "Purchase Order",
"print_hide": 1,
"read_only": 1,
"search_index": 1
},
@@ -853,7 +854,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-02-23 00:59:52.614805",
"modified": "2021-06-16 19:57:03.101571",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@@ -12,6 +12,7 @@
"charge_type",
"row_id",
"included_in_print_rate",
"included_in_paid_amount",
"col_break1",
"account_head",
"description",
@@ -21,6 +22,7 @@
"cost_center",
"dimension_col_break",
"section_break_9",
"currency",
"tax_amount",
"tax_amount_after_discount_amount",
"total",
@@ -205,12 +207,28 @@
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"fetch_from": "account_head.account_currency",
"fieldname": "currency",
"fieldtype": "Link",
"label": "Account Currency",
"options": "Currency",
"read_only": 1
},
{
"default": "0",
"depends_on": "eval:['Purchase Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)",
"description": "If checked, the tax amount will be considered as already included in the Paid Amount in Payment Entry",
"fieldname": "included_in_paid_amount",
"fieldtype": "Check",
"label": "Considered In Paid Amount"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2020-09-18 17:26:09.703215",
"modified": "2021-06-14 01:43:50.750455",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Taxes and Charges",

View File

@@ -1,14 +1,14 @@
var globalOnload = frappe.listview_settings['Sales Invoice'].onload;
frappe.listview_settings['Sales Invoice'].onload = function (doclist) {
frappe.listview_settings['Sales Invoice'].onload = function (list_view) {
// Provision in case onload event is added to sales_invoice.js in future
if (globalOnload) {
globalOnload(doclist);
globalOnload(list_view);
}
const action = () => {
const selected_docs = doclist.get_checked_items();
const docnames = doclist.get_checked_items(true);
const selected_docs = list_view.get_checked_items();
const docnames = list_view.get_checked_items(true);
for (let doc of selected_docs) {
if (doc.docstatus !== 1) {
@@ -19,7 +19,7 @@ frappe.listview_settings['Sales Invoice'].onload = function (doclist) {
frappe.call({
method: 'erpnext.regional.india.utils.generate_ewb_json',
args: {
'dt': doclist.doctype,
'dt': list_view.doctype,
'dn': docnames
},
callback: function(r) {
@@ -35,5 +35,140 @@ frappe.listview_settings['Sales Invoice'].onload = function (doclist) {
});
};
doclist.page.add_actions_menu_item(__('Generate E-Way Bill JSON'), action, false);
list_view.page.add_actions_menu_item(__('Generate E-Way Bill JSON'), action, false);
const generate_irns = () => {
const docnames = list_view.get_checked_items(true);
if (docnames && docnames.length) {
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.generate_einvoices',
args: { docnames },
freeze: true,
freeze_message: __('Generating E-Invoices...')
});
} else {
frappe.msgprint({
message: __('Please select at least one sales invoice to generate IRN'),
title: __('No Invoice Selected'),
indicator: 'red'
});
}
};
const cancel_irns = () => {
const docnames = list_view.get_checked_items(true);
const fields = [
{
"label": "Reason",
"fieldname": "reason",
"fieldtype": "Select",
"reqd": 1,
"default": "1-Duplicate",
"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
},
{
"label": "Remark",
"fieldname": "remark",
"fieldtype": "Data",
"reqd": 1
}
];
const d = new frappe.ui.Dialog({
title: __("Cancel IRN"),
fields: fields,
primary_action: function() {
const data = d.get_values();
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.cancel_irns',
args: {
doctype: list_view.doctype,
docnames,
reason: data.reason.split('-')[0],
remark: data.remark
},
freeze: true,
freeze_message: __('Cancelling E-Invoices...'),
});
d.hide();
},
primary_action_label: __('Submit')
});
d.show();
};
let einvoicing_enabled = false;
frappe.db.get_single_value("E Invoice Settings", "enable").then(enabled => {
einvoicing_enabled = enabled;
});
list_view.$result.on("change", "input[type=checkbox]", () => {
if (einvoicing_enabled) {
const docnames = list_view.get_checked_items(true);
// show/hide e-invoicing actions when no sales invoices are checked
if (docnames && docnames.length) {
// prevent adding actions twice if e-invoicing action group already exists
if (list_view.page.get_inner_group_button(__('E-Invoicing')).length == 0) {
list_view.page.add_inner_button(__('Generate IRNs'), generate_irns, __('E-Invoicing'));
list_view.page.add_inner_button(__('Cancel IRNs'), cancel_irns, __('E-Invoicing'));
}
} else {
list_view.page.remove_inner_button(__('Generate IRNs'), __('E-Invoicing'));
list_view.page.remove_inner_button(__('Cancel IRNs'), __('E-Invoicing'));
}
}
});
frappe.realtime.on("bulk_einvoice_generation_complete", (data) => {
const { failures, user, invoices } = data;
if (invoices.length != failures.length) {
frappe.msgprint({
message: __('{0} e-invoices generated successfully', [invoices.length]),
title: __('Bulk E-Invoice Generation Complete'),
indicator: 'orange'
});
}
if (failures && failures.length && user == frappe.session.user) {
let message = `
Failed to generate IRNs for following ${failures.length} sales invoices:
<ul style="padding-left: 20px; padding-top: 5px;">
${failures.map(d => `<li>${d.docname}</li>`).join('')}
</ul>
`;
frappe.msgprint({
message: message,
title: __('Bulk E-Invoice Generation Complete'),
indicator: 'orange'
});
}
});
frappe.realtime.on("bulk_einvoice_cancellation_complete", (data) => {
const { failures, user, invoices } = data;
if (invoices.length != failures.length) {
frappe.msgprint({
message: __('{0} e-invoices cancelled successfully', [invoices.length]),
title: __('Bulk E-Invoice Cancellation Complete'),
indicator: 'orange'
});
}
if (failures && failures.length && user == frappe.session.user) {
let message = `
Failed to cancel IRNs for following ${failures.length} sales invoices:
<ul style="padding-left: 20px; padding-top: 5px;">
${failures.map(d => `<li>${d.docname}</li>`).join('')}
</ul>
`;
frappe.msgprint({
message: message,
title: __('Bulk E-Invoice Cancellation Complete'),
indicator: 'orange'
});
}
});
};

View File

@@ -1,9 +1,6 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
// print heading
cur_frm.pformat.print_heading = 'Invoice';
{% include 'erpnext/selling/sales_common.js' %};
frappe.provide("erpnext.accounts");
@@ -20,7 +17,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
var me = this;
this._super();
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice'];
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', 'POS Closing Entry'];
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
// show debit_to in print format
this.frm.set_df_property("debit_to", "print_hide", 0);
@@ -359,11 +356,11 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
},
items_on_form_rendered: function() {
erpnext.setup_serial_no();
erpnext.setup_serial_or_batch_no();
},
packed_items_on_form_rendered: function(doc, grid_row) {
erpnext.setup_serial_no();
erpnext.setup_serial_or_batch_no();
},
make_sales_return: function() {
@@ -585,6 +582,16 @@ frappe.ui.form.on('Sales Invoice', {
};
});
frm.set_query("adjustment_against", function() {
return {
filters: {
company: frm.doc.company,
customer: frm.doc.customer,
docstatus: 1
}
};
});
frm.custom_make_buttons = {
'Delivery Note': 'Delivery',
'Sales Invoice': 'Return / Credit Note',
@@ -688,14 +695,16 @@ frappe.ui.form.on('Sales Invoice', {
},
project: function(frm){
frm.call({
method: "add_timesheet_data",
doc: frm.doc,
callback: function(r, rt) {
refresh_field(['timesheets'])
}
})
frm.refresh();
if (!frm.doc.is_return) {
frm.call({
method: "add_timesheet_data",
doc: frm.doc,
callback: function(r, rt) {
refresh_field(['timesheets'])
}
})
frm.refresh();
}
},
onload: function(frm) {
@@ -810,14 +819,27 @@ frappe.ui.form.on('Sales Invoice', {
}
},
add_timesheet_row: function(frm, row, exchange_rate) {
frm.add_child('timesheets', {
'activity_type': row.activity_type,
'description': row.description,
'time_sheet': row.parent,
'billing_hours': row.billing_hours,
'billing_amount': flt(row.billing_amount) * flt(exchange_rate),
'timesheet_detail': row.name
});
frm.refresh_field('timesheets');
calculate_total_billing_amount(frm);
},
refresh: function(frm) {
if (frm.doc.project) {
if (frm.doc.docstatus===0 && !frm.doc.is_return) {
frm.add_custom_button(__('Fetch Timesheet'), function() {
let d = new frappe.ui.Dialog({
title: __('Fetch Timesheet'),
fields: [
{
"label" : "From",
"label" : __("From"),
"fieldname": "from_time",
"fieldtype": "Date",
"reqd": 1,
@@ -827,11 +849,18 @@ frappe.ui.form.on('Sales Invoice', {
fieldname: 'col_break_1',
},
{
"label" : "To",
"label" : __("To"),
"fieldname": "to_time",
"fieldtype": "Date",
"reqd": 1,
}
},
{
"label" : __("Project"),
"fieldname": "project",
"fieldtype": "Link",
"options": "Project",
"default": frm.doc.project
},
],
primary_action: function() {
let data = d.get_values();
@@ -840,27 +869,35 @@ frappe.ui.form.on('Sales Invoice', {
args: {
from_time: data.from_time,
to_time: data.to_time,
project: frm.doc.project
project: data.project
},
callback: function(r) {
if(!r.exc) {
if(r.message.length > 0) {
frm.clear_table('timesheets')
r.message.forEach((d) => {
frm.add_child('timesheets',{
'time_sheet': d.parent,
'billing_hours': d.billing_hours,
'billing_amount': d.billing_amt,
'timesheet_detail': d.name
if (!r.exc && r.message.length > 0) {
frm.clear_table('timesheets')
r.message.forEach((d) => {
let exchange_rate = 1.0;
if (frm.doc.currency != d.currency) {
frappe.call({
method: 'erpnext.setup.utils.get_exchange_rate',
args: {
from_currency: d.currency,
to_currency: frm.doc.currency
},
callback: function(r) {
if (r.message) {
exchange_rate = r.message;
frm.events.add_timesheet_row(frm, d, exchange_rate);
}
}
});
});
frm.refresh_field('timesheets')
}
else {
frappe.msgprint(__('No Timesheet Found.'))
}
d.hide();
} else {
frm.events.add_timesheet_row(frm, d, exchange_rate);
}
});
} else {
frappe.msgprint(__('No Timesheets found with the selected filters.'))
}
d.hide();
}
});
},
@@ -870,6 +907,10 @@ frappe.ui.form.on('Sales Invoice', {
})
}
if (frm.doc.is_debit_note) {
frm.set_df_property('return_against', 'label', 'Adjustment Against');
}
if (frappe.boot.active_domains.includes("Healthcare")) {
frm.set_df_property("patient", "hidden", 0);
frm.set_df_property("patient_name", "hidden", 0);
@@ -916,7 +957,7 @@ frappe.ui.form.on('Sales Invoice Timesheet', {
},
callback: function(r, rt) {
if(r.message){
data = r.message;
let data = r.message;
frappe.model.set_value(cdt, cdn, "billing_hours", data.billing_hours);
frappe.model.set_value(cdt, cdn, "billing_amount", data.billing_amount);
frappe.model.set_value(cdt, cdn, "timesheet_detail", data.timesheet_detail);

View File

@@ -16,6 +16,7 @@
"is_pos",
"is_consolidated",
"is_return",
"is_debit_note",
"update_billed_amount_in_sales_order",
"column_break1",
"company",
@@ -118,6 +119,7 @@
"in_words",
"total_advance",
"outstanding_amount",
"disable_rounded_total",
"advances_section",
"allocate_advances_automatically",
"get_advances",
@@ -391,7 +393,7 @@
"read_only": 1
},
{
"depends_on": "return_against",
"depends_on": "eval:doc.return_against || doc.is_debit_note",
"fieldname": "return_against",
"fieldtype": "Link",
"hide_days": 1,
@@ -400,7 +402,7 @@
"no_copy": 1,
"options": "Sales Invoice",
"print_hide": 1,
"read_only": 1,
"read_only_depends_on": "eval:doc.is_return",
"search_index": 1
},
{
@@ -747,6 +749,7 @@
{
"collapsible": 1,
"collapsible_depends_on": "eval:doc.total_billing_amount > 0",
"depends_on": "eval: !doc.is_return",
"fieldname": "time_sheet_list",
"fieldtype": "Section Break",
"hide_days": 1,
@@ -769,6 +772,7 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Total Billing Amount",
"options": "currency",
"print_hide": 1,
"read_only": 1
},
@@ -1109,6 +1113,7 @@
"reqd": 1
},
{
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "base_rounding_adjustment",
"fieldtype": "Currency",
"hide_days": 1,
@@ -1120,6 +1125,7 @@
"read_only": 1
},
{
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "base_rounded_total",
"fieldtype": "Currency",
"hide_days": 1,
@@ -1168,6 +1174,7 @@
"reqd": 1
},
{
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "rounding_adjustment",
"fieldtype": "Currency",
"hide_days": 1,
@@ -1180,6 +1187,7 @@
},
{
"bold": 1,
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "rounded_total",
"fieldtype": "Currency",
"hide_days": 1,
@@ -1945,6 +1953,19 @@
"fieldtype": "Link",
"label": "Set Target Warehouse",
"options": "Warehouse"
},
{
"default": "0",
"fieldname": "is_debit_note",
"fieldtype": "Check",
"label": "Is Debit Note"
},
{
"default": "0",
"depends_on": "grand_total",
"fieldname": "disable_rounded_total",
"fieldtype": "Check",
"label": "Disable Rounded Total"
}
],
"icon": "fa fa-file-text",
@@ -1957,7 +1978,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2021-03-31 15:42:26.261540",
"modified": "2021-05-20 22:48:33.988881",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -24,6 +24,7 @@ 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
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
from erpnext.healthcare.utils import manage_invoice_submit_cancel
@@ -45,7 +46,6 @@ class SalesInvoice(SellingController):
'target_parent_dt': 'Sales Order',
'target_parent_field': 'per_billed',
'source_field': 'amount',
'join_field': 'so_detail',
'percent_join_field': 'sales_order',
'status_field': 'billing_status',
'keyword': 'Billed',
@@ -125,6 +125,8 @@ class SalesInvoice(SellingController):
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount", "items")
if not self.is_return:
self.validate_serial_numbers()
else:
self.timesheets = []
self.update_packing_list()
self.set_billing_hours_and_amount()
self.update_timesheet_billing_for_project()
@@ -211,6 +213,9 @@ class SalesInvoice(SellingController):
# this sequence because outstanding may get -ve
self.make_gl_entries()
if self.update_stock == 1:
self.repost_future_sle_and_gle()
if self.update_stock == 1:
self.repost_future_sle_and_gle()
@@ -272,7 +277,7 @@ class SalesInvoice(SellingController):
pluck="pos_closing_entry"
)
if pos_closing_entry:
msg = _("To cancel a {} you need to cancel the POS Closing Entry {}. ").format(
msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format(
frappe.bold("Consolidated Sales Invoice"),
get_link_to_form("POS Closing Entry", pos_closing_entry[0])
)
@@ -334,7 +339,7 @@ class SalesInvoice(SellingController):
if "Healthcare" in active_domains:
manage_invoice_submit_cancel(self, "on_cancel")
self.unlink_sales_invoice_from_timesheets()
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
def update_status_updater_args(self):
@@ -390,6 +395,18 @@ class SalesInvoice(SellingController):
if validate_against_credit_limit:
check_credit_limit(self.customer, self.company, bypass_credit_limit_check_at_sales_order)
def unlink_sales_invoice_from_timesheets(self):
for row in self.timesheets:
timesheet = frappe.get_doc('Timesheet', row.time_sheet)
for time_log in timesheet.time_logs:
if time_log.sales_invoice == self.name:
time_log.sales_invoice = None
timesheet.calculate_total_amounts()
timesheet.calculate_percentage_billed()
timesheet.flags.ignore_validate_update_after_submit = True
timesheet.set_status()
timesheet.db_update_all()
@frappe.whitelist()
def set_missing_values(self, for_validate=False):
pos = self.set_pos_fields(for_validate)
@@ -424,7 +441,7 @@ class SalesInvoice(SellingController):
timesheet.calculate_percentage_billed()
timesheet.flags.ignore_validate_update_after_submit = True
timesheet.set_status()
timesheet.save()
timesheet.db_update_all()
def update_time_sheet_detail(self, timesheet, args, sales_invoice):
for data in timesheet.time_logs:
@@ -514,7 +531,7 @@ class SalesInvoice(SellingController):
# set pos values in items
for item in self.get("items"):
if item.get('item_code'):
profile_details = get_pos_profile_item_details(pos, frappe._dict(item.as_dict()), pos)
profile_details = get_pos_profile_item_details(pos, frappe._dict(item.as_dict()), pos, update_data=True)
for fname, val in iteritems(profile_details):
if (not for_validate) or (for_validate and not item.get(fname)):
item.set(fname, val)
@@ -545,12 +562,12 @@ class SalesInvoice(SellingController):
frappe.throw(_("Debit To is required"), title=_("Account Missing"))
if account.report_type != "Balance Sheet":
msg = _("Please ensure {} account is a Balance Sheet account. ").format(frappe.bold("Debit To"))
msg = _("Please ensure {} account is a Balance Sheet account.").format(frappe.bold("Debit To")) + " "
msg += _("You can change the parent account to a Balance Sheet account or select a different account.")
frappe.throw(msg, title=_("Invalid Account"))
if self.customer and account.account_type != "Receivable":
msg = _("Please ensure {} account is a Receivable account. ").format(frappe.bold("Debit To"))
msg = _("Please ensure {} account is a Receivable account.").format(frappe.bold("Debit To")) + " "
msg += _("Change the account type to Receivable or select a different account.")
frappe.throw(msg, title=_("Invalid Account"))
@@ -738,8 +755,10 @@ class SalesInvoice(SellingController):
self.append('timesheets', {
'time_sheet': data.parent,
'billing_hours': data.billing_hours,
'billing_amount': data.billing_amt,
'timesheet_detail': data.name
'billing_amount': data.billing_amount,
'timesheet_detail': data.name,
'activity_type': data.activity_type,
'description': data.description
})
self.calculate_billing_amount_for_timesheet()
@@ -821,8 +840,11 @@ class SalesInvoice(SellingController):
self.make_customer_gl_entry(gl_entries)
self.make_tax_gl_entries(gl_entries)
self.make_exchange_gain_loss_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries)
self.allocate_advance_taxes(gl_entries)
self.make_item_gl_entries(gl_entries)
# merge gl entries before adding pos entries
@@ -830,7 +852,6 @@ class SalesInvoice(SellingController):
self.make_loyalty_point_redemption_gle(gl_entries)
self.make_pos_gl_entries(gl_entries)
self.make_gle_for_change_amount(gl_entries)
self.make_write_off_gl_entry(gl_entries)
self.make_gle_for_rounding_adjustment(gl_entries)
@@ -964,7 +985,13 @@ class SalesInvoice(SellingController):
def make_pos_gl_entries(self, gl_entries):
if cint(self.is_pos):
skip_change_gl_entries = not cint(frappe.db.get_single_value('Accounts Settings', 'post_change_gl_entries'))
for payment_mode in self.payments:
if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount:
payment_mode.base_amount -= flt(self.change_amount)
if payment_mode.amount:
# POS, make payment entries
gl_entries.append(
@@ -996,8 +1023,11 @@ class SalesInvoice(SellingController):
}, payment_mode_account_currency, item=self)
)
if not skip_change_gl_entries:
self.make_gle_for_change_amount(gl_entries)
def make_gle_for_change_amount(self, gl_entries):
if cint(self.is_pos) and self.change_amount:
if self.change_amount:
if self.account_for_change_amount:
gl_entries.append(
self.get_gl_dict({
@@ -1108,7 +1138,7 @@ class SalesInvoice(SellingController):
if not item.serial_no:
continue
for serial_no in item.serial_no.split("\n"):
for serial_no in get_serial_nos(item.serial_no):
if serial_no and frappe.db.get_value('Serial No', serial_no, 'item_code') == item.item_code:
frappe.db.set_value('Serial No', serial_no, 'sales_invoice', invoice)
@@ -1118,7 +1148,6 @@ class SalesInvoice(SellingController):
"""
self.set_serial_no_against_delivery_note()
self.validate_serial_against_delivery_note()
self.validate_serial_against_sales_invoice()
def set_serial_no_against_delivery_note(self):
for item in self.items:
@@ -1149,26 +1178,6 @@ class SalesInvoice(SellingController):
frappe.throw(_("Row {0}: {1} Serial numbers required for Item {2}. You have provided {3}.").format(
item.idx, item.qty, item.item_code, len(si_serial_nos)))
def validate_serial_against_sales_invoice(self):
""" check if serial number is already used in other sales invoice """
for item in self.items:
if not item.serial_no:
continue
for serial_no in item.serial_no.split("\n"):
serial_no_details = frappe.db.get_value("Serial No", serial_no,
["sales_invoice", "item_code"], as_dict=1)
if not serial_no_details:
continue
if serial_no_details.sales_invoice and serial_no_details.item_code == item.item_code \
and self.name != serial_no_details.sales_invoice:
sales_invoice_company = frappe.db.get_value("Sales Invoice", serial_no_details.sales_invoice, "company")
if sales_invoice_company == self.company:
frappe.throw(_("Serial Number: {0} is already referenced in Sales Invoice: {1}")
.format(serial_no, serial_no_details.sales_invoice))
def update_project(self):
if self.project:
project = frappe.get_doc("Project", self.project)
@@ -1752,15 +1761,10 @@ def update_pr_items(doc, sales_item_map, purchase_item_map, parent_child_map, wa
item.purchase_order = parent_child_map.get(sales_item_map.get(item.delivery_note_item))
def get_delivery_note_details(internal_reference):
so_item_map = {}
si_item_details = frappe.get_all('Delivery Note Item', fields=['name', 'so_detail'],
filters={'parent': internal_reference})
for d in si_item_details:
so_item_map.setdefault(d.name, d.so_detail)
return so_item_map
return {d.name: d.so_detail for d in si_item_details if d.so_detail}
def get_sales_invoice_details(internal_reference):
dn_item_map = {}

View File

@@ -713,7 +713,7 @@ class TestSalesInvoice(unittest.TestCase):
si.submit()
self.assertEqual(si.paid_amount, 100.0)
self.pos_gl_entry(si, pos, 50)
self.validate_pos_gl_entry(si, pos, 50)
def test_pos_returns_with_repayment(self):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
@@ -749,7 +749,7 @@ class TestSalesInvoice(unittest.TestCase):
make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1",
expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1")
pr = make_purchase_receipt(company= "_Test Company with perpetual inventory",
make_purchase_receipt(company= "_Test Company with perpetual inventory",
item_code= "_Test FG Item",warehouse= "Stores - TCP1", cost_center= "Main - TCP1")
pos = create_sales_invoice(company= "_Test Company with perpetual inventory",
@@ -770,7 +770,45 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(pos.grand_total, 100.0)
self.assertEqual(pos.write_off_amount, -5)
def pos_gl_entry(self, si, pos, cash_amount):
def test_pos_with_no_gl_entry_for_change_amount(self):
frappe.db.set_value('Accounts Settings', None, 'post_change_gl_entries', 0)
make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1",
expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1")
make_purchase_receipt(company= "_Test Company with perpetual inventory",
item_code= "_Test FG Item",warehouse= "Stores - TCP1", cost_center= "Main - TCP1")
pos = create_sales_invoice(company= "_Test Company with perpetual inventory",
debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1",
income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1",
cost_center = "Main - TCP1", do_not_save=True)
pos.is_pos = 1
pos.update_stock = 1
taxes = get_taxes_and_charges()
pos.taxes = []
for tax in taxes:
pos.append("taxes", tax)
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 50})
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - TCP1', 'amount': 60})
pos.insert()
pos.submit()
self.assertEqual(pos.grand_total, 100.0)
self.assertEqual(pos.change_amount, 10)
self.validate_pos_gl_entry(pos, pos, 60, validate_without_change_gle=True)
frappe.db.set_value('Accounts Settings', None, 'post_change_gl_entries', 1)
def validate_pos_gl_entry(self, si, pos, cash_amount, validate_without_change_gle=False):
if validate_without_change_gle:
cash_amount -= pos.change_amount
# check stock ledger entries
sle = frappe.db.sql("""select * from `tabStock Ledger Entry`
where voucher_type = 'Sales Invoice' and voucher_no = %s""",
@@ -933,12 +971,6 @@ class TestSalesInvoice(unittest.TestCase):
self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "warehouse"))
self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0],
"delivery_document_no"), si.name)
self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "sales_invoice"),
si.name)
# check if the serial number is already linked with any other Sales Invoice
_si = frappe.copy_doc(si.as_dict())
self.assertRaises(frappe.ValidationError, _si.insert)
return si
@@ -1166,10 +1198,12 @@ class TestSalesInvoice(unittest.TestCase):
def test_create_so_with_margin(self):
si = create_sales_invoice(item_code="_Test Item", qty=1, do_not_submit=True)
price_list_rate = 100
price_list_rate = flt(100) * flt(si.plc_conversion_rate)
si.items[0].price_list_rate = price_list_rate
si.items[0].margin_type = 'Percentage'
si.items[0].margin_rate_or_amount = 25
si.items[0].discount_amount = 0.0
si.items[0].discount_percentage = 0.0
si.save()
self.assertEqual(si.get("items")[0].rate, flt((price_list_rate*25)/100 + price_list_rate))
@@ -1877,7 +1911,17 @@ class TestSalesInvoice(unittest.TestCase):
def test_einvoice_submission_without_irn(self):
# init
frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 1)
einvoice_settings = frappe.get_doc('E Invoice Settings')
einvoice_settings.enable = 1
einvoice_settings.applicable_from = nowdate()
einvoice_settings.append('credentials', {
'company': '_Test Company',
'gstin': '27AAECE4835E1ZR',
'username': 'test',
'password': 'test'
})
einvoice_settings.save()
country = frappe.flags.country
frappe.flags.country = 'India'
@@ -1888,73 +1932,86 @@ class TestSalesInvoice(unittest.TestCase):
si.submit()
# reset
frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 0)
einvoice_settings = frappe.get_doc('E Invoice Settings')
einvoice_settings.enable = 0
frappe.flags.country = country
def test_einvoice_json(self):
from erpnext.regional.india.e_invoice.utils import make_einvoice
from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals
si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####'
si.items = []
si.append("items", {
"item_code": "_Test Item",
"uom": "Nos",
"warehouse": "_Test Warehouse - _TC",
"qty": 2000,
"rate": 12,
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
})
si.append("items", {
"item_code": "_Test Item 2",
"uom": "Nos",
"warehouse": "_Test Warehouse - _TC",
"qty": 420,
"rate": 15,
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
})
si = get_sales_invoice_for_e_invoice()
si.discount_amount = 100
si.save()
einvoice = make_einvoice(si)
total_item_ass_value = 0
total_item_cgst_value = 0
total_item_sgst_value = 0
total_item_igst_value = 0
total_item_value = 0
for item in einvoice['ItemList']:
total_item_ass_value += item['AssAmt']
total_item_cgst_value += item['CgstAmt']
total_item_sgst_value += item['SgstAmt']
total_item_igst_value += item['IgstAmt']
total_item_value += item['TotItemVal']
self.assertTrue(item['AssAmt'], item['TotAmt'] - item['Discount'])
self.assertTrue(item['TotItemVal'], item['AssAmt'] + item['CgstAmt'] + item['SgstAmt'] + item['IgstAmt'])
value_details = einvoice['ValDtls']
self.assertEqual(einvoice['Version'], '1.1')
self.assertEqual(value_details['AssVal'], total_item_ass_value)
self.assertEqual(value_details['CgstVal'], total_item_cgst_value)
self.assertEqual(value_details['SgstVal'], total_item_sgst_value)
self.assertEqual(value_details['IgstVal'], total_item_igst_value)
calculated_invoice_value = \
value_details['AssVal'] + value_details['CgstVal'] \
+ value_details['SgstVal'] + value_details['IgstVal'] \
+ value_details['OthChrg'] - value_details['Discount']
self.assertTrue(value_details['TotInvVal'] - calculated_invoice_value < 0.1)
self.assertEqual(value_details['TotInvVal'], si.base_grand_total)
self.assertTrue(einvoice['EwbDtls'])
validate_totals(einvoice)
si.apply_discount_on = 'Net Total'
si.save()
einvoice = make_einvoice(si)
validate_totals(einvoice)
[d.set('included_in_print_rate', 1) for d in si.taxes]
si.save()
einvoice = make_einvoice(si)
validate_totals(einvoice)
def test_item_tax_net_range(self):
item = create_item("T Shirt")
item.set('taxes', [])
item.append("taxes", {
"item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
"minimum_net_rate": 0,
"maximum_net_rate": 500
})
item.append("taxes", {
"item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
"minimum_net_rate": 501,
"maximum_net_rate": 1000
})
item.save()
sales_invoice = create_sales_invoice(item = "T Shirt", rate=700, do_not_submit=True)
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
# Apply discount
sales_invoice.apply_discount_on = 'Net Total'
sales_invoice.discount_amount = 300
sales_invoice.save()
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####'
si.items = []
si.append("items", {
"item_code": "_Test Item",
"uom": "Nos",
"warehouse": "_Test Warehouse - _TC",
"qty": 2000,
"rate": 12,
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
})
si.append("items", {
"item_code": "_Test Item 2",
"uom": "Nos",
"warehouse": "_Test Warehouse - _TC",
"qty": 420,
"rate": 15,
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
})
return si
def make_test_address_for_ewaybill():
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
@@ -2031,9 +2088,9 @@ def make_sales_invoice_for_ewaybill():
if not gst_account:
gst_settings.append("gst_accounts", {
"company": "_Test Company",
"cgst_account": "CGST - _TC",
"sgst_account": "SGST - _TC",
"igst_account": "IGST - _TC",
"cgst_account": "Output Tax CGST - _TC",
"sgst_account": "Output Tax SGST - _TC",
"igst_account": "Output Tax IGST - _TC",
})
gst_settings.save()
@@ -2050,7 +2107,7 @@ def make_sales_invoice_for_ewaybill():
si.append("taxes", {
"charge_type": "On Net Total",
"account_head": "CGST - _TC",
"account_head": "Output Tax CGST - _TC",
"cost_center": "Main - _TC",
"description": "CGST @ 9.0",
"rate": 9
@@ -2058,7 +2115,7 @@ def make_sales_invoice_for_ewaybill():
si.append("taxes", {
"charge_type": "On Net Total",
"account_head": "SGST - _TC",
"account_head": "Output Tax SGST - _TC",
"cost_center": "Main - _TC",
"description": "SGST @ 9.0",
"rate": 9
@@ -2078,27 +2135,6 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
doc.assertEqual(expected_gle[i][2], gle.credit)
doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
def test_item_tax_validity(self):
item = frappe.get_doc("Item", "_Test Item 2")
if item.taxes:
item.taxes = []
item.save()
item.append("taxes", {
"item_tax_template": "_Test Item Tax Template 1 - _TC",
"valid_from": add_days(nowdate(), 1)
})
item.save()
sales_invoice = create_sales_invoice(item = "_Test Item 2", do_not_save=1)
sales_invoice.items[0].item_tax_template = "_Test Item Tax Template 1 - _TC"
self.assertRaises(frappe.ValidationError, sales_invoice.save)
item.taxes = []
item.save()
def create_sales_invoice(**args):
si = frappe.new_doc("Sales Invoice")
args = frappe._dict(args)

View File

@@ -1,235 +1,128 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2013-02-22 01:27:41",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"actions": [],
"creation": "2013-02-22 01:27:41",
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"reference_type",
"reference_name",
"remarks",
"reference_row",
"col_break1",
"advance_amount",
"allocated_amount",
"exchange_gain_loss",
"ref_exchange_rate"
],
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "reference_type",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Reference Type",
"length": 0,
"no_copy": 1,
"oldfieldname": "journal_voucher",
"oldfieldtype": "Link",
"options": "DocType",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "250px",
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"fieldname": "reference_type",
"fieldtype": "Link",
"label": "Reference Type",
"no_copy": 1,
"oldfieldname": "journal_voucher",
"oldfieldtype": "Link",
"options": "DocType",
"print_width": "250px",
"read_only": 1,
"width": "250px"
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 3,
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Reference Name",
"length": 0,
"no_copy": 1,
"options": "reference_type",
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"columns": 3,
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Reference Name",
"no_copy": 1,
"options": "reference_type",
"print_hide": 1,
"read_only": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 3,
"fieldname": "remarks",
"fieldtype": "Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Remarks",
"length": 0,
"no_copy": 1,
"oldfieldname": "remarks",
"oldfieldtype": "Small Text",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "150px",
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"columns": 3,
"fieldname": "remarks",
"fieldtype": "Text",
"in_list_view": 1,
"label": "Remarks",
"no_copy": 1,
"oldfieldname": "remarks",
"oldfieldtype": "Small Text",
"print_width": "150px",
"read_only": 1,
"width": "150px"
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "reference_row",
"fieldtype": "Data",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Reference Row",
"length": 0,
"no_copy": 1,
"oldfieldname": "jv_detail_no",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 1,
"print_hide_if_no_value": 0,
"print_width": "120px",
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"fieldname": "reference_row",
"fieldtype": "Data",
"hidden": 1,
"label": "Reference Row",
"no_copy": 1,
"oldfieldname": "jv_detail_no",
"oldfieldtype": "Data",
"print_hide": 1,
"print_width": "120px",
"read_only": 1,
"width": "120px"
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "col_break1",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "col_break1",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2,
"fieldname": "advance_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Advance amount",
"length": 0,
"no_copy": 1,
"oldfieldname": "advance_amount",
"oldfieldtype": "Currency",
"options": "party_account_currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "120px",
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"columns": 2,
"fieldname": "advance_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Advance amount",
"no_copy": 1,
"oldfieldname": "advance_amount",
"oldfieldtype": "Currency",
"options": "party_account_currency",
"print_width": "120px",
"read_only": 1,
"width": "120px"
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2,
"fieldname": "allocated_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Allocated amount",
"length": 0,
"no_copy": 1,
"oldfieldname": "allocated_amount",
"oldfieldtype": "Currency",
"options": "party_account_currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "120px",
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"columns": 2,
"fieldname": "allocated_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Allocated amount",
"no_copy": 1,
"oldfieldname": "allocated_amount",
"oldfieldtype": "Currency",
"options": "party_account_currency",
"print_width": "120px",
"width": "120px"
},
{
"fieldname": "exchange_gain_loss",
"fieldtype": "Currency",
"label": "Exchange Gain/Loss",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "ref_exchange_rate",
"fieldtype": "Float",
"label": "Reference Exchange Rate",
"non_negative": 1,
"read_only": 1
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"menu_index": 0,
"modified": "2016-08-26 02:36:10.718057",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Advance",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"sort_order": "DESC",
"track_seen": 0
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-06-04 20:25:49.832052",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Advance",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@@ -1,172 +1,78 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2016-06-14 19:21:34.321662",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"creation": "2016-06-14 19:21:34.321662",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"activity_type",
"description",
"billing_hours",
"billing_amount",
"time_sheet",
"timesheet_detail"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "time_sheet",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Time Sheet",
"length": 0,
"no_copy": 0,
"options": "Timesheet",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "time_sheet",
"fieldtype": "Link",
"in_global_search": 1,
"in_list_view": 1,
"label": "Time Sheet",
"options": "Timesheet",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "billing_hours",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Billing Hours",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "billing_hours",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Billing Hours",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "billing_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Billing Amount",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "billing_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Billing Amount",
"options": "currency",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "timesheet_detail",
"fieldtype": "Data",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Timesheet Detail",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"allow_on_submit": 1,
"fieldname": "timesheet_detail",
"fieldtype": "Data",
"hidden": 1,
"label": "Timesheet Detail",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "activity_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Activity Type",
"options": "Activity Type",
"read_only": 1
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Description",
"read_only": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2019-02-18 18:50:44.770361",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Timesheet",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
],
"istable": 1,
"links": [],
"modified": "2021-05-20 22:33:57.234846",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Timesheet",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -1,8 +1,10 @@
{
"actions": [],
"creation": "2013-04-24 11:39:32",
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"charge_type",
"row_id",
@@ -10,12 +12,14 @@
"col_break_1",
"description",
"included_in_print_rate",
"included_in_paid_amount",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"section_break_8",
"rate",
"section_break_9",
"currency",
"tax_amount",
"total",
"tax_amount_after_discount_amount",
@@ -23,8 +27,7 @@
"base_tax_amount",
"base_total",
"base_tax_amount_after_discount_amount",
"item_wise_tax_detail",
"parenttype"
"item_wise_tax_detail"
],
"fields": [
{
@@ -173,17 +176,6 @@
"oldfieldtype": "Small Text",
"read_only": 1
},
{
"fieldname": "parenttype",
"fieldtype": "Data",
"hidden": 1,
"in_filter": 1,
"label": "Parenttype",
"oldfieldname": "parenttype",
"oldfieldtype": "Data",
"print_hide": 1,
"search_index": 1
},
{
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
@@ -192,15 +184,34 @@
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"fetch_from": "account_head.account_currency",
"fieldname": "currency",
"fieldtype": "Link",
"label": "Account Currency",
"options": "Currency",
"read_only": 1
},
{
"default": "0",
"depends_on": "eval:['Sales Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)",
"description": "If checked, the tax amount will be considered as already included in the Paid Amount in Payment Entry",
"fieldname": "included_in_paid_amount",
"fieldtype": "Check",
"label": "Considered In Paid Amount"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"modified": "2019-05-25 22:59:38.740883",
"links": [],
"modified": "2021-06-14 01:44:36.899147",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Taxes and Charges",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "ASC"
}

View File

@@ -36,6 +36,7 @@
"additional_discount_percentage",
"additional_discount_amount",
"sb_3",
"submit_invoice",
"invoices",
"accounting_dimensions_section",
"cost_center",
@@ -45,9 +46,7 @@
{
"allow_on_submit": 1,
"fieldname": "cb_1",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
"fieldtype": "Column Break"
},
{
"fieldname": "status",
@@ -55,97 +54,73 @@
"label": "Status",
"no_copy": 1,
"options": "\nTrialling\nActive\nPast Due Date\nCancelled\nUnpaid\nCompleted",
"read_only": 1,
"show_days": 1,
"show_seconds": 1
"read_only": 1
},
{
"fieldname": "subscription_period",
"fieldtype": "Section Break",
"label": "Subscription Period",
"show_days": 1,
"show_seconds": 1
"label": "Subscription Period"
},
{
"fieldname": "cancelation_date",
"fieldtype": "Date",
"label": "Cancelation Date",
"read_only": 1,
"show_days": 1,
"show_seconds": 1
"read_only": 1
},
{
"allow_on_submit": 1,
"fieldname": "trial_period_start",
"fieldtype": "Date",
"label": "Trial Period Start Date",
"set_only_once": 1,
"show_days": 1,
"show_seconds": 1
"set_only_once": 1
},
{
"depends_on": "eval:doc.trial_period_start",
"fieldname": "trial_period_end",
"fieldtype": "Date",
"label": "Trial Period End Date",
"set_only_once": 1,
"show_days": 1,
"show_seconds": 1
"set_only_once": 1
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
"fieldtype": "Column Break"
},
{
"fieldname": "current_invoice_start",
"fieldtype": "Date",
"label": "Current Invoice Start Date",
"read_only": 1,
"show_days": 1,
"show_seconds": 1
"read_only": 1
},
{
"fieldname": "current_invoice_end",
"fieldtype": "Date",
"label": "Current Invoice End Date",
"read_only": 1,
"show_days": 1,
"show_seconds": 1
"read_only": 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",
"show_days": 1,
"show_seconds": 1
"label": "Days Until Due"
},
{
"default": "0",
"fieldname": "cancel_at_period_end",
"fieldtype": "Check",
"label": "Cancel At End Of Period",
"show_days": 1,
"show_seconds": 1
"label": "Cancel At End Of Period"
},
{
"default": "0",
"fieldname": "generate_invoice_at_period_start",
"fieldtype": "Check",
"label": "Generate Invoice At Beginning Of Period",
"show_days": 1,
"show_seconds": 1
"label": "Generate Invoice At Beginning Of Period"
},
{
"allow_on_submit": 1,
"fieldname": "sb_4",
"fieldtype": "Section Break",
"label": "Plans",
"show_days": 1,
"show_seconds": 1
"label": "Plans"
},
{
"allow_on_submit": 1,
@@ -153,84 +128,62 @@
"fieldtype": "Table",
"label": "Plans",
"options": "Subscription Plan Detail",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
"reqd": 1
},
{
"depends_on": "eval:['Customer', 'Supplier'].includes(doc.party_type)",
"fieldname": "sb_1",
"fieldtype": "Section Break",
"label": "Taxes",
"show_days": 1,
"show_seconds": 1
"label": "Taxes"
},
{
"fieldname": "sb_2",
"fieldtype": "Section Break",
"label": "Discounts",
"show_days": 1,
"show_seconds": 1
"label": "Discounts"
},
{
"fieldname": "apply_additional_discount",
"fieldtype": "Select",
"label": "Apply Additional Discount On",
"options": "\nGrand Total\nNet Total",
"show_days": 1,
"show_seconds": 1
"options": "\nGrand Total\nNet Total"
},
{
"fieldname": "cb_2",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
"fieldtype": "Column Break"
},
{
"fieldname": "additional_discount_percentage",
"fieldtype": "Percent",
"label": "Additional DIscount Percentage",
"show_days": 1,
"show_seconds": 1
"label": "Additional DIscount Percentage"
},
{
"collapsible": 1,
"fieldname": "additional_discount_amount",
"fieldtype": "Currency",
"label": "Additional DIscount Amount",
"show_days": 1,
"show_seconds": 1
"label": "Additional DIscount Amount"
},
{
"depends_on": "eval:doc.invoices",
"fieldname": "sb_3",
"fieldtype": "Section Break",
"label": "Invoices",
"show_days": 1,
"show_seconds": 1
"label": "Invoices"
},
{
"collapsible": 1,
"fieldname": "invoices",
"fieldtype": "Table",
"label": "Invoices",
"options": "Subscription Invoice",
"show_days": 1,
"show_seconds": 1
"options": "Subscription Invoice"
},
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions",
"show_days": 1,
"show_seconds": 1
"label": "Accounting Dimensions"
},
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
"fieldtype": "Column Break"
},
{
"fieldname": "party_type",
@@ -238,9 +191,7 @@
"label": "Party Type",
"options": "DocType",
"reqd": 1,
"set_only_once": 1,
"show_days": 1,
"show_seconds": 1
"set_only_once": 1
},
{
"fieldname": "party",
@@ -249,27 +200,21 @@
"label": "Party",
"options": "party_type",
"reqd": 1,
"set_only_once": 1,
"show_days": 1,
"show_seconds": 1
"set_only_once": 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",
"show_days": 1,
"show_seconds": 1
"options": "Sales Taxes and Charges Template"
},
{
"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",
"show_days": 1,
"show_seconds": 1
"options": "Purchase Taxes and Charges Template"
},
{
"default": "0",
@@ -277,55 +222,49 @@
"fieldname": "follow_calendar_months",
"fieldtype": "Check",
"label": "Follow Calendar Months",
"set_only_once": 1,
"show_days": 1,
"show_seconds": 1
"set_only_once": 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",
"show_days": 1,
"show_seconds": 1
"label": "Generate New Invoices Past Due Date"
},
{
"fieldname": "end_date",
"fieldtype": "Date",
"label": "Subscription End Date",
"set_only_once": 1,
"show_days": 1,
"show_seconds": 1
"set_only_once": 1
},
{
"fieldname": "start_date",
"fieldtype": "Date",
"label": "Subscription Start Date",
"set_only_once": 1,
"show_days": 1,
"show_seconds": 1
"set_only_once": 1
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center",
"show_days": 1,
"show_seconds": 1
"options": "Cost Center"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"show_days": 1,
"show_seconds": 1
"options": "Company"
},
{
"default": "1",
"fieldname": "submit_invoice",
"fieldtype": "Check",
"label": "Submit Invoice Automatically"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-02-09 15:44:20.024789",
"modified": "2021-04-19 15:24:27.550797",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Subscription",

View File

@@ -276,7 +276,7 @@ class Subscription(Document):
frappe.throw(_('Subscription End Date is mandatory to follow calendar months'))
if billing_info[0]['billing_interval'] != 'Month':
frappe.throw('Billing Interval in Subscription Plan must be Month to follow calendar months')
frappe.throw(_('Billing Interval in Subscription Plan must be Month to follow calendar months'))
def after_insert(self):
# todo: deal with users who collect prepayments. Maybe a new Subscription Invoice doctype?
@@ -383,7 +383,9 @@ class Subscription(Document):
invoice.flags.ignore_mandatory = True
invoice.save()
invoice.submit()
if self.submit_invoice:
invoice.submit()
return invoice

View File

@@ -1,24 +1,6 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
cur_frm.add_fetch("customer", "customer_group", "customer_group" );
cur_frm.add_fetch("supplier", "supplier_group_name", "supplier_group" );
frappe.ui.form.on("Tax Rule", "tax_type", function(frm) {
frm.toggle_reqd("sales_tax_template", frm.doc.tax_type=="Sales");
frm.toggle_reqd("purchase_tax_template", frm.doc.tax_type=="Purchase");
})
frappe.ui.form.on("Tax Rule", "onload", function(frm) {
if(frm.doc.__islocal) {
frm.set_value("use_for_shopping_cart", 1);
}
})
frappe.ui.form.on("Tax Rule", "refresh", function(frm) {
frappe.ui.form.trigger("Tax Rule", "tax_type");
})
frappe.ui.form.on("Tax Rule", "customer", function(frm) {
if(frm.doc.customer) {
frappe.call({

File diff suppressed because it is too large Load Diff

View File

@@ -50,7 +50,7 @@ class TestTaxRule(unittest.TestCase):
tax_rule1 = make_tax_rule(customer_group= "All Customer Groups",
sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, from_date = "2015-01-01")
tax_rule1.save()
self.assertEqual(get_tax_template("2015-01-01", {"customer_group" : "Commercial", "use_for_shopping_cart":0}),
self.assertEqual(get_tax_template("2015-01-01", {"customer_group" : "Commercial", "use_for_shopping_cart":1}),
"_Test Sales Taxes and Charges Template - _TC")
def test_conflict_with_overlapping_dates(self):

View File

@@ -1,263 +1,151 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 1,
"autoname": "Prompt",
"beta": 0,
"creation": "2018-04-13 18:42:06.431683",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "Prompt",
"creation": "2018-04-13 18:42:06.431683",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"category_details_section",
"category_name",
"round_off_tax_amount",
"column_break_2",
"consider_party_ledger_amount",
"tax_on_excess_amount",
"section_break_8",
"rates",
"section_break_7",
"accounts"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "category_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Category Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"show_days": 1,
"show_seconds": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_8",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Tax Withholding Rates",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"show_days": 1,
"show_seconds": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "rates",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Rates",
"length": 0,
"no_copy": 0,
"options": "Tax Withholding Rate",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"show_days": 1,
"show_seconds": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_7",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"fieldname": "section_break_7",
"fieldtype": "Section Break",
"label": "Account Details",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"show_days": 1,
"show_seconds": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "accounts",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Accounts",
"length": 0,
"no_copy": 0,
"options": "Tax Withholding Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldname": "accounts",
"fieldtype": "Table",
"label": "Accounts",
"options": "Tax Withholding Account",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "category_details_section",
"fieldtype": "Section Break",
"label": "Category Details",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"description": "Even invoices with apply tax withholding unchecked will be considered for checking cumulative threshold breach",
"fieldname": "consider_party_ledger_amount",
"fieldtype": "Check",
"label": "Consider Entire Party Ledger Amount",
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"description": "Tax will be withheld only for amount exceeding the cumulative threshold",
"fieldname": "tax_on_excess_amount",
"fieldtype": "Check",
"label": "Only Deduct Tax On Excess Amount ",
"show_days": 1,
"show_seconds": 1
},
{
"description": "Checking this will round off the tax amount to the nearest integer",
"fieldname": "round_off_tax_amount",
"fieldtype": "Check",
"label": "Round Off Tax Amount",
"show_days": 1,
"show_seconds": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-07-17 22:53:26.193179",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withholding Category",
"name_case": "",
"owner": "Administrator",
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-07-27 21:47:34.396071",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withholding Category",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
},
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

Some files were not shown because too many files have changed in this diff Show More