Merge branch 'version-13-beta-pre-release' of https://github.com/frappe/erpnext into pre-release

This commit is contained in:
Anupam 2020-11-06 16:38:16 +05:30
commit beba8da0a6
761 changed files with 125038 additions and 30530 deletions

48
.github/helper/documentation.py vendored Normal file
View File

@ -0,0 +1,48 @@
import sys
import requests
from urllib.parse import urlparse
docs_repos = [
"frappe_docs",
"erpnext_documentation",
"erpnext_com",
"frappe_io",
]
def uri_validator(x):
result = urlparse(x)
return all([result.scheme, result.netloc, result.path])
def docs_link_exists(body):
for line in body.splitlines():
for word in line.split():
if word.startswith('http') and uri_validator(word):
parsed_url = urlparse(word)
if parsed_url.netloc == "github.com":
_, org, repo, _type, ref = parsed_url.path.split('/')
if org == "frappe" and repo in docs_repos:
return True
if __name__ == "__main__":
pr = sys.argv[1]
response = requests.get("https://api.github.com/repos/frappe/erpnext/pulls/{}".format(pr))
if response.ok:
payload = response.json()
title = payload.get("title", "").lower()
head_sha = payload.get("head", {}).get("sha")
body = payload.get("body", "").lower()
if title.startswith("feat") and head_sha and "no-docs" not in body:
if docs_link_exists(body):
print("Documentation Link Found. You're Awesome! 🎉")
else:
print("Documentation Link Not Found! ⚠️")
sys.exit(1)
else:
print("Skipping documentation checks... 🏃")

60
.github/helper/translation.py vendored Normal file
View File

@ -0,0 +1,60 @@
import re
import sys
errors_encounter = 0
pattern = re.compile(r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)")
words_pattern = re.compile(r"_{1,2}\([\"'`]{1,3}.*?[a-zA-Z]")
start_pattern = re.compile(r"_{1,2}\([f\"'`]{1,3}")
f_string_pattern = re.compile(r"_\(f[\"']")
starts_with_f_pattern = re.compile(r"_\(f")
# skip first argument
files = sys.argv[1:]
files_to_scan = [_file for _file in files if _file.endswith(('.py', '.js'))]
for _file in files_to_scan:
with open(_file, 'r') as f:
print(f'Checking: {_file}')
file_lines = f.readlines()
for line_number, line in enumerate(file_lines, 1):
if 'frappe-lint: disable-translate' in line:
continue
start_matches = start_pattern.search(line)
if start_matches:
starts_with_f = starts_with_f_pattern.search(line)
if starts_with_f:
has_f_string = f_string_pattern.search(line)
if has_f_string:
errors_encounter += 1
print(f'\nF-strings are not supported for translations at line number {line_number + 1}\n{line.strip()[:100]}')
continue
else:
continue
match = pattern.search(line)
error_found = False
if not match and line.endswith(',\n'):
# concat remaining text to validate multiline pattern
line = "".join(file_lines[line_number - 1:])
line = line[start_matches.start() + 1:]
match = pattern.match(line)
if not match:
error_found = True
print(f'\nTranslation syntax error at line number {line_number + 1}\n{line.strip()[:100]}')
if not error_found and not words_pattern.search(line):
error_found = True
print(f'\nTranslation is useless because it has no words at line number {line_number + 1}\n{line.strip()[:100]}')
if error_found:
errors_encounter += 1
if errors_encounter > 0:
print('\nVisit "https://frappeframework.com/docs/user/en/translations" to learn about valid translation strings.')
sys.exit(1)
else:
print('\nGood To Go!')

24
.github/workflows/docs-checker.yml vendored Normal file
View File

@ -0,0 +1,24 @@
name: 'Documentation Required'
on:
pull_request:
types: [ opened, synchronize, reopened, edited ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: 'Setup Environment'
uses: actions/setup-python@v2
with:
python-version: 3.6
- name: 'Clone repo'
uses: actions/checkout@v2
- name: Validate Docs
env:
PR_NUMBER: ${{ github.event.number }}
run: |
pip install requests --quiet
python $GITHUB_WORKSPACE/.github/helper/documentation.py $PR_NUMBER

View File

@ -0,0 +1,22 @@
name: Frappe Linter
on:
pull_request:
branches:
- develop
- version-12-hotfix
- version-11-hotfix
jobs:
check_translation:
name: Translation Syntax Check
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- name: Setup python3
uses: actions/setup-python@v1
with:
python-version: 3.6
- name: Validating Translation Syntax
run: |
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
python $GITHUB_WORKSPACE/.github/helper/translation.py $files

View File

@ -16,7 +16,7 @@
ERPNext as a monolith includes the following areas for managing businesses: ERPNext as a monolith includes the following areas for managing businesses:
1. [Accounting](https://erpnext.com/open-source-accounting) 1. [Accounting](https://erpnext.com/open-source-accounting)
1. [Inventory](https://erpnext.com/distribution/inventory-management-system) 1. [Warehouse Management](https://erpnext.com/distribution/warehouse-management-system)
1. [CRM](https://erpnext.com/open-source-crm) 1. [CRM](https://erpnext.com/open-source-crm)
1. [Sales](https://erpnext.com/open-source-sales-purchase) 1. [Sales](https://erpnext.com/open-source-sales-purchase)
1. [Purchase](https://erpnext.com/open-source-sales-purchase) 1. [Purchase](https://erpnext.com/open-source-sales-purchase)

View File

@ -5,6 +5,7 @@
"_comments": null, "_comments": null,
"_liked_by": null, "_liked_by": null,
"_user_tags": null, "_user_tags": null,
"allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
@ -17,17 +18,24 @@
"docstatus": 0, "docstatus": 0,
"dt": "Address", "dt": "Address",
"fetch_from": null, "fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "tax_category", "fieldname": "tax_category",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0, "hidden": 0,
"idx": 14, "hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"idx": 15,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_global_search": 0, "in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"insert_after": "fax", "insert_after": "fax",
"label": "Tax Category", "label": "Tax Category",
"length": 0,
"mandatory_depends_on": null,
"modified": "2018-12-28 22:29:21.828090", "modified": "2018-12-28 22:29:21.828090",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Address-tax_category", "name": "Address-tax_category",
@ -43,6 +51,66 @@
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
"print_width": null, "print_width": null,
"read_only": 0, "read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"translatable": 0,
"unique": 0,
"width": null
},
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"creation": "2020-10-14 17:41:40.878179",
"default": "0",
"depends_on": null,
"description": null,
"docstatus": 0,
"dt": "Address",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "is_your_company_address",
"fieldtype": "Check",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"idx": 20,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "linked_with",
"label": "Is Your Company Address",
"length": 0,
"mandatory_depends_on": null,
"modified": "2020-10-14 17:41:40.878179",
"modified_by": "Administrator",
"name": "Address-is_your_company_address",
"no_copy": 0,
"options": null,
"owner": "Administrator",
"parent": null,
"parentfield": null,
"parenttype": null,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
"search_index": 0, "search_index": 0,

View File

@ -0,0 +1,42 @@
import frappe
from frappe import _
from frappe.contacts.doctype.address.address import Address
from frappe.contacts.doctype.address.address import get_address_templates
class ERPNextAddress(Address):
def validate(self):
self.validate_reference()
super(ERPNextAddress, self).validate()
def link_address(self):
"""Link address based on owner"""
if self.is_your_company_address:
return
return super(ERPNextAddress, self).link_address()
def validate_reference(self):
if self.is_your_company_address and not [
row for row in self.links if row.link_doctype == "Company"
]:
frappe.throw(_("Address needs to be linked to a Company. Please add a row for Company in the Links table."),
title=_("Company Not Linked"))
@frappe.whitelist()
def get_shipping_address(company, address = None):
filters = [
["Dynamic Link", "link_doctype", "=", "Company"],
["Dynamic Link", "link_name", "=", company],
["Address", "is_your_company_address", "=", 1]
]
fields = ["*"]
if address and frappe.db.get_value('Dynamic Link',
{'parent': address, 'link_name': company}):
filters.append(["Address", "name", "=", address])
address = frappe.get_all("Address", filters=filters, fields=fields) or {}
if address:
address_as_dict = address[0]
name, address_template = get_address_templates(address_as_dict)
return address_as_dict.get("name"), frappe.render_template(address_template, address_as_dict)

View File

@ -43,7 +43,7 @@
{ {
"hidden": 0, "hidden": 0,
"label": "Bank Statement", "label": "Bank Statement",
"links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Transaction Entry\",\n \"name\": \"Bank Statement Transaction Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Settings\",\n \"name\": \"Bank Statement Settings\",\n \"type\": \"doctype\"\n }\n]" "links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Clearance\",\n \"name\": \"Bank Clearance\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Reconciliation\",\n \"name\": \"bank-reconciliation\",\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Bank Reconciliation Statement\",\n \"name\": \"Bank Reconciliation Statement\",\n \"type\": \"report\"\n },\n {\n \"label\": \"Bank Statement Transaction Entry\",\n \"name\": \"Bank Statement Transaction Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Settings\",\n \"name\": \"Bank Statement Settings\",\n \"type\": \"doctype\"\n }\n]"
}, },
{ {
"hidden": 0, "hidden": 0,
@ -53,7 +53,7 @@
{ {
"hidden": 0, "hidden": 0,
"label": "Goods and Services Tax (GST India)", "label": "Goods and Services Tax (GST India)",
"links": "[\n {\n \"label\": \"GST Settings\",\n \"name\": \"GST Settings\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"GST HSN Code\",\n \"name\": \"GST HSN Code\",\n \"type\": \"doctype\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GSTR-1\",\n \"name\": \"GSTR-1\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GSTR-2\",\n \"name\": \"GSTR-2\",\n \"type\": \"report\"\n },\n {\n \"label\": \"GSTR 3B Report\",\n \"name\": \"GSTR 3B Report\",\n \"type\": \"doctype\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Sales Register\",\n \"name\": \"GST Sales Register\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Purchase Register\",\n \"name\": \"GST Purchase Register\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Itemised Sales Register\",\n \"name\": \"GST Itemised Sales Register\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Itemised Purchase Register\",\n \"name\": \"GST Itemised Purchase Register\",\n \"type\": \"report\"\n },\n {\n \"country\": \"India\",\n \"description\": \"C-Form records\",\n \"label\": \"C-Form\",\n \"name\": \"C-Form\",\n \"type\": \"doctype\"\n }\n]" "links": "[\n {\n \"label\": \"GST Settings\",\n \"name\": \"GST Settings\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"GST HSN Code\",\n \"name\": \"GST HSN Code\",\n \"type\": \"doctype\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GSTR-1\",\n \"name\": \"GSTR-1\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GSTR-2\",\n \"name\": \"GSTR-2\",\n \"type\": \"report\"\n },\n {\n \"label\": \"GSTR 3B Report\",\n \"name\": \"GSTR 3B Report\",\n \"type\": \"doctype\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Sales Register\",\n \"name\": \"GST Sales Register\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Purchase Register\",\n \"name\": \"GST Purchase Register\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Itemised Sales Register\",\n \"name\": \"GST Itemised Sales Register\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Itemised Purchase Register\",\n \"name\": \"GST Itemised Purchase Register\",\n \"type\": \"report\"\n },\n {\n \"country\": \"India\",\n \"description\": \"C-Form records\",\n \"label\": \"C-Form\",\n \"name\": \"C-Form\",\n \"type\": \"doctype\"\n },\n {\n \"country\": \"India\",\n \"label\": \"Lower Deduction Certificate\",\n \"name\": \"Lower Deduction Certificate\",\n \"type\": \"doctype\"\n }\n]"
}, },
{ {
"hidden": 0, "hidden": 0,
@ -98,7 +98,7 @@
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"label": "Accounting", "label": "Accounting",
"modified": "2020-06-19 12:42:44.054598", "modified": "2020-11-06 13:05:58.650150",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounting", "name": "Accounting",
@ -147,11 +147,6 @@
"link_to": "Trial Balance", "link_to": "Trial Balance",
"type": "Report" "type": "Report"
}, },
{
"label": "Point of Sale",
"link_to": "point-of-sale",
"type": "Page"
},
{ {
"label": "Dashboard", "label": "Dashboard",
"link_to": "Accounts", "link_to": "Accounts",

View File

@ -117,7 +117,9 @@ class Account(NestedSet):
for d in frappe.db.get_values('Account', filters=filters, fieldname=["company", "name"], as_dict=True): for d in frappe.db.get_values('Account', filters=filters, fieldname=["company", "name"], as_dict=True):
parent_acc_name_map[d["company"]] = d["name"] parent_acc_name_map[d["company"]] = d["name"]
if not parent_acc_name_map: return if not parent_acc_name_map: return
self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name) self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name)
def validate_group_or_ledger(self): def validate_group_or_ledger(self):
@ -289,10 +291,30 @@ def validate_account_number(name, account_number, company):
.format(account_number, account_with_same_number)) .format(account_number, account_with_same_number))
@frappe.whitelist() @frappe.whitelist()
def update_account_number(name, account_name, account_number=None): def update_account_number(name, account_name, account_number=None, from_descendant=False):
account = frappe.db.get_value("Account", name, "company", as_dict=True) account = frappe.db.get_value("Account", name, "company", as_dict=True)
if not account: return if not account: return
old_acc_name, old_acc_number = frappe.db.get_value('Account', name, \
["account_name", "account_number"])
# check if account exists in parent company
ancestors = get_ancestors_of("Company", account.company)
allow_independent_account_creation = frappe.get_value("Company", account.company, "allow_account_creation_against_child_company")
if ancestors and not allow_independent_account_creation:
for ancestor in ancestors:
if frappe.db.get_value("Account", {'account_name': old_acc_name, 'company': ancestor}, 'name'):
# same account in parent company exists
allow_child_account_creation = _("Allow Account Creation Against Child Company")
message = _("Account {0} exists in parent company {1}.").format(frappe.bold(old_acc_name), frappe.bold(ancestor))
message += "<br>" + _("Renaming it is only allowed via parent company {0}, \
to avoid mismatch.").format(frappe.bold(ancestor)) + "<br><br>"
message += _("To overrule this, enable '{0}' in company {1}").format(allow_child_account_creation, frappe.bold(account.company))
frappe.throw(message, title=_("Rename Not Allowed"))
validate_account_number(name, account_number, account.company) validate_account_number(name, account_number, account.company)
if account_number: if account_number:
frappe.db.set_value("Account", name, "account_number", account_number.strip()) frappe.db.set_value("Account", name, "account_number", account_number.strip())
@ -300,6 +322,12 @@ def update_account_number(name, account_name, account_number=None):
frappe.db.set_value("Account", name, "account_number", "") frappe.db.set_value("Account", name, "account_number", "")
frappe.db.set_value("Account", name, "account_name", account_name.strip()) frappe.db.set_value("Account", name, "account_name", account_name.strip())
if not from_descendant:
# Update and rename in child company accounts as well
descendants = get_descendants_of('Company', account.company)
if descendants:
sync_update_account_number_in_child(descendants, old_acc_name, account_name, account_number, old_acc_number)
new_name = get_account_autoname(account_number, account_name, account.company) new_name = get_account_autoname(account_number, account_name, account.company)
if name != new_name: if name != new_name:
frappe.rename_doc("Account", name, new_name, force=1) frappe.rename_doc("Account", name, new_name, force=1)
@ -330,3 +358,14 @@ def get_root_company(company):
# return the topmost company in the hierarchy # return the topmost company in the hierarchy
ancestors = get_ancestors_of('Company', company, "lft asc") ancestors = get_ancestors_of('Company', company, "lft asc")
return [ancestors[0]] if ancestors else [] return [ancestors[0]] if ancestors else []
def sync_update_account_number_in_child(descendants, old_acc_name, account_name, account_number=None, old_acc_number=None):
filters = {
"company": ["in", descendants],
"account_name": old_acc_name,
}
if old_acc_number:
filters["account_number"] = old_acc_number
for d in frappe.db.get_values('Account', filters=filters, fieldname=["company", "name"], as_dict=True):
update_account_number(d["name"], account_name, account_number, from_descendant=True)

View File

@ -2,7 +2,7 @@ frappe.provide("frappe.treeview_settings")
frappe.treeview_settings["Account"] = { frappe.treeview_settings["Account"] = {
breadcrumb: "Accounts", breadcrumb: "Accounts",
title: __("Chart Of Accounts"), title: __("Chart of Accounts"),
get_tree_root: false, get_tree_root: false,
filters: [ filters: [
{ {
@ -97,7 +97,7 @@ frappe.treeview_settings["Account"] = {
treeview.page.add_inner_button(__("Journal Entry"), function() { treeview.page.add_inner_button(__("Journal Entry"), function() {
frappe.new_doc('Journal Entry', {company: get_company()}); frappe.new_doc('Journal Entry', {company: get_company()});
}, __('Create')); }, __('Create'));
treeview.page.add_inner_button(__("New Company"), function() { treeview.page.add_inner_button(__("Company"), function() {
frappe.new_doc('Company'); frappe.new_doc('Company');
}, __('Create')); }, __('Create'));

View File

@ -225,7 +225,7 @@ def build_tree_from_json(chart_template, chart_data=None):
account['parent_account'] = parent account['parent_account'] = parent
account['expandable'] = True if identify_is_group(child) else False account['expandable'] = True if identify_is_group(child) else False
account['value'] = (child.get('account_number') + ' - ' + account_name) \ account['value'] = (cstr(child.get('account_number')).strip() + ' - ' + account_name) \
if child.get('account_number') else account_name if child.get('account_number') else account_name
accounts.append(account) accounts.append(account)
_import_accounts(child, account['value']) _import_accounts(child, account['value'])

View File

@ -5,8 +5,7 @@ from __future__ import unicode_literals
import unittest import unittest
import frappe import frappe
from erpnext.stock import get_warehouse_account, get_company_default_inventory_account from erpnext.stock import get_warehouse_account, get_company_default_inventory_account
from erpnext.accounts.doctype.account.account import update_account_number from erpnext.accounts.doctype.account.account import update_account_number, merge_account
from erpnext.accounts.doctype.account.account import merge_account
class TestAccount(unittest.TestCase): class TestAccount(unittest.TestCase):
def test_rename_account(self): def test_rename_account(self):
@ -99,7 +98,8 @@ class TestAccount(unittest.TestCase):
"Softwares - _TC", doc.is_group, doc.root_type, doc.company) "Softwares - _TC", doc.is_group, doc.root_type, doc.company)
def test_account_sync(self): def test_account_sync(self):
del frappe.local.flags["ignore_root_company_validation"] frappe.local.flags.pop("ignore_root_company_validation", None)
acc = frappe.new_doc("Account") acc = frappe.new_doc("Account")
acc.account_name = "Test Sync Account" acc.account_name = "Test Sync Account"
acc.parent_account = "Temporary Accounts - _TC3" acc.parent_account = "Temporary Accounts - _TC3"
@ -111,6 +111,55 @@ class TestAccount(unittest.TestCase):
self.assertEqual(acc_tc_4, "Test Sync Account - _TC4") self.assertEqual(acc_tc_4, "Test Sync Account - _TC4")
self.assertEqual(acc_tc_5, "Test Sync Account - _TC5") self.assertEqual(acc_tc_5, "Test Sync Account - _TC5")
def test_account_rename_sync(self):
frappe.local.flags.pop("ignore_root_company_validation", None)
acc = frappe.new_doc("Account")
acc.account_name = "Test Rename Account"
acc.parent_account = "Temporary Accounts - _TC3"
acc.company = "_Test Company 3"
acc.insert()
# Rename account in parent company
update_account_number(acc.name, "Test Rename Sync Account", "1234")
# Check if renamed in children
self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Rename Sync Account", "company": "_Test Company 4", "account_number": "1234"}))
self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Rename Sync Account", "company": "_Test Company 5", "account_number": "1234"}))
frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC3")
frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC4")
frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC5")
def test_child_company_account_rename_sync(self):
frappe.local.flags.pop("ignore_root_company_validation", None)
acc = frappe.new_doc("Account")
acc.account_name = "Test Group Account"
acc.parent_account = "Temporary Accounts - _TC3"
acc.is_group = 1
acc.company = "_Test Company 3"
acc.insert()
self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Group Account", "company": "_Test Company 4"}))
self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Group Account", "company": "_Test Company 5"}))
# Try renaming child company account
acc_tc_5 = frappe.db.get_value('Account', {'account_name': "Test Group Account", "company": "_Test Company 5"})
self.assertRaises(frappe.ValidationError, update_account_number, acc_tc_5, "Test Modified Account")
# Rename child company account with allow_account_creation_against_child_company enabled
frappe.db.set_value("Company", "_Test Company 5", "allow_account_creation_against_child_company", 1)
update_account_number(acc_tc_5, "Test Modified Account")
self.assertTrue(frappe.db.exists("Account", {'name': "Test Modified Account - _TC5", "company": "_Test Company 5"}))
frappe.db.set_value("Company", "_Test Company 5", "allow_account_creation_against_child_company", 0)
to_delete = ["Test Group Account - _TC3", "Test Group Account - _TC4", "Test Modified Account - _TC5"]
for doc in to_delete:
frappe.delete_doc("Account", doc)
def _make_test_records(verbose): def _make_test_records(verbose):
from frappe.test_runner import make_test_objects from frappe.test_runner import make_test_objects

View File

@ -7,7 +7,7 @@ frappe.ui.form.on('Accounting Dimension', {
frm.set_query('document_type', () => { frm.set_query('document_type', () => {
let invalid_doctypes = frappe.model.core_doctypes_list; let invalid_doctypes = frappe.model.core_doctypes_list;
invalid_doctypes.push('Accounting Dimension', 'Project', invalid_doctypes.push('Accounting Dimension', 'Project',
'Cost Center', 'Accounting Dimension Detail'); 'Cost Center', 'Accounting Dimension Detail', 'Company');
return { return {
filters: { filters: {

View File

@ -19,7 +19,7 @@ class AccountingDimension(Document):
def validate(self): def validate(self):
if self.document_type in core_doctypes_list + ('Accounting Dimension', 'Project', if self.document_type in core_doctypes_list + ('Accounting Dimension', 'Project',
'Cost Center', 'Accounting Dimension Detail') : 'Cost Center', 'Accounting Dimension Detail', 'Company') :
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type) msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
frappe.throw(msg) frappe.throw(msg)

View File

@ -40,7 +40,7 @@
"fields": [ "fields": [
{ {
"default": "1", "default": "1",
"description": "If enabled, the system will post accounting entries for inventory automatically.", "description": "If enabled, the system will post accounting entries for inventory automatically",
"fieldname": "auto_accounting_for_stock", "fieldname": "auto_accounting_for_stock",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 1, "hidden": 1,
@ -48,23 +48,23 @@
"label": "Make Accounting Entry For Every Stock Movement" "label": "Make Accounting Entry For Every Stock Movement"
}, },
{ {
"description": "Accounting entry frozen up to this date, nobody can do / modify entry except role specified below.", "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", "fieldname": "acc_frozen_upto",
"fieldtype": "Date", "fieldtype": "Date",
"in_list_view": 1, "in_list_view": 1,
"label": "Accounts Frozen Upto" "label": "Accounts Frozen Till Date"
}, },
{ {
"description": "Users with this role are allowed to set frozen accounts and create / modify accounting entries against frozen accounts", "description": "Users with this role are allowed to set frozen accounts and create / modify accounting entries against frozen accounts",
"fieldname": "frozen_accounts_modifier", "fieldname": "frozen_accounts_modifier",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Role Allowed to Set Frozen Accounts & Edit Frozen Entries", "label": "Role Allowed to Set Frozen Accounts and Edit Frozen Entries",
"options": "Role" "options": "Role"
}, },
{ {
"default": "Billing Address", "default": "Billing Address",
"description": "Address used to determine Tax Category in transactions.", "description": "Address used to determine Tax Category in transactions",
"fieldname": "determine_address_tax_category_from", "fieldname": "determine_address_tax_category_from",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Determine Address Tax Category From", "label": "Determine Address Tax Category From",
@ -75,7 +75,7 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"description": "Role that is allowed to submit transactions that exceed credit limits set.", "description": "This role is allowed to submit transactions that exceed credit limits",
"fieldname": "credit_controller", "fieldname": "credit_controller",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
@ -104,7 +104,7 @@
"default": "1", "default": "1",
"fieldname": "unlink_advance_payment_on_cancelation_of_order", "fieldname": "unlink_advance_payment_on_cancelation_of_order",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Unlink Advance Payment on Cancelation of Order" "label": "Unlink Advance Payment on Cancellation of Order"
}, },
{ {
"default": "1", "default": "1",
@ -127,7 +127,7 @@
"default": "0", "default": "0",
"fieldname": "show_inclusive_tax_in_print", "fieldname": "show_inclusive_tax_in_print",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Show Inclusive Tax In Print" "label": "Show Inclusive Tax in Print"
}, },
{ {
"fieldname": "column_break_12", "fieldname": "column_break_12",
@ -165,7 +165,7 @@
}, },
{ {
"default": "0", "default": "0",
"description": "Only select if you have setup Cash Flow Mapper documents", "description": "Only select this if you have set up the Cash Flow Mapper documents",
"fieldname": "use_custom_cash_flow", "fieldname": "use_custom_cash_flow",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Use Custom Cash Flow Format" "label": "Use Custom Cash Flow Format"
@ -177,7 +177,7 @@
"label": "Automatically Fetch Payment Terms" "label": "Automatically Fetch Payment Terms"
}, },
{ {
"description": "Percentage you are allowed to bill more against the amount ordered. For example: If the order value is $100 for an item and tolerance is set as 10% then you are allowed to bill for $110.", "description": "The percentage you are allowed to bill more against the amount ordered. For example, if the order value is $100 for an item and tolerance is set as 10%, then you are allowed to bill up to $110 ",
"fieldname": "over_billing_allowance", "fieldname": "over_billing_allowance",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Over Billing Allowance (%)" "label": "Over Billing Allowance (%)"
@ -199,7 +199,7 @@
}, },
{ {
"default": "0", "default": "0",
"description": "If this is unchecked direct GL Entries will be created to book Deferred Revenue/Expense", "description": "If this is unchecked, direct GL entries will be created to book deferred revenue or expense",
"fieldname": "book_deferred_entries_via_journal_entry", "fieldname": "book_deferred_entries_via_journal_entry",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Book Deferred Entries Via Journal Entry" "label": "Book Deferred Entries Via Journal Entry"
@ -214,7 +214,7 @@
}, },
{ {
"default": "Days", "default": "Days",
"description": "If \"Months\" is selected then fixed amount will be booked as deferred revenue or expense for each month irrespective of number of days in a month. Will be prorated if deferred revenue or expense is not booked for an entire month.", "description": "If \"Months\" is selected, a fixed amount will be booked as deferred revenue or expense for each month irrespective of the number of days in a month. It will be prorated if deferred revenue or expense is not booked for an entire month",
"fieldname": "book_deferred_entries_based_on", "fieldname": "book_deferred_entries_based_on",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Book Deferred Entries Based On", "label": "Book Deferred Entries Based On",
@ -223,9 +223,10 @@
], ],
"icon": "icon-cog", "icon": "icon-cog",
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2020-08-03 20:13:26.043092", "modified": "2020-10-13 11:32:52.268826",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@ -27,4 +27,4 @@ def get_vouchar_detials(column_list, doctype, docname):
for col in column_list: for col in column_list:
sanitize_searchfield(col) sanitize_searchfield(col)
return frappe.db.sql(''' select {columns} from `tab{doctype}` where name=%s''' return frappe.db.sql(''' select {columns} from `tab{doctype}` where name=%s'''
.format(columns=", ".join(json.loads(column_list)), doctype=doctype), docname, as_dict=1)[0] .format(columns=", ".join(column_list), doctype=doctype), docname, as_dict=1)[0]

View File

@ -55,7 +55,7 @@ class BankStatementTransactionEntry(Document):
def populate_payment_entries(self): def populate_payment_entries(self):
if self.bank_statement is None: return if self.bank_statement is None: return
filename = self.bank_statement.split("/")[-1] file_url = self.bank_statement
if (len(self.new_transaction_items + self.reconciled_transaction_items) > 0): if (len(self.new_transaction_items + self.reconciled_transaction_items) > 0):
frappe.throw(_("Transactions already retreived from the statement")) frappe.throw(_("Transactions already retreived from the statement"))
@ -65,7 +65,7 @@ class BankStatementTransactionEntry(Document):
if self.bank_settings: if self.bank_settings:
mapped_items = frappe.get_doc("Bank Statement Settings", self.bank_settings).mapped_items mapped_items = frappe.get_doc("Bank Statement Settings", self.bank_settings).mapped_items
statement_headers = self.get_statement_headers() statement_headers = self.get_statement_headers()
transactions = get_transaction_entries(filename, statement_headers) transactions = get_transaction_entries(file_url, statement_headers)
for entry in transactions: for entry in transactions:
date = entry[statement_headers["Date"]].strip() date = entry[statement_headers["Date"]].strip()
#print("Processing entry DESC:{0}-W:{1}-D:{2}-DT:{3}".format(entry["Particulars"], entry["Withdrawals"], entry["Deposits"], entry["Date"])) #print("Processing entry DESC:{0}-W:{1}-D:{2}-DT:{3}".format(entry["Particulars"], entry["Withdrawals"], entry["Deposits"], entry["Date"]))
@ -398,20 +398,21 @@ def get_transaction_info(headers, header_index, row):
transaction[header] = "" transaction[header] = ""
return transaction return transaction
def get_transaction_entries(filename, headers): def get_transaction_entries(file_url, headers):
header_index = {} header_index = {}
rows, transactions = [], [] rows, transactions = [], []
if (filename.lower().endswith("xlsx")): if (file_url.lower().endswith("xlsx")):
from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file
rows = read_xlsx_file_from_attached_file(file_id=filename) rows = read_xlsx_file_from_attached_file(file_url=file_url)
elif (filename.lower().endswith("csv")): elif (file_url.lower().endswith("csv")):
from frappe.utils.csvutils import read_csv_content from frappe.utils.csvutils import read_csv_content
_file = frappe.get_doc("File", {"file_name": filename}) _file = frappe.get_doc("File", {"file_url": file_url})
filepath = _file.get_full_path() filepath = _file.get_full_path()
with open(filepath,'rb') as csvfile: with open(filepath,'rb') as csvfile:
rows = read_csv_content(csvfile.read()) rows = read_csv_content(csvfile.read())
elif (filename.lower().endswith("xls")): elif (file_url.lower().endswith("xls")):
filename = file_url.split("/")[-1]
rows = get_rows_from_xls_file(filename) rows = get_rows_from_xls_file(filename)
else: else:
frappe.throw(_("Only .csv and .xlsx files are supported currently")) frappe.throw(_("Only .csv and .xlsx files are supported currently"))

View File

@ -91,15 +91,11 @@ class TestBankTransaction(unittest.TestCase):
self.assertEqual(frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount"), 0) self.assertEqual(frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount"), 0)
self.assertTrue(frappe.db.get_value("Sales Invoice Payment", dict(parent=payment.name), "clearance_date") is not None) self.assertTrue(frappe.db.get_value("Sales Invoice Payment", dict(parent=payment.name), "clearance_date") is not None)
def add_transactions(): def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
if frappe.flags.test_bank_transactions_created:
return
frappe.set_user("Administrator")
try: try:
frappe.get_doc({ frappe.get_doc({
"doctype": "Bank", "doctype": "Bank",
"bank_name":"Citi Bank", "bank_name":bank_name,
}).insert() }).insert()
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
@ -108,12 +104,19 @@ def add_transactions():
frappe.get_doc({ frappe.get_doc({
"doctype": "Bank Account", "doctype": "Bank Account",
"account_name":"Checking Account", "account_name":"Checking Account",
"bank": "Citi Bank", "bank": bank_name,
"account": "_Test Bank - _TC" "account": account_name
}).insert() }).insert()
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
def add_transactions():
if frappe.flags.test_bank_transactions_created:
return
frappe.set_user("Administrator")
create_bank_account()
doc = frappe.get_doc({ doc = frappe.get_doc({
"doctype": "Bank Transaction", "doctype": "Bank Transaction",
"description":"1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G", "description":"1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G",

View File

@ -1,785 +1,204 @@
{ {
"allow_copy": 0, "actions": [],
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 1, "allow_import": 1,
"allow_rename": 0,
"beta": 0,
"creation": "2016-05-16 11:42:29.632528", "creation": "2016-05-16 11:42:29.632528",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"budget_against",
"company",
"cost_center",
"project",
"fiscal_year",
"column_break_3",
"monthly_distribution",
"amended_from",
"section_break_6",
"applicable_on_material_request",
"action_if_annual_budget_exceeded_on_mr",
"action_if_accumulated_monthly_budget_exceeded_on_mr",
"column_break_13",
"applicable_on_purchase_order",
"action_if_annual_budget_exceeded_on_po",
"action_if_accumulated_monthly_budget_exceeded_on_po",
"section_break_16",
"applicable_on_booking_actual_expenses",
"action_if_annual_budget_exceeded",
"action_if_accumulated_monthly_budget_exceeded",
"section_break_21",
"accounts"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Cost Center", "default": "Cost Center",
"fetch_if_empty": 0,
"fieldname": "budget_against", "fieldname": "budget_against",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Budget Against", "label": "Budget Against",
"length": 0,
"no_copy": 0,
"options": "\nCost Center\nProject", "options": "\nCost Center\nProject",
"permlevel": 0, "reqd": 1
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Company", "label": "Company",
"length": 0,
"no_copy": 0,
"options": "Company", "options": "Company",
"permlevel": 0, "reqd": 1
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.budget_against == 'Cost Center'", "depends_on": "eval:doc.budget_against == 'Cost Center'",
"fetch_if_empty": 0,
"fieldname": "cost_center", "fieldname": "cost_center",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1, "in_global_search": 1,
"in_list_view": 0,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Cost Center", "label": "Cost Center",
"length": 0, "options": "Cost Center"
"no_copy": 0,
"options": "Cost Center",
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.budget_against == 'Project'", "depends_on": "eval:doc.budget_against == 'Project'",
"fetch_if_empty": 0,
"fieldname": "project", "fieldname": "project",
"fieldtype": "Link", "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": 1, "in_standard_filter": 1,
"label": "Project", "label": "Project",
"length": 0, "options": "Project"
"no_copy": 0,
"options": "Project",
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "fiscal_year", "fieldname": "fiscal_year",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Fiscal Year", "label": "Fiscal Year",
"length": 0,
"no_copy": 0,
"options": "Fiscal Year", "options": "Fiscal Year",
"permlevel": 0, "reqd": 1
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break", "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,
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:in_list([\"Stop\", \"Warn\"], doc.action_if_accumulated_monthly_budget_exceeded_on_po || doc.action_if_accumulated_monthly_budget_exceeded_on_mr || doc.action_if_accumulated_monthly_budget_exceeded_on_actual)", "depends_on": "eval:in_list([\"Stop\", \"Warn\"], doc.action_if_accumulated_monthly_budget_exceeded_on_po || doc.action_if_accumulated_monthly_budget_exceeded_on_mr || doc.action_if_accumulated_monthly_budget_exceeded_on_actual)",
"fetch_if_empty": 0,
"fieldname": "monthly_distribution", "fieldname": "monthly_distribution",
"fieldtype": "Link", "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": "Monthly Distribution", "label": "Monthly Distribution",
"length": 0, "options": "Monthly Distribution"
"no_copy": 0,
"options": "Monthly Distribution",
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "amended_from", "fieldname": "amended_from",
"fieldtype": "Link", "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": "Amended From", "label": "Amended From",
"length": 0,
"no_copy": 1, "no_copy": 1,
"options": "Budget", "options": "Budget",
"permlevel": 0,
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0, "read_only": 1
"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_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "section_break_6", "fieldname": "section_break_6",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Control Action"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Control Action",
"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
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "applicable_on_material_request", "fieldname": "applicable_on_material_request",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Applicable on Material Request"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Applicable on Material Request",
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 1, "allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Stop", "default": "Stop",
"depends_on": "eval:doc.applicable_on_material_request == 1", "depends_on": "eval:doc.applicable_on_material_request == 1",
"fetch_if_empty": 0,
"fieldname": "action_if_annual_budget_exceeded_on_mr", "fieldname": "action_if_annual_budget_exceeded_on_mr",
"fieldtype": "Select", "fieldtype": "Select",
"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": "Action if Annual Budget Exceeded on MR", "label": "Action if Annual Budget Exceeded on MR",
"length": 0, "options": "\nStop\nWarn\nIgnore"
"no_copy": 0,
"options": "\nStop\nWarn\nIgnore",
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 1, "allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Warn", "default": "Warn",
"depends_on": "eval:doc.applicable_on_material_request == 1", "depends_on": "eval:doc.applicable_on_material_request == 1",
"fetch_if_empty": 0,
"fieldname": "action_if_accumulated_monthly_budget_exceeded_on_mr", "fieldname": "action_if_accumulated_monthly_budget_exceeded_on_mr",
"fieldtype": "Select", "fieldtype": "Select",
"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": "Action if Accumulated Monthly Budget Exceeded on MR", "label": "Action if Accumulated Monthly Budget Exceeded on MR",
"length": 0, "options": "\nStop\nWarn\nIgnore"
"no_copy": 0,
"options": "\nStop\nWarn\nIgnore",
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_13", "fieldname": "column_break_13",
"fieldtype": "Column Break", "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,
"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
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "applicable_on_purchase_order", "fieldname": "applicable_on_purchase_order",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Applicable on Purchase Order"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Applicable on Purchase Order",
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 1, "allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Stop", "default": "Stop",
"depends_on": "eval:doc.applicable_on_purchase_order == 1", "depends_on": "eval:doc.applicable_on_purchase_order == 1",
"fetch_if_empty": 0,
"fieldname": "action_if_annual_budget_exceeded_on_po", "fieldname": "action_if_annual_budget_exceeded_on_po",
"fieldtype": "Select", "fieldtype": "Select",
"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": "Action if Annual Budget Exceeded on PO", "label": "Action if Annual Budget Exceeded on PO",
"length": 0, "options": "\nStop\nWarn\nIgnore"
"no_copy": 0,
"options": "\nStop\nWarn\nIgnore",
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 1, "allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Warn", "default": "Warn",
"depends_on": "eval:doc.applicable_on_purchase_order == 1", "depends_on": "eval:doc.applicable_on_purchase_order == 1",
"fetch_if_empty": 0,
"fieldname": "action_if_accumulated_monthly_budget_exceeded_on_po", "fieldname": "action_if_accumulated_monthly_budget_exceeded_on_po",
"fieldtype": "Select", "fieldtype": "Select",
"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": "Action if Accumulated Monthly Budget Exceeded on PO", "label": "Action if Accumulated Monthly Budget Exceeded on PO",
"length": 0, "options": "\nStop\nWarn\nIgnore"
"no_copy": 0,
"options": "\nStop\nWarn\nIgnore",
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "section_break_16", "fieldname": "section_break_16",
"fieldtype": "Section Break", "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,
"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
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "applicable_on_booking_actual_expenses", "fieldname": "applicable_on_booking_actual_expenses",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Applicable on booking actual expenses"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Applicable on booking actual expenses",
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 1, "allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Stop", "default": "Stop",
"depends_on": "eval:doc.applicable_on_booking_actual_expenses == 1", "depends_on": "eval:doc.applicable_on_booking_actual_expenses == 1",
"fetch_if_empty": 0,
"fieldname": "action_if_annual_budget_exceeded", "fieldname": "action_if_annual_budget_exceeded",
"fieldtype": "Select", "fieldtype": "Select",
"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": "Action if Annual Budget Exceeded on Actual", "label": "Action if Annual Budget Exceeded on Actual",
"length": 0, "options": "\nStop\nWarn\nIgnore"
"no_copy": 0,
"options": "\nStop\nWarn\nIgnore",
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 1, "allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Warn", "default": "Warn",
"depends_on": "eval:doc.applicable_on_booking_actual_expenses == 1", "depends_on": "eval:doc.applicable_on_booking_actual_expenses == 1",
"fetch_if_empty": 0,
"fieldname": "action_if_accumulated_monthly_budget_exceeded", "fieldname": "action_if_accumulated_monthly_budget_exceeded",
"fieldtype": "Select", "fieldtype": "Select",
"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": "Action if Accumulated Monthly Budget Exceeded on Actual", "label": "Action if Accumulated Monthly Budget Exceeded on Actual",
"length": 0, "options": "\nStop\nWarn\nIgnore"
"no_copy": 0,
"options": "\nStop\nWarn\nIgnore",
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "section_break_21", "fieldname": "section_break_21",
"fieldtype": "Section Break", "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,
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fetch_if_empty": 0,
"fieldname": "accounts", "fieldname": "accounts",
"fieldtype": "Table", "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": "Budget Accounts", "label": "Budget Accounts",
"length": 0,
"no_copy": 0,
"options": "Budget Account", "options": "Budget Account",
"permlevel": 0, "reqd": 1
"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
} }
], ],
"has_web_view": 0, "index_web_pages_for_search": 1,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 1, "is_submittable": 1,
"issingle": 0, "links": [],
"istable": 0, "modified": "2020-10-06 15:13:54.055854",
"max_attachments": 0,
"modified": "2019-03-22 12:06:02.323099",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Budget", "name": "Budget",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@ -789,26 +208,17 @@
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 1, "import": 1,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Accounts Manager", "role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 1, "submit": 1,
"write": 1 "write": 1
} }
], ],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1, "track_changes": 1
"track_seen": 0,
"track_views": 0
} }

View File

@ -158,8 +158,11 @@ class TestBudget(unittest.TestCase):
set_total_expense_zero(nowdate(), "cost_center") set_total_expense_zero(nowdate(), "cost_center")
budget = make_budget(budget_against="Cost Center") budget = make_budget(budget_against="Cost Center")
month = now_datetime().month
if month > 10:
month = 10
for i in range(now_datetime().month): for i in range(month):
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True) "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True)
@ -177,8 +180,11 @@ class TestBudget(unittest.TestCase):
set_total_expense_zero(nowdate(), "project") set_total_expense_zero(nowdate(), "project")
budget = make_budget(budget_against="Project") budget = make_budget(budget_against="Project")
month = now_datetime().month
if month > 10:
month = 10
for i in range(now_datetime().month): for i in range(month):
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True, project="_Test Project") "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True, project="_Test Project")

View File

@ -29,7 +29,7 @@ class CashierClosing(Document):
for i in self.payments: for i in self.payments:
total += flt(i.amount) total += flt(i.amount)
self.net_amount = total + self.outstanding_amount + self.expense - self.custody + self.returns self.net_amount = total + self.outstanding_amount + flt(self.expense) - flt(self.custody) + flt(self.returns)
def validate_time(self): def validate_time(self):
if self.from_time >= self.time: if self.from_time >= self.time:

View File

@ -135,7 +135,7 @@ var create_import_button = function(frm) {
callback: function(r) { callback: function(r) {
if(!r.exc) { if(!r.exc) {
clearInterval(frm.page["interval"]); clearInterval(frm.page["interval"]);
frm.page.set_indicator(__('Import Successfull'), 'blue'); frm.page.set_indicator(__('Import Successful'), 'blue');
create_reset_button(frm); create_reset_button(frm);
} }
} }

View File

@ -195,7 +195,7 @@ def build_response_as_excel(writer):
reader = csv.reader(f) reader = csv.reader(f)
from frappe.utils.xlsxutils import make_xlsx from frappe.utils.xlsxutils import make_xlsx
xlsx_file = make_xlsx(reader, "Chart Of Accounts Importer Template") xlsx_file = make_xlsx(reader, "Chart of Accounts Importer Template")
f.close() f.close()
os.remove(filename) os.remove(filename)

View File

@ -9,6 +9,8 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde
from erpnext.stock.get_item_details import get_item_details from erpnext.stock.get_item_details import get_item_details
from frappe.test_runner import make_test_objects from frappe.test_runner import make_test_objects
test_dependencies = ['Item']
def test_create_test_data(): def test_create_test_data():
frappe.set_user("Administrator") frappe.set_user("Administrator")
# create test item # create test item
@ -95,7 +97,6 @@ def test_create_test_data():
}) })
coupon_code.insert() coupon_code.insert()
class TestCouponCode(unittest.TestCase): class TestCouponCode(unittest.TestCase):
def setUp(self): def setUp(self):
test_create_test_data() test_create_test_data()

View File

@ -93,6 +93,7 @@ def resolve_dunning(doc, state):
def calculate_interest_and_amount(posting_date, outstanding_amount, rate_of_interest, dunning_fee, overdue_days): def calculate_interest_and_amount(posting_date, outstanding_amount, rate_of_interest, dunning_fee, overdue_days):
interest_amount = 0 interest_amount = 0
grand_total = 0
if rate_of_interest: if rate_of_interest:
interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100 interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100
interest_amount = (interest_per_year * cint(overdue_days)) / 365 interest_amount = (interest_per_year * cint(overdue_days)) / 365

View File

@ -13,7 +13,7 @@ def get_data():
}, },
{ {
'label': _('References'), 'label': _('References'),
'items': ['Period Closing Voucher', 'Request for Quotation', 'Tax Withholding Category'] 'items': ['Period Closing Voucher', 'Tax Withholding Category']
}, },
{ {
'label': _('Target Details'), 'label': _('Target Details'),

View File

@ -38,8 +38,8 @@
"reqd": 1 "reqd": 1
} }
], ],
"modified": "2020-06-18 20:27:42.615842", "modified": "2020-09-18 17:26:09.703215",
"modified_by": "ahmad@havenir.com", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Item Tax Template", "name": "Item Tax Template",
"owner": "Administrator", "owner": "Administrator",

View File

@ -210,7 +210,7 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({
$.each(this.frm.doc.accounts || [], function(i, jvd) { $.each(this.frm.doc.accounts || [], function(i, jvd) {
frappe.model.set_default_values(jvd); frappe.model.set_default_values(jvd);
}); });
var posting_date = this.frm.posting_date; var posting_date = this.frm.doc.posting_date;
if(!this.frm.doc.amended_from) this.frm.set_value('posting_date', posting_date || frappe.datetime.get_today()); if(!this.frm.doc.amended_from) this.frm.set_value('posting_date', posting_date || frappe.datetime.get_today());
} }
}, },
@ -638,20 +638,12 @@ $.extend(erpnext.journal_entry, {
return { filters: filters }; return { filters: filters };
}, },
reverse_journal_entry: function(frm) { reverse_journal_entry: function() {
var me = frm.doc; frappe.model.open_mapped_doc({
for(var i=0; i<me.accounts.length; i++) { method: "erpnext.accounts.doctype.journal_entry.journal_entry.make_reverse_journal_entry",
me.accounts[i].credit += me.accounts[i].debit; frm: cur_frm
me.accounts[i].debit = me.accounts[i].credit - me.accounts[i].debit; })
me.accounts[i].credit -= me.accounts[i].debit; },
me.accounts[i].credit_in_account_currency = me.accounts[i].credit;
me.accounts[i].debit_in_account_currency = me.accounts[i].debit;
me.accounts[i].reference_type = "Journal Entry";
me.accounts[i].reference_name = me.name
}
frm.copy_doc();
cur_frm.reload_doc();
}
}); });
$.extend(erpnext.journal_entry, { $.extend(erpnext.journal_entry, {

View File

@ -22,8 +22,12 @@ class JournalEntry(AccountsController):
return self.voucher_type return self.voucher_type
def validate(self): def validate(self):
if self.voucher_type == 'Opening Entry':
self.is_opening = 'Yes'
if not self.is_opening: if not self.is_opening:
self.is_opening='No' self.is_opening='No'
self.clearance_date = None self.clearance_date = None
self.validate_party() self.validate_party()
@ -1021,3 +1025,34 @@ def make_inter_company_journal_entry(name, voucher_type, company):
journal_entry.posting_date = nowdate() journal_entry.posting_date = nowdate()
journal_entry.inter_company_journal_entry_reference = name journal_entry.inter_company_journal_entry_reference = name
return journal_entry.as_dict() return journal_entry.as_dict()
@frappe.whitelist()
def make_reverse_journal_entry(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc
def update_accounts(source, target, source_parent):
target.reference_type = "Journal Entry"
target.reference_name = source_parent.name
doclist = get_mapped_doc("Journal Entry", source_name, {
"Journal Entry": {
"doctype": "Journal Entry",
"validation": {
"docstatus": ["=", 1]
}
},
"Journal Entry Account": {
"doctype": "Journal Entry Account",
"field_map": {
"account_currency": "account_currency",
"exchange_rate": "exchange_rate",
"debit_in_account_currency": "credit_in_account_currency",
"debit": "credit",
"credit_in_account_currency": "debit_in_account_currency",
"credit": "debit",
},
"postprocess": update_accounts,
},
}, target_doc)
return doclist

View File

@ -167,6 +167,49 @@ class TestJournalEntry(unittest.TestCase):
self.assertFalse(gle) self.assertFalse(gle)
def test_reverse_journal_entry(self):
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
jv = make_journal_entry("_Test Bank USD - _TC",
"Sales - _TC", 100, exchange_rate=50, save=False)
jv.get("accounts")[1].credit_in_account_currency = 5000
jv.get("accounts")[1].exchange_rate = 1
jv.submit()
rjv = make_reverse_journal_entry(jv.name)
rjv.posting_date = nowdate()
rjv.submit()
gl_entries = frappe.db.sql("""select account, account_currency, debit, credit,
debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s
order by account asc""", rjv.name, as_dict=1)
self.assertTrue(gl_entries)
expected_values = {
"_Test Bank USD - _TC": {
"account_currency": "USD",
"debit": 0,
"debit_in_account_currency": 0,
"credit": 5000,
"credit_in_account_currency": 100,
},
"Sales - _TC": {
"account_currency": "INR",
"debit": 5000,
"debit_in_account_currency": 5000,
"credit": 0,
"credit_in_account_currency": 0,
}
}
for field in ("account_currency", "debit", "debit_in_account_currency", "credit", "credit_in_account_currency"):
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account][field], gle[field])
def test_disallow_change_in_account_currency_for_a_party(self): def test_disallow_change_in_account_currency_for_a_party(self):
# create jv in USD # create jv in USD
jv = make_journal_entry("_Test Bank USD - _TC", jv = make_journal_entry("_Test Bank USD - _TC",

View File

@ -195,9 +195,7 @@ def create_sales_invoice_record(qty=1):
def create_records(): def create_records():
# create a new loyalty Account # create a new loyalty Account
if frappe.db.exists("Account", "Loyalty - _TC"): if not frappe.db.exists("Account", "Loyalty - _TC"):
return
frappe.get_doc({ frappe.get_doc({
"doctype": "Account", "doctype": "Account",
"account_name": "Loyalty", "account_name": "Loyalty",
@ -208,6 +206,7 @@ def create_records():
}).insert() }).insert()
# create a new loyalty program Single tier # create a new loyalty program Single tier
if not frappe.db.exists("Loyalty Program","Test Single Loyalty"):
frappe.get_doc({ frappe.get_doc({
"doctype": "Loyalty Program", "doctype": "Loyalty Program",
"loyalty_program_name": "Test Single Loyalty", "loyalty_program_name": "Test Single Loyalty",
@ -227,6 +226,7 @@ def create_records():
}).insert() }).insert()
# create a new customer # create a new customer
if not frappe.db.exists("Customer","Test Loyalty Customer"):
frappe.get_doc({ frappe.get_doc({
"customer_group": "_Test Customer Group", "customer_group": "_Test Customer Group",
"customer_name": "Test Loyalty Customer", "customer_name": "Test Loyalty Customer",
@ -236,6 +236,7 @@ def create_records():
}).insert() }).insert()
# create a new loyalty program Multiple tier # create a new loyalty program Multiple tier
if not frappe.db.exists("Loyalty Program","Test Multiple Loyalty"):
frappe.get_doc({ frappe.get_doc({
"doctype": "Loyalty Program", "doctype": "Loyalty Program",
"loyalty_program_name": "Test Multiple Loyalty", "loyalty_program_name": "Test Multiple Loyalty",
@ -262,7 +263,8 @@ def create_records():
}).insert() }).insert()
# create an item # create an item
item = frappe.get_doc({ if not frappe.db.exists("Item", "Loyal Item"):
frappe.get_doc({
"doctype": "Item", "doctype": "Item",
"item_code": "Loyal Item", "item_code": "Loyal Item",
"item_name": "Loyal Item", "item_name": "Loyal Item",
@ -274,9 +276,10 @@ def create_records():
}).insert() }).insert()
# create item price # create item price
if not frappe.db.exists("Item Price", {"price_list": "Standard Selling", "item_code": "Loyal Item"}):
frappe.get_doc({ frappe.get_doc({
"doctype": "Item Price", "doctype": "Item Price",
"price_list": "Standard Selling", "price_list": "Standard Selling",
"item_code": item.item_code, "item_code": "Loyal Item",
"price_list_rate": 10000 "price_list_rate": 10000
}).insert() }).insert()

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
"autoname": "field:mode_of_payment", "autoname": "field:mode_of_payment",
@ -28,7 +29,7 @@
"fieldtype": "Select", "fieldtype": "Select",
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Type", "label": "Type",
"options": "Cash\nBank\nGeneral" "options": "Cash\nBank\nGeneral\nPhone"
}, },
{ {
"fieldname": "accounts", "fieldname": "accounts",
@ -45,11 +46,13 @@
], ],
"icon": "fa fa-credit-card", "icon": "fa fa-credit-card",
"idx": 1, "idx": 1,
"modified": "2019-08-14 14:58:42.079115", "index_web_pages_for_search": 1,
"modified_by": "sammish.thundiyil@gmail.com", "links": [],
"modified": "2020-09-18 17:57:23.835236",
"modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Mode of Payment", "name": "Mode of Payment",
"owner": "harshada@webnotestech.com", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"create": 1, "create": 1,

View File

@ -14,29 +14,46 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
if (frm.doc.company) { if (frm.doc.company) {
frm.trigger('setup_company_filters'); frm.trigger('setup_company_filters');
} }
frappe.realtime.on('opening_invoice_creation_progress', data => {
if (!frm.doc.import_in_progress) {
frm.dashboard.reset();
frm.doc.import_in_progress = true;
}
if (data.user != frappe.session.user) return;
if (data.count == data.total) {
setTimeout((title) => {
frm.doc.import_in_progress = false;
frm.clear_table("invoices");
frm.refresh_fields();
frm.page.clear_indicator();
frm.dashboard.hide_progress(title);
frappe.msgprint(__("Opening {0} Invoice created", [frm.doc.invoice_type]));
}, 1500, data.title);
return;
}
frm.dashboard.show_progress(data.title, (data.count / data.total) * 100, data.message);
frm.page.set_indicator(__('In Progress'), 'orange');
});
}, },
refresh: function(frm) { refresh: function(frm) {
frm.disable_save(); frm.disable_save();
frm.trigger("make_dashboard"); !frm.doc.import_in_progress && frm.trigger("make_dashboard");
frm.page.set_primary_action(__('Create Invoices'), () => { frm.page.set_primary_action(__('Create Invoices'), () => {
let btn_primary = frm.page.btn_primary.get(0); let btn_primary = frm.page.btn_primary.get(0);
return frm.call({ return frm.call({
doc: frm.doc, doc: frm.doc,
freeze: true,
btn: $(btn_primary), btn: $(btn_primary),
method: "make_invoices", method: "make_invoices",
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type]), freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type])
callback: (r) => {
if(!r.exc){
frappe.msgprint(__("Opening {0} Invoice created", [frm.doc.invoice_type]));
frm.clear_table("invoices");
frm.refresh_fields();
frm.reload_doc();
}
}
}); });
}); });
if (frm.doc.create_missing_party) {
frm.set_df_property("party", "fieldtype", "Data", frm.doc.name, "invoices");
}
}, },
setup_company_filters: function(frm) { setup_company_filters: function(frm) {

View File

@ -4,9 +4,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
import traceback
from json import dumps
from frappe import _, scrub from frappe import _, scrub
from frappe.utils import flt, nowdate from frappe.utils import flt, nowdate
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils.background_jobs import enqueue
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
@ -62,66 +65,47 @@ class OpeningInvoiceCreationTool(Document):
return invoices_summary, max_count return invoices_summary, max_count
def make_invoices(self): def validate_company(self):
names = []
mandatory_error_msg = _("Row {0}: {1} is required to create the Opening {2} Invoices")
if not self.company: if not self.company:
frappe.throw(_("Please select the Company")) frappe.throw(_("Please select the Company"))
company_details = frappe.get_cached_value('Company', self.company, def set_missing_values(self, row):
["default_currency", "default_letter_head"], as_dict=1) or {} row.qty = row.qty or 1.0
row.temporary_opening_account = row.temporary_opening_account or get_temporary_opening_account(self.company)
for row in self.invoices:
if not row.qty:
row.qty = 1.0
# always mandatory fields for the invoices
if not row.temporary_opening_account:
row.temporary_opening_account = get_temporary_opening_account(self.company)
row.party_type = "Customer" if self.invoice_type == "Sales" else "Supplier" row.party_type = "Customer" if self.invoice_type == "Sales" else "Supplier"
row.item_name = row.item_name or _("Opening Invoice Item")
row.posting_date = row.posting_date or nowdate()
row.due_date = row.due_date or nowdate()
# Allow to create invoice even if no party present in customer or supplier. def validate_mandatory_invoice_fields(self, row):
if not frappe.db.exists(row.party_type, row.party): if not frappe.db.exists(row.party_type, row.party):
if self.create_missing_party: if self.create_missing_party:
self.add_party(row.party_type, row.party) self.add_party(row.party_type, row.party)
else: else:
frappe.throw(_("{0} {1} does not exist.").format(frappe.bold(row.party_type), frappe.bold(row.party))) frappe.throw(_("Row #{}: {} {} does not exist.").format(row.idx, frappe.bold(row.party_type), frappe.bold(row.party)))
if not row.item_name:
row.item_name = _("Opening Invoice Item")
if not row.posting_date:
row.posting_date = nowdate()
if not row.due_date:
row.due_date = nowdate()
mandatory_error_msg = _("Row #{0}: {1} is required to create the Opening {2} Invoices")
for d in ("Party", "Outstanding Amount", "Temporary Opening Account"): for d in ("Party", "Outstanding Amount", "Temporary Opening Account"):
if not row.get(scrub(d)): if not row.get(scrub(d)):
frappe.throw(mandatory_error_msg.format(row.idx, _(d), self.invoice_type)) frappe.throw(mandatory_error_msg.format(row.idx, d, self.invoice_type))
args = self.get_invoice_dict(row=row) def get_invoices(self):
if not args: invoices = []
for row in self.invoices:
if not row:
continue continue
self.set_missing_values(row)
self.validate_mandatory_invoice_fields(row)
invoice = self.get_invoice_dict(row)
company_details = frappe.get_cached_value('Company', self.company, ["default_currency", "default_letter_head"], as_dict=1) or {}
if company_details: if company_details:
args.update({ invoice.update({
"currency": company_details.get("default_currency"), "currency": company_details.get("default_currency"),
"letter_head": company_details.get("default_letter_head") "letter_head": company_details.get("default_letter_head")
}) })
invoices.append(invoice)
doc = frappe.get_doc(args).insert() return invoices
doc.submit()
names.append(doc.name)
if len(self.invoices) > 5:
frappe.publish_realtime(
"progress", dict(
progress=[row.idx, len(self.invoices)],
title=_('Creating {0}').format(doc.doctype)
),
user=frappe.session.user
)
return names
def add_party(self, party_type, party): def add_party(self, party_type, party):
party_doc = frappe.new_doc(party_type) party_doc = frappe.new_doc(party_type)
@ -140,14 +124,12 @@ class OpeningInvoiceCreationTool(Document):
def get_invoice_dict(self, row=None): def get_invoice_dict(self, row=None):
def get_item_dict(): def get_item_dict():
default_uom = frappe.db.get_single_value("Stock Settings", "stock_uom") or _("Nos") cost_center = row.get('cost_center') or frappe.get_cached_value('Company', self.company, "cost_center")
cost_center = row.get('cost_center') or frappe.get_cached_value('Company',
self.company, "cost_center")
if not cost_center: if not cost_center:
frappe.throw( frappe.throw(_("Please set the Default Cost Center in {0} company.").format(frappe.bold(self.company)))
_("Please set the Default Cost Center in {0} company.").format(frappe.bold(self.company))
) income_expense_account_field = "income_account" if row.party_type == "Customer" else "expense_account"
default_uom = frappe.db.get_single_value("Stock Settings", "stock_uom") or _("Nos")
rate = flt(row.outstanding_amount) / flt(row.qty) rate = flt(row.outstanding_amount) / flt(row.qty)
return frappe._dict({ return frappe._dict({
@ -161,18 +143,9 @@ class OpeningInvoiceCreationTool(Document):
"cost_center": cost_center "cost_center": cost_center
}) })
if not row:
return None
party_type = "Customer"
income_expense_account_field = "income_account"
if self.invoice_type == "Purchase":
party_type = "Supplier"
income_expense_account_field = "expense_account"
item = get_item_dict() item = get_item_dict()
args = frappe._dict({ invoice = frappe._dict({
"items": [item], "items": [item],
"is_opening": "Yes", "is_opening": "Yes",
"set_posting_time": 1, "set_posting_time": 1,
@ -180,21 +153,76 @@ class OpeningInvoiceCreationTool(Document):
"cost_center": self.cost_center, "cost_center": self.cost_center,
"due_date": row.due_date, "due_date": row.due_date,
"posting_date": row.posting_date, "posting_date": row.posting_date,
frappe.scrub(party_type): row.party, frappe.scrub(row.party_type): row.party,
"is_pos": 0,
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice" "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice"
}) })
accounting_dimension = get_accounting_dimensions() accounting_dimension = get_accounting_dimensions()
for dimension in accounting_dimension: for dimension in accounting_dimension:
args.update({ invoice.update({
dimension: item.get(dimension) dimension: item.get(dimension)
}) })
if self.invoice_type == "Sales": return invoice
args["is_pos"] = 0
return args def make_invoices(self):
self.validate_company()
invoices = self.get_invoices()
if len(invoices) < 50:
return start_import(invoices)
else:
from frappe.core.page.background_jobs.background_jobs import get_info
from frappe.utils.scheduler import is_scheduler_inactive
if is_scheduler_inactive() and not frappe.flags.in_test:
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
enqueued_jobs = [d.get("job_name") for d in get_info()]
if self.name not in enqueued_jobs:
enqueue(
start_import,
queue="default",
timeout=6000,
event="opening_invoice_creation",
job_name=self.name,
invoices=invoices,
now=frappe.conf.developer_mode or frappe.flags.in_test
)
def start_import(invoices):
errors = 0
names = []
for idx, d in enumerate(invoices):
try:
publish(idx, len(invoices), d.doctype)
doc = frappe.get_doc(d)
doc.insert()
doc.submit()
frappe.db.commit()
names.append(doc.name)
except Exception:
errors += 1
frappe.db.rollback()
message = "\n".join(["Data:", dumps(d, default=str, indent=4), "--" * 50, "\nException:", traceback.format_exc()])
frappe.log_error(title="Error while creating Opening Invoice", message=message)
frappe.db.commit()
if errors:
frappe.msgprint(_("You had {} errors while creating opening invoices. Check {} for more details")
.format(errors, "<a href='#List/Error Log' class='variant-click'>Error Log</a>"), indicator="red", title=_("Error Occured"))
return names
def publish(index, total, doctype):
if total < 5: return
frappe.publish_realtime(
"opening_invoice_creation_progress",
dict(
title=_("Opening Invoice Creation In Progress"),
message=_('Creating {} out of {} {}').format(index + 1, total, doctype),
user=frappe.session.user,
count=index+1,
total=total
))
@frappe.whitelist() @frappe.whitelist()
def get_temporary_opening_account(company=None): def get_temporary_opening_account(company=None):

View File

@ -44,7 +44,7 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
0: ["_Test Supplier", 300, "Overdue"], 0: ["_Test Supplier", 300, "Overdue"],
1: ["_Test Supplier 1", 250, "Overdue"], 1: ["_Test Supplier 1", 250, "Overdue"],
} }
self.check_expected_values(invoices, expected_value, invoice_type="Purchase", ) self.check_expected_values(invoices, expected_value, "Purchase")
def get_opening_invoice_creation_dict(**args): def get_opening_invoice_creation_dict(**args):
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier" party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"

View File

@ -12,9 +12,10 @@ frappe.ui.form.on('Payment Entry', {
setup: function(frm) { setup: function(frm) {
frm.set_query("paid_from", function() { frm.set_query("paid_from", function() {
frm.events.validate_company(frm);
var account_types = in_list(["Pay", "Internal Transfer"], frm.doc.payment_type) ? var account_types = in_list(["Pay", "Internal Transfer"], frm.doc.payment_type) ?
["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]]; ["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]];
return { return {
filters: { filters: {
"account_type": ["in", account_types], "account_type": ["in", account_types],
@ -23,29 +24,35 @@ frappe.ui.form.on('Payment Entry', {
} }
} }
}); });
frm.set_query("party_type", function() { frm.set_query("party_type", function() {
frm.events.validate_company(frm);
return{ return{
"filters": { filters: {
"name": ["in", Object.keys(frappe.boot.party_account_types)], "name": ["in", Object.keys(frappe.boot.party_account_types)],
} }
} }
}); });
frm.set_query("party_bank_account", function() { frm.set_query("party_bank_account", function() {
return { return {
filters: { filters: {
"is_company_account":0, is_company_account: 0,
party_type: frm.doc.party_type, party_type: frm.doc.party_type,
party: frm.doc.party party: frm.doc.party
} }
} }
}); });
frm.set_query("bank_account", function() { frm.set_query("bank_account", function() {
return { return {
filters: { filters: {
"is_company_account":1 is_company_account: 1,
company: frm.doc.company
} }
} }
}); });
frm.set_query("contact_person", function() { frm.set_query("contact_person", function() {
if (frm.doc.party) { if (frm.doc.party) {
return { return {
@ -57,10 +64,12 @@ frappe.ui.form.on('Payment Entry', {
}; };
} }
}); });
frm.set_query("paid_to", function() { frm.set_query("paid_to", function() {
frm.events.validate_company(frm);
var account_types = in_list(["Receive", "Internal Transfer"], frm.doc.payment_type) ? var account_types = in_list(["Receive", "Internal Transfer"], frm.doc.payment_type) ?
["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]]; ["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]];
return { return {
filters: { filters: {
"account_type": ["in", account_types], "account_type": ["in", account_types],
@ -149,6 +158,12 @@ frappe.ui.form.on('Payment Entry', {
frm.events.show_general_ledger(frm); frm.events.show_general_ledger(frm);
}, },
validate_company: (frm) => {
if (!frm.doc.company){
frappe.throw({message:__("Please select a Company first."), title: __("Mandatory")});
}
},
company: function(frm) { company: function(frm) {
frm.events.hide_unhide_fields(frm); frm.events.hide_unhide_fields(frm);
frm.events.set_dynamic_labels(frm); frm.events.set_dynamic_labels(frm);
@ -342,7 +357,7 @@ frappe.ui.form.on('Payment Entry', {
() => { () => {
frm.set_party_account_based_on_party = false; frm.set_party_account_based_on_party = false;
if (r.message.bank_account) { if (r.message.bank_account) {
frm.set_value("party_bank_account", r.message.bank_account); frm.set_value("bank_account", r.message.bank_account);
} }
} }
]); ]);

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"allow_import": 1, "allow_import": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2016-06-01 14:38:51.012597", "creation": "2016-06-01 14:38:51.012597",
@ -63,6 +64,7 @@
"cost_center", "cost_center",
"section_break_12", "section_break_12",
"status", "status",
"custom_remarks",
"remarks", "remarks",
"column_break_16", "column_break_16",
"letter_head", "letter_head",
@ -462,7 +464,8 @@
"fieldname": "remarks", "fieldname": "remarks",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Remarks", "label": "Remarks",
"no_copy": 1 "no_copy": 1,
"read_only_depends_on": "eval:doc.custom_remarks == 0"
}, },
{ {
"fieldname": "column_break_16", "fieldname": "column_break_16",
@ -573,10 +576,18 @@
"label": "Status", "label": "Status",
"options": "\nDraft\nSubmitted\nCancelled", "options": "\nDraft\nSubmitted\nCancelled",
"read_only": 1 "read_only": 1
},
{
"default": "0",
"fieldname": "custom_remarks",
"fieldtype": "Check",
"label": "Custom Remarks"
} }
], ],
"index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"modified": "2019-12-08 13:02:30.016610", "links": [],
"modified": "2020-09-02 13:39:43.383705",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry", "name": "Payment Entry",

View File

@ -453,7 +453,7 @@ class PaymentEntry(AccountsController):
frappe.throw(_("Reference No and Reference Date is mandatory for Bank transaction")) frappe.throw(_("Reference No and Reference Date is mandatory for Bank transaction"))
def set_remarks(self): def set_remarks(self):
if self.remarks: return if self.custom_remarks: return
if self.payment_type=="Internal Transfer": if self.payment_type=="Internal Transfer":
remarks = [_("Amount {0} {1} transferred from {2} to {3}") remarks = [_("Amount {0} {1} transferred from {2} to {3}")
@ -897,7 +897,7 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
total_amount = ref_doc.get("grand_total") total_amount = ref_doc.get("grand_total")
exchange_rate = 1 exchange_rate = 1
outstanding_amount = ref_doc.get("outstanding_amount") outstanding_amount = ref_doc.get("outstanding_amount")
if reference_doctype == "Dunning": elif reference_doctype == "Dunning":
total_amount = ref_doc.get("dunning_amount") total_amount = ref_doc.get("dunning_amount")
exchange_rate = 1 exchange_rate = 1
outstanding_amount = ref_doc.get("dunning_amount") outstanding_amount = ref_doc.get("dunning_amount")
@ -1172,30 +1172,23 @@ def make_payment_order(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
def set_missing_values(source, target): def set_missing_values(source, target):
target.payment_order_type = "Payment Entry" target.payment_order_type = "Payment Entry"
target.append('references', dict(
def update_item(source_doc, target_doc, source_parent): reference_doctype="Payment Entry",
target_doc.bank_account = source_parent.party_bank_account reference_name=source.name,
target_doc.amount = source_doc.allocated_amount bank_account=source.party_bank_account,
target_doc.account = source_parent.paid_to amount=source.paid_amount,
target_doc.payment_entry = source_parent.name account=source.paid_to,
target_doc.supplier = source_parent.party supplier=source.party,
target_doc.mode_of_payment = source_parent.mode_of_payment mode_of_payment=source.mode_of_payment,
))
doclist = get_mapped_doc("Payment Entry", source_name, { doclist = get_mapped_doc("Payment Entry", source_name, {
"Payment Entry": { "Payment Entry": {
"doctype": "Payment Order", "doctype": "Payment Order",
"validation": { "validation": {
"docstatus": ["=", 1] "docstatus": ["=", 1]
},
} }
},
"Payment Entry Reference": {
"doctype": "Payment Order Reference",
"validation": {
"docstatus": ["=", 1]
},
"postprocess": update_item
},
}, target_doc, set_missing_values) }, target_doc, set_missing_values)

View File

@ -1,6 +1,7 @@
frappe.listview_settings['Payment Entry'] = { frappe.listview_settings['Payment Entry'] = {
onload: function(listview) { onload: function(listview) {
if (listview.page.fields_dict.party_type) {
listview.page.fields_dict.party_type.get_query = function() { listview.page.fields_dict.party_type.get_query = function() {
return { return {
"filters": { "filters": {
@ -9,4 +10,5 @@ frappe.listview_settings['Payment Entry'] = {
}; };
}; };
} }
}
}; };

View File

@ -1,313 +1,98 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2015-12-23 21:31:52.699821", "creation": "2015-12-23 21:31:52.699821",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "",
"editable_grid": 1, "editable_grid": 1,
"field_order": [
"payment_gateway",
"payment_channel",
"is_default",
"column_break_4",
"payment_account",
"currency",
"payment_request_message",
"message",
"message_examples"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "payment_gateway", "fieldname": "payment_gateway",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Payment Gateway", "label": "Payment Gateway",
"length": 0,
"no_copy": 0,
"options": "Payment Gateway", "options": "Payment Gateway",
"permlevel": 0, "reqd": 1
"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
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "is_default", "fieldname": "is_default",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Is Default"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Is Default",
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_4", "fieldname": "column_break_4",
"fieldtype": "Column Break", "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,
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "payment_account", "fieldname": "payment_account",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Payment Account", "label": "Payment Account",
"length": 0,
"no_copy": 0,
"options": "Account", "options": "Account",
"permlevel": 0, "reqd": 1
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "payment_account.account_currency", "fetch_from": "payment_account.account_currency",
"fieldname": "currency", "fieldname": "currency",
"fieldtype": "Read Only", "fieldtype": "Read Only",
"hidden": 0, "label": "Currency"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Currency",
"length": 0,
"no_copy": 0,
"options": "",
"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
}, },
{ {
"allow_bulk_edit": 0, "depends_on": "eval: doc.payment_channel !== \"Phone\"",
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "payment_request_message", "fieldname": "payment_request_message",
"fieldtype": "Section Break", "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": "",
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Please click on the link below to make your payment", "default": "Please click on the link below to make your payment",
"fieldname": "message", "fieldname": "message",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"hidden": 0, "label": "Default Payment Request Message"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Default Payment Request Message",
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "message_examples", "fieldname": "message_examples",
"fieldtype": "HTML", "fieldtype": "HTML",
"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": "Message Examples", "label": "Message Examples",
"length": 0, "options": "<pre><h5>Message Example</h5>\n\n&lt;p&gt; Thank You for being a part of {{ doc.company }}! We hope you are enjoying the service.&lt;/p&gt;\n\n&lt;p&gt; Please find enclosed the E Bill statement. The outstanding amount is {{ doc.grand_total }}.&lt;/p&gt;\n\n&lt;p&gt; We don't want you to be spending time running around in order to pay for your Bill.<br>After all, life is beautiful and the time you have in hand should be spent to enjoy it!<br>So here are our little ways to help you get more time for life! &lt;/p&gt;\n\n&lt;a href=\"{{ payment_url }}\"&gt; click here to pay &lt;/a&gt;\n\n</pre>\n"
"no_copy": 0, },
"options": "<pre><h5>Message Example</h5>\n\n&lt;p&gt; Thank You for being a part of {{ doc.company }}! We hope you are enjoying the service.&lt;/p&gt;\n\n&lt;p&gt; Please find enclosed the E Bill statement. The outstanding amount is {{ doc.grand_total }}.&lt;/p&gt;\n\n&lt;p&gt; We don't want you to be spending time running around in order to pay for your Bill.<br>After all, life is beautiful and the time you have in hand should be spent to enjoy it!<br>So here are our little ways to help you get more time for life! &lt;/p&gt;\n\n&lt;a href=\"{{ payment_url }}\"&gt; click here to pay &lt;/a&gt;\n\n</pre>\n", {
"permlevel": 0, "default": "Email",
"precision": "", "fieldname": "payment_channel",
"print_hide": 0, "fieldtype": "Select",
"print_hide_if_no_value": 0, "label": "Payment Channel",
"read_only": 0, "options": "\nEmail\nPhone"
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0, "index_web_pages_for_search": 1,
"hide_heading": 0, "links": [],
"hide_toolbar": 0, "modified": "2020-09-20 13:30:27.722852",
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-05-16 22:43:34.970491",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Gateway Account", "name": "Payment Gateway Account",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Accounts Manager", "role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC"
"track_changes": 0,
"track_seen": 0
} }

View File

@ -21,10 +21,15 @@ class PaymentOrder(Document):
if cancel: if cancel:
status = 'Initiated' status = 'Initiated'
ref_field = "status" if self.payment_order_type == "Payment Request" else "payment_order_status" if self.payment_order_type == "Payment Request":
ref_field = "status"
ref_doc_field = frappe.scrub(self.payment_order_type)
else:
ref_field = "payment_order_status"
ref_doc_field = "reference_name"
for d in self.references: for d in self.references:
frappe.db.set_value(self.payment_order_type, d.get(frappe.scrub(self.payment_order_type)), ref_field, status) frappe.db.set_value(self.payment_order_type, d.get(ref_doc_field), ref_field, status)
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs

View File

@ -5,6 +5,45 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
from frappe.utils import getdate
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry, make_payment_order
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
class TestPaymentOrder(unittest.TestCase): class TestPaymentOrder(unittest.TestCase):
pass def setUp(self):
create_bank_account()
def tearDown(self):
for bt in frappe.get_all("Payment Order"):
doc = frappe.get_doc("Payment Order", bt.name)
doc.cancel()
doc.delete()
def test_payment_order_creation_against_payment_entry(self):
purchase_invoice = make_purchase_invoice()
payment_entry = get_payment_entry("Purchase Invoice", purchase_invoice.name, bank_account="_Test Bank - _TC")
payment_entry.reference_no = "_Test_Payment_Order"
payment_entry.reference_date = getdate()
payment_entry.party_bank_account = "Checking Account - Citi Bank"
payment_entry.insert()
payment_entry.submit()
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)
def create_payment_order_against_payment_entry(ref_doc, order_type):
payment_order = frappe.get_doc(dict(
doctype="Payment Order",
company="_Test Company",
payment_order_type=order_type,
company_bank_account="Checking Account - Citi Bank"
))
doc = make_payment_order(ref_doc.name, payment_order)
doc.save()
doc.submit()
return doc

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"creation": "2018-07-20 16:38:06.630813", "creation": "2018-07-20 16:38:06.630813",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
@ -10,7 +11,6 @@
"column_break_4", "column_break_4",
"supplier", "supplier",
"payment_request", "payment_request",
"payment_entry",
"mode_of_payment", "mode_of_payment",
"bank_account_details", "bank_account_details",
"bank_account", "bank_account",
@ -103,17 +103,12 @@
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "payment_entry",
"fieldtype": "Link",
"label": "Payment Entry",
"options": "Payment Entry",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"modified": "2019-05-08 13:56:25.724557", "links": [],
"modified": "2020-09-04 08:29:51.014390",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Order Reference", "name": "Payment Order Reference",

View File

@ -37,6 +37,11 @@ frappe.ui.form.on("Payment Reconciliation Payment", {
erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.extend({ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.extend({
onload: function() { onload: function() {
var me = this; var me = this;
this.frm.set_query("party", function() {
check_mandatory(me.frm);
});
this.frm.set_query("party_type", function() { this.frm.set_query("party_type", function() {
return { return {
"filters": { "filters": {
@ -46,9 +51,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
}); });
this.frm.set_query('receivable_payable_account', function() { this.frm.set_query('receivable_payable_account', function() {
if(!me.frm.doc.company || !me.frm.doc.party_type) { check_mandatory(me.frm);
frappe.msgprint(__("Please select Company and Party Type first"));
} else {
return { return {
filters: { filters: {
"company": me.frm.doc.company, "company": me.frm.doc.company,
@ -56,14 +59,10 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
"account_type": frappe.boot.party_account_types[me.frm.doc.party_type] "account_type": frappe.boot.party_account_types[me.frm.doc.party_type]
} }
}; };
}
}); });
this.frm.set_query('bank_cash_account', function() { this.frm.set_query('bank_cash_account', function() {
if(!me.frm.doc.company) { check_mandatory(me.frm, true);
frappe.msgprint(__("Please select Company first"));
} else {
return { return {
filters:[ filters:[
['Account', 'company', '=', me.frm.doc.company], ['Account', 'company', '=', me.frm.doc.company],
@ -71,12 +70,20 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
['Account', 'account_type', 'in', ['Bank', 'Cash']] ['Account', 'account_type', 'in', ['Bank', 'Cash']]
] ]
}; };
}
}); });
this.frm.set_value('party_type', ''); this.frm.set_value('party_type', '');
this.frm.set_value('party', ''); this.frm.set_value('party', '');
this.frm.set_value('receivable_payable_account', ''); this.frm.set_value('receivable_payable_account', '');
var check_mandatory = (frm, only_company=false) => {
var title = __("Mandatory");
if (only_company && !frm.doc.company) {
frappe.throw({message: __("Please Select a Company First"), title: title});
} else if (!frm.doc.company || !frm.doc.party_type) {
frappe.throw({message: __("Please Select Both Company and Party Type First"), title: title});
}
};
}, },
refresh: function() { refresh: function() {

View File

@ -99,6 +99,7 @@ class PaymentReconciliation(Document):
and `tabGL Entry`.against_voucher_type = %(voucher_type)s and `tabGL Entry`.against_voucher_type = %(voucher_type)s
and `tab{doc}`.docstatus = 1 and `tabGL Entry`.party = %(party)s and `tab{doc}`.docstatus = 1 and `tabGL Entry`.party = %(party)s
and `tabGL Entry`.party_type = %(party_type)s and `tabGL Entry`.account = %(account)s and `tabGL Entry`.party_type = %(party_type)s and `tabGL Entry`.account = %(account)s
and `tabGL Entry`.is_cancelled = 0
GROUP BY `tab{doc}`.name GROUP BY `tab{doc}`.name
Having Having
amount > 0 amount > 0

View File

@ -25,7 +25,7 @@ frappe.ui.form.on("Payment Request", "onload", function(frm, dt, dn){
}) })
frappe.ui.form.on("Payment Request", "refresh", function(frm) { frappe.ui.form.on("Payment Request", "refresh", function(frm) {
if(frm.doc.payment_request_type == 'Inward' && if(frm.doc.payment_request_type == 'Inward' && frm.doc.payment_channel !== "Phone" &&
!in_list(["Initiated", "Paid"], frm.doc.status) && !frm.doc.__islocal && frm.doc.docstatus==1){ !in_list(["Initiated", "Paid"], frm.doc.status) && !frm.doc.__islocal && frm.doc.docstatus==1){
frm.add_custom_button(__('Resend Payment Email'), function(){ frm.add_custom_button(__('Resend Payment Email'), function(){
frappe.call({ frappe.call({

View File

@ -48,6 +48,7 @@
"section_break_7", "section_break_7",
"payment_gateway", "payment_gateway",
"payment_account", "payment_account",
"payment_channel",
"payment_order", "payment_order",
"amended_from" "amended_from"
], ],
@ -230,6 +231,7 @@
"label": "Recipient Message And Payment Details" "label": "Recipient Message And Payment Details"
}, },
{ {
"depends_on": "eval: doc.payment_channel != \"Phone\"",
"fieldname": "print_format", "fieldname": "print_format",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Print Format" "label": "Print Format"
@ -241,6 +243,7 @@
"label": "To" "label": "To"
}, },
{ {
"depends_on": "eval: doc.payment_channel != \"Phone\"",
"fieldname": "subject", "fieldname": "subject",
"fieldtype": "Data", "fieldtype": "Data",
"in_global_search": 1, "in_global_search": 1,
@ -277,16 +280,18 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval: doc.payment_request_type == 'Inward'", "depends_on": "eval: doc.payment_request_type == 'Inward' || doc.payment_channel != \"Phone\"",
"fieldname": "section_break_10", "fieldname": "section_break_10",
"fieldtype": "Section Break" "fieldtype": "Section Break"
}, },
{ {
"depends_on": "eval: doc.payment_channel != \"Phone\"",
"fieldname": "message", "fieldname": "message",
"fieldtype": "Text", "fieldtype": "Text",
"label": "Message" "label": "Message"
}, },
{ {
"depends_on": "eval: doc.payment_channel != \"Phone\"",
"fieldname": "message_examples", "fieldname": "message_examples",
"fieldtype": "HTML", "fieldtype": "HTML",
"label": "Message Examples", "label": "Message Examples",
@ -347,12 +352,21 @@
"options": "Payment Request", "options": "Payment Request",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"fetch_from": "payment_gateway_account.payment_channel",
"fieldname": "payment_channel",
"fieldtype": "Select",
"label": "Payment Channel",
"options": "\nEmail\nPhone",
"read_only": 1
} }
], ],
"in_create": 1, "in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-07-17 14:06:42.185763", "modified": "2020-09-18 12:24:14.178853",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Request", "name": "Payment Request",

View File

@ -36,7 +36,7 @@ class PaymentRequest(Document):
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
if (hasattr(ref_doc, "order_type") \ if (hasattr(ref_doc, "order_type") \
and getattr(ref_doc, "order_type") != "Shopping Cart"): and getattr(ref_doc, "order_type") != "Shopping Cart"):
ref_amount = get_amount(ref_doc) ref_amount = get_amount(ref_doc, self.payment_account)
if existing_payment_request_amount + flt(self.grand_total)> ref_amount: if existing_payment_request_amount + flt(self.grand_total)> ref_amount:
frappe.throw(_("Total Payment Request amount cannot be greater than {0} amount") frappe.throw(_("Total Payment Request amount cannot be greater than {0} amount")
@ -76,11 +76,25 @@ class PaymentRequest(Document):
or self.flags.mute_email: or self.flags.mute_email:
send_mail = False send_mail = False
if send_mail: if send_mail and self.payment_channel != "Phone":
self.set_payment_request_url() self.set_payment_request_url()
self.send_email() self.send_email()
self.make_communication_entry() self.make_communication_entry()
elif self.payment_channel == "Phone":
controller = get_payment_gateway_controller(self.payment_gateway)
payment_record = dict(
reference_doctype="Payment Request",
reference_docname=self.name,
payment_reference=self.reference_name,
grand_total=self.grand_total,
sender=self.email_to,
currency=self.currency,
payment_gateway=self.payment_gateway
)
controller.validate_transaction_currency(self.currency)
controller.request_for_payment(**payment_record)
def on_cancel(self): def on_cancel(self):
self.check_if_payment_entry_exists() self.check_if_payment_entry_exists()
self.set_as_cancelled() self.set_as_cancelled()
@ -105,13 +119,14 @@ class PaymentRequest(Document):
return False return False
def set_payment_request_url(self): def set_payment_request_url(self):
if self.payment_account: if self.payment_account and self.payment_channel != "Phone":
self.payment_url = self.get_payment_url() self.payment_url = self.get_payment_url()
if self.payment_url: if self.payment_url:
self.db_set('payment_url', self.payment_url) self.db_set('payment_url', self.payment_url)
if self.payment_url or not self.payment_gateway_account: if self.payment_url or not self.payment_gateway_account \
or (self.payment_gateway_account and self.payment_channel == "Phone"):
self.db_set('status', 'Initiated') self.db_set('status', 'Initiated')
def get_payment_url(self): def get_payment_url(self):
@ -140,6 +155,10 @@ class PaymentRequest(Document):
}) })
def set_as_paid(self): def set_as_paid(self):
if self.payment_channel == "Phone":
self.db_set("status", "Paid")
else:
payment_entry = self.create_payment_entry() payment_entry = self.create_payment_entry()
self.make_invoice() self.make_invoice()
@ -151,7 +170,7 @@ class PaymentRequest(Document):
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
if self.reference_doctype == "Sales Invoice": if self.reference_doctype in ["Sales Invoice", "POS Invoice"]:
party_account = ref_doc.debit_to party_account = ref_doc.debit_to
elif self.reference_doctype == "Purchase Invoice": elif self.reference_doctype == "Purchase Invoice":
party_account = ref_doc.credit_to party_account = ref_doc.credit_to
@ -166,8 +185,8 @@ class PaymentRequest(Document):
else: else:
party_amount = self.grand_total party_amount = self.grand_total
payment_entry = get_payment_entry(self.reference_doctype, self.reference_name, payment_entry = get_payment_entry(self.reference_doctype, self.reference_name, party_amount=party_amount,
party_amount=party_amount, bank_account=self.payment_account, bank_amount=bank_amount) bank_account=self.payment_account, bank_amount=bank_amount)
payment_entry.update({ payment_entry.update({
"reference_no": self.name, "reference_no": self.name,
@ -255,7 +274,7 @@ class PaymentRequest(Document):
# if shopping cart enabled and in session # if shopping cart enabled and in session
if (shopping_cart_settings.enabled and hasattr(frappe.local, "session") if (shopping_cart_settings.enabled and hasattr(frappe.local, "session")
and frappe.local.session.user != "Guest"): and frappe.local.session.user != "Guest") and self.payment_channel != "Phone":
success_url = shopping_cart_settings.payment_success_url success_url = shopping_cart_settings.payment_success_url
if success_url: if success_url:
@ -280,7 +299,9 @@ def make_payment_request(**args):
args = frappe._dict(args) args = frappe._dict(args)
ref_doc = frappe.get_doc(args.dt, args.dn) ref_doc = frappe.get_doc(args.dt, args.dn)
grand_total = get_amount(ref_doc) gateway_account = get_gateway_details(args) or frappe._dict()
grand_total = get_amount(ref_doc, gateway_account.get("payment_account"))
if args.loyalty_points and args.dt == "Sales Order": if args.loyalty_points and args.dt == "Sales Order":
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
loyalty_amount = validate_loyalty_points(ref_doc, int(args.loyalty_points)) loyalty_amount = validate_loyalty_points(ref_doc, int(args.loyalty_points))
@ -288,8 +309,6 @@ def make_payment_request(**args):
frappe.db.set_value("Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False) frappe.db.set_value("Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False)
grand_total = grand_total - loyalty_amount grand_total = grand_total - loyalty_amount
gateway_account = get_gateway_details(args) or frappe._dict()
bank_account = (get_party_bank_account(args.get('party_type'), args.get('party')) bank_account = (get_party_bank_account(args.get('party_type'), args.get('party'))
if args.get('party_type') else '') if args.get('party_type') else '')
@ -314,9 +333,11 @@ def make_payment_request(**args):
"payment_gateway_account": gateway_account.get("name"), "payment_gateway_account": gateway_account.get("name"),
"payment_gateway": gateway_account.get("payment_gateway"), "payment_gateway": gateway_account.get("payment_gateway"),
"payment_account": gateway_account.get("payment_account"), "payment_account": gateway_account.get("payment_account"),
"payment_channel": gateway_account.get("payment_channel"),
"payment_request_type": args.get("payment_request_type"), "payment_request_type": args.get("payment_request_type"),
"currency": ref_doc.currency, "currency": ref_doc.currency,
"grand_total": grand_total, "grand_total": grand_total,
"mode_of_payment": args.mode_of_payment,
"email_to": args.recipient_id or ref_doc.owner, "email_to": args.recipient_id or ref_doc.owner,
"subject": _("Payment Request for {0}").format(args.dn), "subject": _("Payment Request for {0}").format(args.dn),
"message": gateway_account.get("message") or get_dummy_message(ref_doc), "message": gateway_account.get("message") or get_dummy_message(ref_doc),
@ -344,7 +365,7 @@ def make_payment_request(**args):
return pr.as_dict() return pr.as_dict()
def get_amount(ref_doc): def get_amount(ref_doc, payment_account=None):
"""get amount based on doctype""" """get amount based on doctype"""
dt = ref_doc.doctype dt = ref_doc.doctype
if dt in ["Sales Order", "Purchase Order"]: if dt in ["Sales Order", "Purchase Order"]:
@ -356,6 +377,12 @@ def get_amount(ref_doc):
else: else:
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
elif dt == "POS Invoice":
for pay in ref_doc.payments:
if pay.type == "Phone" and pay.account == payment_account:
grand_total = pay.amount
break
elif dt == "Fees": elif dt == "Fees":
grand_total = ref_doc.outstanding_amount grand_total = ref_doc.outstanding_amount
@ -366,6 +393,10 @@ def get_amount(ref_doc):
frappe.throw(_("Payment Entry is already created")) frappe.throw(_("Payment Entry is already created"))
def get_existing_payment_request_amount(ref_dt, ref_dn): def get_existing_payment_request_amount(ref_dt, ref_dn):
"""
Get the existing payment request which are unpaid or partially paid for payment channel other than Phone
and get the summation of existing paid payment request for Phone payment channel.
"""
existing_payment_request_amount = frappe.db.sql(""" existing_payment_request_amount = frappe.db.sql("""
select sum(grand_total) select sum(grand_total)
from `tabPayment Request` from `tabPayment Request`
@ -373,7 +404,9 @@ def get_existing_payment_request_amount(ref_dt, ref_dn):
reference_doctype = %s reference_doctype = %s
and reference_name = %s and reference_name = %s
and docstatus = 1 and docstatus = 1
and status != 'Paid' and (status != 'Paid'
or (payment_channel = 'Phone'
and status = 'Paid'))
""", (ref_dt, ref_dn)) """, (ref_dt, ref_dn))
return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0 return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0

View File

@ -45,6 +45,7 @@
"unique": 0 "unique": 0
}, },
{ {
"description": "Provide the invoice portion in percent",
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 1, "bold": 1,
@ -170,6 +171,7 @@
"unique": 0 "unique": 0
}, },
{ {
"description": "Give number of days according to prior selection",
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 1, "bold": 1,
@ -305,7 +307,7 @@
"issingle": 0, "issingle": 0,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2018-03-08 10:47:32.830478", "modified": "2020-10-14 10:47:32.830478",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Term", "name": "Payment Term",

View File

@ -291,11 +291,11 @@
"issingle": 0, "issingle": 0,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2018-08-21 16:15:49.089450", "modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Period Closing Voucher", "name": "Period Closing Voucher",
"owner": "jai@webnotestech.com", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 1, "amend": 1,

View File

@ -51,18 +51,53 @@ frappe.ui.form.on('POS Closing Entry', {
args: { args: {
start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date), start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date),
end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date), end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date),
pos_profile: frm.doc.pos_profile,
user: frm.doc.user user: frm.doc.user
}, },
callback: (r) => { callback: (r) => {
let pos_docs = r.message; let pos_docs = r.message;
set_form_data(pos_docs, frm) set_form_data(pos_docs, frm);
refresh_fields(frm) refresh_fields(frm);
set_html_data(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', { frappe.ui.form.on('POS Closing Entry Detail', {
closing_amount: (frm, cdt, cdn) => { closing_amount: (frm, cdt, cdn) => {
const row = locals[cdt][cdn]; const row = locals[cdt][cdn];
@ -76,8 +111,8 @@ function set_form_data(data, frm) {
frm.doc.grand_total += flt(d.grand_total); frm.doc.grand_total += flt(d.grand_total);
frm.doc.net_total += flt(d.net_total); frm.doc.net_total += flt(d.net_total);
frm.doc.total_quantity += flt(d.total_qty); frm.doc.total_quantity += flt(d.total_qty);
add_to_payments(d, frm); refresh_payments(d, frm);
add_to_taxes(d, frm); refresh_taxes(d, frm);
}); });
} }
@ -90,11 +125,12 @@ function add_to_pos_transaction(d, frm) {
}) })
} }
function add_to_payments(d, frm) { function refresh_payments(d, frm, remove) {
d.payments.forEach(p => { d.payments.forEach(p => {
const payment = frm.doc.payment_reconciliation.find(pay => pay.mode_of_payment === p.mode_of_payment); const payment = frm.doc.payment_reconciliation.find(pay => pay.mode_of_payment === p.mode_of_payment);
if (payment) { if (payment) {
payment.expected_amount += flt(p.amount); if (!remove) payment.expected_amount += flt(p.amount);
else payment.expected_amount -= flt(p.amount);
} else { } else {
frm.add_child("payment_reconciliation", { frm.add_child("payment_reconciliation", {
mode_of_payment: p.mode_of_payment, mode_of_payment: p.mode_of_payment,
@ -105,11 +141,12 @@ function add_to_payments(d, frm) {
}) })
} }
function add_to_taxes(d, frm) { function refresh_taxes(d, frm, remove) {
d.taxes.forEach(t => { d.taxes.forEach(t => {
const tax = frm.doc.taxes.find(tx => tx.account_head === t.account_head && tx.rate === t.rate); const tax = frm.doc.taxes.find(tx => tx.account_head === t.account_head && tx.rate === t.rate);
if (tax) { if (tax) {
tax.amount += flt(t.tax_amount); if (!remove) tax.amount += flt(t.tax_amount);
else tax.amount -= flt(t.tax_amount);
} else { } else {
frm.add_child("taxes", { frm.add_child("taxes", {
account_head: t.account_head, account_head: t.account_head,

View File

@ -14,19 +14,51 @@ from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import
class POSClosingEntry(Document): class POSClosingEntry(Document):
def validate(self): def validate(self):
user = frappe.get_all('POS Closing Entry', if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
filters = { 'user': self.user, 'docstatus': 1 }, 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.get_all("POS Closing Entry",
filters = { "user": self.user, "docstatus": 1, "pos_profile": self.pos_profile },
or_filters = { or_filters = {
'period_start_date': ('between', [self.period_start_date, self.period_end_date]), "period_start_date": ("between", [self.period_start_date, self.period_end_date]),
'period_end_date': ('between', [self.period_start_date, self.period_end_date]) "period_end_date": ("between", [self.period_start_date, self.period_end_date])
}) })
if user: if user:
frappe.throw(_("POS Closing Entry {} against {} between selected period" bold_already_exists = frappe.bold(_("already exists"))
.format(frappe.bold("already exists"), frappe.bold(self.user))), title=_("Invalid Period")) bold_user = frappe.bold(self.user)
frappe.throw(_("POS Closing Entry {} against {} between selected period")
.format(bold_already_exists, bold_user), title=_("Invalid Period"))
if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open": def validate_pos_invoices(self):
frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry")) invalid_rows = []
for d in self.pos_transactions:
invalid_row = {'idx': d.idx}
pos_invoice = frappe.db.get_values("POS Invoice", d.pos_invoice,
["consolidated_invoice", "pos_profile", "docstatus", "owner"], as_dict=1)[0]
if pos_invoice.consolidated_invoice:
invalid_row.setdefault('msg', []).append(_('POS Invoice is {}').format(frappe.bold("already consolidated")))
invalid_rows.append(invalid_row)
continue
if pos_invoice.pos_profile != self.pos_profile:
invalid_row.setdefault('msg', []).append(_("POS Profile doesn't matches {}").format(frappe.bold(self.pos_profile)))
if pos_invoice.docstatus != 1:
invalid_row.setdefault('msg', []).append(_('POS Invoice is not {}').format(frappe.bold("submitted")))
if pos_invoice.owner != self.user:
invalid_row.setdefault('msg', []).append(_("POS Invoice isn't created by user {}").format(frappe.bold(self.owner)))
if invalid_row.get('msg'):
invalid_rows.append(invalid_row)
if not invalid_rows:
return
error_list = [_("Row #{}: {}").format(row.get('idx'), row.get('msg')) for row in invalid_rows]
frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True)
def on_submit(self): def on_submit(self):
merge_pos_invoices(self.pos_transactions) merge_pos_invoices(self.pos_transactions)
@ -47,16 +79,15 @@ def get_cashiers(doctype, txt, searchfield, start, page_len, filters):
return [c['user'] for c in cashiers_list] return [c['user'] for c in cashiers_list]
@frappe.whitelist() @frappe.whitelist()
def get_pos_invoices(start, end, user): def get_pos_invoices(start, end, pos_profile, user):
data = frappe.db.sql(""" data = frappe.db.sql("""
select select
name, timestamp(posting_date, posting_time) as "timestamp" name, timestamp(posting_date, posting_time) as "timestamp"
from from
`tabPOS Invoice` `tabPOS Invoice`
where where
owner = %s and docstatus = 1 and owner = %s and docstatus = 1 and pos_profile = %s and ifnull(consolidated_invoice,'') = ''
(consolidated_invoice is NULL or consolidated_invoice = '') """, (user, pos_profile), as_dict=1)
""", (user), as_dict=1)
data = list(filter(lambda d: get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end), data)) data = list(filter(lambda d: get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end), data))
# need to get taxes and payments so can't avoid get_doc # need to get taxes and payments so can't avoid get_doc
@ -76,7 +107,8 @@ def make_closing_entry_from_opening(opening_entry):
closing_entry.net_total = 0 closing_entry.net_total = 0
closing_entry.total_quantity = 0 closing_entry.total_quantity = 0
invoices = get_pos_invoices(closing_entry.period_start_date, closing_entry.period_end_date, closing_entry.user) invoices = get_pos_invoices(closing_entry.period_start_date, closing_entry.period_end_date,
closing_entry.pos_profile, closing_entry.user)
pos_transactions = [] pos_transactions = []
taxes = [] taxes = []

View File

@ -45,7 +45,7 @@ class TestPOSClosingEntry(unittest.TestCase):
frappe.set_user("Administrator") frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Profile`")
def init_user_and_profile(): def init_user_and_profile(**args):
user = 'test@example.com' user = 'test@example.com'
test_user = frappe.get_doc('User', user) test_user = frappe.get_doc('User', user)
@ -53,7 +53,7 @@ def init_user_and_profile():
test_user.add_roles(*roles) test_user.add_roles(*roles)
frappe.set_user(user) frappe.set_user(user)
pos_profile = make_pos_profile() pos_profile = make_pos_profile(**args)
pos_profile.append('applicable_for_users', { pos_profile.append('applicable_for_users', {
'default': 1, 'default': 1,
'user': user 'user': user

View File

@ -7,8 +7,8 @@
"field_order": [ "field_order": [
"mode_of_payment", "mode_of_payment",
"opening_amount", "opening_amount",
"closing_amount",
"expected_amount", "expected_amount",
"closing_amount",
"difference" "difference"
], ],
"fields": [ "fields": [
@ -26,8 +26,7 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Expected Amount", "label": "Expected Amount",
"options": "company:company_currency", "options": "company:company_currency",
"read_only": 1, "read_only": 1
"reqd": 1
}, },
{ {
"fieldname": "difference", "fieldname": "difference",
@ -55,9 +54,10 @@
"reqd": 1 "reqd": 1
} }
], ],
"index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-05-29 15:03:34.533607", "modified": "2020-10-23 16:45:43.662034",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Closing Entry Detail", "name": "POS Closing Entry Detail",

View File

@ -9,80 +9,63 @@ erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend(
this._super(doc); this._super(doc);
}, },
onload() { onload(doc) {
this._super(); this._super();
if(this.frm.doc.__islocal && this.frm.doc.is_pos) { if(doc.__islocal && doc.is_pos && frappe.get_route_str() !== 'point-of-sale') {
//Load pos profile data on the invoice if the default value of Is POS is 1 this.frm.script_manager.trigger("is_pos");
this.frm.refresh_fields();
me.frm.script_manager.trigger("is_pos");
me.frm.refresh_fields();
} }
}, },
refresh(doc) { refresh(doc) {
this._super(); this._super();
if (doc.docstatus == 1 && !doc.is_return) { if (doc.docstatus == 1 && !doc.is_return) {
if(doc.outstanding_amount >= 0 || Math.abs(flt(doc.outstanding_amount)) < flt(doc.grand_total)) { this.frm.add_custom_button(__('Return'), this.make_sales_return, __('Create'));
cur_frm.add_custom_button(__('Return'), this.frm.page.set_inner_btn_group_as_primary(__('Create'));
this.make_sales_return, __('Create'));
cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
}
} }
if (this.frm.doc.is_return) { if (doc.is_return && doc.__islocal) {
this.frm.return_print_format = "Sales Invoice Return"; this.frm.return_print_format = "Sales Invoice Return";
cur_frm.set_value('consolidated_invoice', ''); this.frm.set_value('consolidated_invoice', '');
} }
}, },
is_pos: function(frm){ is_pos: function() {
this.set_pos_data(); this.set_pos_data();
}, },
set_pos_data: function() { set_pos_data: async function() {
if(this.frm.doc.is_pos) { if(this.frm.doc.is_pos) {
this.frm.set_value("allocate_advances_automatically", 0); this.frm.set_value("allocate_advances_automatically", 0);
if(!this.frm.doc.company) { if(!this.frm.doc.company) {
this.frm.set_value("is_pos", 0); this.frm.set_value("is_pos", 0);
frappe.msgprint(__("Please specify Company to proceed")); frappe.msgprint(__("Please specify Company to proceed"));
} else { } else {
var me = this; const r = await this.frm.call({
return this.frm.call({ doc: this.frm.doc,
doc: me.frm.doc,
method: "set_missing_values", method: "set_missing_values",
callback: function(r) { freeze: true
});
if(!r.exc) { if(!r.exc) {
if(r.message) { if(r.message) {
me.frm.pos_print_format = r.message.print_format || ""; this.frm.pos_print_format = r.message.print_format || "";
me.frm.meta.default_print_format = r.message.print_format || ""; this.frm.meta.default_print_format = r.message.print_format || "";
me.frm.allow_edit_rate = r.message.allow_edit_rate; this.frm.doc.campaign = r.message.campaign;
me.frm.allow_edit_discount = r.message.allow_edit_discount; this.frm.allow_print_before_pay = r.message.allow_print_before_pay;
me.frm.doc.campaign = r.message.campaign;
me.frm.allow_print_before_pay = r.message.allow_print_before_pay;
} }
me.frm.script_manager.trigger("update_stock"); this.frm.script_manager.trigger("update_stock");
me.calculate_taxes_and_totals(); this.calculate_taxes_and_totals();
if(me.frm.doc.taxes_and_charges) { this.frm.doc.taxes_and_charges && this.frm.script_manager.trigger("taxes_and_charges");
me.frm.script_manager.trigger("taxes_and_charges"); frappe.model.set_default_values(this.frm.doc);
} this.set_dynamic_labels();
frappe.model.set_default_values(me.frm.doc);
me.set_dynamic_labels();
} }
} }
});
} }
}
else this.frm.trigger("refresh");
}, },
customer() { customer() {
if (!this.frm.doc.customer) return if (!this.frm.doc.customer) return
const pos_profile = this.frm.doc.pos_profile;
if (this.frm.doc.is_pos){
var pos_profile = this.frm.doc.pos_profile;
}
var me = this;
if(this.frm.updating_party_details) return; if(this.frm.updating_party_details) return;
erpnext.utils.get_party_details(this.frm, erpnext.utils.get_party_details(this.frm,
"erpnext.accounts.party.get_party_details", { "erpnext.accounts.party.get_party_details", {
@ -92,8 +75,8 @@ erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend(
account: this.frm.doc.debit_to, account: this.frm.doc.debit_to,
price_list: this.frm.doc.selling_price_list, price_list: this.frm.doc.selling_price_list,
pos_profile: pos_profile pos_profile: pos_profile
}, function() { }, () => {
me.apply_pricing_rule(); this.apply_pricing_rule();
}); });
}, },
@ -201,5 +184,22 @@ frappe.ui.form.on('POS Invoice', {
} }
frm.set_value("loyalty_amount", loyalty_amount); frm.set_value("loyalty_amount", loyalty_amount);
} }
},
request_for_payment: function (frm) {
frm.save().then(() => {
frappe.dom.freeze();
frappe.call({
method: 'create_payment_request',
doc: frm.doc,
})
.fail(() => {
frappe.dom.unfreeze();
frappe.msgprint('Payment request failed');
})
.then(() => {
frappe.msgprint('Payment request sent successfully');
});
});
} }
}); });

View File

@ -460,7 +460,7 @@
}, },
{ {
"fieldname": "contact_mobile", "fieldname": "contact_mobile",
"fieldtype": "Small Text", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "Mobile No", "label": "Mobile No",
"read_only": 1 "read_only": 1
@ -1580,7 +1580,7 @@
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-05-29 15:08:39.337385", "modified": "2020-09-28 16:51:24.641755",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice", "name": "POS Invoice",

View File

@ -10,11 +10,10 @@ from erpnext.controllers.selling_controller import SellingController
from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate
from erpnext.accounts.utils import get_account_currency from erpnext.accounts.utils import get_account_currency
from erpnext.accounts.party import get_party_account, get_due_date from erpnext.accounts.party import get_party_account, get_due_date
from erpnext.accounts.doctype.loyalty_program.loyalty_program import \ from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
get_loyalty_program_details_with_points, validate_loyalty_points from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice, get_bank_cash_account, update_multi_mode_option from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice, get_bank_cash_account, update_multi_mode_option, get_mode_of_payment_info
from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos
from six import iteritems from six import iteritems
@ -29,8 +28,7 @@ class POSInvoice(SalesInvoice):
# run on validate method of selling controller # run on validate method of selling controller
super(SalesInvoice, self).validate() super(SalesInvoice, self).validate()
self.validate_auto_set_posting_time() self.validate_auto_set_posting_time()
self.validate_pos_paid_amount() self.validate_mode_of_payment()
self.validate_pos_return()
self.validate_uom_is_integer("stock_uom", "stock_qty") self.validate_uom_is_integer("stock_uom", "stock_qty")
self.validate_uom_is_integer("uom", "qty") self.validate_uom_is_integer("uom", "qty")
self.validate_debit_to_acc() self.validate_debit_to_acc()
@ -40,11 +38,11 @@ class POSInvoice(SalesInvoice):
self.validate_item_cost_centers() self.validate_item_cost_centers()
self.validate_serialised_or_batched_item() self.validate_serialised_or_batched_item()
self.validate_stock_availablility() self.validate_stock_availablility()
self.validate_return_items() self.validate_return_items_qty()
self.set_status() self.set_status()
self.set_account_for_mode_of_payment() self.set_account_for_mode_of_payment()
self.validate_pos() self.validate_pos()
self.verify_payment_amount() self.validate_payment_amount()
self.validate_loyalty_transaction() self.validate_loyalty_transaction()
def on_submit(self): def on_submit(self):
@ -57,6 +55,7 @@ class POSInvoice(SalesInvoice):
against_psi_doc.make_loyalty_point_entry() against_psi_doc.make_loyalty_point_entry()
if self.redeem_loyalty_points and self.loyalty_points: if self.redeem_loyalty_points and self.loyalty_points:
self.apply_loyalty_points() self.apply_loyalty_points()
self.check_phone_payments()
self.set_status(update=True) self.set_status(update=True)
def on_cancel(self): def on_cancel(self):
@ -69,77 +68,115 @@ class POSInvoice(SalesInvoice):
against_psi_doc.delete_loyalty_point_entry() against_psi_doc.delete_loyalty_point_entry()
against_psi_doc.make_loyalty_point_entry() against_psi_doc.make_loyalty_point_entry()
def validate_stock_availablility(self): def check_phone_payments(self):
allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock') for pay in self.payments:
if pay.type == "Phone" and pay.amount >= 0:
paid_amt = frappe.db.get_value("Payment Request",
filters=dict(
reference_doctype="POS Invoice", reference_name=self.name,
mode_of_payment=pay.mode_of_payment, status="Paid"),
fieldname="grand_total")
if pay.amount != paid_amt:
return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment))
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 = []
for d in self.get('items'): for d in self.get('items'):
msg = ""
if d.serial_no: if d.serial_no:
filters = { filters = { "item_code": d.item_code, "warehouse": d.warehouse }
"item_code": d.item_code,
"warehouse": d.warehouse,
"delivery_document_no": "",
"sales_invoice": ""
}
if d.batch_no: if d.batch_no:
filters["batch_no"] = d.batch_no filters["batch_no"] = d.batch_no
reserved_serial_nos, unreserved_serial_nos = get_pos_reserved_serial_nos(filters) reserved_serial_nos = get_pos_reserved_serial_nos(filters)
serial_nos = d.serial_no.split("\n") serial_nos = get_serial_nos(d.serial_no)
serial_nos = ' '.join(serial_nos).split() # remove whitespaces invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos]
invalid_serial_nos = []
for s in serial_nos: bold_invalid_serial_nos = frappe.bold(', '.join(invalid_serial_nos))
if s in reserved_serial_nos: if len(invalid_serial_nos) == 1:
invalid_serial_nos.append(s) 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))
if len(invalid_serial_nos):
multiple_nos = 's' if len(invalid_serial_nos) > 1 else ''
frappe.throw(_("Row #{}: Serial No{}. {} has already been transacted into another POS Invoice. \
Please select valid serial no.".format(d.idx, multiple_nos,
frappe.bold(', '.join(invalid_serial_nos)))), title=_("Not Available"))
else: else:
if allow_negative_stock: if allow_negative_stock:
return return
available_stock = get_stock_availability(d.item_code, d.warehouse) available_stock = get_stock_availability(d.item_code, d.warehouse)
if not (flt(available_stock) > 0): item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)
frappe.throw(_('Row #{}: Item Code: {} is not available under warehouse {}.' if flt(available_stock) <= 0:
.format(d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse))), title=_("Not Available")) msg = (_('Row #{}: Item Code: {} is not available under warehouse {}.').format(d.idx, item_code, warehouse))
elif flt(available_stock) < flt(d.qty): elif flt(available_stock) < flt(d.qty):
frappe.msgprint(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. \ msg = (_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.')
Available quantity {}.'.format(d.idx, frappe.bold(d.item_code), .format(d.idx, item_code, warehouse, qty))
frappe.bold(d.warehouse), frappe.bold(d.qty))), title=_("Not Available")) if msg:
error_msg.append(msg)
if error_msg:
frappe.throw(error_msg, title=_("Item Unavailable"), as_list=True)
def validate_serialised_or_batched_item(self): def validate_serialised_or_batched_item(self):
error_msg = []
for d in self.get("items"): for d in self.get("items"):
serialized = d.get("has_serial_no") serialized = d.get("has_serial_no")
batched = d.get("has_batch_no") batched = d.get("has_batch_no")
no_serial_selected = not d.get("serial_no") no_serial_selected = not d.get("serial_no")
no_batch_selected = not d.get("batch_no") no_batch_selected = not d.get("batch_no")
msg = ""
item_code = frappe.bold(d.item_code)
if serialized and batched and (no_batch_selected or no_serial_selected): if serialized and batched and (no_batch_selected or no_serial_selected):
frappe.throw(_('Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction.' msg = (_('Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction.')
.format(d.idx, frappe.bold(d.item_code))), title=_("Invalid Item")) .format(d.idx, item_code))
if serialized and no_serial_selected: if serialized and no_serial_selected:
frappe.throw(_('Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction.' msg = (_('Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction.')
.format(d.idx, frappe.bold(d.item_code))), title=_("Invalid Item")) .format(d.idx, item_code))
if batched and no_batch_selected: if batched and no_batch_selected:
frappe.throw(_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.' msg = (_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.')
.format(d.idx, frappe.bold(d.item_code))), title=_("Invalid Item")) .format(d.idx, item_code))
if msg:
error_msg.append(msg)
def validate_return_items(self): if error_msg:
frappe.throw(error_msg, title=_("Invalid Item"), as_list=True)
def validate_return_items_qty(self):
if not self.get("is_return"): return if not self.get("is_return"): return
for d in self.get("items"): for d in self.get("items"):
if d.get("qty") > 0: if d.get("qty") > 0:
frappe.throw(_("Row #{}: You cannot add postive quantities in a return invoice. Please remove item {} to complete the return.") frappe.throw(
.format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item")) _("Row #{}: You cannot add postive quantities in a return invoice. Please remove item {} to complete the return.")
.format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item")
)
if d.get("serial_no"):
serial_nos = get_serial_nos(d.serial_no)
for sr in serial_nos:
serial_no_exists = frappe.db.exists("POS Invoice Item", {
"parent": self.return_against,
"serial_no": ["like", d.get("serial_no")]
})
if not serial_no_exists:
bold_return_against = frappe.bold(self.return_against)
bold_serial_no = frappe.bold(sr)
frappe.throw(
_("Row #{}: Serial No {} cannot be returned since it was not transacted in original invoice {}")
.format(d.idx, bold_serial_no, bold_return_against)
)
def validate_pos_paid_amount(self): def validate_mode_of_payment(self):
if len(self.payments) == 0 and self.is_pos: if len(self.payments) == 0:
frappe.throw(_("At least one mode of payment is required for POS invoice.")) frappe.throw(_("At least one mode of payment is required for POS invoice."))
def validate_change_account(self): def validate_change_account(self):
if frappe.db.get_value("Account", self.account_for_change_amount, "company") != self.company: if self.change_amount and self.account_for_change_amount and \
frappe.db.get_value("Account", self.account_for_change_amount, "company") != self.company:
frappe.throw(_("The selected change account {} doesn't belongs to Company {}.").format(self.account_for_change_amount, self.company)) frappe.throw(_("The selected change account {} doesn't belongs to Company {}.").format(self.account_for_change_amount, self.company))
def validate_change_amount(self): def validate_change_amount(self):
@ -150,23 +187,21 @@ class POSInvoice(SalesInvoice):
self.base_change_amount = flt(self.base_paid_amount - base_grand_total + flt(self.base_write_off_amount)) self.base_change_amount = flt(self.base_paid_amount - base_grand_total + flt(self.base_write_off_amount))
if flt(self.change_amount) and not self.account_for_change_amount: if flt(self.change_amount) and not self.account_for_change_amount:
msgprint(_("Please enter Account for Change Amount"), raise_exception=1) frappe.msgprint(_("Please enter Account for Change Amount"), raise_exception=1)
def verify_payment_amount(self): def validate_payment_amount(self):
total_amount_in_payments = 0
for entry in self.payments: for entry in self.payments:
total_amount_in_payments += entry.amount
if not self.is_return and entry.amount < 0: if not self.is_return and entry.amount < 0:
frappe.throw(_("Row #{0} (Payment Table): Amount must be positive").format(entry.idx)) frappe.throw(_("Row #{0} (Payment Table): Amount must be positive").format(entry.idx))
if self.is_return and entry.amount > 0: if self.is_return and entry.amount > 0:
frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx)) frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx))
def validate_pos_return(self): if self.is_return:
if self.is_pos and self.is_return:
total_amount_in_payments = 0
for payment in self.payments:
total_amount_in_payments += payment.amount
invoice_total = self.rounded_total or self.grand_total invoice_total = self.rounded_total or self.grand_total
if total_amount_in_payments < invoice_total: if total_amount_in_payments and total_amount_in_payments < invoice_total:
frappe.throw(_("Total payments amount can't be greater than {}".format(-invoice_total))) frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total))
def validate_loyalty_transaction(self): def validate_loyalty_transaction(self):
if self.redeem_loyalty_points and (not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center): if self.redeem_loyalty_points and (not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center):
@ -220,55 +255,45 @@ class POSInvoice(SalesInvoice):
pos_profile = get_pos_profile(self.company) or {} pos_profile = get_pos_profile(self.company) or {}
self.pos_profile = pos_profile.get('name') self.pos_profile = pos_profile.get('name')
pos = {} profile = {}
if self.pos_profile: if self.pos_profile:
pos = frappe.get_doc('POS Profile', self.pos_profile) profile = frappe.get_doc('POS Profile', self.pos_profile)
if not self.get('payments') and not for_validate: if not self.get('payments') and not for_validate:
update_multi_mode_option(self, pos) update_multi_mode_option(self, profile)
if not self.account_for_change_amount: if self.is_return and not for_validate:
self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account') add_return_modes(self, profile)
if pos:
if not for_validate:
self.tax_category = pos.get("tax_category")
if profile:
if not for_validate and not self.customer: if not for_validate and not self.customer:
self.customer = pos.customer self.customer = profile.customer
self.ignore_pricing_rule = pos.ignore_pricing_rule self.ignore_pricing_rule = profile.ignore_pricing_rule
if pos.get('account_for_change_amount'): self.account_for_change_amount = profile.get('account_for_change_amount') or self.account_for_change_amount
self.account_for_change_amount = pos.get('account_for_change_amount') self.set_warehouse = profile.get('warehouse') or self.set_warehouse
if pos.get('warehouse'):
self.set_warehouse = pos.get('warehouse')
for fieldname in ('naming_series', 'currency', 'letter_head', 'tc_name', for fieldname in ('currency', 'letter_head', 'tc_name',
'company', 'select_print_heading', 'write_off_account', 'taxes_and_charges', 'company', 'select_print_heading', 'write_off_account', 'taxes_and_charges',
'write_off_cost_center', 'apply_discount_on', 'cost_center'): 'write_off_cost_center', 'apply_discount_on', 'cost_center', 'tax_category',
if (not for_validate) or (for_validate and not self.get(fieldname)): 'ignore_pricing_rule', 'company_address', 'update_stock'):
self.set(fieldname, pos.get(fieldname)) if not for_validate:
self.set(fieldname, profile.get(fieldname))
if pos.get("company_address"):
self.company_address = pos.get("company_address")
if self.customer: if self.customer:
customer_price_list, customer_group = frappe.db.get_value("Customer", self.customer, ['default_price_list', 'customer_group']) customer_price_list, customer_group = frappe.db.get_value("Customer", self.customer, ['default_price_list', 'customer_group'])
customer_group_price_list = frappe.db.get_value("Customer Group", customer_group, 'default_price_list') customer_group_price_list = frappe.db.get_value("Customer Group", customer_group, 'default_price_list')
selling_price_list = customer_price_list or customer_group_price_list or pos.get('selling_price_list') selling_price_list = customer_price_list or customer_group_price_list or profile.get('selling_price_list')
else: else:
selling_price_list = pos.get('selling_price_list') selling_price_list = profile.get('selling_price_list')
if selling_price_list: if selling_price_list:
self.set('selling_price_list', selling_price_list) self.set('selling_price_list', selling_price_list)
if not for_validate:
self.update_stock = cint(pos.get("update_stock"))
# set pos values in items # set pos values in items
for item in self.get("items"): for item in self.get("items"):
if item.get('item_code'): 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(profile.get("company"), frappe._dict(item.as_dict()), profile)
for fname, val in iteritems(profile_details): for fname, val in iteritems(profile_details):
if (not for_validate) or (for_validate and not item.get(fname)): if (not for_validate) or (for_validate and not item.get(fname)):
item.set(fname, val) item.set(fname, val)
@ -281,10 +306,13 @@ class POSInvoice(SalesInvoice):
if self.taxes_and_charges and not len(self.get("taxes")): if self.taxes_and_charges and not len(self.get("taxes")):
self.set_taxes() self.set_taxes()
return pos if not self.account_for_change_amount:
self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account')
return profile
def set_missing_values(self, for_validate=False): def set_missing_values(self, for_validate=False):
pos = self.set_pos_fields(for_validate) profile = self.set_pos_fields(for_validate)
if not self.debit_to: if not self.debit_to:
self.debit_to = get_party_account("Customer", self.customer, self.company) self.debit_to = get_party_account("Customer", self.customer, self.company)
@ -294,17 +322,15 @@ class POSInvoice(SalesInvoice):
super(SalesInvoice, self).set_missing_values(for_validate) super(SalesInvoice, self).set_missing_values(for_validate)
print_format = pos.get("print_format") if pos else None print_format = profile.get("print_format") if profile else None
if not print_format and not cint(frappe.db.get_value('Print Format', 'POS Invoice', 'disabled')): if not print_format and not cint(frappe.db.get_value('Print Format', 'POS Invoice', 'disabled')):
print_format = 'POS Invoice' print_format = 'POS Invoice'
if pos: if profile:
return { return {
"print_format": print_format, "print_format": print_format,
"allow_edit_rate": pos.get("allow_user_to_edit_rate"), "campaign": profile.get("campaign"),
"allow_edit_discount": pos.get("allow_user_to_edit_discount"), "allow_print_before_pay": profile.get("allow_print_before_pay")
"campaign": pos.get("campaign"),
"allow_print_before_pay": pos.get("allow_print_before_pay")
} }
def set_account_for_mode_of_payment(self): def set_account_for_mode_of_payment(self):
@ -313,6 +339,47 @@ class POSInvoice(SalesInvoice):
if not pay.account: if not pay.account:
pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account") pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account")
def create_payment_request(self):
for pay in self.payments:
if pay.type == "Phone":
if pay.amount <= 0:
frappe.throw(_("Payment amount cannot be less than or equal to 0"))
if not self.contact_mobile:
frappe.throw(_("Please enter the phone number first"))
payment_gateway = frappe.db.get_value("Payment Gateway Account", {
"payment_account": pay.account,
})
record = {
"payment_gateway": payment_gateway,
"dt": "POS Invoice",
"dn": self.name,
"payment_request_type": "Inward",
"party_type": "Customer",
"party": self.customer,
"mode_of_payment": pay.mode_of_payment,
"recipient_id": self.contact_mobile,
"submit_doc": True
}
return make_payment_request(**record)
def add_return_modes(doc, pos_profile):
def append_payment(payment_mode):
payment = doc.append('payments', {})
payment.default = payment_mode.default
payment.mode_of_payment = payment_mode.parent
payment.account = payment_mode.default_account
payment.type = payment_mode.type
for pos_payment_method in pos_profile.get('payments'):
pos_payment_method = pos_payment_method.as_dict()
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])
@frappe.whitelist() @frappe.whitelist()
def get_stock_availability(item_code, warehouse): def get_stock_availability(item_code, warehouse):
latest_sle = frappe.db.sql("""select qty_after_transaction latest_sle = frappe.db.sql("""select qty_after_transaction
@ -334,11 +401,9 @@ def get_stock_availability(item_code, warehouse):
sle_qty = latest_sle[0].qty_after_transaction or 0 if latest_sle else 0 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 pos_sales_qty = pos_sales_qty[0].qty or 0 if pos_sales_qty else 0
if sle_qty and pos_sales_qty and sle_qty > pos_sales_qty: if sle_qty and pos_sales_qty:
return sle_qty - pos_sales_qty return sle_qty - pos_sales_qty
else: else:
# when sle_qty is 0
# when sle_qty > 0 and pos_sales_qty is 0
return sle_qty return sle_qty
@frappe.whitelist() @frappe.whitelist()

View File

@ -7,6 +7,8 @@ import frappe
import unittest, copy, time import unittest, copy, time
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return 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
class TestPOSInvoice(unittest.TestCase): class TestPOSInvoice(unittest.TestCase):
def test_timestamp_change(self): def test_timestamp_change(self):
@ -182,8 +184,9 @@ class TestPOSInvoice(unittest.TestCase):
def test_pos_returns_with_repayment(self): def test_pos_returns_with_repayment(self):
pos = create_pos_invoice(qty = 10, do_not_save=True) pos = create_pos_invoice(qty = 10, do_not_save=True)
pos.set('payments', [])
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500}) pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500})
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500}) pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500, 'default': 1})
pos.insert() pos.insert()
pos.submit() pos.submit()
@ -200,8 +203,9 @@ class TestPOSInvoice(unittest.TestCase):
income_account = "Sales - _TC", expense_account = "Cost of Goods Sold - _TC", rate=105, income_account = "Sales - _TC", expense_account = "Cost of Goods Sold - _TC", rate=105,
cost_center = "Main - _TC", do_not_save=True) cost_center = "Main - _TC", do_not_save=True)
pos.set('payments', [])
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 50}) pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 50})
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60}) pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60, 'default': 1})
pos.insert() pos.insert()
pos.submit() pos.submit()
@ -219,17 +223,27 @@ class TestPOSInvoice(unittest.TestCase):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item 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 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
se = make_serialized_item(target_warehouse="_Test Warehouse - _TC") 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) serial_nos = get_serial_nos(se.get("items")[0].serial_no)
pos = create_pos_invoice(item=se.get("items")[0].item_code, rate=1000, do_not_save=1) pos = 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)
pos.get("items")[0].serial_no = serial_nos[0] pos.get("items")[0].serial_no = serial_nos[0]
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000}) pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000})
pos.insert() pos.insert()
pos.submit() pos.submit()
pos2 = create_pos_invoice(item=se.get("items")[0].item_code, rate=1000, do_not_save=1) 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.get("items")[0].serial_no = serial_nos[0]
pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000}) pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000})
@ -274,6 +288,119 @@ class TestPOSInvoice(unittest.TestCase):
after_redeem_lp_details = get_loyalty_program_details_with_points(inv.customer, company=inv.company, loyalty_program=inv.loyalty_program) after_redeem_lp_details = get_loyalty_program_details_with_points(inv.customer, company=inv.company, loyalty_program=inv.loyalty_program)
self.assertEqual(after_redeem_lp_details.loyalty_points, 9) self.assertEqual(after_redeem_lp_details.loyalty_points, 9)
def test_merging_into_sales_invoice_with_discount(self):
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices
frappe.db.sql("delete from `tabPOS Invoice`")
test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(rate=300, additional_discount_percentage=10, do_not_submit=1)
pos_inv.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 270
})
pos_inv.submit()
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
pos_inv2.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200
})
pos_inv2.submit()
merge_pos_invoices()
pos_inv.load_from_db()
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total")
self.assertEqual(rounded_total, 3470)
frappe.set_user("Administrator")
def test_merging_into_sales_invoice_with_discount_and_inclusive_tax(self):
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices
frappe.db.sql("delete from `tabPOS Invoice`")
test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
pos_inv.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300
})
pos_inv.append('taxes', {
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Service Tax",
"rate": 14,
'included_in_print_rate': 1
})
pos_inv.submit()
pos_inv2 = create_pos_invoice(rate=300, qty=2, do_not_submit=1)
pos_inv2.additional_discount_percentage = 10
pos_inv2.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 540
})
pos_inv2.append('taxes', {
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Service Tax",
"rate": 14,
'included_in_print_rate': 1
})
pos_inv2.submit()
merge_pos_invoices()
pos_inv.load_from_db()
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total")
self.assertEqual(rounded_total, 840)
frappe.set_user("Administrator")
def test_merging_with_validate_selling_price(self):
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices
if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"):
frappe.db.set_value("Selling Settings", "Selling Settings", "validate_selling_price", 1)
make_purchase_receipt(item_code="_Test Item", warehouse="_Test Warehouse - _TC", qty=1, rate=300)
frappe.db.sql("delete from `tabPOS Invoice`")
test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
pos_inv.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300
})
pos_inv.append('taxes', {
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Service Tax",
"rate": 14,
'included_in_print_rate': 1
})
self.assertRaises(frappe.ValidationError, pos_inv.submit)
pos_inv2 = create_pos_invoice(rate=400, do_not_submit=1)
pos_inv2.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 400
})
pos_inv2.append('taxes', {
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Service Tax",
"rate": 14,
'included_in_print_rate': 1
})
pos_inv2.submit()
merge_pos_invoices()
pos_inv2.load_from_db()
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total")
self.assertEqual(rounded_total, 400)
frappe.set_user("Administrator")
frappe.db.set_value("Selling Settings", "Selling Settings", "validate_selling_price", 0)
def create_pos_invoice(**args): def create_pos_invoice(**args):
args = frappe._dict(args) args = frappe._dict(args)
pos_profile = None pos_profile = None
@ -282,12 +409,11 @@ def create_pos_invoice(**args):
pos_profile.save() pos_profile.save()
pos_inv = frappe.new_doc("POS Invoice") pos_inv = frappe.new_doc("POS Invoice")
pos_inv.update(args)
pos_inv.update_stock = 1 pos_inv.update_stock = 1
pos_inv.is_pos = 1 pos_inv.is_pos = 1
pos_inv.pos_profile = args.pos_profile or pos_profile.name pos_inv.pos_profile = args.pos_profile or pos_profile.name
pos_inv.set_missing_values()
if args.posting_date: if args.posting_date:
pos_inv.set_posting_time = 1 pos_inv.set_posting_time = 1
pos_inv.posting_date = args.posting_date or frappe.utils.nowdate() pos_inv.posting_date = args.posting_date or frappe.utils.nowdate()
@ -299,7 +425,9 @@ def create_pos_invoice(**args):
pos_inv.return_against = args.return_against pos_inv.return_against = args.return_against
pos_inv.currency=args.currency or "INR" pos_inv.currency=args.currency or "INR"
pos_inv.conversion_rate = args.conversion_rate or 1 pos_inv.conversion_rate = args.conversion_rate or 1
pos_inv.account_for_change_amount = "Cash - _TC" pos_inv.account_for_change_amount = args.account_for_change_amount or "Cash - _TC"
pos_inv.set_missing_values()
pos_inv.append("items", { pos_inv.append("items", {
"item_code": args.item or args.item_code or "_Test Item", "item_code": args.item or args.item_code or "_Test Item",

View File

@ -24,11 +24,27 @@ class POSInvoiceMergeLog(Document):
def validate_pos_invoice_status(self): def validate_pos_invoice_status(self):
for d in self.pos_invoices: for d in self.pos_invoices:
status, docstatus = frappe.db.get_value('POS Invoice', d.pos_invoice, ['status', 'docstatus']) status, docstatus, is_return, return_against = frappe.db.get_value(
'POS Invoice', d.pos_invoice, ['status', 'docstatus', 'is_return', 'return_against'])
bold_pos_invoice = frappe.bold(d.pos_invoice)
bold_status = frappe.bold(status)
if docstatus != 1: if docstatus != 1:
frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, d.pos_invoice)) frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, bold_pos_invoice))
if status in ['Consolidated']: if status == "Consolidated":
frappe.throw(_("Row #{}: POS Invoice {} has been {}").format(d.idx, d.pos_invoice, status)) frappe.throw(_("Row #{}: POS Invoice {} has been {}").format(d.idx, bold_pos_invoice, bold_status))
if is_return and return_against and return_against not in [d.pos_invoice for d in self.pos_invoices]:
bold_return_against = frappe.bold(return_against)
return_against_status = frappe.db.get_value('POS Invoice', return_against, "status")
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 {}. ")
.format(d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated))
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)
frappe.throw(msg)
def on_submit(self): def on_submit(self):
pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
@ -36,12 +52,12 @@ class POSInvoiceMergeLog(Document):
returns = [d for d in pos_invoice_docs if d.get('is_return') == 1] returns = [d for d in pos_invoice_docs if d.get('is_return') == 1]
sales = [d for d in pos_invoice_docs if d.get('is_return') == 0] 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) sales_invoice = self.process_merging_into_sales_invoice(sales)
if len(returns): if returns:
credit_note = self.process_merging_into_credit_note(returns) credit_note = self.process_merging_into_credit_note(returns)
else:
credit_note = ""
self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log
@ -87,17 +103,28 @@ class POSInvoiceMergeLog(Document):
loyalty_amount_sum += doc.loyalty_amount loyalty_amount_sum += doc.loyalty_amount
for item in doc.get('items'): for item in doc.get('items'):
found = False
for i in items:
if (i.item_code == item.item_code and not i.serial_no and not i.batch_no and
i.uom == item.uom and i.net_rate == item.net_rate):
found = True
i.qty = i.qty + item.qty
if not found:
item.rate = item.net_rate
items.append(item) items.append(item)
for tax in doc.get('taxes'): for tax in doc.get('taxes'):
found = False found = False
for t in taxes: for t in taxes:
if t.account_head == tax.account_head and t.cost_center == tax.cost_center and t.rate == tax.rate: 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) 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) t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount)
found = True found = True
if not found: if not found:
tax.charge_type = 'Actual' 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
taxes.append(tax) taxes.append(tax)
for payment in doc.get('payments'): for payment in doc.get('payments'):
@ -118,6 +145,8 @@ class POSInvoiceMergeLog(Document):
invoice.set('items', items) invoice.set('items', items)
invoice.set('payments', payments) invoice.set('payments', payments)
invoice.set('taxes', taxes) invoice.set('taxes', taxes)
invoice.additional_discount_percentage = 0
invoice.discount_amount = 0.0
return invoice return invoice

View File

@ -5,21 +5,37 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import cint from frappe.utils import cint, get_link_to_form
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.controllers.status_updater import StatusUpdater from erpnext.controllers.status_updater import StatusUpdater
class POSOpeningEntry(StatusUpdater): class POSOpeningEntry(StatusUpdater):
def validate(self): def validate(self):
self.validate_pos_profile_and_cashier() self.validate_pos_profile_and_cashier()
self.validate_payment_method_account()
self.set_status() self.set_status()
def validate_pos_profile_and_cashier(self): def validate_pos_profile_and_cashier(self):
if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"): if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"):
frappe.throw(_("POS Profile {} does not belongs to company {}".format(self.pos_profile, self.company))) frappe.throw(_("POS Profile {} does not belongs to company {}").format(self.pos_profile, self.company))
if not cint(frappe.db.get_value("User", self.user, "enabled")): if not cint(frappe.db.get_value("User", self.user, "enabled")):
frappe.throw(_("User {} has been disabled. Please select valid user/cashier".format(self.user))) frappe.throw(_("User {} is disabled. Please select valid user/cashier").format(self.user))
def validate_payment_method_account(self):
invalid_modes = []
for d in self.balance_details:
account = frappe.db.get_value("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))
if invalid_modes:
if invalid_modes == 1:
msg = _("Please set default Cash or Bank account in Mode of Payment {}")
else:
msg = _("Please set default Cash or Bank account in Mode of Payments {}")
frappe.throw(msg.format(", ".join(invalid_modes)), title=_("Missing Account"))
def on_submit(self): def on_submit(self):
self.set_status(update=True) self.set_status(update=True)

View File

@ -6,6 +6,7 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"default", "default",
"allow_in_returns",
"mode_of_payment" "mode_of_payment"
], ],
"fields": [ "fields": [
@ -24,11 +25,19 @@
"label": "Mode of Payment", "label": "Mode of Payment",
"options": "Mode of Payment", "options": "Mode of Payment",
"reqd": 1 "reqd": 1
},
{
"default": "0",
"fieldname": "allow_in_returns",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Allow In Returns"
} }
], ],
"index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-05-29 15:08:41.704844", "modified": "2020-10-20 12:58:46.114456",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Payment Method", "name": "POS Payment Method",

View File

@ -15,15 +15,6 @@ frappe.ui.form.on("POS Profile", "onload", function(frm) {
erpnext.queries.setup_queries(frm, "Warehouse", function() { erpnext.queries.setup_queries(frm, "Warehouse", function() {
return erpnext.queries.warehouse(frm.doc); return erpnext.queries.warehouse(frm.doc);
}); });
frm.call({
method: "erpnext.accounts.doctype.pos_profile.pos_profile.get_series",
callback: function(r) {
if(!r.exc) {
set_field_options("naming_series", r.message);
}
}
});
}); });
frappe.ui.form.on('POS Profile', { frappe.ui.form.on('POS Profile', {
@ -31,8 +22,7 @@ frappe.ui.form.on('POS Profile', {
frm.set_query("print_format", function() { frm.set_query("print_format", function() {
return { return {
filters: [ filters: [
['Print Format', 'doc_type', '=', 'Sales Invoice'], ['Print Format', 'doc_type', '=', 'POS Invoice']
['Print Format', 'print_format_type', '=', 'Jinja'],
] ]
}; };
}); });
@ -45,10 +35,6 @@ frappe.ui.form.on('POS Profile', {
}; };
}); });
frm.set_query("print_format", function() {
return { filters: { doc_type: "Sales Invoice", print_format_type: "JS"} };
});
frm.set_query('company_address', function(doc) { frm.set_query('company_address', function(doc) {
if(!doc.company) { if(!doc.company) {
frappe.throw(__('Please set Company')); frappe.throw(__('Please set Company'));

View File

@ -8,13 +8,13 @@
"field_order": [ "field_order": [
"disabled", "disabled",
"section_break_2", "section_break_2",
"naming_series",
"customer", "customer",
"company", "company",
"country", "country",
"column_break_9", "column_break_9",
"update_stock", "update_stock",
"ignore_pricing_rule", "ignore_pricing_rule",
"hide_unavailable_items",
"warehouse", "warehouse",
"campaign", "campaign",
"company_address", "company_address",
@ -59,17 +59,6 @@
"fieldname": "section_break_2", "fieldname": "section_break_2",
"fieldtype": "Section Break" "fieldtype": "Section Break"
}, },
{
"fieldname": "naming_series",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Series",
"no_copy": 1,
"oldfieldname": "naming_series",
"oldfieldtype": "Select",
"options": "[Select]",
"reqd": 1
},
{ {
"fieldname": "customer", "fieldname": "customer",
"fieldtype": "Link", "fieldtype": "Link",
@ -307,23 +296,31 @@
"options": "Warehouse", "options": "Warehouse",
"reqd": 1 "reqd": 1
}, },
{
"default": "0",
"fieldname": "update_stock",
"fieldtype": "Check",
"label": "Update Stock"
},
{ {
"default": "0", "default": "0",
"fieldname": "ignore_pricing_rule", "fieldname": "ignore_pricing_rule",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Ignore Pricing Rule" "label": "Ignore Pricing Rule"
},
{
"default": "1",
"fieldname": "update_stock",
"fieldtype": "Check",
"label": "Update Stock",
"read_only": 1
},
{
"default": "0",
"fieldname": "hide_unavailable_items",
"fieldtype": "Check",
"label": "Hide Unavailable Items"
} }
], ],
"icon": "icon-cog", "icon": "icon-cog",
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2020-06-29 12:20:30.977272", "modified": "2020-10-29 13:18:38.795925",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Profile", "name": "POS Profile",

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import msgprint, _ from frappe import msgprint, _
from frappe.utils import cint, now from frappe.utils import cint, now, get_link_to_form
from six import iteritems from six import iteritems
from frappe.model.document import Document from frappe.model.document import Document
@ -13,7 +13,7 @@ class POSProfile(Document):
self.validate_default_profile() self.validate_default_profile()
self.validate_all_link_fields() self.validate_all_link_fields()
self.validate_duplicate_groups() self.validate_duplicate_groups()
self.check_default_payment() self.validate_payment_methods()
def validate_default_profile(self): def validate_default_profile(self):
for row in self.applicable_for_users: for row in self.applicable_for_users:
@ -52,14 +52,33 @@ class POSProfile(Document):
if len(customer_groups) != len(set(customer_groups)): if len(customer_groups) != len(set(customer_groups)):
frappe.throw(_("Duplicate customer group found in the cutomer group table"), title = "Duplicate Customer Group") frappe.throw(_("Duplicate customer group found in the cutomer group table"), title = "Duplicate Customer Group")
def check_default_payment(self): def validate_payment_methods(self):
if self.payments: if not self.payments:
default_mode_of_payment = [d.default for d in self.payments if d.default] frappe.throw(_("Payment methods are mandatory. Please add at least one payment method."))
if not default_mode_of_payment:
frappe.throw(_("Set default mode of payment"))
if len(default_mode_of_payment) > 1: default_mode = [d.default for d in self.payments if d.default]
frappe.throw(_("Multiple default mode of payment is not allowed")) if not default_mode:
frappe.throw(_("Please select a default mode of payment"))
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",
{"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))
if invalid_modes:
if invalid_modes == 1:
msg = _("Please set default Cash or Bank account in Mode of Payment {}")
else:
msg = _("Please set default Cash or Bank account in Mode of Payments {}")
frappe.throw(msg.format(", ".join(invalid_modes)), title=_("Missing Account"))
def on_update(self): def on_update(self):
self.set_defaults() self.set_defaults()
@ -100,10 +119,6 @@ def get_child_nodes(group_type, root):
return frappe.db.sql(""" Select name, lft, rgt from `tab{tab}` where return frappe.db.sql(""" Select name, lft, rgt from `tab{tab}` where
lft >= {lft} and rgt <= {rgt} order by lft""".format(tab=group_type, lft=lft, rgt=rgt), as_dict=1) lft >= {lft} and rgt <= {rgt} order by lft""".format(tab=group_type, lft=lft, rgt=rgt), as_dict=1)
@frappe.whitelist()
def get_series():
return frappe.get_meta("POS Invoice").get_field("naming_series").options or "s"
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def pos_profile_query(doctype, txt, searchfield, start, page_len, filters): def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):

View File

@ -8,6 +8,8 @@ import unittest
from erpnext.stock.get_item_details import get_pos_profile from erpnext.stock.get_item_details import get_pos_profile
from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes
test_dependencies = ['Item']
class TestPOSProfile(unittest.TestCase): class TestPOSProfile(unittest.TestCase):
def test_pos_profile(self): def test_pos_profile(self):
make_pos_profile() make_pos_profile()

View File

@ -7,10 +7,9 @@ frappe.ui.form.on('POS Settings', {
}, },
get_invoice_fields: function(frm) { get_invoice_fields: function(frm) {
frappe.model.with_doctype("Sales Invoice", () => { frappe.model.with_doctype("POS Invoice", () => {
var fields = $.map(frappe.get_doc("DocType", "Sales Invoice").fields, function(d) { var fields = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function(d) {
if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 || if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 || ['Button'].includes(d.fieldtype)) {
d.fieldtype === 'Table') {
return { label: d.label + ' (' + d.fieldtype + ')', value: d.fieldname }; return { label: d.label + ' (' + d.fieldtype + ')', value: d.fieldname };
} else { } else {
return null; return null;
@ -25,7 +24,7 @@ frappe.ui.form.on('POS Settings', {
frappe.ui.form.on("POS Field", { frappe.ui.form.on("POS Field", {
fieldname: function(frm, doctype, name) { fieldname: function(frm, doctype, name) {
var doc = frappe.get_doc(doctype, name); var doc = frappe.get_doc(doctype, name);
var df = $.map(frappe.get_doc("DocType", "Sales Invoice").fields, function(d) { var df = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function(d) {
return doc.fieldname == d.fieldname ? d : null; return doc.fieldname == d.fieldname ? d : null;
})[0]; })[0];

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
"autoname": "field:title", "autoname": "field:title",
@ -71,6 +72,7 @@
"section_break_13", "section_break_13",
"threshold_percentage", "threshold_percentage",
"priority", "priority",
"condition",
"column_break_66", "column_break_66",
"apply_multiple_pricing_rules", "apply_multiple_pricing_rules",
"apply_discount_on_rate", "apply_discount_on_rate",
@ -502,10 +504,10 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "eval:in_list(['Discount Percentage', 'Discount Amount'], doc.rate_or_discount) && doc.apply_multiple_pricing_rules", "depends_on": "eval:in_list(['Discount Percentage'], doc.rate_or_discount) && doc.apply_multiple_pricing_rules",
"fieldname": "apply_discount_on_rate", "fieldname": "apply_discount_on_rate",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Apply Discount on Rate" "label": "Apply Discount on Discounted Rate"
}, },
{ {
"default": "0", "default": "0",
@ -550,11 +552,18 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Promotional Scheme", "label": "Promotional Scheme",
"options": "Promotional Scheme" "options": "Promotional Scheme"
},
{
"description": "Simple Python Expression, Example: territory != 'All Territories'",
"fieldname": "condition",
"fieldtype": "Code",
"label": "Condition"
} }
], ],
"icon": "fa fa-gift", "icon": "fa fa-gift",
"idx": 1, "idx": 1,
"modified": "2019-12-18 17:29:22.957077", "links": [],
"modified": "2020-10-28 16:53:14.416172",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Pricing Rule", "name": "Pricing Rule",

View File

@ -1,5 +1,4 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
# For license information, please see license.txt # For license information, please see license.txt
@ -7,9 +6,10 @@ from __future__ import unicode_literals
import frappe import frappe
import json import json
import copy import copy
import re
from frappe import throw, _ from frappe import throw, _
from frappe.utils import flt, cint, getdate from frappe.utils import flt, cint, getdate
from frappe.model.document import Document from frappe.model.document import Document
from six import string_types from six import string_types
@ -31,6 +31,7 @@ class PricingRule(Document):
self.validate_max_discount() self.validate_max_discount()
self.validate_price_list_with_currency() self.validate_price_list_with_currency()
self.validate_dates() self.validate_dates()
self.validate_condition()
if not self.margin_type: self.margin_rate_or_amount = 0.0 if not self.margin_type: self.margin_rate_or_amount = 0.0
@ -59,6 +60,15 @@ class PricingRule(Document):
if self.price_or_product_discount == 'Price' and not self.rate_or_discount: if self.price_or_product_discount == 'Price' and not self.rate_or_discount:
throw(_("Rate or Discount is required for the price discount."), frappe.MandatoryError) throw(_("Rate or Discount is required for the price discount."), frappe.MandatoryError)
if self.apply_discount_on_rate:
if not self.priority:
throw(_("As the field {0} is enabled, the field {1} is mandatory.")
.format(frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority")))
if self.priority and cint(self.priority) == 1:
throw(_("As the field {0} is enabled, the value of the field {1} should be more than 1.")
.format(frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority")))
def validate_applicable_for_selling_or_buying(self): def validate_applicable_for_selling_or_buying(self):
if not self.selling and not self.buying: if not self.selling and not self.buying:
throw(_("Atleast one of the Selling or Buying must be selected")) throw(_("Atleast one of the Selling or Buying must be selected"))
@ -141,6 +151,10 @@ class PricingRule(Document):
if self.valid_from and self.valid_upto and getdate(self.valid_from) > getdate(self.valid_upto): if self.valid_from and self.valid_upto and getdate(self.valid_from) > getdate(self.valid_upto):
frappe.throw(_("Valid from date must be less than valid upto date")) 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):
frappe.throw(_("Invalid condition expression"))
#-------------------------------------------------------------------------------- #--------------------------------------------------------------------------------
@frappe.whitelist() @frappe.whitelist()
@ -221,12 +235,11 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
item_details = frappe._dict({ item_details = frappe._dict({
"doctype": args.doctype, "doctype": args.doctype,
"has_margin": False,
"name": args.name, "name": args.name,
"parent": args.parent, "parent": args.parent,
"parenttype": args.parenttype, "parenttype": args.parenttype,
"child_docname": args.get('child_docname'), "child_docname": args.get('child_docname')
"discount_percentage_on_rate": [],
"discount_amount_on_rate": []
}) })
if args.ignore_pricing_rule or not args.item_code: if args.ignore_pricing_rule or not args.item_code:
@ -237,7 +250,7 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
update_args_for_pricing_rule(args) update_args_for_pricing_rule(args)
pricing_rules = (get_applied_pricing_rules(args) pricing_rules = (get_applied_pricing_rules(args.get('pricing_rules'))
if for_validate and args.get("pricing_rules") else get_pricing_rules(args, doc)) if for_validate and args.get("pricing_rules") else get_pricing_rules(args, doc))
if pricing_rules: if pricing_rules:
@ -274,6 +287,10 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
else: else:
get_product_discount_rule(pricing_rule, item_details, args, doc) get_product_discount_rule(pricing_rule, item_details, args, doc)
if not item_details.get("has_margin"):
item_details.margin_type = None
item_details.margin_rate_or_amount = 0.0
item_details.has_pricing_rule = 1 item_details.has_pricing_rule = 1
item_details.pricing_rules = frappe.as_json([d.pricing_rule for d in rules]) item_details.pricing_rules = frappe.as_json([d.pricing_rule for d in rules])
@ -325,13 +342,11 @@ def get_pricing_rule_details(args, pricing_rule):
def apply_price_discount_rule(pricing_rule, item_details, args): def apply_price_discount_rule(pricing_rule, item_details, args):
item_details.pricing_rule_for = pricing_rule.rate_or_discount item_details.pricing_rule_for = pricing_rule.rate_or_discount
if ((pricing_rule.margin_type == 'Amount' and pricing_rule.currency == args.currency) if ((pricing_rule.margin_type in ['Amount', 'Percentage'] and pricing_rule.currency == args.currency)
or (pricing_rule.margin_type == 'Percentage')): or (pricing_rule.margin_type == 'Percentage')):
item_details.margin_type = pricing_rule.margin_type item_details.margin_type = pricing_rule.margin_type
item_details.margin_rate_or_amount = pricing_rule.margin_rate_or_amount item_details.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
else: item_details.has_margin = True
item_details.margin_type = None
item_details.margin_rate_or_amount = 0.0
if pricing_rule.rate_or_discount == 'Rate': if pricing_rule.rate_or_discount == 'Rate':
pricing_rule_rate = 0.0 pricing_rule_rate = 0.0
@ -346,9 +361,9 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
if pricing_rule.rate_or_discount != apply_on: continue if pricing_rule.rate_or_discount != apply_on: continue
field = frappe.scrub(apply_on) field = frappe.scrub(apply_on)
if pricing_rule.apply_discount_on_rate: if pricing_rule.apply_discount_on_rate and item_details.get("discount_percentage"):
discount_field = "{0}_on_rate".format(field) # Apply discount on discounted rate
item_details[discount_field].append(pricing_rule.get(field, 0)) item_details[field] += ((100 - item_details[field]) * (pricing_rule.get(field, 0) / 100))
else: else:
if field not in item_details: if field not in item_details:
item_details.setdefault(field, 0) item_details.setdefault(field, 0)
@ -356,17 +371,10 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
item_details[field] += (pricing_rule.get(field, 0) item_details[field] += (pricing_rule.get(field, 0)
if pricing_rule else args.get(field, 0)) if pricing_rule else args.get(field, 0))
def set_discount_amount(rate, item_details):
for field in ['discount_percentage_on_rate', 'discount_amount_on_rate']:
for d in item_details.get(field):
dis_amount = (rate * d / 100
if field == 'discount_percentage_on_rate' else d)
rate -= dis_amount
item_details.rate = rate
def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None): def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None):
from erpnext.accounts.doctype.pricing_rule.utils import get_pricing_rule_items from erpnext.accounts.doctype.pricing_rule.utils import (get_applied_pricing_rules,
for d in json.loads(pricing_rules): get_pricing_rule_items)
for d in get_applied_pricing_rules(pricing_rules):
if not d or not frappe.db.exists("Pricing Rule", d): continue if not d or not frappe.db.exists("Pricing Rule", d): continue
pricing_rule = frappe.get_cached_doc('Pricing Rule', d) pricing_rule = frappe.get_cached_doc('Pricing Rule', d)

View File

@ -430,6 +430,60 @@ class TestPricingRule(unittest.TestCase):
self.assertTrue(details) self.assertTrue(details)
def test_pricing_rule_for_condition(self):
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
make_pricing_rule(selling=1, margin_type="Percentage", \
condition="customer=='_Test Customer 1' and is_return==0", discount_percentage=10)
# Incorrect Customer and Correct is_return value
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 2", is_return=0)
si.items[0].price_list_rate = 1000
si.submit()
item = si.items[0]
self.assertEquals(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)
# 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)
def test_multiple_pricing_rules(self):
make_pricing_rule(discount_percentage=20, selling=1, priority=1, apply_multiple_pricing_rules=1,
title="_Test Pricing Rule 1")
make_pricing_rule(discount_percentage=10, selling=1, title="_Test Pricing Rule 2", priority=2,
apply_multiple_pricing_rules=1)
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1)
self.assertEqual(si.items[0].discount_percentage, 30)
si.delete()
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
def test_multiple_pricing_rules_with_apply_discount_on_discounted_rate(self):
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
make_pricing_rule(discount_percentage=20, selling=1, priority=1, apply_multiple_pricing_rules=1,
title="_Test Pricing Rule 1")
make_pricing_rule(discount_percentage=10, selling=1, priority=2,
apply_discount_on_rate=1, title="_Test Pricing Rule 2", apply_multiple_pricing_rules=1)
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1)
self.assertEqual(si.items[0].discount_percentage, 28)
si.delete()
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
def make_pricing_rule(**args): def make_pricing_rule(**args):
args = frappe._dict(args) args = frappe._dict(args)
@ -441,6 +495,7 @@ def make_pricing_rule(**args):
"applicable_for": args.applicable_for, "applicable_for": args.applicable_for,
"selling": args.selling or 0, "selling": args.selling or 0,
"currency": "USD", "currency": "USD",
"apply_discount_on_rate": args.apply_discount_on_rate or 0,
"buying": args.buying or 0, "buying": args.buying or 0,
"min_qty": args.min_qty or 0.0, "min_qty": args.min_qty or 0.0,
"max_qty": args.max_qty or 0.0, "max_qty": args.max_qty or 0.0,
@ -448,9 +503,14 @@ def make_pricing_rule(**args):
"discount_percentage": args.discount_percentage or 0.0, "discount_percentage": args.discount_percentage or 0.0,
"rate": args.rate or 0.0, "rate": args.rate or 0.0,
"margin_type": args.margin_type, "margin_type": args.margin_type,
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0 "margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
"condition": args.condition or '',
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0
}) })
if args.get("priority"):
doc.priority = args.get("priority")
apply_on = doc.apply_on.replace(' ', '_').lower() apply_on = doc.apply_on.replace(' ', '_').lower()
child_table = {'Item Code': 'items', 'Item Group': 'item_groups', 'Brand': 'brands'} child_table = {'Item Code': 'items', 'Item Group': 'item_groups', 'Brand': 'brands'}
doc.append(child_table.get(doc.apply_on), { doc.append(child_table.get(doc.apply_on), {

View File

@ -14,9 +14,8 @@ import frappe
from erpnext.setup.doctype.item_group.item_group import get_child_item_groups from erpnext.setup.doctype.item_group.item_group import get_child_item_groups
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.get_item_details import get_conversion_factor
from frappe import _, throw from frappe import _, bold
from frappe.utils import cint, flt, get_datetime, get_link_to_form, getdate, today from frappe.utils import cint, flt, get_link_to_form, getdate, today, fmt_money
class MultiplePricingRuleConflict(frappe.ValidationError): pass class MultiplePricingRuleConflict(frappe.ValidationError): pass
@ -37,9 +36,12 @@ def get_pricing_rules(args, doc=None):
rules = [] rules = []
pricing_rules = filter_pricing_rule_based_on_condition(pricing_rules, doc)
if not pricing_rules: return [] if not pricing_rules: return []
if apply_multiple_pricing_rules(pricing_rules): if apply_multiple_pricing_rules(pricing_rules):
pricing_rules = sorted_by_priority(pricing_rules)
for pricing_rule in pricing_rules: for pricing_rule in pricing_rules:
pricing_rule = filter_pricing_rules(args, pricing_rule, doc) pricing_rule = filter_pricing_rules(args, pricing_rule, doc)
if pricing_rule: if pricing_rule:
@ -51,6 +53,37 @@ def get_pricing_rules(args, doc=None):
return rules return rules
def sorted_by_priority(pricing_rules):
# If more than one pricing rules, then sort by priority
pricing_rules_list = []
pricing_rule_dict = {}
for pricing_rule in pricing_rules:
if not pricing_rule.get("priority"): continue
pricing_rule_dict.setdefault(cint(pricing_rule.get("priority")), []).append(pricing_rule)
for key in sorted(pricing_rule_dict):
pricing_rules_list.append(pricing_rule_dict.get(key))
return pricing_rules_list or pricing_rules
def filter_pricing_rule_based_on_condition(pricing_rules, doc=None):
filtered_pricing_rules = []
if doc:
for pricing_rule in pricing_rules:
if pricing_rule.condition:
try:
if frappe.safe_eval(pricing_rule.condition, None, doc.as_dict()):
filtered_pricing_rules.append(pricing_rule)
except:
pass
else:
filtered_pricing_rules.append(pricing_rule)
else:
filtered_pricing_rules = pricing_rules
return filtered_pricing_rules
def _get_pricing_rules(apply_on, args, values): def _get_pricing_rules(apply_on, args, values):
apply_on_field = frappe.scrub(apply_on) apply_on_field = frappe.scrub(apply_on)
@ -265,12 +298,13 @@ def validate_quantity_and_amount_for_suggestion(args, qty, amount, item_code, tr
fieldname = field fieldname = field
if fieldname: if fieldname:
msg = _("""If you {0} {1} quantities of the item <b>{2}</b>, the scheme <b>{3}</b> msg = (_("If you {0} {1} quantities of the item {2}, the scheme {3} will be applied on the item.")
will be applied on the item.""").format(type_of_transaction, args.get(fieldname), item_code, args.rule_description) .format(type_of_transaction, args.get(fieldname), bold(item_code), bold(args.rule_description)))
if fieldname in ['min_amt', 'max_amt']: if fieldname in ['min_amt', 'max_amt']:
msg = _("""If you {0} {1} worth item <b>{2}</b>, the scheme <b>{3}</b> will be applied on the item. msg = (_("If you {0} {1} worth item {2}, the scheme {3} will be applied on the item.")
""").format(frappe.fmt_money(type_of_transaction, args.get(fieldname)), item_code, args.rule_description) .format(type_of_transaction, fmt_money(args.get(fieldname), currency=args.get("currency")),
bold(item_code), bold(args.rule_description)))
frappe.msgprint(msg) frappe.msgprint(msg)
@ -447,9 +481,14 @@ def apply_pricing_rule_on_transaction(doc):
apply_pricing_rule_for_free_items(doc, item_details.free_item_data) apply_pricing_rule_for_free_items(doc, item_details.free_item_data)
doc.set_missing_values() doc.set_missing_values()
def get_applied_pricing_rules(item_row): def get_applied_pricing_rules(pricing_rules):
return (json.loads(item_row.get("pricing_rules")) if pricing_rules:
if item_row.get("pricing_rules") else []) if pricing_rules.startswith('['):
return json.loads(pricing_rules)
else:
return pricing_rules.split(',')
return []
def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None): def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
free_item = pricing_rule.free_item free_item = pricing_rule.free_item

View File

@ -10,13 +10,15 @@ frappe.ui.form.on('Process Deferred Accounting', {
} }
}; };
}); });
},
if (frm.doc.company) { type: function(frm) {
if (frm.doc.company && frm.doc.type) {
frm.set_query("account", function() { frm.set_query("account", function() {
return { return {
filters: { filters: {
'company': frm.doc.company, 'company': frm.doc.company,
'root_type': 'Liability', 'root_type': frm.doc.type === 'Income' ? 'Liability' : 'Asset',
'is_group': 0 'is_group': 0
} }
}; };

View File

@ -60,6 +60,7 @@
"reqd": 1 "reqd": 1
}, },
{ {
"depends_on": "eval: doc.type",
"fieldname": "account", "fieldname": "account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Account", "label": "Account",
@ -73,9 +74,10 @@
"reqd": 1 "reqd": 1
} }
], ],
"index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-02-06 18:18:09.852844", "modified": "2020-09-03 18:07:02.463754",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Process Deferred Accounting", "name": "Process Deferred Accounting",

View File

@ -0,0 +1,89 @@
<h1 class="text-center" style="page-break-before:always">{{ filters.party[0] }}</h1>
<h3 class="text-center">{{ _("Statement of Accounts") }}</h3>
<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 %}
<tr>
{% if(row.posting_date) %}
<td>{{ frappe.format(row.posting_date, 'Date') }}</td>
<td>{{ row.voucher_type }}
<br>{{ row.voucher_no }}</td>
<td>
{% if not (filters.party or filters.account) %}
{{ row.party or row.account }}
<br>
{% endif %}
{{ _("Against") }}: {{ row.against }}
<br>{{ _("Remarks") }}: {{ row.remarks }}
{% if row.bill_no %}
<br>{{ _("Supplier Invoice No") }}: {{ row.bill_no }}
{% endif %}
</td>
<td style="text-align: right">
{{ frappe.utils.fmt_money(row.debit, filters.presentation_currency) }}</td>
<td style="text-align: right">
{{ frappe.utils.fmt_money(row.credit, 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) }}
</td>
<td style="text-align: right">
{{ row.account and frappe.utils.fmt_money(row.credit, filters.presentation_currency) }}
</td>
{% endif %}
<td style="text-align: right">
{{ frappe.utils.fmt_money(row.balance, 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>

View File

@ -0,0 +1,132 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Process Statement Of Accounts', {
view_properties: function(frm) {
frappe.route_options = {doc_type: 'Customer'};
frappe.set_route("Form", "Customize Form");
},
refresh: function(frm){
if(!frm.doc.__islocal) {
frm.add_custom_button('Send Emails',function(){
frappe.call({
method: "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_emails",
args: {
"document_name": frm.doc.name,
},
callback: function(r) {
if(r && r.message) {
frappe.show_alert({message: __('Emails Queued'), indicator: 'blue'});
}
else{
frappe.msgprint('No Records for these settings.')
}
}
});
});
frm.add_custom_button('Download',function(){
var url = frappe.urllib.get_full_url(
'/api/method/erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.download_statements?'
+ 'document_name='+encodeURIComponent(frm.doc.name))
$.ajax({
url: url,
type: 'GET',
success: function(result) {
if(jQuery.isEmptyObject(result)){
frappe.msgprint('No Records for these settings.');
}
else{
window.location = url;
}
}
});
});
}
},
onload: function(frm) {
frm.set_query('currency', function(){
return {
filters: {
'enabled': 1
}
}
});
if(frm.doc.__islocal){
frm.set_value('from_date', frappe.datetime.add_months(frappe.datetime.get_today(), -1));
frm.set_value('to_date', frappe.datetime.get_today());
}
},
customer_collection: function(frm){
frm.set_value('collection_name', '');
if(frm.doc.customer_collection){
frm.get_field('collection_name').set_label(frm.doc.customer_collection);
}
},
frequency: function(frm){
if(frm.doc.frequency != ''){
frm.set_value('start_date', frappe.datetime.get_today());
}
else{
frm.set_value('start_date', '');
}
},
fetch_customers: function(frm){
if(frm.doc.collection_name){
frappe.call({
method: "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.fetch_customers",
args: {
'customer_collection': frm.doc.customer_collection,
'collection_name': frm.doc.collection_name,
'primary_mandatory': frm.doc.primary_mandatory
},
callback: function(r) {
if(!r.exc) {
if(r.message.length){
frm.clear_table('customers');
for (const customer of r.message){
var row = frm.add_child('customers');
row.customer = customer.name;
row.primary_email = customer.primary_email;
row.billing_email = customer.billing_email;
}
frm.refresh_field('customers');
}
else{
frappe.throw('No Customers found with selected options.');
}
}
}
});
}
else {
frappe.throw('Enter ' + frm.doc.customer_collection + ' name.');
}
}
});
frappe.ui.form.on('Process Statement Of Accounts Customer', {
customer: function(frm, cdt, cdn){
var row = locals[cdt][cdn];
if (!row.customer){
return;
}
frappe.call({
method: 'erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.get_customer_emails',
args: {
'customer_name': row.customer,
'primary_mandatory': frm.doc.primary_mandatory
},
callback: function(r){
if(!r.exe){
if(r.message.length){
frappe.model.set_value(cdt, cdn, "primary_email", r.message[0])
frappe.model.set_value(cdt, cdn, "billing_email", r.message[1])
}
else {
return
}
}
}
})
}
});

View File

@ -0,0 +1,310 @@
{
"actions": [],
"allow_workflow": 1,
"autoname": "Prompt",
"creation": "2020-05-22 16:46:18.712954",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"section_break_11",
"from_date",
"company",
"account",
"group_by",
"cost_center",
"column_break_14",
"to_date",
"finance_book",
"currency",
"project",
"section_break_3",
"customer_collection",
"collection_name",
"fetch_customers",
"column_break_6",
"primary_mandatory",
"column_break_17",
"customers",
"preferences",
"orientation",
"section_break_14",
"include_ageing",
"ageing_based_on",
"section_break_1",
"enable_auto_email",
"section_break_18",
"frequency",
"filter_duration",
"column_break_21",
"start_date",
"section_break_33",
"subject",
"column_break_28",
"cc_to",
"section_break_30",
"body",
"help_text"
],
"fields": [
{
"fieldname": "frequency",
"fieldtype": "Select",
"label": "Frequency",
"options": "Weekly\nMonthly\nQuarterly"
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"depends_on": "eval:doc.enable_auto_email == 0;",
"fieldname": "from_date",
"fieldtype": "Date",
"label": "From Date",
"mandatory_depends_on": "eval:doc.frequency == '';"
},
{
"depends_on": "eval:doc.enable_auto_email == 0;",
"fieldname": "to_date",
"fieldtype": "Date",
"label": "To Date",
"mandatory_depends_on": "eval:doc.frequency == '';"
},
{
"fieldname": "cost_center",
"fieldtype": "Table MultiSelect",
"label": "Cost Center",
"options": "PSOA Cost Center"
},
{
"fieldname": "project",
"fieldtype": "Table MultiSelect",
"label": "Project",
"options": "PSOA Project"
},
{
"fieldname": "section_break_3",
"fieldtype": "Section Break",
"label": "Customers"
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_11",
"fieldtype": "Section Break",
"label": "General Ledger Filters"
},
{
"fieldname": "column_break_14",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_17",
"fieldtype": "Section Break",
"hide_border": 1
},
{
"fieldname": "customer_collection",
"fieldtype": "Select",
"label": "Select Customers By",
"options": "\nCustomer Group\nTerritory\nSales Partner\nSales Person"
},
{
"depends_on": "eval: doc.customer_collection !== ''",
"fieldname": "collection_name",
"fieldtype": "Dynamic Link",
"label": "Recipient",
"options": "customer_collection"
},
{
"fieldname": "section_break_1",
"fieldtype": "Section Break",
"label": "Email Settings"
},
{
"fieldname": "account",
"fieldtype": "Link",
"label": "Account",
"options": "Account"
},
{
"fieldname": "finance_book",
"fieldtype": "Link",
"label": "Finance Book",
"options": "Finance Book"
},
{
"fieldname": "preferences",
"fieldtype": "Section Break",
"label": "Print Preferences"
},
{
"fieldname": "orientation",
"fieldtype": "Select",
"label": "Orientation",
"options": "Landscape\nPortrait"
},
{
"default": "Today",
"fieldname": "start_date",
"fieldtype": "Date",
"label": "Start Date"
},
{
"default": "Group by Voucher (Consolidated)",
"fieldname": "group_by",
"fieldtype": "Select",
"label": "Group By",
"options": "\nGroup by Voucher\nGroup by Voucher (Consolidated)"
},
{
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
"options": "Currency"
},
{
"default": "0",
"fieldname": "include_ageing",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Include Ageing Summary"
},
{
"default": "Due Date",
"depends_on": "eval:doc.include_ageing === 1",
"fieldname": "ageing_based_on",
"fieldtype": "Select",
"label": "Ageing Based On",
"options": "Due Date\nPosting Date"
},
{
"default": "0",
"fieldname": "enable_auto_email",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Enable Auto Email"
},
{
"fieldname": "section_break_14",
"fieldtype": "Column Break",
"hide_border": 1
},
{
"depends_on": "eval: doc.enable_auto_email ==1",
"fieldname": "section_break_18",
"fieldtype": "Section Break",
"hide_border": 1
},
{
"fieldname": "column_break_21",
"fieldtype": "Column Break"
},
{
"depends_on": "eval: doc.customer_collection !== ''",
"fieldname": "fetch_customers",
"fieldtype": "Button",
"label": "Fetch Customers",
"options": "fetch_customers",
"print_hide": 1,
"report_hide": 1
},
{
"default": "1",
"fieldname": "primary_mandatory",
"fieldtype": "Check",
"label": "Send To Primary Contact"
},
{
"fieldname": "cc_to",
"fieldtype": "Link",
"label": "CC To",
"options": "User"
},
{
"default": "1",
"fieldname": "filter_duration",
"fieldtype": "Int",
"label": "Filter Duration (Months)"
},
{
"fieldname": "customers",
"fieldtype": "Table",
"label": "Customers",
"options": "Process Statement Of Accounts Customer",
"reqd": 1
},
{
"fieldname": "column_break_28",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_30",
"fieldtype": "Section Break",
"hide_border": 1
},
{
"fieldname": "section_break_33",
"fieldtype": "Section Break",
"hide_border": 1
},
{
"fieldname": "help_text",
"fieldtype": "HTML",
"label": "Help Text",
"options": "<br>\n<h4>Note</h4>\n<ul>\n<li>\nYou can use <a href=\"https://jinja.palletsprojects.com/en/2.11.x/\" target=\"_blank\">Jinja tags</a> in <b>Subject</b> and <b>Body</b> fields for dynamic values.\n</li><li>\n All fields in this doctype are available under the <b>doc</b> object and all fields for the customer to whom the mail will go to is available under the <b>customer</b> object.\n</li></ul>\n<h4> Examples</h4>\n<!-- {% raw %} -->\n<ul>\n <li><b>Subject</b>:<br><br><pre><code>Statement Of Accounts for {{ customer.name }}</code></pre><br></li>\n <li><b>Body</b>: <br><br>\n<pre><code>Hello {{ customer.name }},<br>PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}.</code> </pre></li>\n</ul>\n<!-- {% endraw %} -->"
},
{
"fieldname": "subject",
"fieldtype": "Data",
"label": "Subject"
},
{
"fieldname": "body",
"fieldtype": "Text Editor",
"label": "Body"
}
],
"links": [],
"modified": "2020-08-08 08:47:09.185728",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,273 @@
# -*- 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
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 frappe.utils.print_format import report_to_pdf
from frappe.utils.pdf import get_pdf
from frappe.utils import today, add_days, add_months, getdate, format_date
from frappe.utils.jinja import validate_template
import copy
from datetime import timedelta
from frappe.www.printview import get_print_style
class ProcessStatementOfAccounts(Document):
def validate(self):
if not self.subject:
self.subject = 'Statement Of Accounts for {{ customer.name }}'
if not self.body:
self.body = 'Hello {{ customer.name }},<br>PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}.'
validate_template(self.subject)
validate_template(self.body)
if not self.customers:
frappe.throw(frappe._('Customers not selected.'))
if self.enable_auto_email:
self.to_date = self.start_date
self.from_date = add_months(self.to_date, -1 * self.filter_duration)
def get_report_pdf(doc, consolidated=True):
statement_dict = {}
aging = ''
base_template_path = "frappe/www/printview.html"
template_path = "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
for entry in doc.customers:
if doc.include_ageing:
ageing_filters = frappe._dict({
'company': doc.company,
'report_date': doc.to_date,
'ageing_based_on': doc.ageing_based_on,
'range1': 30,
'range2': 60,
'range3': 90,
'range4': 120,
'customer': entry.customer
})
col1, aging = get_ageing(ageing_filters)
aging[0]['ageing_based_on'] = doc.ageing_based_on
tax_id = frappe.get_doc('Customer', entry.customer).tax_id
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,
'party_type': 'Customer',
'party': [entry.customer],
'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)
for x in [0, -2, -1]:
res[x]['account'] = res[x]['account'].replace("'","")
if len(res) == 3:
continue
html = frappe.render_template(template_path, \
{"filters": filters, "data": res, "aging": aging[0] if doc.include_ageing 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:
result = ''.join(list(statement_dict.values()))
return get_pdf(result, {'orientation': doc.orientation})
else:
for customer, statement_html in statement_dict.items():
statement_dict[customer]=get_pdf(statement_html, {'orientation': doc.orientation})
return statement_dict
def get_customers_based_on_territory_or_customer_group(customer_collection, collection_name):
fields_dict = {
'Customer Group': 'customer_group',
'Territory': 'territory',
}
collection = frappe.get_doc(customer_collection, collection_name)
selected = [customer.name for customer in frappe.get_list(customer_collection, filters=[
['lft', '>=', collection.lft],
['rgt', '<=', collection.rgt]
],
fields=['name'],
order_by='lft asc, rgt desc'
)]
return frappe.get_list('Customer', fields=['name', 'email_id'], \
filters=[[fields_dict[customer_collection], 'IN', selected]])
def get_customers_based_on_sales_person(sales_person):
lft, rgt = frappe.db.get_value("Sales Person",
sales_person, ["lft", "rgt"])
records = frappe.db.sql("""
select distinct parent, parenttype
from `tabSales Team` steam
where parenttype = 'Customer'
and exists(select name from `tabSales Person` where lft >= %s and rgt <= %s and name = steam.sales_person)
""", (lft, rgt), as_dict=1)
sales_person_records = frappe._dict()
for d in records:
sales_person_records.setdefault(d.parenttype, set()).add(d.parent)
if sales_person_records.get('Customer'):
return frappe.get_list('Customer', fields=['name', 'email_id'], \
filters=[['name', 'in', list(sales_person_records['Customer'])]])
else:
return []
def get_recipients_and_cc(customer, doc):
recipients = []
for clist in doc.customers:
if clist.customer == customer:
recipients.append(clist.billing_email)
if doc.primary_mandatory and clist.primary_email:
recipients.append(clist.primary_email)
cc = []
if doc.cc_to != '':
try:
cc=[frappe.get_value('User', doc.cc_to, 'email')]
except:
pass
return recipients, cc
def get_context(customer, doc):
template_doc = copy.deepcopy(doc)
del template_doc.customers
template_doc.from_date = format_date(template_doc.from_date)
template_doc.to_date = format_date(template_doc.to_date)
return {
'doc': template_doc,
'customer': frappe.get_doc('Customer', customer),
'frappe': frappe.utils
}
@frappe.whitelist()
def fetch_customers(customer_collection, collection_name, primary_mandatory):
customer_list = []
customers = []
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.')
else:
if customer_collection == 'Sales Partner':
customers = frappe.get_list('Customer', fields=['name', 'email_id'], \
filters=[['default_sales_partner', '=', collection_name]])
else:
customers = get_customers_based_on_territory_or_customer_group(customer_collection, collection_name)
for customer in customers:
primary_email = customer.get('email_id') or ''
billing_email = get_customer_emails(customer.name, 1, billing_and_primary=False)
if billing_email == '' or (primary_email == '' and int(primary_mandatory)):
continue
customer_list.append({
'name': customer.name,
'primary_email': primary_email,
'billing_email': billing_email
})
return customer_list
@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""")
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)
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)
return [primary_email or '', billing_email[0][0]]
else:
return billing_email[0][0] or ''
@frappe.whitelist()
def download_statements(document_name):
doc = frappe.get_doc('Process Statement Of Accounts', document_name)
report = get_report_pdf(doc)
if report:
frappe.local.response.filename = doc.name + '.pdf'
frappe.local.response.filecontent = report
frappe.local.response.type = "download"
@frappe.whitelist()
def send_emails(document_name, from_scheduler=False):
doc = frappe.get_doc('Process Statement Of Accounts', document_name)
report = get_report_pdf(doc, consolidated=False)
if report:
for customer, report_pdf in report.items():
attachments = [{
'fname': customer + '.pdf',
'fcontent': report_pdf
}]
recipients, cc = get_recipients_and_cc(customer, doc)
context = get_context(customer, doc)
subject = frappe.render_template(doc.subject, context)
message = frappe.render_template(doc.body, context)
frappe.enqueue(
queue='short',
method=frappe.sendmail,
recipients=recipients,
sender=frappe.session.user,
cc=cc,
subject=subject,
message=message,
now=True,
reference_doctype='Process Statement Of Accounts',
reference_name=document_name,
attachments=attachments
)
if doc.enable_auto_email and from_scheduler:
new_to_date = getdate(today())
if doc.frequency == 'Weekly':
new_to_date = add_days(new_to_date, 7)
else:
new_to_date = add_months(new_to_date, 1 if doc.frequency == 'Monthly' else 3)
new_from_date = add_months(new_to_date, -1 * doc.filter_duration)
doc.add_comment('Comment', 'Emails sent on: ' + frappe.utils.format_datetime(frappe.utils.now()))
doc.db_set('to_date', new_to_date, commit=True)
doc.db_set('from_date', new_from_date, commit=True)
return True
else:
return False
@frappe.whitelist()
def send_auto_email():
selected = frappe.get_list('Process Statement Of Accounts', filters={'to_date': format_date(today()), 'enable_auto_email': 1})
for entry in selected:
send_emails(entry.name, from_scheduler=True)
return True

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestProcessStatementOfAccounts(unittest.TestCase):
pass

View File

@ -0,0 +1,47 @@
{
"actions": [],
"allow_workflow": 1,
"creation": "2020-08-03 16:35:21.852178",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"customer",
"billing_email",
"primary_email"
],
"fields": [
{
"fieldname": "customer",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Customer",
"options": "Customer",
"reqd": 1
},
{
"fieldname": "primary_email",
"fieldtype": "Read Only",
"in_list_view": 1,
"label": "Primary Contact Email"
},
{
"fieldname": "billing_email",
"fieldtype": "Read Only",
"in_list_view": 1,
"label": "Billing Email"
}
],
"istable": 1,
"links": [],
"modified": "2020-08-03 22:55:38.875601",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts Customer",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

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 ProcessStatementOfAccountsCustomer(Document):
pass

View File

@ -0,0 +1,30 @@
{
"actions": [],
"creation": "2020-08-03 16:56:45.744905",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"cost_center_name"
],
"fields": [
{
"fieldname": "cost_center_name",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
}
],
"istable": 1,
"links": [],
"modified": "2020-08-03 16:56:45.744905",
"modified_by": "Administrator",
"module": "Accounts",
"name": "PSOA Cost Center",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

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 PSOACostCenter(Document):
pass

View File

@ -0,0 +1,30 @@
{
"actions": [],
"creation": "2020-08-03 16:52:14.731978",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"project_name"
],
"fields": [
{
"fieldname": "project_name",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
}
],
"istable": 1,
"links": [],
"modified": "2020-08-03 16:53:39.219736",
"modified_by": "Administrator",
"module": "Accounts",
"name": "PSOA Project",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

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 PSOAProject(Document):
pass

View File

@ -25,6 +25,12 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
this.frm.set_df_property("credit_to", "print_hide", 0); this.frm.set_df_property("credit_to", "print_hide", 0);
} }
} }
// Trigger supplier event on load if supplier is available
// The reason for this is PI can be created from PR or PO and supplier is pre populated
if (this.frm.doc.supplier && this.frm.doc.__islocal) {
this.frm.trigger('supplier');
}
}, },
refresh: function(doc) { refresh: function(doc) {
@ -135,6 +141,8 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
} }
}); });
} }
this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1);
}, },
unblock_invoice: function() { unblock_invoice: function() {

View File

@ -180,7 +180,7 @@
"no_copy": 1, "no_copy": 1,
"oldfieldname": "naming_series", "oldfieldname": "naming_series",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "ACC-PINV-.YYYY.-", "options": "ACC-PINV-.YYYY.-\nACC-PINV-RET-.YYYY.-",
"print_hide": 1, "print_hide": 1,
"reqd": 1, "reqd": 1,
"set_only_once": 1 "set_only_once": 1
@ -361,6 +361,7 @@
"fieldname": "bill_date", "fieldname": "bill_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Supplier Invoice Date", "label": "Supplier Invoice Date",
"no_copy": 1,
"oldfieldname": "bill_date", "oldfieldname": "bill_date",
"oldfieldtype": "Date", "oldfieldtype": "Date",
"print_hide": 1 "print_hide": 1
@ -1333,8 +1334,7 @@
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "modified": "2020-09-21 12:22:09.164068",
"modified": "2020-08-03 12:46:01.411074",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

@ -132,6 +132,11 @@ class PurchaseInvoice(BuyingController):
if not self.due_date: if not self.due_date:
self.due_date = get_due_date(self.posting_date, "Supplier", self.supplier, self.company, self.bill_date) self.due_date = get_due_date(self.posting_date, "Supplier", self.supplier, self.company, self.bill_date)
tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
if tds_category and not for_validate:
self.apply_tds = 1
self.tax_withholding_category = tds_category
super(PurchaseInvoice, self).set_missing_values(for_validate) super(PurchaseInvoice, self).set_missing_values(for_validate)
def check_conversion_rate(self): def check_conversion_rate(self):
@ -146,14 +151,16 @@ class PurchaseInvoice(BuyingController):
["account_type", "report_type", "account_currency"], as_dict=True) ["account_type", "report_type", "account_currency"], as_dict=True)
if account.report_type != "Balance Sheet": if account.report_type != "Balance Sheet":
frappe.throw(_("Please ensure {} account is a Balance Sheet account. \ frappe.throw(
You can change the parent account to a Balance Sheet account or select a different account.") _("Please ensure {} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account.")
.format(frappe.bold("Credit To")), title=_("Invalid Account")) .format(frappe.bold("Credit To")), title=_("Invalid Account")
)
if self.supplier and account.account_type != "Payable": if self.supplier and account.account_type != "Payable":
frappe.throw(_("Please ensure {} account is a Payable account. \ frappe.throw(
Change the account type to Payable or select a different account.") _("Please ensure {} account is a Payable account. Change the account type to Payable or select a different account.")
.format(frappe.bold("Credit To")), title=_("Invalid Account")) .format(frappe.bold("Credit To")), title=_("Invalid Account")
)
self.party_account_currency = account.account_currency self.party_account_currency = account.account_currency
@ -239,10 +246,10 @@ class PurchaseInvoice(BuyingController):
if self.update_stock and (not item.from_warehouse): 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"]: if for_validate and item.expense_account and item.expense_account != warehouse_account[item.warehouse]["account"]:
frappe.msgprint(_('''Row {0}: Expense Head changed to {1} because account {2} msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]))
is not linked to warehouse {3} or it is not the default inventory account'''.format( msg += _("because account {} is not linked to warehouse {} ").format(frappe.bold(item.expense_account), frappe.bold(item.warehouse))
item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]), msg += _("or it is not the default inventory 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"] item.expense_account = warehouse_account[item.warehouse]["account"]
else: else:
@ -254,19 +261,19 @@ class PurchaseInvoice(BuyingController):
if negative_expense_booked_in_pr: if negative_expense_booked_in_pr:
if for_validate and item.expense_account and item.expense_account != stock_not_billed_account: if for_validate and item.expense_account and item.expense_account != stock_not_billed_account:
frappe.msgprint(_('''Row {0}: Expense Head changed to {1} because msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(stock_not_billed_account))
expense is booked against this account in Purchase Receipt {2}'''.format( msg += _("because expense is booked against this account in Purchase Receipt {}").format(frappe.bold(item.purchase_receipt))
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 item.expense_account = stock_not_billed_account
else: else:
# If no purchase receipt present then book expense in 'Stock Received But Not Billed' # 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 # 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: if for_validate and item.expense_account and item.expense_account != stock_not_billed_account:
frappe.msgprint(_('''Row {0}: Expense Head changed to {1} as no Purchase msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(stock_not_billed_account))
Receipt is created against Item {2}. This is done to handle accounting for cases msg += _("as no Purchase Receipt is created against Item {}. ").format(frappe.bold(item.item_code))
when Purchase Receipt is created after Purchase Invoice'''.format( msg += _("This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice")
item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.item_code)))) frappe.msgprint(msg, title=_("Expense Head Changed"))
item.expense_account = stock_not_billed_account item.expense_account = stock_not_billed_account
@ -294,10 +301,11 @@ class PurchaseInvoice(BuyingController):
for d in self.get('items'): for d in self.get('items'):
if not d.purchase_order: if not d.purchase_order:
throw(_("""Purchase Order Required for item {0} msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code))
To submit the invoice without purchase order please set msg += "<br><br>"
{1} as {2} in {3}""").format(frappe.bold(d.item_code), frappe.bold(_('Purchase Order Required')), msg += _("To submit the invoice without purchase order please set {} ").format(frappe.bold(_('Purchase Order Required')))
frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))) msg += _("as {} in {}").format(frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))
throw(msg, title=_("Mandatory Purchase Order"))
def pr_required(self): def pr_required(self):
stock_items = self.get_stock_items() stock_items = self.get_stock_items()
@ -308,10 +316,11 @@ class PurchaseInvoice(BuyingController):
for d in self.get('items'): for d in self.get('items'):
if not d.purchase_receipt and d.item_code in stock_items: if not d.purchase_receipt and d.item_code in stock_items:
throw(_("""Purchase Receipt Required for item {0} msg = _("Purchase Receipt Required for item {}").format(frappe.bold(d.item_code))
To submit the invoice without purchase receipt please set msg += "<br><br>"
{1} as {2} in {3}""").format(frappe.bold(d.item_code), frappe.bold(_('Purchase Receipt Required')), msg += _("To submit the invoice without purchase receipt please set {} ").format(frappe.bold(_('Purchase Receipt Required')))
frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))) msg += _("as {} in {}").format(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): def validate_write_off_account(self):
if self.write_off_amount and not self.write_off_account: if self.write_off_amount and not self.write_off_account:
@ -400,8 +409,6 @@ class PurchaseInvoice(BuyingController):
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
def make_gl_entries(self, gl_entries=None): def make_gl_entries(self, gl_entries=None):
if not self.grand_total:
return
if not gl_entries: if not gl_entries:
gl_entries = self.get_gl_entries() gl_entries = self.get_gl_entries()
@ -708,7 +715,8 @@ class PurchaseInvoice(BuyingController):
item.item_tax_amount / self.conversion_rate) item.item_tax_amount / self.conversion_rate)
}, item=item)) }, item=item))
else: else:
cwip_account = get_asset_account("capital_work_in_progress_account", company = self.company) cwip_account = get_asset_account("capital_work_in_progress_account",
asset_category=item.asset_category,company=self.company)
cwip_account_currency = get_account_currency(cwip_account) cwip_account_currency = get_account_currency(cwip_account)
gl_entries.append(self.get_gl_dict({ gl_entries.append(self.get_gl_dict({

View File

@ -1002,7 +1002,8 @@ def make_purchase_invoice(**args):
"cost_center": args.cost_center or "_Test Cost Center - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC",
"project": args.project, "project": args.project,
"rejected_warehouse": args.rejected_warehouse or "", "rejected_warehouse": args.rejected_warehouse or "",
"rejected_serial_no": args.rejected_serial_no or "" "rejected_serial_no": args.rejected_serial_no or "",
"asset_location": args.location or ""
}) })
if args.get_taxes_and_charges: if args.get_taxes_and_charges:

View File

@ -1,5 +1,4 @@
{ {
"actions": [],
"autoname": "hash", "autoname": "hash",
"creation": "2013-05-22 12:43:10", "creation": "2013-05-22 12:43:10",
"doctype": "DocType", "doctype": "DocType",
@ -82,6 +81,7 @@
"item_tax_rate", "item_tax_rate",
"bom", "bom",
"include_exploded_items", "include_exploded_items",
"purchase_invoice_item",
"col_break6", "col_break6",
"purchase_order", "purchase_order",
"po_detail", "po_detail",
@ -769,12 +769,21 @@
"collapsible": 1, "collapsible": 1,
"fieldname": "col_break7", "fieldname": "col_break7",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"depends_on": "eval:parent.update_stock == 1",
"fieldname": "purchase_invoice_item",
"fieldtype": "Data",
"ignore_user_permissions": 1,
"label": "Purchase Invoice Item",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "modified": "2020-08-20 11:48:01.398356",
"modified": "2020-04-22 10:37:35.103176",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "name": "Purchase Invoice Item",

View File

@ -210,7 +210,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-03-12 14:53:47.679439", "modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Taxes and Charges", "name": "Purchase Taxes and Charges",

View File

@ -78,7 +78,7 @@
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Taxes and Charges Template", "name": "Purchase Taxes and Charges Template",
"owner": "wasim@webnotestech.com", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"email": 1, "email": 1,

View File

@ -1,7 +1,6 @@
{ {
"actions": [], "actions": [],
"allow_import": 1, "allow_import": 1,
"allow_workflow": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2013-05-24 19:29:05", "creation": "2013-05-24 19:29:05",
"doctype": "DocType", "doctype": "DocType",
@ -20,6 +19,7 @@
"is_return", "is_return",
"column_break1", "column_break1",
"company", "company",
"company_tax_id",
"posting_date", "posting_date",
"posting_time", "posting_time",
"set_posting_time", "set_posting_time",
@ -217,7 +217,7 @@
"no_copy": 1, "no_copy": 1,
"oldfieldname": "naming_series", "oldfieldname": "naming_series",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "ACC-SINV-.YYYY.-", "options": "ACC-SINV-.YYYY.-\nACC-SINV-RET-.YYYY.-",
"print_hide": 1, "print_hide": 1,
"reqd": 1, "reqd": 1,
"set_only_once": 1 "set_only_once": 1
@ -448,7 +448,7 @@
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"fieldname": "po_no", "fieldname": "po_no",
"fieldtype": "Small Text", "fieldtype": "Data",
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "Customer's Purchase Order", "label": "Customer's Purchase Order",
@ -1826,7 +1826,7 @@
"fieldtype": "Table", "fieldtype": "Table",
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "Sales Team1", "label": "Sales Contributions and Incentives",
"oldfieldname": "sales_team", "oldfieldname": "sales_team",
"oldfieldtype": "Table", "oldfieldtype": "Table",
"options": "Sales Team", "options": "Sales Team",
@ -1927,6 +1927,7 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "eval:(doc.is_pos && doc.is_consolidated)",
"fieldname": "is_consolidated", "fieldname": "is_consolidated",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Is Consolidated", "label": "Is Consolidated",
@ -1941,13 +1942,20 @@
"hide_seconds": 1, "hide_seconds": 1,
"label": "Is Internal Customer", "label": "Is Internal Customer",
"read_only": 1 "read_only": 1
},
{
"fetch_from": "company.tax_id",
"fieldname": "company_tax_id",
"fieldtype": "Data",
"label": "Company Tax ID",
"read_only": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 181, "idx": 181,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-07-18 05:07:16.725974", "modified": "2020-10-09 15:59:57.544736",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, erpnext import frappe, erpnext
import frappe.defaults import frappe.defaults
from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate, get_link_to_form
from frappe import _, msgprint, throw from frappe import _, msgprint, throw
from erpnext.accounts.party import get_party_account, get_due_date from erpnext.accounts.party import get_party_account, get_due_date
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
@ -428,7 +428,7 @@ class SalesInvoice(SellingController):
if pos.get('account_for_change_amount'): if pos.get('account_for_change_amount'):
self.account_for_change_amount = pos.get('account_for_change_amount') self.account_for_change_amount = pos.get('account_for_change_amount')
for fieldname in ('naming_series', 'currency', 'letter_head', 'tc_name', for fieldname in ('currency', 'letter_head', 'tc_name',
'company', 'select_print_heading', 'write_off_account', 'taxes_and_charges', 'company', 'select_print_heading', 'write_off_account', 'taxes_and_charges',
'write_off_cost_center', 'apply_discount_on', 'cost_center'): 'write_off_cost_center', 'apply_discount_on', 'cost_center'):
if (not for_validate) or (for_validate and not self.get(fieldname)): if (not for_validate) or (for_validate and not self.get(fieldname)):
@ -479,14 +479,14 @@ class SalesInvoice(SellingController):
frappe.throw(_("Debit To is required"), title=_("Account Missing")) frappe.throw(_("Debit To is required"), title=_("Account Missing"))
if account.report_type != "Balance Sheet": if account.report_type != "Balance Sheet":
frappe.throw(_("Please ensure {} account is a Balance Sheet account. \ msg = _("Please ensure {} account is a Balance Sheet account. ").format(frappe.bold("Debit To"))
You can change the parent account to a Balance Sheet account or select a different account.") msg += _("You can change the parent account to a Balance Sheet account or select a different account.")
.format(frappe.bold("Debit To")), title=_("Invalid Account")) frappe.throw(msg, title=_("Invalid Account"))
if self.customer and account.account_type != "Receivable": if self.customer and account.account_type != "Receivable":
frappe.throw(_("Please ensure {} account is a Receivable account. \ msg = _("Please ensure {} account is a Receivable account. ").format(frappe.bold("Debit To"))
Change the account type to Receivable or select a different account.") msg += _("Change the account type to Receivable or select a different account.")
.format(frappe.bold("Debit To")), title=_("Invalid Account")) frappe.throw(msg, title=_("Invalid Account"))
self.party_account_currency = account.account_currency self.party_account_currency = account.account_currency
@ -572,7 +572,8 @@ class SalesInvoice(SellingController):
def validate_pos(self): def validate_pos(self):
if self.is_return: if self.is_return:
if flt(self.paid_amount) + flt(self.write_off_amount) - flt(self.grand_total) > \ invoice_total = self.rounded_total or self.grand_total
if flt(self.paid_amount) + flt(self.write_off_amount) - flt(invoice_total) > \
1.0/(10.0**(self.precision("grand_total") + 1.0)): 1.0/(10.0**(self.precision("grand_total") + 1.0)):
frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total")) frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total"))
@ -1140,8 +1141,10 @@ class SalesInvoice(SellingController):
where redeem_against=%s''', (lp_entry[0].name), as_dict=1) where redeem_against=%s''', (lp_entry[0].name), as_dict=1)
if against_lp_entry: if against_lp_entry:
invoice_list = ", ".join([d.invoice for d in against_lp_entry]) invoice_list = ", ".join([d.invoice for d in against_lp_entry])
frappe.throw(_('''{} can't be cancelled since the Loyalty Points earned has been redeemed. frappe.throw(
First cancel the {} No {}''').format(self.doctype, self.doctype, invoice_list)) _('''{} can't be cancelled since the Loyalty Points earned has been redeemed. First cancel the {} No {}''')
.format(self.doctype, self.doctype, invoice_list)
)
else: else:
frappe.db.sql('''delete from `tabLoyalty Point Entry` where invoice=%s''', (self.name)) frappe.db.sql('''delete from `tabLoyalty Point Entry` where invoice=%s''', (self.name))
# Set loyalty program # Set loyalty program
@ -1372,7 +1375,7 @@ def get_bank_cash_account(mode_of_payment, company):
{"parent": mode_of_payment, "company": company}, "default_account") {"parent": mode_of_payment, "company": company}, "default_account")
if not account: if not account:
frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}") frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}")
.format(mode_of_payment)) .format(get_link_to_form("Mode of Payment", mode_of_payment)), title=_("Missing Account"))
return { return {
"account": account "account": account
} }
@ -1612,18 +1615,25 @@ def update_multi_mode_option(doc, pos_profile):
payment.type = payment_mode.type payment.type = payment_mode.type
doc.set('payments', []) doc.set('payments', [])
if not pos_profile or not pos_profile.get('payments'): invalid_modes = []
for payment_mode in get_all_mode_of_payments(doc):
append_payment(payment_mode)
return
for pos_payment_method in pos_profile.get('payments'): for pos_payment_method in pos_profile.get('payments'):
pos_payment_method = pos_payment_method.as_dict() pos_payment_method = pos_payment_method.as_dict()
payment_mode = get_mode_of_payment_info(pos_payment_method.mode_of_payment, doc.company) payment_mode = get_mode_of_payment_info(pos_payment_method.mode_of_payment, doc.company)
if not payment_mode:
invalid_modes.append(get_link_to_form("Mode of Payment", pos_payment_method.mode_of_payment))
continue
payment_mode[0].default = pos_payment_method.default payment_mode[0].default = pos_payment_method.default
append_payment(payment_mode[0]) append_payment(payment_mode[0])
if invalid_modes:
if invalid_modes == 1:
msg = _("Please set default Cash or Bank account in Mode of Payment {}")
else:
msg = _("Please set default Cash or Bank account in Mode of Payments {}")
frappe.throw(msg.format(", ".join(invalid_modes)), title=_("Missing Account"))
def get_all_mode_of_payments(doc): def get_all_mode_of_payments(doc):
return frappe.db.sql(""" return frappe.db.sql("""
select mpa.default_account, mpa.parent, mp.type as type select mpa.default_account, mpa.parent, mp.type as type

View File

@ -206,10 +206,19 @@ class TestSalesInvoice(unittest.TestCase):
"rate": 14, "rate": 14,
'included_in_print_rate': 1 'included_in_print_rate': 1
}) })
si.append("taxes", {
"charge_type": "On Item Quantity",
"account_head": "_Test Account Education Cess - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "CESS",
"rate": 5,
'included_in_print_rate': 1
})
si.insert() si.insert()
# with inclusive tax # with inclusive tax
self.assertEqual(si.net_total, 4385.96) self.assertEqual(si.items[0].net_amount, 3947.368421052631)
self.assertEqual(si.net_total, 3947.37)
self.assertEqual(si.grand_total, 5000) self.assertEqual(si.grand_total, 5000)
si.reload() si.reload()
@ -222,8 +231,8 @@ class TestSalesInvoice(unittest.TestCase):
si.save() si.save()
# with inclusive tax and additional discount # with inclusive tax and additional discount
self.assertEqual(si.net_total, 4285.96) self.assertEqual(si.net_total, 3847.37)
self.assertEqual(si.grand_total, 4885.99) self.assertEqual(si.grand_total, 4886)
si.reload() si.reload()
@ -235,7 +244,7 @@ class TestSalesInvoice(unittest.TestCase):
si.save() si.save()
# with inclusive tax and additional discount # with inclusive tax and additional discount
self.assertEqual(si.net_total, 4298.25) self.assertEqual(si.net_total, 3859.65)
self.assertEqual(si.grand_total, 4900.00) self.assertEqual(si.grand_total, 4900.00)
def test_sales_invoice_discount_amount(self): def test_sales_invoice_discount_amount(self):

View File

@ -1,5 +1,4 @@
{ {
"actions": [],
"autoname": "hash", "autoname": "hash",
"creation": "2013-06-04 11:02:19", "creation": "2013-06-04 11:02:19",
"doctype": "DocType", "doctype": "DocType",
@ -87,6 +86,7 @@
"edit_references", "edit_references",
"sales_order", "sales_order",
"so_detail", "so_detail",
"sales_invoice_item",
"column_break_74", "column_break_74",
"delivery_note", "delivery_note",
"dn_detail", "dn_detail",
@ -790,12 +790,22 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Project", "label": "Project",
"options": "Project" "options": "Project"
},
{
"depends_on": "eval:parent.update_stock == 1",
"fieldname": "sales_invoice_item",
"fieldtype": "Data",
"ignore_user_permissions": 1,
"label": "Sales Invoice Item",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-07-18 12:24:41.749986", "modified": "2020-08-20 11:24:41.749986",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Item", "name": "Sales Invoice Item",

View File

@ -8,7 +8,7 @@ def get_data():
'fieldname': 'taxes_and_charges', 'fieldname': 'taxes_and_charges',
'non_standard_fieldnames': { 'non_standard_fieldnames': {
'Tax Rule': 'sales_tax_template', 'Tax Rule': 'sales_tax_template',
'Subscription': 'tax_template', 'Subscription': 'sales_tax_template',
'Restaurant': 'default_tax_template' 'Restaurant': 'default_tax_template'
}, },
'transactions': [ 'transactions': [

View File

@ -3,6 +3,22 @@
frappe.ui.form.on('Shipping Rule', { frappe.ui.form.on('Shipping Rule', {
refresh: function(frm) { refresh: function(frm) {
frm.set_query("cost_center", function() {
return {
filters: {
company: frm.doc.company
}
}
})
frm.set_query("account", function() {
return {
filters: {
company: frm.doc.company
}
}
})
frm.trigger('toggle_reqd'); frm.trigger('toggle_reqd');
}, },
calculate_based_on: function(frm) { calculate_based_on: function(frm) {

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