fix: resolved conflicts

This commit is contained in:
Anupam 2021-01-12 17:43:24 +05:30
commit 5abb997a27
986 changed files with 154853 additions and 37333 deletions

14
.editorconfig Normal file
View File

@ -0,0 +1,14 @@
# Root editor config file
root = true
# Common settings
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
# python, js indentation settings
[{*.py,*.js}]
indent_style = tab
indent_size = 4

View File

@ -5,7 +5,7 @@
"es6": true
},
"parserOptions": {
"ecmaVersion": 6,
"ecmaVersion": 9,
"sourceType": "module"
},
"extends": "eslint:recommended",
@ -15,6 +15,14 @@
"tab",
{ "SwitchCase": 1 }
],
"brace-style": [
"error",
"1tbs"
],
"space-unary-ops": [
"error",
{ "words": true }
],
"linebreak-style": [
"error",
"unix"
@ -44,12 +52,10 @@
"no-control-regex": [
"off"
],
"spaced-comment": [
"warn"
],
"no-trailing-spaces": [
"warn"
]
"space-before-blocks": "warn",
"keyword-spacing": "warn",
"comma-spacing": "warn",
"key-spacing": "warn"
},
"root": true,
"globals": {

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Community Forum
url: https://discuss.erpnext.com/
about: For general QnA, discussions and community help.

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":
parts = parsed_url.path.split('/')
if len(parts) == 5 and parts[1] == "frappe" and parts[2] 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

@ -9,5 +9,6 @@
"root_login": "root",
"root_password": "travis",
"host_name": "http://test_site:8000",
"install_apps": ["erpnext"]
"install_apps": ["erpnext"],
"throttle_user_limit": 100
}

View File

@ -5,7 +5,7 @@
<p>ERP made simple</p>
</p>
[![Build Status](https://travis-ci.com/frappe/erpnext.svg)](https://travis-ci.com/frappe/erpnext)
[![Build Status](https://api.travis-ci.com/frappe/erpnext.svg?branch=develop)](https://travis-ci.com/frappe/erpnext)
[![Open Source Helpers](https://www.codetriage.com/frappe/erpnext/badges/users.svg)](https://www.codetriage.com/frappe/erpnext)
[![Coverage Status](https://coveralls.io/repos/github/frappe/erpnext/badge.svg?branch=develop)](https://coveralls.io/github/frappe/erpnext?branch=develop)
@ -16,7 +16,7 @@
ERPNext as a monolith includes the following areas for managing businesses:
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. [Sales](https://erpnext.com/open-source-sales-purchase)
1. [Purchase](https://erpnext.com/open-source-sales-purchase)

View File

@ -1,58 +1,126 @@
{
"custom_fields": [
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"creation": "2018-12-28 22:29:21.828090",
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"dt": "Address",
"fetch_from": null,
"fieldname": "tax_category",
"fieldtype": "Link",
"hidden": 0,
"idx": 14,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"insert_after": "fax",
"label": "Tax Category",
"modified": "2018-12-28 22:29:21.828090",
"modified_by": "Administrator",
"name": "Address-tax_category",
"no_copy": 0,
"options": "Tax Category",
"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,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"translatable": 0,
"unique": 0,
"_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": "2018-12-28 22:29:21.828090",
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"dt": "Address",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "tax_category",
"fieldtype": "Link",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"idx": 15,
"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": "fax",
"label": "Tax Category",
"length": 0,
"mandatory_depends_on": null,
"modified": "2018-12-28 22:29:21.828090",
"modified_by": "Administrator",
"name": "Address-tax_category",
"no_copy": 0,
"options": "Tax Category",
"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,
"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,
"reqd": 0,
"search_index": 0,
"translatable": 0,
"unique": 0,
"width": null
}
],
"custom_perms": [],
"doctype": "Address",
"property_setters": [],
],
"custom_perms": [],
"doctype": "Address",
"property_setters": [],
"sync_on_migrate": 1
}

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

@ -6,9 +6,8 @@ import frappe, json
from frappe import _
from frappe.utils import add_to_date, date_diff, getdate, nowdate, get_last_day, formatdate, get_link_to_form
from erpnext.accounts.report.general_ledger.general_ledger import execute
from frappe.utils.dashboard import cache_source, get_from_date_from_timespan
from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_period_ending
from frappe.utils.dashboard import cache_source
from frappe.utils.dateutils import get_from_date_from_timespan, get_period_ending
from frappe.utils.nestedset import get_descendants_of
@frappe.whitelist()

View File

@ -23,7 +23,7 @@
{
"hidden": 0,
"label": "Reports",
"links": "[\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Trial Balance for Party\",\n \"name\": \"Trial Balance for Party\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Journal Entry\"\n ],\n \"doctype\": \"Journal Entry\",\n \"is_query_report\": true,\n \"label\": \"Payment Period Based On Invoice Date\",\n \"name\": \"Payment Period Based On Invoice Date\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Partners Commission\",\n \"name\": \"Sales Partners Commission\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"is_query_report\": true,\n \"label\": \"Customer Credit Balance\",\n \"name\": \"Customer Credit Balance\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Payment Summary\",\n \"name\": \"Sales Payment Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Address\"\n ],\n \"doctype\": \"Address\",\n \"is_query_report\": true,\n \"label\": \"Address And Contacts\",\n \"name\": \"Address And Contacts\",\n \"type\": \"report\"\n }\n]"
"links": "[\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Trial Balance for Party\",\n \"name\": \"Trial Balance for Party\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Journal Entry\"\n ],\n \"doctype\": \"Journal Entry\",\n \"is_query_report\": true,\n \"label\": \"Payment Period Based On Invoice Date\",\n \"name\": \"Payment Period Based On Invoice Date\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Partners Commission\",\n \"name\": \"Sales Partners Commission\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"is_query_report\": true,\n \"label\": \"Customer Credit Balance\",\n \"name\": \"Customer Credit Balance\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Payment Summary\",\n \"name\": \"Sales Payment Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Address\"\n ],\n \"doctype\": \"Address\",\n \"is_query_report\": true,\n \"label\": \"Address And Contacts\",\n \"name\": \"Address And Contacts\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"DATEV Export\",\n \"name\": \"DATEV\",\n \"type\": \"report\"\n }\n]"
},
{
"hidden": 0,
@ -43,7 +43,7 @@
{
"hidden": 0,
"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 Reconciliation\",\n \"name\": \"bank-reconciliation\",\n \"type\": \"page\"\n },\n {\n \"label\": \"Bank Clearance\",\n \"name\": \"Bank Clearance\",\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]"
},
{
"hidden": 0,
@ -53,7 +53,7 @@
{
"hidden": 0,
"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,
@ -79,6 +79,11 @@
"hidden": 0,
"label": "Profitability",
"links": "[\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Gross Profit\",\n \"name\": \"Gross Profit\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Profitability Analysis\",\n \"name\": \"Profitability Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Invoice Trends\",\n \"name\": \"Sales Invoice Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Invoice\"\n ],\n \"doctype\": \"Purchase Invoice\",\n \"is_query_report\": true,\n \"label\": \"Purchase Invoice Trends\",\n \"name\": \"Purchase Invoice Trends\",\n \"type\": \"report\"\n }\n]"
},
{
"hidden": 0,
"label": "Value-Added Tax (VAT UAE)",
"links": "[\n {\n \"country\": \"United Arab Emirates\",\n \"label\": \"UAE VAT Settings\",\n \"name\": \"UAE VAT Settings\",\n \"type\": \"doctype\"\n },\n {\n \"country\": \"United Arab Emirates\",\n \"is_query_report\": true,\n \"label\": \"UAE VAT 201\",\n \"name\": \"UAE VAT 201\",\n \"type\": \"report\"\n }\n\n]"
}
],
"category": "Modules",
@ -98,7 +103,7 @@
"idx": 0,
"is_standard": 1,
"label": "Accounting",
"modified": "2020-09-03 10:37:07.865801",
"modified": "2020-11-11 18:35:11.542909",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting",
@ -147,11 +152,6 @@
"link_to": "Trial Balance",
"type": "Report"
},
{
"label": "Point of Sale",
"link_to": "point-of-sale",
"type": "Page"
},
{
"label": "Dashboard",
"link_to": "Accounts",

View File

@ -101,7 +101,7 @@ class Account(NestedSet):
return
if not frappe.db.get_value("Account",
{'account_name': self.account_name, 'company': ancestors[0]}, 'name'):
frappe.throw(_("Please add the account to root level Company - %s" % ancestors[0]))
frappe.throw(_("Please add the account to root level Company - {}").format(ancestors[0]))
elif self.parent_account:
descendants = get_descendants_of('Company', self.company)
if not descendants: return
@ -117,7 +117,9 @@ class Account(NestedSet):
for d in frappe.db.get_values('Account', filters=filters, fieldname=["company", "name"], as_dict=True):
parent_acc_name_map[d["company"]] = d["name"]
if not parent_acc_name_map: return
self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name)
def validate_group_or_ledger(self):
@ -162,9 +164,19 @@ class Account(NestedSet):
def create_account_for_child_company(self, parent_acc_name_map, descendants, parent_acc_name):
for company in descendants:
company_bold = frappe.bold(company)
parent_acc_name_bold = frappe.bold(parent_acc_name)
if not parent_acc_name_map.get(company):
frappe.throw(_("While creating account for child Company {0}, parent account {1} not found. Please create the parent account in corresponding COA")
.format(company, parent_acc_name))
frappe.throw(_("While creating account for Child Company {0}, parent account {1} not found. Please create the parent account in corresponding COA")
.format(company_bold, parent_acc_name_bold), title=_("Account Not Found"))
# validate if parent of child company account to be added is a group
if (frappe.db.get_value("Account", self.parent_account, "is_group")
and not frappe.db.get_value("Account", parent_acc_name_map[company], "is_group")):
msg = _("While creating account for Child Company {0}, parent account {1} found as a ledger account.").format(company_bold, parent_acc_name_bold)
msg += "<br><br>"
msg += _("Please convert the parent account in corresponding child company to a group account.")
frappe.throw(msg, title=_("Invalid Parent Account"))
filters = {
"account_name": self.account_name,
@ -289,10 +301,31 @@ def validate_account_number(name, account_number, company):
.format(account_number, account_with_same_number))
@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)
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>"
message += _("Renaming it is only allowed via parent company {0}, to avoid mismatch.").format(frappe.bold(ancestor))
message += "<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)
if account_number:
frappe.db.set_value("Account", name, "account_number", account_number.strip())
@ -300,6 +333,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_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)
if name != new_name:
frappe.rename_doc("Account", name, new_name, force=1)
@ -330,3 +369,14 @@ def get_root_company(company):
# return the topmost company in the hierarchy
ancestors = get_ancestors_of('Company', company, "lft asc")
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"] = {
breadcrumb: "Accounts",
title: __("Chart Of Accounts"),
title: __("Chart of Accounts"),
get_tree_root: false,
filters: [
{
@ -97,7 +97,7 @@ frappe.treeview_settings["Account"] = {
treeview.page.add_inner_button(__("Journal Entry"), function() {
frappe.new_doc('Journal Entry', {company: get_company()});
}, __('Create'));
treeview.page.add_inner_button(__("New Company"), function() {
treeview.page.add_inner_button(__("Company"), function() {
frappe.new_doc('Company');
}, __('Create'));

View File

@ -910,98 +910,8 @@
},
"is_group": 1
},
"Passiva": {
"Passiva - Verbindlichkeiten": {
"root_type": "Liability",
"A - Eigenkapital": {
"account_type": "Equity",
"is_group": 1,
"I - Gezeichnetes Kapital": {
"account_type": "Equity",
"is_group": 1,
"Gezeichnetes Kapital": {
"account_type": "Equity",
"account_number": "2900"
},
"Ausstehende Einlagen auf das gezeichnete Kapital": {
"account_number": "2910",
"is_group": 1
}
},
"II - Kapitalr\u00fccklage": {
"account_type": "Equity",
"is_group": 1,
"Kapitalr\u00fccklage": {
"account_number": "2920"
}
},
"III - Gewinnr\u00fccklagen": {
"account_type": "Equity",
"1 - gesetzliche R\u00fccklage": {
"account_type": "Equity",
"is_group": 1,
"Gesetzliche R\u00fccklage": {
"account_number": "2930"
}
},
"2 - R\u00fccklage f. Anteile an einem herrschenden oder mehrheitlich beteiligten Unternehmen": {
"account_type": "Equity",
"is_group": 1
},
"3 - satzungsm\u00e4\u00dfige R\u00fccklagen": {
"account_type": "Equity",
"is_group": 1,
"Satzungsm\u00e4\u00dfige R\u00fccklagen": {
"account_number": "2950"
}
},
"4 - andere Gewinnr\u00fccklagen": {
"account_type": "Equity",
"is_group": 1,
"Gewinnr\u00fccklagen aus den \u00dcbergangsvorschriften BilMoG": {
"is_group": 1,
"Gewinnr\u00fccklagen (BilMoG)": {
"account_number": "2963"
},
"Gewinnr\u00fccklagen aus Zuschreibung Sachanlageverm\u00f6gen (BilMoG)": {
"account_number": "2964"
},
"Gewinnr\u00fccklagen aus Zuschreibung Finanzanlageverm\u00f6gen (BilMoG)": {
"account_number": "2965"
},
"Gewinnr\u00fccklagen aus Aufl\u00f6sung der Sonderposten mit R\u00fccklageanteil (BilMoG)": {
"account_number": "2966"
}
},
"Latente Steuern (Gewinnr\u00fccklage Haben) aus erfolgsneutralen Verrechnungen": {
"account_number": "2967"
},
"Latente Steuern (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
"account_number": "2968"
},
"Rechnungsabgrenzungsposten (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
"account_number": "2969"
}
},
"is_group": 1
},
"IV - Gewinnvortrag/Verlustvortrag": {
"account_type": "Equity",
"is_group": 1,
"Gewinnvortrag vor Verwendung": {
"account_number": "2970"
},
"Verlustvortrag vor Verwendung": {
"account_number": "2978"
}
},
"V - Jahres\u00fcberschu\u00df/Jahresfehlbetrag": {
"account_type": "Equity",
"is_group": 1
},
"Einlagen stiller Gesellschafter": {
"account_number": "9295"
}
},
"B - R\u00fcckstellungen": {
"is_group": 1,
"1 - R\u00fcckstellungen f. Pensionen und \u00e4hnliche Verplicht.": {
@ -1618,6 +1528,143 @@
},
"is_group": 1
},
"Passiva - Eigenkapital": {
"root_type": "Equity",
"A - Eigenkapital": {
"account_type": "Equity",
"is_group": 1,
"I - Gezeichnetes Kapital": {
"account_type": "Equity",
"is_group": 1,
"Gezeichnetes Kapital": {
"account_number": "2900",
"account_type": "Equity"
},
"Gesch\u00e4ftsguthaben der verbleibenden Mitglieder": {
"account_number": "2901"
},
"Gesch\u00e4ftsguthaben der ausscheidenden Mitglieder": {
"account_number": "2902"
},
"Gesch\u00e4ftsguthaben aus gek\u00fcndigten Gesch\u00e4ftsanteilen": {
"account_number": "2903"
},
"R\u00fcckst\u00e4ndige f\u00e4llige Einzahlungen auf Gesch\u00e4ftsanteile, vermerkt": {
"account_number": "2906"
},
"Gegenkonto R\u00fcckst\u00e4ndige f\u00e4llige Einzahlungen auf Gesch\u00e4ftsanteile, vermerkt": {
"account_number": "2907"
},
"Kapitalerh\u00f6hung aus Gesellschaftsmitteln": {
"account_number": "2908"
},
"Ausstehende Einlagen auf das gezeichnete Kapital, nicht eingefordert": {
"account_number": "2910"
}
},
"II - Kapitalr\u00fccklage": {
"account_type": "Equity",
"is_group": 1,
"Kapitalr\u00fccklage": {
"account_number": "2920"
},
"Kapitalr\u00fccklage durch Ausgabe von Anteilen \u00fcber Nennbetrag": {
"account_number": "2925"
},
"Kapitalr\u00fccklage durch Ausgabe von Schuldverschreibungen": {
"account_number": "2926"
},
"Kapitalr\u00fccklage durch Zuzahlungen gegen Gew\u00e4hrung eines Vorzugs": {
"account_number": "2927"
},
"Kapitalr\u00fccklage durch Zuzahlungen in das Eigenkapital": {
"account_number": "2928"
},
"Nachschusskapital (Gegenkonto 1299)": {
"account_number": "2929"
}
},
"III - Gewinnr\u00fccklagen": {
"account_type": "Equity",
"1 - gesetzliche R\u00fccklage": {
"account_type": "Equity",
"is_group": 1,
"Gesetzliche R\u00fccklage": {
"account_number": "2930"
}
},
"2 - R\u00fccklage f. Anteile an einem herrschenden oder mehrheitlich beteiligten Unternehmen": {
"account_type": "Equity",
"is_group": 1,
"R\u00fccklage f. Anteile an einem herrschenden oder mehrheitlich beteiligten Unternehmen": {
"account_number": "2935"
}
},
"3 - satzungsm\u00e4\u00dfige R\u00fccklagen": {
"account_type": "Equity",
"is_group": 1,
"Satzungsm\u00e4\u00dfige R\u00fccklagen": {
"account_number": "2950"
}
},
"4 - andere Gewinnr\u00fccklagen": {
"account_type": "Equity",
"is_group": 1,
"Andere Gewinnr\u00fccklagen": {
"account_number": "2960"
},
"Andere Gewinnr\u00fccklagen aus dem Erwerb eigener Anteile": {
"account_number": "2961"
},
"Eigenkapitalanteil von Wertaufholungen": {
"account_number": "2962"
},
"Gewinnr\u00fccklagen aus den \u00dcbergangsvorschriften BilMoG": {
"is_group": 1,
"Gewinnr\u00fccklagen (BilMoG)": {
"account_number": "2963"
},
"Gewinnr\u00fccklagen aus Zuschreibung Sachanlageverm\u00f6gen (BilMoG)": {
"account_number": "2964"
},
"Gewinnr\u00fccklagen aus Zuschreibung Finanzanlageverm\u00f6gen (BilMoG)": {
"account_number": "2965"
},
"Gewinnr\u00fccklagen aus Aufl\u00f6sung der Sonderposten mit R\u00fccklageanteil (BilMoG)": {
"account_number": "2966"
}
},
"Latente Steuern (Gewinnr\u00fccklage Haben) aus erfolgsneutralen Verrechnungen": {
"account_number": "2967"
},
"Latente Steuern (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
"account_number": "2968"
},
"Rechnungsabgrenzungsposten (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
"account_number": "2969"
}
},
"is_group": 1
},
"IV - Gewinnvortrag/Verlustvortrag": {
"account_type": "Equity",
"is_group": 1,
"Gewinnvortrag vor Verwendung": {
"account_number": "2970"
},
"Verlustvortrag vor Verwendung": {
"account_number": "2978"
}
},
"V - Jahres\u00fcberschu\u00df/Jahresfehlbetrag": {
"account_type": "Equity",
"is_group": 1
},
"Einlagen stiller Gesellschafter": {
"account_number": "9295"
}
}
},
"1 - Umsatzerl\u00f6se": {
"root_type": "Income",
"is_group": 1,

View File

@ -245,6 +245,9 @@ def get():
"account_number": "2200"
},
_("Duties and Taxes"): {
_("TDS Payable"): {
"account_number": "2310"
},
"account_type": "Tax",
"is_group": 1,
"account_number": "2300"

View File

@ -5,8 +5,7 @@ from __future__ import unicode_literals
import unittest
import frappe
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 merge_account
from erpnext.accounts.doctype.account.account import update_account_number, merge_account
class TestAccount(unittest.TestCase):
def test_rename_account(self):
@ -99,7 +98,8 @@ class TestAccount(unittest.TestCase):
"Softwares - _TC", doc.is_group, doc.root_type, doc.company)
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.account_name = "Test Sync Account"
acc.parent_account = "Temporary Accounts - _TC3"
@ -111,7 +111,68 @@ class TestAccount(unittest.TestCase):
self.assertEqual(acc_tc_4, "Test Sync Account - _TC4")
self.assertEqual(acc_tc_5, "Test Sync Account - _TC5")
def _make_test_records(verbose):
def test_add_account_to_a_group(self):
frappe.db.set_value("Account", "Office Rent - _TC3", "is_group", 1)
acc = frappe.new_doc("Account")
acc.account_name = "Test Group Account"
acc.parent_account = "Office Rent - _TC3"
acc.company = "_Test Company 3"
self.assertRaises(frappe.ValidationError, acc.insert)
frappe.db.set_value("Account", "Office Rent - _TC3", "is_group", 0)
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=None):
from frappe.test_runner import make_test_objects
accounts = [

View File

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

View File

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

View File

@ -40,7 +40,7 @@
"fields": [
{
"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",
"fieldtype": "Check",
"hidden": 1,
@ -48,23 +48,23 @@
"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",
"fieldtype": "Date",
"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",
"fieldname": "frozen_accounts_modifier",
"fieldtype": "Link",
"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"
},
{
"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",
"fieldtype": "Select",
"label": "Determine Address Tax Category From",
@ -75,7 +75,7 @@
"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",
"fieldtype": "Link",
"in_list_view": 1,
@ -104,7 +104,7 @@
"default": "1",
"fieldname": "unlink_advance_payment_on_cancelation_of_order",
"fieldtype": "Check",
"label": "Unlink Advance Payment on Cancelation of Order"
"label": "Unlink Advance Payment on Cancellation of Order"
},
{
"default": "1",
@ -127,7 +127,7 @@
"default": "0",
"fieldname": "show_inclusive_tax_in_print",
"fieldtype": "Check",
"label": "Show Inclusive Tax In Print"
"label": "Show Inclusive Tax in Print"
},
{
"fieldname": "column_break_12",
@ -165,7 +165,7 @@
},
{
"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",
"fieldtype": "Check",
"label": "Use Custom Cash Flow Format"
@ -177,7 +177,7 @@
"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",
"fieldtype": "Currency",
"label": "Over Billing Allowance (%)"
@ -199,7 +199,7 @@
},
{
"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",
"fieldtype": "Check",
"label": "Book Deferred Entries Via Journal Entry"
@ -214,7 +214,7 @@
},
{
"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",
"fieldtype": "Select",
"label": "Book Deferred Entries Based On",
@ -223,9 +223,10 @@
],
"icon": "icon-cog",
"idx": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2020-08-03 20:13:26.043092",
"modified": "2020-10-13 11:32:52.268826",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
@ -253,4 +254,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
}
}

View File

@ -55,7 +55,7 @@ class BankStatementTransactionEntry(Document):
def populate_payment_entries(self):
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):
frappe.throw(_("Transactions already retreived from the statement"))
@ -65,7 +65,7 @@ class BankStatementTransactionEntry(Document):
if self.bank_settings:
mapped_items = frappe.get_doc("Bank Statement Settings", self.bank_settings).mapped_items
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:
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"]))
@ -398,20 +398,21 @@ def get_transaction_info(headers, header_index, row):
transaction[header] = ""
return transaction
def get_transaction_entries(filename, headers):
def get_transaction_entries(file_url, headers):
header_index = {}
rows, transactions = [], []
if (filename.lower().endswith("xlsx")):
if (file_url.lower().endswith("xlsx")):
from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file
rows = read_xlsx_file_from_attached_file(file_id=filename)
elif (filename.lower().endswith("csv")):
rows = read_xlsx_file_from_attached_file(file_url=file_url)
elif (file_url.lower().endswith("csv")):
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()
with open(filepath,'rb') as csvfile:
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)
else:
frappe.throw(_("Only .csv and .xlsx files are supported currently"))

View File

@ -9,11 +9,13 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
from erpnext.accounts.page.bank_reconciliation.bank_reconciliation import reconcile, get_linked_payments
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
test_dependencies = ["Item", "Cost Center"]
class TestBankTransaction(unittest.TestCase):
def setUp(self):
make_pos_profile()
add_transactions()
add_payments()
@ -27,6 +29,9 @@ class TestBankTransaction(unittest.TestCase):
frappe.db.sql("""delete from `tabPayment Entry Reference`""")
frappe.db.sql("""delete from `tabPayment Entry`""")
# Delete POS Profile
frappe.db.sql("delete from `tabPOS Profile`")
frappe.flags.test_bank_transactions_created = False
frappe.flags.test_payments_created = False
@ -91,15 +96,11 @@ class TestBankTransaction(unittest.TestCase):
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)
def add_transactions():
if frappe.flags.test_bank_transactions_created:
return
frappe.set_user("Administrator")
def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
try:
frappe.get_doc({
"doctype": "Bank",
"bank_name":"Citi Bank",
"bank_name":bank_name,
}).insert()
except frappe.DuplicateEntryError:
pass
@ -108,12 +109,19 @@ def add_transactions():
frappe.get_doc({
"doctype": "Bank Account",
"account_name":"Checking Account",
"bank": "Citi Bank",
"account": "_Test Bank - _TC"
"bank": bank_name,
"account": account_name
}).insert()
except frappe.DuplicateEntryError:
pass
def add_transactions():
if frappe.flags.test_bank_transactions_created:
return
frappe.set_user("Administrator")
create_bank_account()
doc = frappe.get_doc({
"doctype": "Bank Transaction",
"description":"1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G",

File diff suppressed because it is too large Load Diff

View File

@ -158,8 +158,11 @@ class TestBudget(unittest.TestCase):
set_total_expense_zero(nowdate(), "cost_center")
budget = make_budget(budget_against="Cost Center")
month = now_datetime().month
if month > 9:
month = 9
for i in range(now_datetime().month):
for i in range(month+1):
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_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")
budget = make_budget(budget_against="Project")
month = now_datetime().month
if month > 9:
month = 9
for i in range(now_datetime().month):
for i in range(month + 1):
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")

View File

@ -23,13 +23,13 @@ class CashierClosing(Document):
where posting_date=%s and posting_time>=%s and posting_time<=%s and owner=%s
""", (self.date, self.from_time, self.time, self.user))
self.outstanding_amount = flt(values[0][0] if values else 0)
def make_calculations(self):
total = 0.00
for i in self.payments:
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):
if self.from_time >= self.time:

View File

@ -94,8 +94,7 @@ frappe.ui.form.on('Chart of Accounts Importer', {
callback: function(r) {
if(r.message===false) {
frm.set_value("company", "");
frappe.throw(__(`Transactions against the company already exist!
Chart Of accounts can be imported for company with no transactions`));
frappe.throw(__("Transactions against the Company already exist! Chart of Accounts can only be imported for a Company with no transactions."));
} else {
frm.trigger("refresh");
}

View File

@ -195,7 +195,7 @@ def build_response_as_excel(writer):
reader = csv.reader(f)
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()
os.remove(filename)

View File

@ -28,22 +28,22 @@ def test_create_test_data():
"item_group": "_Test Item Group",
"item_name": "_Test Tesla Car",
"apply_warehouse_wise_reorder_level": 0,
"warehouse":"Stores - TCP1",
"warehouse":"Stores - _TC",
"gst_hsn_code": "999800",
"valuation_rate": 5000,
"standard_rate":5000,
"item_defaults": [{
"company": "_Test Company with perpetual inventory",
"default_warehouse": "Stores - TCP1",
"company": "_Test Company",
"default_warehouse": "Stores - _TC",
"default_price_list":"_Test Price List",
"expense_account": "Cost of Goods Sold - TCP1",
"buying_cost_center": "Main - TCP1",
"selling_cost_center": "Main - TCP1",
"income_account": "Sales - TCP1"
"expense_account": "Cost of Goods Sold - _TC",
"buying_cost_center": "Main - _TC",
"selling_cost_center": "Main - _TC",
"income_account": "Sales - _TC"
}],
"show_in_website": 1,
"route":"-test-tesla-car",
"website_warehouse": "Stores - TCP1"
"website_warehouse": "Stores - _TC"
})
item.insert()
# create test item price
@ -65,12 +65,12 @@ def test_create_test_data():
"items": [{
"item_code": "_Test Tesla Car"
}],
"warehouse":"Stores - TCP1",
"warehouse":"Stores - _TC",
"coupon_code_based":1,
"selling": 1,
"rate_or_discount": "Discount Percentage",
"discount_percentage": 30,
"company": "_Test Company with perpetual inventory",
"company": "_Test Company",
"currency":"INR",
"for_price_list":"_Test Price List"
})
@ -85,7 +85,7 @@ def test_create_test_data():
})
sales_partner.insert()
# create test item coupon code
if not frappe.db.exists("Coupon Code","SAVE30"):
if not frappe.db.exists("Coupon Code", "SAVE30"):
coupon_code = frappe.get_doc({
"doctype": "Coupon Code",
"coupon_name":"SAVE30",
@ -102,35 +102,27 @@ class TestCouponCode(unittest.TestCase):
test_create_test_data()
def tearDown(self):
frappe.set_user("Administrator")
frappe.set_user("Administrator")
def test_1_check_coupon_code_used_before_so(self):
coupon_code = frappe.get_doc("Coupon Code", frappe.db.get_value("Coupon Code", {"coupon_name":"SAVE30"}))
# reset used coupon code count
coupon_code.used=0
coupon_code.save()
# check no coupon code is used before sales order is made
self.assertEqual(coupon_code.get("used"),0)
def test_sales_order_with_coupon_code(self):
frappe.db.set_value("Coupon Code", "SAVE30", "used", 0)
def test_2_sales_order_with_coupon_code(self):
so = make_sales_order(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1',
customer="_Test Customer", selling_price_list="_Test Price List", item_code="_Test Tesla Car", rate=5000,qty=1,
so = make_sales_order(company='_Test Company', warehouse='Stores - _TC',
customer="_Test Customer", selling_price_list="_Test Price List",
item_code="_Test Tesla Car", rate=5000, qty=1,
do_not_submit=True)
so = frappe.get_doc('Sales Order', so.name)
# check item price before coupon code is applied
self.assertEqual(so.items[0].rate, 5000)
so.coupon_code='SAVE30'
so.sales_partner='_Test Coupon Partner'
so.save()
# check item price after coupon code is applied
self.assertEqual(so.items[0].rate, 3500)
so.submit()
def test_3_check_coupon_code_used_after_so(self):
doc = frappe.get_doc("Coupon Code", frappe.db.get_value("Coupon Code", {"coupon_name":"SAVE30"}))
# check no coupon code is used before sales order is made
self.assertEqual(doc.get("used"),1)
self.assertEqual(frappe.db.get_value("Coupon Code", "SAVE30", "used"), 1)

View File

@ -9,11 +9,7 @@ frappe.ui.form.on('Fiscal Year', {
}
},
refresh: function (frm) {
let doc = frm.doc;
frm.toggle_enable('year_start_date', doc.__islocal);
frm.toggle_enable('year_end_date', doc.__islocal);
if (!doc.__islocal && (doc.name != frappe.sys_defaults.fiscal_year)) {
if (!frm.doc.__islocal && (frm.doc.name != frappe.sys_defaults.fiscal_year)) {
frm.add_custom_button(__("Set as Default"), () => frm.events.set_as_default(frm));
frm.set_intro(__("To set this Fiscal Year as Default, click on 'Set as Default'"));
} else {
@ -24,8 +20,10 @@ frappe.ui.form.on('Fiscal Year', {
return frm.call('set_as_default');
},
year_start_date: function(frm) {
let year_end_date =
frappe.datetime.add_days(frappe.datetime.add_months(frm.doc.year_start_date, 12), -1);
frm.set_value("year_end_date", year_end_date);
if (!frm.doc.is_short_year) {
let year_end_date =
frappe.datetime.add_days(frappe.datetime.add_months(frm.doc.year_start_date, 12), -1);
frm.set_value("year_end_date", year_end_date);
}
},
});

View File

@ -1,347 +1,126 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 0,
"autoname": "field:year",
"beta": 0,
"creation": "2013-01-22 16:50:25",
"custom": 0,
"description": "**Fiscal Year** represents a Financial Year. All accounting entries and other major transactions are tracked against **Fiscal Year**.",
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 0,
"actions": [],
"allow_import": 1,
"autoname": "field:year",
"creation": "2013-01-22 16:50:25",
"description": "**Fiscal Year** represents a Financial Year. All accounting entries and other major transactions are tracked against **Fiscal Year**.",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
"year",
"disabled",
"is_short_year",
"year_start_date",
"year_end_date",
"companies",
"auto_created"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "For e.g. 2012, 2012-13",
"fieldname": "year",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Year Name",
"length": 0,
"no_copy": 0,
"oldfieldname": "year",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"description": "For e.g. 2012, 2012-13",
"fieldname": "year",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Year Name",
"oldfieldname": "year",
"oldfieldtype": "Data",
"reqd": 1,
"unique": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "disabled",
"fieldtype": "Check",
"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": "Disabled",
"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
},
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "year_start_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Year Start Date",
"length": 0,
"no_copy": 1,
"oldfieldname": "year_start_date",
"oldfieldtype": "Date",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "year_start_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Year Start Date",
"no_copy": 1,
"oldfieldname": "year_start_date",
"oldfieldtype": "Date",
"reqd": 1,
"set_only_once": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "year_end_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Year End Date",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "year_end_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Year End Date",
"no_copy": 1,
"reqd": 1,
"set_only_once": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "companies",
"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": "Companies",
"length": 0,
"no_copy": 0,
"options": "Fiscal Year Company",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "companies",
"fieldtype": "Table",
"label": "Companies",
"options": "Fiscal Year Company"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "auto_created",
"fieldtype": "Check",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Auto Created",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"default": "0",
"fieldname": "auto_created",
"fieldtype": "Check",
"hidden": 1,
"label": "Auto Created",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"default": "0",
"description": "Less than 12 months.",
"fieldname": "is_short_year",
"fieldtype": "Check",
"label": "Is Short Year",
"set_only_once": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-calendar",
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-04-25 14:21:41.273354",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Fiscal Year",
"owner": "Administrator",
],
"icon": "fa fa-calendar",
"idx": 1,
"links": [],
"modified": "2020-11-05 12:16:53.081573",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Fiscal Year",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
},
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Sales User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
},
"read": 1,
"role": "Sales User"
},
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Purchase User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
},
"read": 1,
"role": "Purchase User"
},
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Accounts User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
},
"read": 1,
"role": "Accounts User"
},
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Stock User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
},
"read": 1,
"role": "Stock User"
},
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Employee",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
"read": 1,
"role": "Employee"
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 1,
"sort_field": "name",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0
],
"show_name_in_global_search": 1,
"sort_field": "name",
"sort_order": "DESC"
}

View File

@ -36,6 +36,11 @@ class FiscalYear(Document):
frappe.throw(_("Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved."))
def validate_dates(self):
if self.is_short_year:
# Fiscal Year can be shorter than one year, in some jurisdictions
# under certain circumstances. For example, in the USA and Germany.
return
if getdate(self.year_start_date) > getdate(self.year_end_date):
frappe.throw(_("Fiscal Year Start Date should be one year earlier than Fiscal Year End Date"),
FiscalYearIncorrectDate)
@ -116,12 +121,8 @@ def auto_create_fiscal_year():
pass
def get_from_and_to_date(fiscal_year):
from_and_to_date_tuple = frappe.db.sql("""select year_start_date, year_end_date
from `tabFiscal Year` where name=%s""", (fiscal_year))[0]
from_and_to_date = {
"from_date": from_and_to_date_tuple[0],
"to_date": from_and_to_date_tuple[1]
}
return from_and_to_date
fields = [
"year_start_date as from_date",
"year_end_date as to_date"
]
return frappe.db.get_value("Fiscal Year", fiscal_year, fields, as_dict=1)

View File

@ -11,6 +11,7 @@ test_records = frappe.get_test_records('Fiscal Year')
test_ignore = ["Company"]
class TestFiscalYear(unittest.TestCase):
def test_extra_year(self):
if frappe.db.exists("Fiscal Year", "_Test Fiscal Year 2000"):
frappe.delete_doc("Fiscal Year", "_Test Fiscal Year 2000")

View File

@ -1,4 +1,11 @@
[
{
"doctype": "Fiscal Year",
"year": "_Test Short Fiscal Year 2011",
"is_short_year": 1,
"year_end_date": "2011-04-01",
"year_start_date": "2011-12-31"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2012",

View File

@ -30,20 +30,22 @@ class GLEntry(Document):
self.pl_must_have_cost_center()
self.validate_cost_center()
self.check_pl_account()
self.validate_party()
self.validate_currency()
if not self.flags.from_repost:
self.check_pl_account()
self.validate_party()
self.validate_currency()
def on_update_with_args(self, adv_adj, update_outstanding = 'Yes'):
self.validate_account_details(adv_adj)
self.validate_dimensions_for_pl_and_bs()
def on_update_with_args(self, adv_adj, update_outstanding = 'Yes', from_repost=False):
if not from_repost:
self.validate_account_details(adv_adj)
self.validate_dimensions_for_pl_and_bs()
validate_frozen_account(self.account, adv_adj)
validate_balance_type(self.account, adv_adj)
# Update outstanding amt on against voucher
if self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees'] \
and self.against_voucher and update_outstanding == 'Yes':
and self.against_voucher and update_outstanding == 'Yes' and not from_repost:
update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type,
self.against_voucher)
@ -106,8 +108,8 @@ class GLEntry(Document):
from tabAccount where name=%s""", self.account, as_dict=1)[0]
if ret.is_group==1:
frappe.throw(_('''{0} {1}: Account {2} is a Group Account and group accounts cannot be used in
transactions''').format(self.voucher_type, self.voucher_no, self.account))
frappe.throw(_('''{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions''')
.format(self.voucher_type, self.voucher_no, self.account))
if ret.docstatus==2:
frappe.throw(_("{0} {1}: Account {2} is inactive")
@ -136,8 +138,8 @@ class GLEntry(Document):
.format(self.voucher_type, self.voucher_no, self.cost_center, self.company))
if self.cost_center and _check_is_group():
frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot
be used in transactions""").format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center)))
frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""")
.format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center)))
def validate_party(self):
validate_party_frozen_disabled(self.party_type, self.party)

View File

@ -137,11 +137,12 @@ class InvoiceDiscounting(AccountsController):
"cost_center": erpnext.get_default_cost_center(self.company)
})
je.append("accounts", {
"account": self.bank_charges_account,
"debit_in_account_currency": flt(self.bank_charges),
"cost_center": erpnext.get_default_cost_center(self.company)
})
if self.bank_charges:
je.append("accounts", {
"account": self.bank_charges_account,
"debit_in_account_currency": flt(self.bank_charges),
"cost_center": erpnext.get_default_cost_center(self.company)
})
je.append("accounts", {
"account": self.short_term_loan,

View File

@ -80,6 +80,7 @@ class TestInvoiceDiscounting(unittest.TestCase):
short_term_loan=self.short_term_loan,
bank_charges_account=self.bank_charges_account,
bank_account=self.bank_account,
bank_charges=100
)
je = inv_disc.create_disbursement_entry()
@ -289,6 +290,7 @@ def create_invoice_discounting(invoices, **args):
inv_disc.bank_account=args.bank_account
inv_disc.loan_start_date = args.start or nowdate()
inv_disc.loan_period = args.period or 30
inv_disc.bank_charges = flt(args.bank_charges)
for d in invoices:
inv_disc.append("invoices", {

View File

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

View File

@ -20,7 +20,8 @@ def get_data():
'items': ['Purchase Invoice', 'Purchase Order', 'Purchase Receipt']
},
{
'items': ['Item']
'label': _('Stock'),
'items': ['Item Groups', 'Item']
}
]
}

View File

@ -210,7 +210,7 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({
$.each(this.frm.doc.accounts || [], function(i, 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());
}
},

View File

@ -1,5 +1,6 @@
{
"actions": [],
"allow_auto_repeat": 1,
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2013-03-25 10:53:52",
@ -503,7 +504,7 @@
"idx": 176,
"is_submittable": 1,
"links": [],
"modified": "2020-06-02 18:15:46.955697",
"modified": "2020-10-30 13:56:01.121995",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",

View File

@ -6,14 +6,18 @@ import frappe, erpnext, json
from frappe.utils import cstr, flt, fmt_money, formatdate, getdate, nowdate, cint, get_link_to_form
from frappe import msgprint, _, scrub
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.accounts.utils import get_balance_on, get_account_currency
from erpnext.accounts.utils import get_balance_on, get_stock_accounts, get_stock_and_account_balance, \
get_account_currency, check_if_stock_and_account_balance_synced
from erpnext.accounts.party import get_party_account
from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount
from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import get_party_account_based_on_invoice_discounting
from erpnext.accounts.doctype.invoice_discounting.invoice_discounting \
import get_party_account_based_on_invoice_discounting
from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts
from six import string_types, iteritems
class StockAccountInvalidTransaction(frappe.ValidationError): pass
class JournalEntry(AccountsController):
def __init__(self, *args, **kwargs):
super(JournalEntry, self).__init__(*args, **kwargs)
@ -22,14 +26,19 @@ class JournalEntry(AccountsController):
return self.voucher_type
def validate(self):
if self.voucher_type == 'Opening Entry':
self.is_opening = 'Yes'
if not self.is_opening:
self.is_opening='No'
self.clearance_date = None
self.validate_party()
self.validate_entries_for_advance()
self.validate_multi_currency()
self.set_amounts_in_company_currency()
self.validate_debit_credit_amount()
self.validate_total_debit_and_credit()
self.validate_against_jv()
self.validate_reference_doc()
@ -41,6 +50,7 @@ class JournalEntry(AccountsController):
self.validate_empty_accounts_table()
self.set_account_and_party_balance()
self.validate_inter_company_accounts()
self.validate_stock_accounts()
if not self.title:
self.title = self.get_title()
@ -52,6 +62,8 @@ class JournalEntry(AccountsController):
self.update_expense_claim()
self.update_inter_company_jv()
self.update_invoice_discounting()
check_if_stock_and_account_balance_synced(self.posting_date,
self.company, self.doctype, self.name)
def on_cancel(self):
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
@ -90,6 +102,16 @@ class JournalEntry(AccountsController):
if account_currency == previous_account_currency:
if self.total_credit != doc.total_debit or self.total_debit != doc.total_credit:
frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry"))
def validate_stock_accounts(self):
stock_accounts = get_stock_accounts(self.company, self.doctype, self.name)
for account in stock_accounts:
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(account,
self.posting_date, self.company)
if account_bal == stock_bal:
frappe.throw(_("Account: {0} can only be updated via Stock Transactions")
.format(account), StockAccountInvalidTransaction)
def update_inter_company_jv(self):
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
@ -335,8 +357,7 @@ class JournalEntry(AccountsController):
currency=account_currency)
if flt(voucher_total) < (flt(order.advance_paid) + total):
frappe.throw(_("Advance paid against {0} {1} cannot be greater \
than Grand Total {2}").format(reference_type, reference_name, formatted_voucher_total))
frappe.throw(_("Advance paid against {0} {1} cannot be greater than Grand Total {2}").format(reference_type, reference_name, formatted_voucher_total))
def validate_invoices(self):
"""Validate totals and docstatus for invoices"""
@ -365,6 +386,11 @@ class JournalEntry(AccountsController):
if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited)))
if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited)))
def validate_debit_credit_amount(self):
for d in self.get('accounts'):
if not flt(d.debit) and not flt(d.credit):
frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx))
def validate_total_debit_and_credit(self):
self.set_total_debit_credit()
if self.difference:
@ -1023,7 +1049,7 @@ def make_inter_company_journal_entry(name, voucher_type, company):
return journal_entry.as_dict()
@frappe.whitelist()
def make_reverse_journal_entry(source_name, target_doc=None, ignore_permissions=False):
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):
@ -1049,6 +1075,6 @@ def make_reverse_journal_entry(source_name, target_doc=None, ignore_permissions=
},
"postprocess": update_accounts,
},
}, target_doc, ignore_permissions=ignore_permissions)
}, target_doc)
return doclist

View File

@ -6,7 +6,7 @@ import unittest, frappe
from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.exceptions import InvalidAccountCurrency
from erpnext.accounts.general_ledger import StockAccountInvalidTransaction
from erpnext.accounts.doctype.journal_entry.journal_entry import StockAccountInvalidTransaction
class TestJournalEntry(unittest.TestCase):
def test_journal_entry_with_against_jv(self):
@ -75,54 +75,46 @@ class TestJournalEntry(unittest.TestCase):
elif test_voucher.doctype in ["Sales Order", "Purchase Order"]:
# if test_voucher is a Sales Order/Purchase Order, test error on cancellation of test_voucher
frappe.db.set_value("Accounts Settings", "Accounts Settings",
"unlink_advance_payment_on_cancelation_of_order", 0)
submitted_voucher = frappe.get_doc(test_voucher.doctype, test_voucher.name)
self.assertRaises(frappe.LinkExistsError, submitted_voucher.cancel)
def test_jv_against_stock_account(self):
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
set_perpetual_inventory()
company = "_Test Company with perpetual inventory"
stock_account = get_inventory_account(company)
jv = frappe.copy_doc({
"cheque_date": nowdate(),
"cheque_no": "33",
"company": "_Test Company with perpetual inventory",
"doctype": "Journal Entry",
"accounts": [
{
"account": "Debtors - TCP1",
"party_type": "Customer",
"party": "_Test Customer",
"credit_in_account_currency": 400.0,
"debit_in_account_currency": 0.0,
"doctype": "Journal Entry Account",
"parentfield": "accounts",
"cost_center": "Main - TCP1"
},
{
"account": "_Test Bank - TCP1",
"credit_in_account_currency": 0.0,
"debit_in_account_currency": 400.0,
"doctype": "Journal Entry Account",
"parentfield": "accounts",
"cost_center": "Main - TCP1"
}
],
"naming_series": "_T-Journal Entry-",
"posting_date": nowdate(),
"user_remark": "test",
"voucher_type": "Bank Entry"
})
from erpnext.accounts.utils import get_stock_and_account_balance
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(stock_account, nowdate(), company)
diff = flt(account_bal) - flt(stock_bal)
jv.get("accounts")[0].update({
"account": get_inventory_account('_Test Company with perpetual inventory'),
"company": "_Test Company with perpetual inventory",
"party_type": None,
"party": None
if not diff:
diff = 100
jv = frappe.new_doc("Journal Entry")
jv.company = company
jv.posting_date = nowdate()
jv.append("accounts", {
"account": stock_account,
"cost_center": "Main - TCP1",
"debit_in_account_currency": 0 if diff > 0 else abs(diff),
"credit_in_account_currency": diff if diff > 0 else 0
})
jv.append("accounts", {
"account": "Stock Adjustment - TCP1",
"cost_center": "Main - TCP1",
"debit_in_account_currency": diff if diff > 0 else 0,
"credit_in_account_currency": 0 if diff > 0 else abs(diff)
})
jv.insert()
self.assertRaises(StockAccountInvalidTransaction, jv.submit)
jv.cancel()
set_perpetual_inventory(0)
if account_bal == stock_bal:
self.assertRaises(StockAccountInvalidTransaction, jv.submit)
frappe.db.rollback()
else:
jv.submit()
jv.cancel()
def test_multi_currency(self):
jv = make_journal_entry("_Test Bank USD - _TC",

View File

@ -8,12 +8,10 @@ import unittest
from frappe.utils import today, cint, flt, getdate
from erpnext.accounts.doctype.loyalty_program.loyalty_program import get_loyalty_program_details_with_points
from erpnext.accounts.party import get_dashboard_info
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
class TestLoyaltyProgram(unittest.TestCase):
@classmethod
def setUpClass(self):
set_perpetual_inventory(0)
# create relevant item, customer, loyalty program, etc
create_records()
@ -195,88 +193,91 @@ def create_sales_invoice_record(qty=1):
def create_records():
# create a new loyalty Account
if frappe.db.exists("Account", "Loyalty - _TC"):
return
frappe.get_doc({
"doctype": "Account",
"account_name": "Loyalty",
"parent_account": "Direct Expenses - _TC",
"company": "_Test Company",
"is_group": 0,
"account_type": "Expense Account",
}).insert()
if not frappe.db.exists("Account", "Loyalty - _TC"):
frappe.get_doc({
"doctype": "Account",
"account_name": "Loyalty",
"parent_account": "Direct Expenses - _TC",
"company": "_Test Company",
"is_group": 0,
"account_type": "Expense Account",
}).insert()
# create a new loyalty program Single tier
frappe.get_doc({
"doctype": "Loyalty Program",
"loyalty_program_name": "Test Single Loyalty",
"auto_opt_in": 1,
"from_date": today(),
"loyalty_program_type": "Single Tier Program",
"conversion_factor": 1,
"expiry_duration": 10,
"company": "_Test Company",
"cost_center": "Main - _TC",
"expense_account": "Loyalty - _TC",
"collection_rules": [{
'tier_name': 'Silver',
'collection_factor': 1000,
'min_spent': 1000
}]
}).insert()
# create a new customer
frappe.get_doc({
"customer_group": "_Test Customer Group",
"customer_name": "Test Loyalty Customer",
"customer_type": "Individual",
"doctype": "Customer",
"territory": "_Test Territory"
}).insert()
# create a new loyalty program Multiple tier
frappe.get_doc({
"doctype": "Loyalty Program",
"loyalty_program_name": "Test Multiple Loyalty",
"auto_opt_in": 1,
"from_date": today(),
"loyalty_program_type": "Multiple Tier Program",
"conversion_factor": 1,
"expiry_duration": 10,
"company": "_Test Company",
"cost_center": "Main - _TC",
"expense_account": "Loyalty - _TC",
"collection_rules": [
{
if not frappe.db.exists("Loyalty Program","Test Single Loyalty"):
frappe.get_doc({
"doctype": "Loyalty Program",
"loyalty_program_name": "Test Single Loyalty",
"auto_opt_in": 1,
"from_date": today(),
"loyalty_program_type": "Single Tier Program",
"conversion_factor": 1,
"expiry_duration": 10,
"company": "_Test Company",
"cost_center": "Main - _TC",
"expense_account": "Loyalty - _TC",
"collection_rules": [{
'tier_name': 'Silver',
'collection_factor': 1000,
'min_spent': 10000
},
{
'tier_name': 'Gold',
'collection_factor': 1000,
'min_spent': 19000
}
]
}).insert()
'min_spent': 1000
}]
}).insert()
# create a new customer
if not frappe.db.exists("Customer","Test Loyalty Customer"):
frappe.get_doc({
"customer_group": "_Test Customer Group",
"customer_name": "Test Loyalty Customer",
"customer_type": "Individual",
"doctype": "Customer",
"territory": "_Test Territory"
}).insert()
# create a new loyalty program Multiple tier
if not frappe.db.exists("Loyalty Program","Test Multiple Loyalty"):
frappe.get_doc({
"doctype": "Loyalty Program",
"loyalty_program_name": "Test Multiple Loyalty",
"auto_opt_in": 1,
"from_date": today(),
"loyalty_program_type": "Multiple Tier Program",
"conversion_factor": 1,
"expiry_duration": 10,
"company": "_Test Company",
"cost_center": "Main - _TC",
"expense_account": "Loyalty - _TC",
"collection_rules": [
{
'tier_name': 'Silver',
'collection_factor': 1000,
'min_spent': 10000
},
{
'tier_name': 'Gold',
'collection_factor': 1000,
'min_spent': 19000
}
]
}).insert()
# create an item
item = frappe.get_doc({
"doctype": "Item",
"item_code": "Loyal Item",
"item_name": "Loyal Item",
"item_group": "All Item Groups",
"company": "_Test Company",
"is_stock_item": 1,
"opening_stock": 100,
"valuation_rate": 10000,
}).insert()
if not frappe.db.exists("Item", "Loyal Item"):
frappe.get_doc({
"doctype": "Item",
"item_code": "Loyal Item",
"item_name": "Loyal Item",
"item_group": "All Item Groups",
"company": "_Test Company",
"is_stock_item": 1,
"opening_stock": 100,
"valuation_rate": 10000,
}).insert()
# create item price
frappe.get_doc({
"doctype": "Item Price",
"price_list": "Standard Selling",
"item_code": item.item_code,
"price_list_rate": 10000
}).insert()
if not frappe.db.exists("Item Price", {"price_list": "Standard Selling", "item_code": "Loyal Item"}):
frappe.get_doc({
"doctype": "Item Price",
"price_list": "Standard Selling",
"item_code": "Loyal Item",
"price_list_rate": 10000
}).insert()

View File

@ -1,13 +1,17 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
cur_frm.set_query("default_account", "accounts", function(doc, cdt, cdn) {
var d = locals[cdt][cdn];
return{
filters: [
['Account', 'account_type', 'in', 'Bank, Cash, Receivable'],
['Account', 'is_group', '=', 0],
['Account', 'company', '=', d.company]
]
}
});
frappe.ui.form.on('Mode of Payment', {
setup: function(frm) {
frm.set_query("default_account", "accounts", function(doc, cdt, cdn) {
let d = locals[cdt][cdn];
return {
filters: [
['Account', 'account_type', 'in', 'Bank, Cash, Receivable'],
['Account', 'is_group', '=', 0],
['Account', 'company', '=', d.company]
]
};
});
},
});

View File

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

View File

@ -6,7 +6,7 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
frm.set_query('party_type', 'invoices', function(doc, cdt, cdn) {
return {
filters: {
'name': ['in', 'Customer,Supplier']
'name': ['in', 'Customer, Supplier']
}
};
});
@ -14,29 +14,46 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
if (frm.doc.company) {
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) {
frm.disable_save();
frm.trigger("make_dashboard");
!frm.doc.import_in_progress && frm.trigger("make_dashboard");
frm.page.set_primary_action(__('Create Invoices'), () => {
let btn_primary = frm.page.btn_primary.get(0);
return frm.call({
doc: frm.doc,
freeze: true,
btn: $(btn_primary),
method: "make_invoices",
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();
}
}
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type])
});
});
if (frm.doc.create_missing_party) {
frm.set_df_property("party", "fieldtype", "Data", frm.doc.name, "invoices");
}
},
setup_company_filters: function(frm) {

View File

@ -4,9 +4,12 @@
from __future__ import unicode_literals
import frappe
import traceback
from json import dumps
from frappe import _, scrub
from frappe.utils import flt, nowdate
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
@ -61,67 +64,48 @@ class OpeningInvoiceCreationTool(Document):
prepare_invoice_summary(doctype, invoices)
return invoices_summary, max_count
def make_invoices(self):
names = []
mandatory_error_msg = _("Row {0}: {1} is required to create the Opening {2} Invoices")
def validate_company(self):
if not self.company:
frappe.throw(_("Please select the Company"))
def set_missing_values(self, row):
row.qty = row.qty or 1.0
row.temporary_opening_account = row.temporary_opening_account or get_temporary_opening_account(self.company)
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()
company_details = frappe.get_cached_value('Company', self.company,
["default_currency", "default_letter_head"], as_dict=1) or {}
def validate_mandatory_invoice_fields(self, row):
if not frappe.db.exists(row.party_type, row.party):
if self.create_missing_party:
self.add_party(row.party_type, row.party)
else:
frappe.throw(_("Row #{}: {} {} does not exist.").format(row.idx, frappe.bold(row.party_type), frappe.bold(row.party)))
mandatory_error_msg = _("Row #{0}: {1} is required to create the Opening {2} Invoices")
for d in ("Party", "Outstanding Amount", "Temporary Opening Account"):
if not row.get(scrub(d)):
frappe.throw(mandatory_error_msg.format(row.idx, d, self.invoice_type))
def get_invoices(self):
invoices = []
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"
# Allow to create invoice even if no party present in customer or supplier.
if not frappe.db.exists(row.party_type, row.party):
if self.create_missing_party:
self.add_party(row.party_type, row.party)
else:
frappe.throw(_("{0} {1} does not exist.").format(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()
for d in ("Party", "Outstanding Amount", "Temporary Opening Account"):
if not row.get(scrub(d)):
frappe.throw(mandatory_error_msg.format(row.idx, _(d), self.invoice_type))
args = self.get_invoice_dict(row=row)
if not args:
if not row:
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:
args.update({
invoice.update({
"currency": company_details.get("default_currency"),
"letter_head": company_details.get("default_letter_head")
})
invoices.append(invoice)
doc = frappe.get_doc(args).insert()
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
return invoices
def add_party(self, party_type, party):
party_doc = frappe.new_doc(party_type)
@ -140,14 +124,12 @@ class OpeningInvoiceCreationTool(Document):
def get_invoice_dict(self, row=None):
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:
frappe.throw(
_("Please set the Default Cost Center in {0} company.").format(frappe.bold(self.company))
)
frappe.throw(_("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)
return frappe._dict({
@ -161,18 +143,9 @@ class OpeningInvoiceCreationTool(Document):
"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()
args = frappe._dict({
invoice = frappe._dict({
"items": [item],
"is_opening": "Yes",
"set_posting_time": 1,
@ -180,21 +153,77 @@ class OpeningInvoiceCreationTool(Document):
"cost_center": self.cost_center,
"due_date": row.due_date,
"posting_date": row.posting_date,
frappe.scrub(party_type): row.party,
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice"
frappe.scrub(row.party_type): row.party,
"is_pos": 0,
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
"update_stock": 0
})
accounting_dimension = get_accounting_dimensions()
for dimension in accounting_dimension:
args.update({
invoice.update({
dimension: item.get(dimension)
})
if self.invoice_type == "Sales":
args["is_pos"] = 0
return invoice
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()
def get_temporary_opening_account(company=None):

View File

@ -7,17 +7,24 @@ import frappe
import unittest
test_dependencies = ["Customer", "Supplier"]
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import get_temporary_opening_account
class TestOpeningInvoiceCreationTool(unittest.TestCase):
def make_invoices(self, invoice_type="Sales"):
def setUp(self):
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
make_company()
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None):
doc = frappe.get_single("Opening Invoice Creation Tool")
args = get_opening_invoice_creation_dict(invoice_type=invoice_type)
args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company,
party_1=party_1, party_2=party_2)
doc.update(args)
return doc.make_invoices()
def test_opening_sales_invoice_creation(self):
invoices = self.make_invoices()
property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check")
invoices = self.make_invoices(company="_Test Opening Invoice Company")
self.assertEqual(len(invoices), 2)
expected_value = {
@ -27,6 +34,13 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
}
self.check_expected_values(invoices, expected_value)
si = frappe.get_doc("Sales Invoice", invoices[0])
# Check if update stock is not enabled
self.assertEqual(si.update_stock, 0)
property_setter.delete()
def check_expected_values(self, invoices, expected_value, invoice_type="Sales"):
doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice"
@ -36,7 +50,7 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
self.assertEqual(si.get(field, ""), expected_value[invoice_idx][field_idx])
def test_opening_purchase_invoice_creation(self):
invoices = self.make_invoices(invoice_type="Purchase")
invoices = self.make_invoices(invoice_type="Purchase", company="_Test Opening Invoice Company")
self.assertEqual(len(invoices), 2)
expected_value = {
@ -44,7 +58,33 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
0: ["_Test Supplier", 300, "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 test_opening_sales_invoice_creation_with_missing_debit_account(self):
company = "_Test Opening Invoice Company"
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
old_default_receivable_account = frappe.db.get_value("Company", company, "default_receivable_account")
frappe.db.set_value("Company", company, "default_receivable_account", "")
if not frappe.db.exists("Cost Center", "_Test Opening Invoice Company - _TOIC"):
cc = frappe.get_doc({"doctype": "Cost Center", "cost_center_name": "_Test Opening Invoice Company",
"is_group": 1, "company": "_Test Opening Invoice Company"})
cc.insert(ignore_mandatory=True)
cc2 = frappe.get_doc({"doctype": "Cost Center", "cost_center_name": "Main", "is_group": 0,
"company": "_Test Opening Invoice Company", "parent_cost_center": cc.name})
cc2.insert()
frappe.db.set_value("Company", company, "cost_center", "Main - _TOIC")
self.make_invoices(company="_Test Opening Invoice Company", party_1=party_1, party_2=party_2)
# Check if missing debit account error raised
error_log = frappe.db.exists("Error Log", {"error": ["like", "%erpnext.controllers.accounts_controller.AccountMissingError%"]})
self.assertTrue(error_log)
# teardown
frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account)
def get_opening_invoice_creation_dict(**args):
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
@ -57,7 +97,7 @@ def get_opening_invoice_creation_dict(**args):
{
"qty": 1.0,
"outstanding_amount": 300,
"party": "_Test {0}".format(party),
"party": args.get("party_1") or "_Test {0}".format(party),
"item_name": "Opening Item",
"due_date": "2016-09-10",
"posting_date": "2016-09-05",
@ -66,7 +106,7 @@ def get_opening_invoice_creation_dict(**args):
{
"qty": 2.0,
"outstanding_amount": 250,
"party": "_Test {0} 1".format(party),
"party": args.get("party_2") or "_Test {0} 1".format(party),
"item_name": "Opening Item",
"due_date": "2016-09-10",
"posting_date": "2016-09-05",
@ -76,4 +116,31 @@ def get_opening_invoice_creation_dict(**args):
})
invoice_dict.update(args)
return invoice_dict
return invoice_dict
def make_company():
if frappe.db.exists("Company", "_Test Opening Invoice Company"):
return frappe.get_doc("Company", "_Test Opening Invoice Company")
company = frappe.new_doc("Company")
company.company_name = "_Test Opening Invoice Company"
company.abbr = "_TOIC"
company.default_currency = "INR"
company.country = "India"
company.insert()
return company
def make_customer(customer=None):
customer_name = customer or "Opening Customer"
customer = frappe.get_doc({
"doctype": "Customer",
"customer_name": customer_name,
"customer_group": "All Customer Groups",
"customer_type": "Company",
"territory": "All Territories"
})
if not frappe.db.exists("Customer", customer_name):
customer.insert(ignore_permissions=True)
return customer.name
else:
return frappe.db.exists("Customer", customer_name)

View File

@ -12,9 +12,10 @@ frappe.ui.form.on('Payment Entry', {
setup: function(frm) {
frm.set_query("paid_from", function() {
frm.events.validate_company(frm);
var account_types = in_list(["Pay", "Internal Transfer"], frm.doc.payment_type) ?
["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]];
return {
filters: {
"account_type": ["in", account_types],
@ -23,13 +24,16 @@ frappe.ui.form.on('Payment Entry', {
}
}
});
frm.set_query("party_type", function() {
frm.events.validate_company(frm);
return{
filters: {
"name": ["in", Object.keys(frappe.boot.party_account_types)],
}
}
});
frm.set_query("party_bank_account", function() {
return {
filters: {
@ -39,6 +43,7 @@ frappe.ui.form.on('Payment Entry', {
}
}
});
frm.set_query("bank_account", function() {
return {
filters: {
@ -47,6 +52,7 @@ frappe.ui.form.on('Payment Entry', {
}
}
});
frm.set_query("contact_person", function() {
if (frm.doc.party) {
return {
@ -58,10 +64,12 @@ frappe.ui.form.on('Payment Entry', {
};
}
});
frm.set_query("paid_to", function() {
frm.events.validate_company(frm);
var account_types = in_list(["Receive", "Internal Transfer"], frm.doc.payment_type) ?
["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]];
return {
filters: {
"account_type": ["in", account_types],
@ -150,6 +158,12 @@ frappe.ui.form.on('Payment Entry', {
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) {
frm.events.hide_unhide_fields(frm);
frm.events.set_dynamic_labels(frm);

View File

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

View File

@ -202,17 +202,32 @@ class PaymentEntry(AccountsController):
# if account_type not in account_types:
# frappe.throw(_("Account Type for {0} must be {1}").format(account, comma_or(account_types)))
def set_exchange_rate(self):
def set_exchange_rate(self, ref_doc=None):
self.set_source_exchange_rate(ref_doc)
self.set_target_exchange_rate(ref_doc)
def set_source_exchange_rate(self, ref_doc=None):
if self.paid_from and not self.source_exchange_rate:
if self.paid_from_account_currency == self.company_currency:
self.source_exchange_rate = 1
else:
self.source_exchange_rate = get_exchange_rate(self.paid_from_account_currency,
self.company_currency, self.posting_date)
if ref_doc:
if self.paid_from_account_currency == ref_doc.currency:
self.source_exchange_rate = ref_doc.get("exchange_rate")
if not self.source_exchange_rate:
self.source_exchange_rate = get_exchange_rate(self.paid_from_account_currency,
self.company_currency, self.posting_date)
def set_target_exchange_rate(self, ref_doc=None):
if self.paid_to and not self.target_exchange_rate:
self.target_exchange_rate = get_exchange_rate(self.paid_to_account_currency,
self.company_currency, self.posting_date)
if ref_doc:
if self.paid_to_account_currency == ref_doc.currency:
self.target_exchange_rate = ref_doc.get("exchange_rate")
if not self.target_exchange_rate:
self.target_exchange_rate = get_exchange_rate(self.paid_to_account_currency,
self.company_currency, self.posting_date)
def validate_mandatory(self):
for field in ("paid_amount", "received_amount", "source_exchange_rate", "target_exchange_rate"):
@ -282,9 +297,10 @@ class PaymentEntry(AccountsController):
no_oustanding_refs.setdefault(d.reference_doctype, []).append(d)
for k, v in no_oustanding_refs.items():
frappe.msgprint(_("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.<br><br>\
If this is undesirable please cancel the corresponding Payment Entry.")
.format(k, frappe.bold(", ".join([d.reference_name for d in v])), frappe.bold("negative outstanding amount")),
frappe.msgprint(
_("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.")
.format(k, frappe.bold(", ".join([d.reference_name for d in v])), frappe.bold("negative outstanding amount"))
+ "<br><br>" + _("If this is undesirable please cancel the corresponding Payment Entry."),
title=_("Warning"), indicator="orange")
@ -453,7 +469,7 @@ class PaymentEntry(AccountsController):
frappe.throw(_("Reference No and Reference Date is mandatory for Bank transaction"))
def set_remarks(self):
if self.remarks: return
if self.custom_remarks: return
if self.payment_type=="Internal Transfer":
remarks = [_("Amount {0} {1} transferred from {2} to {3}")
@ -909,22 +925,24 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
exchange_rate = 1
outstanding_amount = get_outstanding_on_journal_entry(reference_name)
elif reference_doctype != "Journal Entry":
if party_account_currency == company_currency:
if ref_doc.doctype == "Expense Claim":
if ref_doc.doctype == "Expense Claim":
total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges)
elif ref_doc.doctype == "Employee Advance":
total_amount = ref_doc.advance_amount
else:
elif ref_doc.doctype == "Employee Advance":
total_amount = ref_doc.advance_amount
exchange_rate = ref_doc.get("exchange_rate")
if party_account_currency != ref_doc.currency:
total_amount = flt(total_amount) * flt(exchange_rate)
if not total_amount:
if party_account_currency == company_currency:
total_amount = ref_doc.base_grand_total
exchange_rate = 1
else:
total_amount = ref_doc.grand_total
exchange_rate = 1
else:
total_amount = ref_doc.grand_total
if not exchange_rate:
# Get the exchange rate from the original ref doc
# or get it based on the posting date of the ref doc
# or get it based on the posting date of the ref doc.
exchange_rate = ref_doc.get("conversion_rate") or \
get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date)
if reference_doctype in ("Sales Invoice", "Purchase Invoice"):
outstanding_amount = ref_doc.get("outstanding_amount")
bill_no = ref_doc.get("bill_no")
@ -932,11 +950,15 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
outstanding_amount = flt(ref_doc.get("total_sanctioned_amount")) + flt(ref_doc.get("total_taxes_and_charges"))\
- flt(ref_doc.get("total_amount_reimbursed")) - flt(ref_doc.get("total_advance_amount"))
elif reference_doctype == "Employee Advance":
outstanding_amount = ref_doc.advance_amount - flt(ref_doc.paid_amount)
outstanding_amount = (flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount))
if party_account_currency != ref_doc.currency:
outstanding_amount = flt(outstanding_amount) * flt(exchange_rate)
if party_account_currency == company_currency:
exchange_rate = 1
else:
outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid)
else:
# Get the exchange rate based on the posting date of the ref doc
# Get the exchange rate based on the posting date of the ref doc.
exchange_rate = get_exchange_rate(party_account_currency,
company_currency, ref_doc.posting_date)
@ -948,102 +970,104 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
"bill_no": bill_no
})
def get_amounts_based_on_reference_doctype(reference_doctype, ref_doc, party_account_currency, company_currency, reference_name):
total_amount, outstanding_amount, exchange_rate = None
if reference_doctype == "Fees":
total_amount = ref_doc.get("grand_total")
exchange_rate = 1
outstanding_amount = ref_doc.get("outstanding_amount")
elif reference_doctype == "Dunning":
total_amount = ref_doc.get("dunning_amount")
exchange_rate = 1
outstanding_amount = ref_doc.get("dunning_amount")
elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1:
total_amount = ref_doc.get("total_amount")
if ref_doc.multi_currency:
exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date)
else:
exchange_rate = 1
outstanding_amount = get_outstanding_on_journal_entry(reference_name)
return total_amount, outstanding_amount, exchange_rate
def get_amounts_based_on_ref_doc(reference_doctype, ref_doc, party_account_currency, company_currency):
total_amount, outstanding_amount, exchange_rate = None
if ref_doc.doctype == "Expense Claim":
total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges)
elif ref_doc.doctype == "Employee Advance":
total_amount, exchange_rate = get_total_amount_exchange_rate_for_employee_advance(party_account_currency, ref_doc)
if not total_amount:
total_amount, exchange_rate = get_total_amount_exchange_rate_base_on_currency(
party_account_currency, company_currency, ref_doc)
if not exchange_rate:
# Get the exchange rate from the original ref doc
# or get it based on the posting date of the ref doc
exchange_rate = ref_doc.get("conversion_rate") or \
get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date)
outstanding_amount, exchange_rate, bill_no = get_bill_no_and_update_amounts(
reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency)
return total_amount, outstanding_amount, exchange_rate, bill_no
def get_total_amount_exchange_rate_for_employee_advance(party_account_currency, ref_doc):
total_amount = ref_doc.advance_amount
exchange_rate = ref_doc.get("exchange_rate")
if party_account_currency != ref_doc.currency:
total_amount = flt(total_amount) * flt(exchange_rate)
return total_amount, exchange_rate
def get_total_amount_exchange_rate_base_on_currency(party_account_currency, company_currency, ref_doc):
exchange_rate = None
if party_account_currency == company_currency:
total_amount = ref_doc.base_grand_total
exchange_rate = 1
else:
total_amount = ref_doc.grand_total
return total_amount, exchange_rate
def get_bill_no_and_update_amounts(reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency):
outstanding_amount, bill_no = None
if reference_doctype in ("Sales Invoice", "Purchase Invoice"):
outstanding_amount = ref_doc.get("outstanding_amount")
bill_no = ref_doc.get("bill_no")
elif reference_doctype == "Expense Claim":
outstanding_amount = flt(ref_doc.get("total_sanctioned_amount")) + flt(ref_doc.get("total_taxes_and_charges"))\
- flt(ref_doc.get("total_amount_reimbursed")) - flt(ref_doc.get("total_advance_amount"))
elif reference_doctype == "Employee Advance":
outstanding_amount = (flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount))
if party_account_currency != ref_doc.currency:
outstanding_amount = flt(outstanding_amount) * flt(exchange_rate)
if party_account_currency == company_currency:
exchange_rate = 1
else:
outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid)
return outstanding_amount, exchange_rate, bill_no
@frappe.whitelist()
def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=None):
reference_doc = None
doc = frappe.get_doc(dt, dn)
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) > 0:
frappe.throw(_("Can only make payment against unbilled {0}").format(dt))
if dt in ("Sales Invoice", "Sales Order", "Dunning"):
party_type = "Customer"
elif dt in ("Purchase Invoice", "Purchase Order"):
party_type = "Supplier"
elif dt in ("Expense Claim", "Employee Advance"):
party_type = "Employee"
elif dt in ("Fees"):
party_type = "Student"
# party account
if dt == "Sales Invoice":
party_account = get_party_account_based_on_invoice_discounting(dn) or doc.debit_to
elif dt == "Purchase Invoice":
party_account = doc.credit_to
elif dt == "Fees":
party_account = doc.receivable_account
elif dt == "Employee Advance":
party_account = doc.advance_account
elif dt == "Expense Claim":
party_account = doc.payable_account
else:
party_account = get_party_account(party_type, doc.get(party_type.lower()), doc.company)
if dt not in ("Sales Invoice", "Purchase Invoice"):
party_account_currency = get_account_currency(party_account)
else:
party_account_currency = doc.get("party_account_currency") or get_account_currency(party_account)
# payment type
if (dt == "Sales Order" or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \
or (dt=="Purchase Invoice" and doc.outstanding_amount < 0):
payment_type = "Receive"
else:
payment_type = "Pay"
# amounts
grand_total = outstanding_amount = 0
if party_amount:
grand_total = outstanding_amount = party_amount
elif dt in ("Sales Invoice", "Purchase Invoice"):
if party_account_currency == doc.company_currency:
grand_total = doc.base_rounded_total or doc.base_grand_total
else:
grand_total = doc.rounded_total or doc.grand_total
outstanding_amount = doc.outstanding_amount
elif dt in ("Expense Claim"):
grand_total = doc.total_sanctioned_amount + doc.total_taxes_and_charges
outstanding_amount = doc.grand_total \
- doc.total_amount_reimbursed
elif dt == "Employee Advance":
grand_total = doc.advance_amount
outstanding_amount = flt(doc.advance_amount) - flt(doc.paid_amount)
elif dt == "Fees":
grand_total = doc.grand_total
outstanding_amount = doc.outstanding_amount
elif dt == "Dunning":
grand_total = doc.grand_total
outstanding_amount = doc.grand_total
else:
if party_account_currency == doc.company_currency:
grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total)
else:
grand_total = flt(doc.get("rounded_total") or doc.grand_total)
outstanding_amount = grand_total - flt(doc.advance_paid)
party_type = set_party_type(dt)
party_account = set_party_account(dt, dn, doc, party_type)
party_account_currency = set_party_account_currency(dt, party_account, doc)
payment_type = set_payment_type(dt, doc)
grand_total, outstanding_amount = set_grand_total_and_outstanding_amount(party_amount, dt, party_account_currency, doc)
# bank or cash
bank = get_default_bank_cash_account(doc.company, "Bank", mode_of_payment=doc.get("mode_of_payment"),
account=bank_account)
bank = get_bank_cash_account(doc, bank_account)
if not bank:
bank = get_default_bank_cash_account(doc.company, "Cash", mode_of_payment=doc.get("mode_of_payment"),
account=bank_account)
paid_amount = received_amount = 0
if party_account_currency == bank.account_currency:
paid_amount = received_amount = abs(outstanding_amount)
elif payment_type == "Receive":
paid_amount = abs(outstanding_amount)
if bank_amount:
received_amount = bank_amount
else:
received_amount = paid_amount * doc.get('conversion_rate', 1)
else:
received_amount = abs(outstanding_amount)
if bank_amount:
paid_amount = bank_amount
else:
# if party account currency and bank currency is different then populate paid amount as well
paid_amount = received_amount * doc.get('conversion_rate', 1)
paid_amount, received_amount = set_paid_amount_and_received_amount(
dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc)
pe = frappe.new_doc("Payment Entry")
pe.payment_type = payment_type
@ -1115,10 +1139,120 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
pe.setup_party_account_field()
pe.set_missing_values()
if party_account and bank:
pe.set_exchange_rate()
if dt == "Employee Advance":
reference_doc = doc
pe.set_exchange_rate(ref_doc=reference_doc)
pe.set_amounts()
return pe
def get_bank_cash_account(doc, bank_account):
bank = get_default_bank_cash_account(doc.company, "Bank", mode_of_payment=doc.get("mode_of_payment"),
account=bank_account)
if not bank:
bank = get_default_bank_cash_account(doc.company, "Cash", mode_of_payment=doc.get("mode_of_payment"),
account=bank_account)
return bank
def set_party_type(dt):
if dt in ("Sales Invoice", "Sales Order", "Dunning"):
party_type = "Customer"
elif dt in ("Purchase Invoice", "Purchase Order"):
party_type = "Supplier"
elif dt in ("Expense Claim", "Employee Advance"):
party_type = "Employee"
elif dt in ("Fees"):
party_type = "Student"
return party_type
def set_party_account(dt, dn, doc, party_type):
if dt == "Sales Invoice":
party_account = get_party_account_based_on_invoice_discounting(dn) or doc.debit_to
elif dt == "Purchase Invoice":
party_account = doc.credit_to
elif dt == "Fees":
party_account = doc.receivable_account
elif dt == "Employee Advance":
party_account = doc.advance_account
elif dt == "Expense Claim":
party_account = doc.payable_account
else:
party_account = get_party_account(party_type, doc.get(party_type.lower()), doc.company)
return party_account
def set_party_account_currency(dt, party_account, doc):
if dt not in ("Sales Invoice", "Purchase Invoice"):
party_account_currency = get_account_currency(party_account)
else:
party_account_currency = doc.get("party_account_currency") or get_account_currency(party_account)
return party_account_currency
def set_payment_type(dt, doc):
if (dt == "Sales Order" or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \
or (dt=="Purchase Invoice" and doc.outstanding_amount < 0):
payment_type = "Receive"
else:
payment_type = "Pay"
return payment_type
def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_currency, doc):
grand_total = outstanding_amount = 0
if party_amount:
grand_total = outstanding_amount = party_amount
elif dt in ("Sales Invoice", "Purchase Invoice"):
if party_account_currency == doc.company_currency:
grand_total = doc.base_rounded_total or doc.base_grand_total
else:
grand_total = doc.rounded_total or doc.grand_total
outstanding_amount = doc.outstanding_amount
elif dt in ("Expense Claim"):
grand_total = doc.total_sanctioned_amount + doc.total_taxes_and_charges
outstanding_amount = doc.grand_total \
- doc.total_amount_reimbursed
elif dt == "Employee Advance":
grand_total = flt(doc.advance_amount)
outstanding_amount = flt(doc.advance_amount) - flt(doc.paid_amount)
if party_account_currency != doc.currency:
grand_total = flt(doc.advance_amount) * flt(doc.exchange_rate)
outstanding_amount = (flt(doc.advance_amount) - flt(doc.paid_amount)) * flt(doc.exchange_rate)
elif dt == "Fees":
grand_total = doc.grand_total
outstanding_amount = doc.outstanding_amount
elif dt == "Dunning":
grand_total = doc.grand_total
outstanding_amount = doc.grand_total
else:
if party_account_currency == doc.company_currency:
grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total)
else:
grand_total = flt(doc.get("rounded_total") or doc.grand_total)
outstanding_amount = grand_total - flt(doc.advance_paid)
return grand_total, outstanding_amount
def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc):
paid_amount = received_amount = 0
if party_account_currency == bank.account_currency:
paid_amount = received_amount = abs(outstanding_amount)
elif payment_type == "Receive":
paid_amount = abs(outstanding_amount)
if bank_amount:
received_amount = bank_amount
else:
received_amount = paid_amount * doc.get('conversion_rate', 1)
if dt == "Employee Advance":
received_amount = paid_amount * doc.get('exchange_rate', 1)
else:
received_amount = abs(outstanding_amount)
if bank_amount:
paid_amount = bank_amount
else:
# if party account currency and bank currency is different then populate paid amount as well
paid_amount = received_amount * doc.get('conversion_rate', 1)
if dt == "Employee Advance":
paid_amount = received_amount * doc.get('exchange_rate', 1)
return paid_amount, received_amount
def get_reference_as_per_payment_terms(payment_schedule, dt, dn, doc, grand_total, outstanding_amount):
references = []
for payment_term in payment_schedule:
@ -1172,30 +1306,23 @@ def make_payment_order(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc
def set_missing_values(source, target):
target.payment_order_type = "Payment Entry"
target.append('references', dict(
reference_doctype="Payment Entry",
reference_name=source.name,
bank_account=source.party_bank_account,
amount=source.paid_amount,
account=source.paid_to,
supplier=source.party,
mode_of_payment=source.mode_of_payment,
))
def update_item(source_doc, target_doc, source_parent):
target_doc.bank_account = source_parent.party_bank_account
target_doc.amount = source_doc.allocated_amount
target_doc.account = source_parent.paid_to
target_doc.payment_entry = source_parent.name
target_doc.supplier = source_parent.party
target_doc.mode_of_payment = source_parent.mode_of_payment
doclist = get_mapped_doc("Payment Entry", source_name, {
doclist = get_mapped_doc("Payment Entry", source_name, {
"Payment Entry": {
"doctype": "Payment Order",
"validation": {
"docstatus": ["=", 1]
}
},
"Payment Entry Reference": {
"doctype": "Payment Order Reference",
"validation": {
"docstatus": ["=", 1]
},
"postprocess": update_item
},
}
}, target_doc, set_missing_values)

View File

@ -1,12 +1,14 @@
frappe.listview_settings['Payment Entry'] = {
onload: function(listview) {
listview.page.fields_dict.party_type.get_query = function() {
return {
"filters": {
"name": ["in", Object.keys(frappe.boot.party_account_types)],
}
if (listview.page.fields_dict.party_type) {
listview.page.fields_dict.party_type.get_query = function() {
return {
"filters": {
"name": ["in", Object.keys(frappe.boot.party_account_types)],
}
};
};
};
}
}
};

View File

@ -1,313 +1,98 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2015-12-23 21:31:52.699821",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"actions": [],
"creation": "2015-12-23 21:31:52.699821",
"doctype": "DocType",
"editable_grid": 1,
"field_order": [
"payment_gateway",
"payment_channel",
"is_default",
"column_break_4",
"payment_account",
"currency",
"payment_request_message",
"message",
"message_examples"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "payment_gateway",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"fieldname": "payment_gateway",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Payment Gateway",
"length": 0,
"no_copy": 0,
"options": "Payment Gateway",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"label": "Payment Gateway",
"options": "Payment Gateway",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "is_default",
"fieldtype": "Check",
"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": "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
},
"default": "0",
"fieldname": "is_default",
"fieldtype": "Check",
"label": "Is Default"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_4",
"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
},
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "payment_account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"fieldname": "payment_account",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Payment Account",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"label": "Payment Account",
"options": "Account",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "payment_account.account_currency",
"fieldname": "currency",
"fieldtype": "Read Only",
"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": "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
},
"fieldtype": "Read Only",
"label": "Currency"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "payment_request_message",
"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
},
"depends_on": "eval: doc.payment_channel !== \"Phone\"",
"fieldname": "payment_request_message",
"fieldtype": "Section Break"
},
{
"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",
"fieldname": "message",
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "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
},
"default": "Please click on the link below to make your payment",
"fieldname": "message",
"fieldtype": "Small Text",
"label": "Default Payment Request Message"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "message_examples",
"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",
"length": 0,
"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,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldname": "message_examples",
"fieldtype": "HTML",
"label": "Message Examples",
"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"
},
{
"default": "Email",
"fieldname": "payment_channel",
"fieldtype": "Select",
"label": "Payment Channel",
"options": "\nEmail\nPhone"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-05-16 22:43:34.970491",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Gateway Account",
"name_case": "",
"owner": "Administrator",
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-09-20 13:30:27.722852",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Gateway Account",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0
],
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@ -21,10 +21,15 @@ class PaymentOrder(Document):
if cancel:
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:
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.validate_and_sanitize_search_inputs

View File

@ -5,6 +5,45 @@ from __future__ import unicode_literals
import frappe
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):
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",
"doctype": "DocType",
"editable_grid": 1,
@ -10,7 +11,6 @@
"column_break_4",
"supplier",
"payment_request",
"payment_entry",
"mode_of_payment",
"bank_account_details",
"bank_account",
@ -103,17 +103,12 @@
"no_copy": 1,
"print_hide": 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,
"modified": "2019-05-08 13:56:25.724557",
"links": [],
"modified": "2020-09-04 08:29:51.014390",
"modified_by": "Administrator",
"module": "Accounts",
"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({
onload: function() {
var me = this;
this.frm.set_query("party", function() {
check_mandatory(me.frm);
});
this.frm.set_query("party_type", function() {
return {
"filters": {
@ -46,37 +51,39 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
});
this.frm.set_query('receivable_payable_account', function() {
if(!me.frm.doc.company || !me.frm.doc.party_type) {
frappe.msgprint(__("Please select Company and Party Type first"));
} else {
return{
filters: {
"company": me.frm.doc.company,
"is_group": 0,
"account_type": frappe.boot.party_account_types[me.frm.doc.party_type]
}
};
}
check_mandatory(me.frm);
return {
filters: {
"company": me.frm.doc.company,
"is_group": 0,
"account_type": frappe.boot.party_account_types[me.frm.doc.party_type]
}
};
});
this.frm.set_query('bank_cash_account', function() {
if(!me.frm.doc.company) {
frappe.msgprint(__("Please select Company first"));
} else {
return{
filters:[
['Account', 'company', '=', me.frm.doc.company],
['Account', 'is_group', '=', 0],
['Account', 'account_type', 'in', ['Bank', 'Cash']]
]
};
}
check_mandatory(me.frm, true);
return {
filters:[
['Account', 'company', '=', me.frm.doc.company],
['Account', 'is_group', '=', 0],
['Account', 'account_type', 'in', ['Bank', 'Cash']]
]
};
});
this.frm.set_value('party_type', '');
this.frm.set_value('party', '');
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() {
@ -90,7 +97,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
party: function() {
var me = this
if(!me.frm.doc.receivable_payable_account && me.frm.doc.party_type && me.frm.doc.party) {
if (!me.frm.doc.receivable_payable_account && me.frm.doc.party_type && me.frm.doc.party) {
return frappe.call({
method: "erpnext.accounts.party.get_party_account",
args: {
@ -99,7 +106,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
party: me.frm.doc.party
},
callback: function(r) {
if(!r.exc && r.message) {
if (!r.exc && r.message) {
me.frm.set_value("receivable_payable_account", r.message);
}
}

View File

@ -99,6 +99,7 @@ class PaymentReconciliation(Document):
and `tabGL Entry`.against_voucher_type = %(voucher_type)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`.is_cancelled = 0
GROUP BY `tab{doc}`.name
Having
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) {
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){
frm.add_custom_button(__('Resend Payment Email'), function(){
frappe.call({

View File

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

View File

@ -36,7 +36,7 @@ class PaymentRequest(Document):
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
if (hasattr(ref_doc, "order_type") \
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:
frappe.throw(_("Total Payment Request amount cannot be greater than {0} amount")
@ -76,11 +76,25 @@ class PaymentRequest(Document):
or self.flags.mute_email:
send_mail = False
if send_mail:
if send_mail and self.payment_channel != "Phone":
self.set_payment_request_url()
self.send_email()
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):
self.check_if_payment_entry_exists()
self.set_as_cancelled()
@ -105,13 +119,14 @@ class PaymentRequest(Document):
return False
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()
if 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')
def get_payment_url(self):
@ -140,10 +155,14 @@ class PaymentRequest(Document):
})
def set_as_paid(self):
payment_entry = self.create_payment_entry()
self.make_invoice()
if self.payment_channel == "Phone":
self.db_set("status", "Paid")
return payment_entry
else:
payment_entry = self.create_payment_entry()
self.make_invoice()
return payment_entry
def create_payment_entry(self, submit=True):
"""create entry"""
@ -151,7 +170,7 @@ class PaymentRequest(Document):
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
elif self.reference_doctype == "Purchase Invoice":
party_account = ref_doc.credit_to
@ -166,8 +185,8 @@ class PaymentRequest(Document):
else:
party_amount = self.grand_total
payment_entry = get_payment_entry(self.reference_doctype, self.reference_name,
party_amount=party_amount, bank_account=self.payment_account, bank_amount=bank_amount)
payment_entry = get_payment_entry(self.reference_doctype, self.reference_name, party_amount=party_amount,
bank_account=self.payment_account, bank_amount=bank_amount)
payment_entry.update({
"reference_no": self.name,
@ -255,7 +274,7 @@ class PaymentRequest(Document):
# if shopping cart enabled and in 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
if success_url:
@ -280,7 +299,9 @@ def make_payment_request(**args):
args = frappe._dict(args)
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":
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_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)
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'))
if args.get('party_type') else '')
@ -314,9 +333,11 @@ def make_payment_request(**args):
"payment_gateway_account": gateway_account.get("name"),
"payment_gateway": gateway_account.get("payment_gateway"),
"payment_account": gateway_account.get("payment_account"),
"payment_channel": gateway_account.get("payment_channel"),
"payment_request_type": args.get("payment_request_type"),
"currency": ref_doc.currency,
"grand_total": grand_total,
"mode_of_payment": args.mode_of_payment,
"email_to": args.recipient_id or ref_doc.owner,
"subject": _("Payment Request for {0}").format(args.dn),
"message": gateway_account.get("message") or get_dummy_message(ref_doc),
@ -344,7 +365,7 @@ def make_payment_request(**args):
return pr.as_dict()
def get_amount(ref_doc):
def get_amount(ref_doc, payment_account=None):
"""get amount based on doctype"""
dt = ref_doc.doctype
if dt in ["Sales Order", "Purchase Order"]:
@ -356,6 +377,12 @@ def get_amount(ref_doc):
else:
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":
grand_total = ref_doc.outstanding_amount
@ -366,6 +393,10 @@ def get_amount(ref_doc):
frappe.throw(_("Payment Entry is already created"))
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("""
select sum(grand_total)
from `tabPayment Request`
@ -373,7 +404,9 @@ def get_existing_payment_request_amount(ref_dt, ref_dn):
reference_doctype = %s
and reference_name = %s
and docstatus = 1
and status != 'Paid'
and (status != 'Paid'
or (payment_channel = 'Phone'
and status = 'Paid'))
""", (ref_dt, ref_dn))
return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0

View File

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

View File

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

View File

@ -51,18 +51,53 @@ frappe.ui.form.on('POS Closing Entry', {
args: {
start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date),
end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date),
pos_profile: frm.doc.pos_profile,
user: frm.doc.user
},
callback: (r) => {
let pos_docs = r.message;
set_form_data(pos_docs, frm)
refresh_fields(frm)
set_html_data(frm)
set_form_data(pos_docs, frm);
refresh_fields(frm);
set_html_data(frm);
}
})
}
});
cur_frm.cscript.before_pos_transactions_remove = function(doc, cdt, cdn) {
const removed_row = locals[cdt][cdn];
if (!removed_row.pos_invoice) return;
frappe.db.get_doc("POS Invoice", removed_row.pos_invoice).then(doc => {
cur_frm.doc.grand_total -= flt(doc.grand_total);
cur_frm.doc.net_total -= flt(doc.net_total);
cur_frm.doc.total_quantity -= flt(doc.total_qty);
refresh_payments(doc, cur_frm, 1);
refresh_taxes(doc, cur_frm, 1);
refresh_fields(cur_frm);
set_html_data(cur_frm);
});
}
frappe.ui.form.on('POS Invoice Reference', {
pos_invoice(frm, cdt, cdn) {
const added_row = locals[cdt][cdn];
if (!added_row.pos_invoice) return;
frappe.db.get_doc("POS Invoice", added_row.pos_invoice).then(doc => {
frm.doc.grand_total += flt(doc.grand_total);
frm.doc.net_total += flt(doc.net_total);
frm.doc.total_quantity += flt(doc.total_qty);
refresh_payments(doc, frm);
refresh_taxes(doc, frm);
refresh_fields(frm);
set_html_data(frm);
});
}
})
frappe.ui.form.on('POS Closing Entry Detail', {
closing_amount: (frm, cdt, cdn) => {
const row = locals[cdt][cdn];
@ -76,8 +111,8 @@ function set_form_data(data, frm) {
frm.doc.grand_total += flt(d.grand_total);
frm.doc.net_total += flt(d.net_total);
frm.doc.total_quantity += flt(d.total_qty);
add_to_payments(d, frm);
add_to_taxes(d, frm);
refresh_payments(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 => {
const payment = frm.doc.payment_reconciliation.find(pay => pay.mode_of_payment === p.mode_of_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 {
frm.add_child("payment_reconciliation", {
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 => {
const tax = frm.doc.taxes.find(tx => tx.account_head === t.account_head && tx.rate === t.rate);
if (tax) {
tax.amount += flt(t.tax_amount);
if (!remove) tax.amount += flt(t.tax_amount);
else tax.amount -= flt(t.tax_amount);
} else {
frm.add_child("taxes", {
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):
def validate(self):
user = frappe.get_all('POS Closing Entry',
filters = { 'user': self.user, 'docstatus': 1 },
if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
self.validate_pos_closing()
self.validate_pos_invoices()
def validate_pos_closing(self):
user = frappe.get_all("POS Closing Entry",
filters = { "user": self.user, "docstatus": 1, "pos_profile": self.pos_profile },
or_filters = {
'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_start_date": ("between", [self.period_start_date, self.period_end_date]),
"period_end_date": ("between", [self.period_start_date, self.period_end_date])
})
if user:
frappe.throw(_("POS Closing Entry {} against {} between selected period"
.format(frappe.bold("already exists"), frappe.bold(self.user))), title=_("Invalid Period"))
bold_already_exists = frappe.bold(_("already exists"))
bold_user = frappe.bold(self.user)
frappe.throw(_("POS Closing Entry {} against {} between selected period")
.format(bold_already_exists, bold_user), title=_("Invalid Period"))
def validate_pos_invoices(self):
invalid_rows = []
for d in self.pos_transactions:
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 frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
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):
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]
@frappe.whitelist()
def get_pos_invoices(start, end, user):
def get_pos_invoices(start, end, pos_profile, user):
data = frappe.db.sql("""
select
name, timestamp(posting_date, posting_time) as "timestamp"
from
`tabPOS Invoice`
where
owner = %s and docstatus = 1 and
(consolidated_invoice is NULL or consolidated_invoice = '')
""", (user), as_dict=1)
owner = %s and docstatus = 1 and pos_profile = %s and ifnull(consolidated_invoice,'') = ''
""", (user, pos_profile), as_dict=1)
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
@ -76,7 +107,8 @@ def make_closing_entry_from_opening(opening_entry):
closing_entry.net_total = 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 = []
taxes = []

View File

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

View File

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

View File

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

@ -1,5 +1,6 @@
{
"actions": [],
"allow_auto_repeat": 1,
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2020-01-24 15:29:29.933693",
@ -460,7 +461,7 @@
},
{
"fieldname": "contact_mobile",
"fieldtype": "Small Text",
"fieldtype": "Data",
"hidden": 1,
"label": "Mobile No",
"read_only": 1
@ -1580,7 +1581,7 @@
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2020-05-29 15:08:39.337385",
"modified": "2020-10-30 13:56:51.056083",
"modified_by": "Administrator",
"module": "Accounts",
"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 erpnext.accounts.utils import get_account_currency
from erpnext.accounts.party import get_party_account, get_due_date
from erpnext.accounts.doctype.loyalty_program.loyalty_program import \
get_loyalty_program_details_with_points, validate_loyalty_points
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice, get_bank_cash_account, update_multi_mode_option
from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
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, get_mode_of_payment_info
from six import iteritems
@ -29,8 +28,7 @@ class POSInvoice(SalesInvoice):
# run on validate method of selling controller
super(SalesInvoice, self).validate()
self.validate_auto_set_posting_time()
self.validate_pos_paid_amount()
self.validate_pos_return()
self.validate_mode_of_payment()
self.validate_uom_is_integer("stock_uom", "stock_qty")
self.validate_uom_is_integer("uom", "qty")
self.validate_debit_to_acc()
@ -40,11 +38,12 @@ class POSInvoice(SalesInvoice):
self.validate_item_cost_centers()
self.validate_serialised_or_batched_item()
self.validate_stock_availablility()
self.validate_return_items()
self.validate_return_items_qty()
self.validate_non_stock_items()
self.set_status()
self.set_account_for_mode_of_payment()
self.validate_pos()
self.verify_payment_amount()
self.validate_payment_amount()
self.validate_loyalty_transaction()
def on_submit(self):
@ -57,6 +56,7 @@ class POSInvoice(SalesInvoice):
against_psi_doc.make_loyalty_point_entry()
if self.redeem_loyalty_points and self.loyalty_points:
self.apply_loyalty_points()
self.check_phone_payments()
self.set_status(update=True)
def on_cancel(self):
@ -69,77 +69,128 @@ class POSInvoice(SalesInvoice):
against_psi_doc.delete_loyalty_point_entry()
against_psi_doc.make_loyalty_point_entry()
def validate_stock_availablility(self):
allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock')
def check_phone_payments(self):
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'):
msg = ""
if d.serial_no:
filters = {
"item_code": d.item_code,
"warehouse": d.warehouse,
"delivery_document_no": "",
"sales_invoice": ""
}
filters = { "item_code": d.item_code, "warehouse": d.warehouse }
if d.batch_no:
filters["batch_no"] = d.batch_no
reserved_serial_nos, unreserved_serial_nos = get_pos_reserved_serial_nos(filters)
serial_nos = d.serial_no.split("\n")
serial_nos = ' '.join(serial_nos).split() # remove whitespaces
invalid_serial_nos = []
for s in serial_nos:
if s in reserved_serial_nos:
invalid_serial_nos.append(s)
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"))
reserved_serial_nos = get_pos_reserved_serial_nos(filters)
serial_nos = get_serial_nos(d.serial_no)
invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos]
bold_invalid_serial_nos = frappe.bold(', '.join(invalid_serial_nos))
if len(invalid_serial_nos) == 1:
msg = (_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.")
.format(d.idx, bold_invalid_serial_nos))
elif invalid_serial_nos:
msg = (_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.")
.format(d.idx, bold_invalid_serial_nos))
else:
if allow_negative_stock:
return
available_stock = get_stock_availability(d.item_code, d.warehouse)
if not (flt(available_stock) > 0):
frappe.throw(_('Row #{}: Item Code: {} is not available under warehouse {}.'
.format(d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse))), title=_("Not Available"))
item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)
if flt(available_stock) <= 0:
msg = (_('Row #{}: Item Code: {} is not available under warehouse {}.').format(d.idx, item_code, warehouse))
elif flt(available_stock) < flt(d.qty):
frappe.msgprint(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. \
Available quantity {}.'.format(d.idx, frappe.bold(d.item_code),
frappe.bold(d.warehouse), frappe.bold(d.qty))), title=_("Not Available"))
msg = (_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.')
.format(d.idx, item_code, warehouse, qty))
if msg:
error_msg.append(msg)
if error_msg:
frappe.throw(error_msg, title=_("Item Unavailable"), as_list=True)
def validate_serialised_or_batched_item(self):
error_msg = []
for d in self.get("items"):
serialized = d.get("has_serial_no")
batched = d.get("has_batch_no")
no_serial_selected = not d.get("serial_no")
no_batch_selected = not d.get("batch_no")
msg = ""
item_code = frappe.bold(d.item_code)
serial_nos = get_serial_nos(d.serial_no)
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.'
.format(d.idx, frappe.bold(d.item_code))), title=_("Invalid Item"))
if serialized and no_serial_selected:
frappe.throw(_('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"))
if batched and no_batch_selected:
frappe.throw(_('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"))
msg = (_('Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction.')
.format(d.idx, item_code))
elif serialized and no_serial_selected:
msg = (_('Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction.')
.format(d.idx, item_code))
elif batched and no_batch_selected:
msg = (_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.')
.format(d.idx, item_code))
elif serialized and not no_serial_selected and len(serial_nos) != d.qty:
msg = (_("Row #{}: You must select {} serial numbers for item {}.").format(d.idx, frappe.bold(cint(d.qty)), item_code))
def validate_return_items(self):
if msg:
error_msg.append(msg)
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
for d in self.get("items"):
if d.get("qty") > 0:
frappe.throw(_("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"))
frappe.throw(
_("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_non_stock_items(self):
for d in self.get("items"):
is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item")
if not is_stock_item:
frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice. ").format(
d.idx, frappe.bold(d.item_code)
), title=_("Invalid Item"))
def validate_pos_paid_amount(self):
if len(self.payments) == 0 and self.is_pos:
def validate_mode_of_payment(self):
if len(self.payments) == 0:
frappe.throw(_("At least one mode of payment is required for POS invoice."))
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))
def validate_change_amount(self):
@ -150,23 +201,21 @@ class POSInvoice(SalesInvoice):
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:
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:
total_amount_in_payments += entry.amount
if not self.is_return and entry.amount < 0:
frappe.throw(_("Row #{0} (Payment Table): Amount must be positive").format(entry.idx))
if self.is_return and entry.amount > 0:
frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx))
def validate_pos_return(self):
if self.is_pos and self.is_return:
total_amount_in_payments = 0
for payment in self.payments:
total_amount_in_payments += payment.amount
if self.is_return:
invoice_total = self.rounded_total or self.grand_total
if total_amount_in_payments < invoice_total:
frappe.throw(_("Total payments amount can't be greater than {}".format(-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))
def validate_loyalty_transaction(self):
if self.redeem_loyalty_points and (not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center):
@ -218,57 +267,49 @@ class POSInvoice(SalesInvoice):
from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile
if not self.pos_profile:
pos_profile = get_pos_profile(self.company) or {}
if not pos_profile:
frappe.throw(_("No POS Profile found. Please create a New POS Profile first"))
self.pos_profile = pos_profile.get('name')
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:
update_multi_mode_option(self, pos)
if not self.account_for_change_amount:
self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account')
if pos:
if not for_validate:
self.tax_category = pos.get("tax_category")
update_multi_mode_option(self, profile)
if self.is_return and not for_validate:
add_return_modes(self, profile)
if profile:
if not for_validate and not self.customer:
self.customer = pos.customer
self.customer = profile.customer
self.ignore_pricing_rule = pos.ignore_pricing_rule
if pos.get('account_for_change_amount'):
self.account_for_change_amount = pos.get('account_for_change_amount')
if pos.get('warehouse'):
self.set_warehouse = pos.get('warehouse')
self.ignore_pricing_rule = profile.ignore_pricing_rule
self.account_for_change_amount = profile.get('account_for_change_amount') or self.account_for_change_amount
self.set_warehouse = profile.get('warehouse') or self.set_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',
'write_off_cost_center', 'apply_discount_on', 'cost_center'):
if (not for_validate) or (for_validate and not self.get(fieldname)):
self.set(fieldname, pos.get(fieldname))
if pos.get("company_address"):
self.company_address = pos.get("company_address")
'write_off_cost_center', 'apply_discount_on', 'cost_center', 'tax_category',
'ignore_pricing_rule', 'company_address', 'update_stock'):
if not for_validate:
self.set(fieldname, profile.get(fieldname))
if self.customer:
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')
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:
selling_price_list = pos.get('selling_price_list')
selling_price_list = profile.get('selling_price_list')
if 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
for item in self.get("items"):
if item.get('item_code'):
profile_details = get_pos_profile_item_details(pos, frappe._dict(item.as_dict()), pos)
profile_details = get_pos_profile_item_details(profile.get("company"), frappe._dict(item.as_dict()), profile)
for fname, val in iteritems(profile_details):
if (not for_validate) or (for_validate and not item.get(fname)):
item.set(fname, val)
@ -281,10 +322,13 @@ class POSInvoice(SalesInvoice):
if self.taxes_and_charges and not len(self.get("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):
pos = self.set_pos_fields(for_validate)
profile = self.set_pos_fields(for_validate)
if not self.debit_to:
self.debit_to = get_party_account("Customer", self.customer, self.company)
@ -294,17 +338,15 @@ class POSInvoice(SalesInvoice):
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')):
print_format = 'POS Invoice'
if pos:
if profile:
return {
"print_format": print_format,
"allow_edit_rate": pos.get("allow_user_to_edit_rate"),
"allow_edit_discount": pos.get("allow_user_to_edit_discount"),
"campaign": pos.get("campaign"),
"allow_print_before_pay": pos.get("allow_print_before_pay")
"campaign": profile.get("campaign"),
"allow_print_before_pay": profile.get("allow_print_before_pay")
}
def set_account_for_mode_of_payment(self):
@ -313,6 +355,32 @@ class POSInvoice(SalesInvoice):
if not pay.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)
@frappe.whitelist()
def get_stock_availability(item_code, warehouse):
latest_sle = frappe.db.sql("""select qty_after_transaction
@ -334,11 +402,9 @@ def get_stock_availability(item_code, warehouse):
sle_qty = latest_sle[0].qty_after_transaction or 0 if latest_sle else 0
pos_sales_qty = pos_sales_qty[0].qty or 0 if pos_sales_qty else 0
if sle_qty and pos_sales_qty and sle_qty > pos_sales_qty:
if sle_qty and pos_sales_qty:
return sle_qty - pos_sales_qty
else:
# when sle_qty is 0
# when sle_qty > 0 and pos_sales_qty is 0
return sle_qty
@frappe.whitelist()
@ -371,4 +437,19 @@ def make_merge_log(invoices):
})
if merge_log.get('pos_invoices'):
return merge_log.as_dict()
return merge_log.as_dict()
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])

View File

@ -7,6 +7,8 @@ import frappe
import unittest, copy, time
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.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):
def test_timestamp_change(self):
@ -182,8 +184,9 @@ class TestPOSInvoice(unittest.TestCase):
def test_pos_returns_with_repayment(self):
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': 'Cash', 'account': 'Cash - _TC', 'amount': 500})
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500, 'default': 1})
pos.insert()
pos.submit()
@ -200,8 +203,9 @@ class TestPOSInvoice(unittest.TestCase):
income_account = "Sales - _TC", expense_account = "Cost of Goods Sold - _TC", rate=105,
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': 'Cash', 'account': 'Cash - _TC', 'amount': 60})
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60, 'default': 1})
pos.insert()
pos.submit()
@ -219,29 +223,29 @@ class TestPOSInvoice(unittest.TestCase):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
se = make_serialized_item(company='_Test Company with perpetual inventory',
target_warehouse="Stores - TCP1", cost_center='Main - TCP1', expense_account='Cost of Goods Sold - TCP1')
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)
pos = create_pos_invoice(company='_Test Company with perpetual inventory', debit_to='Debtors - TCP1',
account_for_change_amount='Cash - TCP1', warehouse='Stores - TCP1', income_account='Sales - TCP1',
expense_account='Cost of Goods Sold - TCP1', cost_center='Main - TCP1',
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.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 1000})
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000})
pos.insert()
pos.submit()
pos2 = create_pos_invoice(company='_Test Company with perpetual inventory', debit_to='Debtors - TCP1',
account_for_change_amount='Cash - TCP1', warehouse='Stores - TCP1', income_account='Sales - TCP1',
expense_account='Cost of Goods Sold - TCP1', cost_center='Main - TCP1',
pos2 = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
pos2.get("items")[0].serial_no = serial_nos[0]
pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 1000})
pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000})
self.assertRaises(frappe.ValidationError, pos2.insert)
@ -284,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)
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):
args = frappe._dict(args)
pos_profile = None
@ -292,12 +409,11 @@ def create_pos_invoice(**args):
pos_profile.save()
pos_inv = frappe.new_doc("POS Invoice")
pos_inv.update(args)
pos_inv.update_stock = 1
pos_inv.is_pos = 1
pos_inv.pos_profile = args.pos_profile or pos_profile.name
pos_inv.set_missing_values()
if args.posting_date:
pos_inv.set_posting_time = 1
pos_inv.posting_date = args.posting_date or frappe.utils.nowdate()
@ -311,6 +427,8 @@ def create_pos_invoice(**args):
pos_inv.conversion_rate = args.conversion_rate or 1
pos_inv.account_for_change_amount = args.account_for_change_amount or "Cash - _TC"
pos_inv.set_missing_values()
pos_inv.append("items", {
"item_code": args.item or args.item_code or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC",

View File

@ -24,11 +24,27 @@ class POSInvoiceMergeLog(Document):
def validate_pos_invoice_status(self):
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:
frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, d.pos_invoice))
if status in ['Consolidated']:
frappe.throw(_("Row #{}: POS Invoice {} has been {}").format(d.idx, d.pos_invoice, status))
frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, bold_pos_invoice))
if status == "Consolidated":
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):
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]
sales = [d for d in pos_invoice_docs if d.get('is_return') == 0]
sales_invoice = self.process_merging_into_sales_invoice(sales)
sales_invoice, credit_note = "", ""
if sales:
sales_invoice = self.process_merging_into_sales_invoice(sales)
if len(returns):
if 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
@ -87,17 +103,28 @@ class POSInvoiceMergeLog(Document):
loyalty_amount_sum += doc.loyalty_amount
for item in doc.get('items'):
items.append(item)
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)
for tax in doc.get('taxes'):
found = False
for t in taxes:
if t.account_head == tax.account_head and t.cost_center == tax.cost_center and t.rate == tax.rate:
t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount)
t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount)
if t.account_head == tax.account_head and t.cost_center == tax.cost_center:
t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount)
t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount)
found = True
if not found:
tax.charge_type = 'Actual'
tax.included_in_print_rate = 0
tax.tax_amount = tax.tax_amount_after_discount_amount
tax.base_tax_amount = tax.base_tax_amount_after_discount_amount
taxes.append(tax)
for payment in doc.get('payments'):
@ -118,6 +145,8 @@ class POSInvoiceMergeLog(Document):
invoice.set('items', items)
invoice.set('payments', payments)
invoice.set('taxes', taxes)
invoice.additional_discount_percentage = 0
invoice.discount_amount = 0.0
return invoice

View File

@ -5,21 +5,37 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import cint
from frappe.utils import cint, get_link_to_form
from frappe.model.document import Document
from erpnext.controllers.status_updater import StatusUpdater
class POSOpeningEntry(StatusUpdater):
def validate(self):
self.validate_pos_profile_and_cashier()
self.validate_payment_method_account()
self.set_status()
def validate_pos_profile_and_cashier(self):
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")):
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):
self.set_status(update=True)

View File

@ -6,6 +6,7 @@
"engine": "InnoDB",
"field_order": [
"default",
"allow_in_returns",
"mode_of_payment"
],
"fields": [
@ -24,11 +25,19 @@
"label": "Mode of Payment",
"options": "Mode of Payment",
"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,
"links": [],
"modified": "2020-05-29 15:08:41.704844",
"modified": "2020-10-20 12:58:46.114456",
"modified_by": "Administrator",
"module": "Accounts",
"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() {
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', {
@ -44,6 +35,15 @@ frappe.ui.form.on('POS Profile', {
};
});
frm.set_query("taxes_and_charges", function() {
return {
filters: [
['Sales Taxes and Charges Template', 'company', '=', frm.doc.company],
['Sales Taxes and Charges Template', 'docstatus', '!=', 2]
]
};
});
frm.set_query('company_address', function(doc) {
if(!doc.company) {
frappe.throw(__('Please set Company'));

View File

@ -8,7 +8,6 @@
"field_order": [
"disabled",
"section_break_2",
"naming_series",
"customer",
"company",
"country",
@ -23,6 +22,9 @@
"section_break_11",
"payments",
"section_break_14",
"hide_images",
"hide_unavailable_items",
"auto_add_item_to_cart",
"item_groups",
"column_break_16",
"customer_groups",
@ -59,17 +61,6 @@
"fieldname": "section_break_2",
"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",
"fieldtype": "Link",
@ -135,7 +126,8 @@
},
{
"fieldname": "section_break_14",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"label": "Configuration"
},
{
"description": "Only show Items from these Item Groups",
@ -302,28 +294,48 @@
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"mandatory_depends_on": "update_stock",
"oldfieldname": "warehouse",
"oldfieldtype": "Link",
"options": "Warehouse"
},
{
"default": "0",
"fieldname": "update_stock",
"fieldtype": "Check",
"label": "Update Stock"
"options": "Warehouse",
"reqd": 1
},
{
"default": "0",
"fieldname": "ignore_pricing_rule",
"fieldtype": "Check",
"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"
},
{
"default": "0",
"fieldname": "hide_images",
"fieldtype": "Check",
"label": "Hide Images"
},
{
"default": "0",
"fieldname": "auto_add_item_to_cart",
"fieldtype": "Check",
"label": "Automatically Add Filtered Item To Cart"
}
],
"icon": "icon-cog",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-06-29 12:20:30.977272",
"modified": "2020-12-10 13:59:28.877572",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",
@ -350,4 +362,4 @@
],
"sort_field": "modified",
"sort_order": "DESC"
}
}

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe
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 frappe.model.document import Document
@ -13,7 +13,7 @@ class POSProfile(Document):
self.validate_default_profile()
self.validate_all_link_fields()
self.validate_duplicate_groups()
self.check_default_payment()
self.validate_payment_methods()
def validate_default_profile(self):
for row in self.applicable_for_users:
@ -52,14 +52,33 @@ class POSProfile(Document):
if len(customer_groups) != len(set(customer_groups)):
frappe.throw(_("Duplicate customer group found in the cutomer group table"), title = "Duplicate Customer Group")
def check_default_payment(self):
if self.payments:
default_mode_of_payment = [d.default for d in self.payments if d.default]
if not default_mode_of_payment:
frappe.throw(_("Set default mode of payment"))
def validate_payment_methods(self):
if not self.payments:
frappe.throw(_("Payment methods are mandatory. Please add at least one payment method."))
if len(default_mode_of_payment) > 1:
frappe.throw(_("Multiple default mode of payment is not allowed"))
default_mode = [d.default for d in self.payments if d.default]
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):
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
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.validate_and_sanitize_search_inputs
def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):

View File

@ -70,6 +70,7 @@ def get_items_list(pos_profile, company):
""".format(cond=cond), tuple([company] + args_list), as_dict=1)
def make_pos_profile(**args):
frappe.db.sql("delete from `tabPOS Payment Method`")
frappe.db.sql("delete from `tabPOS Profile`")
args = frappe._dict(args)

View File

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

View File

@ -42,56 +42,56 @@ frappe.ui.form.on('Pricing Rule', {
<tr><td>
<h4>
<i class="fa fa-hand-right"></i>
${__('Notes')}
{{__('Notes')}}
</h4>
<ul>
<li>
${__("Pricing Rule is made to overwrite Price List / define discount percentage, based on some criteria.")}
{{__("Pricing Rule is made to overwrite Price List / define discount percentage, based on some criteria.")}}
</li>
<li>
${__("If selected Pricing Rule is made for 'Rate', it will overwrite Price List. Pricing Rule rate is the final rate, so no further discount should be applied. Hence, in transactions like Sales Order, Purchase Order etc, it will be fetched in 'Rate' field, rather than 'Price List Rate' field.")}
{{__("If selected Pricing Rule is made for 'Rate', it will overwrite Price List. Pricing Rule rate is the final rate, so no further discount should be applied. Hence, in transactions like Sales Order, Purchase Order etc, it will be fetched in 'Rate' field, rather than 'Price List Rate' field.")}}
</li>
<li>
${__('Discount Percentage can be applied either against a Price List or for all Price List.')}
{{__('Discount Percentage can be applied either against a Price List or for all Price List.')}}
</li>
<li>
${__('To not apply Pricing Rule in a particular transaction, all applicable Pricing Rules should be disabled.')}
{{__('To not apply Pricing Rule in a particular transaction, all applicable Pricing Rules should be disabled.')}}
</li>
</ul>
</td></tr>
<tr><td>
<h4><i class="fa fa-question-sign"></i>
${__('How Pricing Rule is applied?')}
{{__('How Pricing Rule is applied?')}}
</h4>
<ol>
<li>
${__("Pricing Rule is first selected based on 'Apply On' field, which can be Item, Item Group or Brand.")}
{{__("Pricing Rule is first selected based on 'Apply On' field, which can be Item, Item Group or Brand.")}}
</li>
<li>
${__("Then Pricing Rules are filtered out based on Customer, Customer Group, Territory, Supplier, Supplier Type, Campaign, Sales Partner etc.")}
{{__("Then Pricing Rules are filtered out based on Customer, Customer Group, Territory, Supplier, Supplier Type, Campaign, Sales Partner etc.")}}
</li>
<li>
${__('Pricing Rules are further filtered based on quantity.')}
{{__('Pricing Rules are further filtered based on quantity.')}}
</li>
<li>
${__('If two or more Pricing Rules are found based on the above conditions, Priority is applied. Priority is a number between 0 to 20 while default value is zero (blank). Higher number means it will take precedence if there are multiple Pricing Rules with same conditions.')}
{{__('If two or more Pricing Rules are found based on the above conditions, Priority is applied. Priority is a number between 0 to 20 while default value is zero (blank). Higher number means it will take precedence if there are multiple Pricing Rules with same conditions.')}}
</li>
<li>
${__('Even if there are multiple Pricing Rules with highest priority, then following internal priorities are applied:')}
{{__('Even if there are multiple Pricing Rules with highest priority, then following internal priorities are applied:')}}
<ul>
<li>
${__('Item Code > Item Group > Brand')}
{{__('Item Code > Item Group > Brand')}}
</li>
<li>
${__('Customer > Customer Group > Territory')}
{{__('Customer > Customer Group > Territory')}}
</li>
<li>
${__('Supplier > Supplier Type')}
{{__('Supplier > Supplier Type')}}
</li>
</ul>
</li>
<li>
${__('If multiple Pricing Rules continue to prevail, users are asked to set Priority manually to resolve conflict.')}
{{__('If multiple Pricing Rules continue to prevail, users are asked to set Priority manually to resolve conflict.')}}
</li>
</ol>
</td></tr>

View File

@ -1,4 +1,5 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:title",
@ -71,6 +72,7 @@
"section_break_13",
"threshold_percentage",
"priority",
"condition",
"column_break_66",
"apply_multiple_pricing_rules",
"apply_discount_on_rate",
@ -404,6 +406,7 @@
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "eval:doc.rate_or_discount==\"Rate\"",
"fieldname": "rate",
"fieldtype": "Currency",
@ -467,6 +470,7 @@
"options": "UOM"
},
{
"description": "If rate is zero them item will be treated as \"Free Item\"",
"fieldname": "free_item_rate",
"fieldtype": "Currency",
"label": "Rate"
@ -502,10 +506,10 @@
},
{
"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",
"fieldtype": "Check",
"label": "Apply Discount on Rate"
"label": "Apply Discount on Discounted Rate"
},
{
"default": "0",
@ -550,11 +554,18 @@
"fieldtype": "Link",
"label": "Promotional Scheme",
"options": "Promotional Scheme"
},
{
"description": "Simple Python Expression, Example: territory != 'All Territories'",
"fieldname": "condition",
"fieldtype": "Code",
"label": "Condition"
}
],
"icon": "fa fa-gift",
"idx": 1,
"modified": "2019-12-18 17:29:22.957077",
"links": [],
"modified": "2020-12-04 00:36:24.698219",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule",

View File

@ -6,9 +6,10 @@ from __future__ import unicode_literals
import frappe
import json
import copy
import re
from frappe import throw, _
from frappe.utils import flt, cint, getdate
from frappe.model.document import Document
from six import string_types
@ -30,6 +31,7 @@ class PricingRule(Document):
self.validate_max_discount()
self.validate_price_list_with_currency()
self.validate_dates()
self.validate_condition()
if not self.margin_type: self.margin_rate_or_amount = 0.0
@ -58,6 +60,15 @@ class PricingRule(Document):
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)
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):
if not self.selling and not self.buying:
throw(_("Atleast one of the Selling or Buying must be selected"))
@ -140,6 +151,10 @@ class PricingRule(Document):
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"))
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()
@ -220,12 +235,11 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
item_details = frappe._dict({
"doctype": args.doctype,
"has_margin": False,
"name": args.name,
"parent": args.parent,
"parenttype": args.parenttype,
"child_docname": args.get('child_docname'),
"discount_percentage_on_rate": [],
"discount_amount_on_rate": []
"child_docname": args.get('child_docname')
})
if args.ignore_pricing_rule or not args.item_code:
@ -273,6 +287,10 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
else:
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.pricing_rules = frappe.as_json([d.pricing_rule for d in rules])
@ -324,20 +342,28 @@ def get_pricing_rule_details(args, pricing_rule):
def apply_price_discount_rule(pricing_rule, item_details, args):
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')):
item_details.margin_type = pricing_rule.margin_type
item_details.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
else:
item_details.margin_type = None
item_details.margin_rate_or_amount = 0.0
item_details.has_margin = True
if pricing_rule.apply_multiple_pricing_rules and item_details.margin_rate_or_amount is not None:
item_details.margin_rate_or_amount += pricing_rule.margin_rate_or_amount
else:
item_details.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
if pricing_rule.rate_or_discount == 'Rate':
pricing_rule_rate = 0.0
if pricing_rule.currency == args.currency:
pricing_rule_rate = pricing_rule.rate
if pricing_rule_rate:
# Override already set price list rate (from item price)
# if pricing_rule_rate > 0
item_details.update({
"price_list_rate": pricing_rule_rate * args.get("conversion_factor", 1),
})
item_details.update({
"price_list_rate": pricing_rule_rate * args.get("conversion_factor", 1),
"discount_percentage": 0.0
})
@ -345,9 +371,9 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
if pricing_rule.rate_or_discount != apply_on: continue
field = frappe.scrub(apply_on)
if pricing_rule.apply_discount_on_rate:
discount_field = "{0}_on_rate".format(field)
item_details[discount_field].append(pricing_rule.get(field, 0))
if pricing_rule.apply_discount_on_rate and item_details.get("discount_percentage"):
# Apply discount on discounted rate
item_details[field] += ((100 - item_details[field]) * (pricing_rule.get(field, 0) / 100))
else:
if field not in item_details:
item_details.setdefault(field, 0)
@ -355,14 +381,6 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
item_details[field] += (pricing_rule.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):
from erpnext.accounts.doctype.pricing_rule.utils import (get_applied_pricing_rules,
get_pricing_rule_items)

View File

@ -385,7 +385,7 @@ class TestPricingRule(unittest.TestCase):
so.load_from_db()
self.assertEqual(so.items[1].is_free_item, 1)
self.assertEqual(so.items[1].item_code, "_Test Item 2")
def test_cumulative_pricing_rule(self):
frappe.delete_doc_if_exists('Pricing Rule', '_Test Cumulative Pricing Rule')
test_record = {
@ -430,6 +430,113 @@ class TestPricingRule(unittest.TestCase):
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 test_item_price_with_pricing_rule(self):
item = make_item("Water Flask")
make_item_price("Water Flask", "_Test Price List", 100)
pricing_rule_record = {
"doctype": "Pricing Rule",
"title": "_Test Water Flask Rule",
"apply_on": "Item Code",
"items": [{
"item_code": "Water Flask",
}],
"selling": 1,
"currency": "INR",
"rate_or_discount": "Rate",
"rate": 0,
"margin_type": "Percentage",
"margin_rate_or_amount": 2,
"company": "_Test Company"
}
rule = frappe.get_doc(pricing_rule_record)
rule.insert()
si = create_sales_invoice(do_not_save=True, item_code="Water Flask")
si.selling_price_list = "_Test Price List"
si.save()
# If rate in Rule is 0, give preference to Item Price if it exists
self.assertEqual(si.items[0].price_list_rate, 100)
self.assertEqual(si.items[0].margin_rate_or_amount, 2)
self.assertEqual(si.items[0].rate_with_margin, 102)
self.assertEqual(si.items[0].rate, 102)
si.delete()
rule.delete()
frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete()
item.delete()
def test_pricing_rule_for_transaction(self):
make_item("Water Flask 1")
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
make_pricing_rule(selling=1, min_qty=5, price_or_product_discount="Product",
apply_on="Transaction", free_item="Water Flask 1", free_qty=1, free_item_rate=10)
si = create_sales_invoice(qty=5, do_not_submit=True)
self.assertEquals(len(si.items), 2)
self.assertEquals(si.items[1].rate, 10)
si1 = create_sales_invoice(qty=2, do_not_submit=True)
self.assertEquals(len(si1.items), 1)
for doc in [si, si1]:
doc.delete()
def make_pricing_rule(**args):
args = frappe._dict(args)
@ -441,21 +548,30 @@ def make_pricing_rule(**args):
"applicable_for": args.applicable_for,
"selling": args.selling or 0,
"currency": "USD",
"apply_discount_on_rate": args.apply_discount_on_rate or 0,
"buying": args.buying or 0,
"min_qty": args.min_qty or 0.0,
"max_qty": args.max_qty or 0.0,
"rate_or_discount": args.rate_or_discount or "Discount Percentage",
"discount_percentage": args.discount_percentage or 0.0,
"rate": args.rate or 0.0,
"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
})
for field in ["free_item", "free_qty", "free_item_rate", "priority",
"margin_type", "price_or_product_discount"]:
if args.get(field):
doc.set(field, args.get(field))
apply_on = doc.apply_on.replace(' ', '_').lower()
child_table = {'Item Code': 'items', 'Item Group': 'item_groups', 'Brand': 'brands'}
doc.append(child_table.get(doc.apply_on), {
apply_on: args.get(apply_on) or "_Test Item"
})
if doc.apply_on != "Transaction":
doc.append(child_table.get(doc.apply_on), {
apply_on: args.get(apply_on) or "_Test Item"
})
doc.insert(ignore_permissions=True)
if args.get(apply_on) and apply_on != "item_code":

View File

@ -14,9 +14,8 @@ import frappe
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.get_item_details import get_conversion_factor
from frappe import _, throw
from frappe.utils import cint, flt, get_datetime, get_link_to_form, getdate, today
from frappe import _, bold
from frappe.utils import cint, flt, get_link_to_form, getdate, today, fmt_money
class MultiplePricingRuleConflict(frappe.ValidationError): pass
@ -37,9 +36,12 @@ def get_pricing_rules(args, doc=None):
rules = []
pricing_rules = filter_pricing_rule_based_on_condition(pricing_rules, doc)
if not pricing_rules: return []
if apply_multiple_pricing_rules(pricing_rules):
pricing_rules = sorted_by_priority(pricing_rules)
for pricing_rule in pricing_rules:
pricing_rule = filter_pricing_rules(args, pricing_rule, doc)
if pricing_rule:
@ -51,6 +53,37 @@ def get_pricing_rules(args, doc=None):
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):
apply_on_field = frappe.scrub(apply_on)
@ -131,7 +164,15 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
frappe.throw(_("Invalid {0}").format(args.get(field)))
parent_groups = frappe.db.sql_list("""select name from `tab%s`
where lft<=%s and rgt>=%s""" % (parenttype, '%s', '%s'), (lft, rgt))
where lft>=%s and rgt<=%s""" % (parenttype, '%s', '%s'), (lft, rgt))
if parenttype in ["Customer Group", "Item Group", "Territory"]:
parent_field = "parent_{0}".format(frappe.scrub(parenttype))
root_name = frappe.db.get_list(parenttype,
{"is_group": 1, parent_field: ("is", "not set")}, "name", as_list=1)
if root_name and root_name[0][0]:
parent_groups.append(root_name[0][0])
if parent_groups:
if allow_blank: parent_groups.append('')
@ -265,12 +306,13 @@ def validate_quantity_and_amount_for_suggestion(args, qty, amount, item_code, tr
fieldname = field
if fieldname:
msg = _("""If you {0} {1} quantities of the item <b>{2}</b>, the scheme <b>{3}</b>
will be applied on the item.""").format(type_of_transaction, args.get(fieldname), item_code, args.rule_description)
msg = (_("If you {0} {1} quantities of the item {2}, the scheme {3} will be applied on the item.")
.format(type_of_transaction, args.get(fieldname), bold(item_code), bold(args.rule_description)))
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.
""").format(frappe.fmt_money(type_of_transaction, args.get(fieldname)), item_code, args.rule_description)
msg = (_("If you {0} {1} worth item {2}, the scheme {3} will be applied on the item.")
.format(type_of_transaction, fmt_money(args.get(fieldname), currency=args.get("currency")),
bold(item_code), bold(args.rule_description)))
frappe.msgprint(msg)
@ -423,6 +465,9 @@ def apply_pricing_rule_on_transaction(doc):
pricing_rules = filter_pricing_rules_for_qty_amount(doc.total_qty,
doc.total, pricing_rules)
if not pricing_rules:
remove_free_item(doc)
for d in pricing_rules:
if d.price_or_product_discount == 'Price':
if d.apply_discount_on:
@ -446,6 +491,12 @@ def apply_pricing_rule_on_transaction(doc):
get_product_discount_rule(d, item_details, doc=doc)
apply_pricing_rule_for_free_items(doc, item_details.free_item_data)
doc.set_missing_values()
doc.calculate_taxes_and_totals()
def remove_free_item(doc):
for d in doc.items:
if d.is_free_item:
doc.remove(d)
def get_applied_pricing_rules(pricing_rules):
if pricing_rules:
@ -458,7 +509,7 @@ def get_applied_pricing_rules(pricing_rules):
def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
free_item = pricing_rule.free_item
if pricing_rule.same_item:
if pricing_rule.same_item and pricing_rule.get("apply_on") != 'Transaction':
free_item = item_details.item_code or args.item_code
if not free_item:

View File

@ -21,7 +21,7 @@ class TestProcessDeferredAccounting(unittest.TestCase):
item.no_of_months = 12
item.save()
si = create_sales_invoice(item=item.name, posting_date="2019-01-10", do_not_submit=True)
si = create_sales_invoice(item=item.name, update_stock=0, posting_date="2019-01-10", do_not_submit=True)
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2019-01-10"
si.items[0].service_end_date = "2019-03-15"

View File

@ -15,6 +15,16 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
return (doc.qty<=doc.received_qty) ? "green" : "orange";
});
}
this.frm.set_query("unrealized_profit_loss_account", function() {
return {
filters: {
company: doc.company,
is_group: 0,
root_type: "Liability",
}
};
});
},
onload: function() {
this._super();
@ -25,6 +35,12 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
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) {
@ -93,6 +109,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
target: me.frm,
setters: {
supplier: me.frm.doc.supplier || undefined,
schedule_date: undefined
},
get_query_filters: {
docstatus: 1,
@ -101,16 +118,16 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
company: me.frm.doc.company
}
})
}, __("Get items from"));
}, __("Get Items From"));
this.frm.add_custom_button(__('Purchase Receipt'), function() {
erpnext.utils.map_current_doc({
method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_invoice",
source_doctype: "Purchase Receipt",
target: me.frm,
date_field: "posting_date",
setters: {
supplier: me.frm.doc.supplier || undefined,
posting_date: undefined
},
get_query_filters: {
docstatus: 1,
@ -119,7 +136,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
is_return: 0
}
})
}, __("Get items from"));
}, __("Get Items From"));
}
this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted==="Yes");
@ -135,6 +152,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() {

View File

@ -126,6 +126,7 @@
"write_off_cost_center",
"advances_section",
"allocate_advances_automatically",
"adjust_advance_taxes",
"get_advances",
"advances",
"payment_schedule_section",
@ -151,9 +152,11 @@
"is_opening",
"against_expense_account",
"column_break_63",
"unrealized_profit_loss_account",
"status",
"inter_company_invoice_reference",
"is_internal_supplier",
"represents_company",
"remarks",
"subscription_section",
"from_date",
@ -361,6 +364,7 @@
"fieldname": "bill_date",
"fieldtype": "Date",
"label": "Supplier Invoice Date",
"no_copy": 1,
"oldfieldname": "bill_date",
"oldfieldtype": "Date",
"print_hide": 1
@ -1221,7 +1225,7 @@
"fieldtype": "Select",
"in_standard_filter": 1,
"label": "Status",
"options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nUnpaid\nOverdue\nCancelled",
"options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nUnpaid\nOverdue\nCancelled\nInternal Transfer",
"print_hide": 1
},
{
@ -1328,13 +1332,37 @@
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"default": "0",
"description": "Taxes paid while advance payment will be adjusted against this invoice",
"fieldname": "adjust_advance_taxes",
"fieldtype": "Check",
"label": "Adjust Advance Taxes"
},
{
"depends_on": "eval:doc.is_internal_supplier",
"description": "Unrealized Profit / Loss account for intra-company transfers",
"fieldname": "unrealized_profit_loss_account",
"fieldtype": "Link",
"label": "Unrealized Profit / Loss Account",
"options": "Account"
},
{
"depends_on": "eval:doc.is_internal_supplier",
"description": "Company which internal supplier represents",
"fetch_from": "supplier.represents_company",
"fieldname": "represents_company",
"fieldtype": "Link",
"label": "Represents Company",
"options": "Company"
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2020-08-03 23:20:04.466153",
"modified": "2020-12-11 12:46:12.796378",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
@ -1396,4 +1424,4 @@
"timeline_field": "supplier",
"title_field": "title",
"track_changes": 1
}
}

View File

@ -132,6 +132,11 @@ class PurchaseInvoice(BuyingController):
if not self.due_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)
def check_conversion_rate(self):
@ -142,18 +147,25 @@ class PurchaseInvoice(BuyingController):
throw(_("Conversion rate cannot be 0 or 1"))
def validate_credit_to_acc(self):
if not self.credit_to:
self.credit_to = get_party_account("Supplier", self.supplier, self.company)
if not self.credit_to:
self.raise_missing_debit_credit_account_error("Supplier", self.supplier)
account = frappe.db.get_value("Account", self.credit_to,
["account_type", "report_type", "account_currency"], as_dict=True)
if account.report_type != "Balance Sheet":
frappe.throw(_("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"))
frappe.throw(
_("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")
)
if self.supplier and account.account_type != "Payable":
frappe.throw(_("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"))
frappe.throw(
_("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")
)
self.party_account_currency = account.account_currency
@ -194,8 +206,8 @@ class PurchaseInvoice(BuyingController):
["Purchase Receipt", "purchase_receipt", "pr_detail"]
])
def validate_warehouse(self):
if self.update_stock:
def validate_warehouse(self, for_validate=True):
if self.update_stock and for_validate:
for d in self.get('items'):
if not d.warehouse:
frappe.throw(_("Warehouse required at Row No {0}, please set default warehouse for the item {1} for the company {2}").
@ -221,7 +233,7 @@ class PurchaseInvoice(BuyingController):
if self.update_stock:
self.validate_item_code()
self.validate_warehouse()
self.validate_warehouse(for_validate)
if auto_accounting_for_stock:
warehouse_account = get_warehouse_account_map(self.company)
@ -239,10 +251,10 @@ class PurchaseInvoice(BuyingController):
if self.update_stock and (not item.from_warehouse):
if for_validate and item.expense_account and item.expense_account != warehouse_account[item.warehouse]["account"]:
frappe.msgprint(_('''Row {0}: Expense Head changed to {1} because account {2}
is not linked to warehouse {3} or it is not the default inventory account'''.format(
item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]),
frappe.bold(item.expense_account), frappe.bold(item.warehouse))))
msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]))
msg += _("because account {} is not linked to warehouse {} ").format(frappe.bold(item.expense_account), frappe.bold(item.warehouse))
msg += _("or it is not the default inventory account")
frappe.msgprint(msg, title=_("Expense Head Changed"))
item.expense_account = warehouse_account[item.warehouse]["account"]
else:
@ -254,19 +266,19 @@ class PurchaseInvoice(BuyingController):
if negative_expense_booked_in_pr:
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
expense is booked against this account in Purchase Receipt {2}'''.format(
item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.purchase_receipt))))
msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(stock_not_billed_account))
msg += _("because expense is booked against this account in Purchase Receipt {}").format(frappe.bold(item.purchase_receipt))
frappe.msgprint(msg, title=_("Expense Head Changed"))
item.expense_account = stock_not_billed_account
else:
# If no purchase receipt present then book expense in 'Stock Received But Not Billed'
# This is done in cases when Purchase Invoice is created before Purchase Receipt
if for_validate and item.expense_account and item.expense_account != stock_not_billed_account:
frappe.msgprint(_('''Row {0}: Expense Head changed to {1} as no Purchase
Receipt is created against Item {2}. This is done to handle accounting for cases
when Purchase Receipt is created after Purchase Invoice'''.format(
item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.item_code))))
msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(stock_not_billed_account))
msg += _("as no Purchase Receipt is created against Item {}. ").format(frappe.bold(item.item_code))
msg += _("This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice")
frappe.msgprint(msg, title=_("Expense Head Changed"))
item.expense_account = stock_not_billed_account
@ -294,10 +306,11 @@ class PurchaseInvoice(BuyingController):
for d in self.get('items'):
if not d.purchase_order:
throw(_("""Purchase Order Required for item {0}
To submit the invoice without purchase order please set
{1} as {2} in {3}""").format(frappe.bold(d.item_code), frappe.bold(_('Purchase Order Required')),
frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings')))
msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code))
msg += "<br><br>"
msg += _("To submit the invoice without purchase order please set {} ").format(frappe.bold(_('Purchase Order Required')))
msg += _("as {} in {}").format(frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))
throw(msg, title=_("Mandatory Purchase Order"))
def pr_required(self):
stock_items = self.get_stock_items()
@ -308,10 +321,11 @@ class PurchaseInvoice(BuyingController):
for d in self.get('items'):
if not d.purchase_receipt and d.item_code in stock_items:
throw(_("""Purchase Receipt Required for item {0}
To submit the invoice without purchase receipt please set
{1} as {2} in {3}""").format(frappe.bold(d.item_code), frappe.bold(_('Purchase Receipt Required')),
frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings')))
msg = _("Purchase Receipt Required for item {}").format(frappe.bold(d.item_code))
msg += "<br><br>"
msg += _("To submit the invoice without purchase receipt please set {} ").format(frappe.bold(_('Purchase Receipt Required')))
msg += _("as {} in {}").format(frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))
throw(msg, title=_("Mandatory Purchase Receipt"))
def validate_write_off_account(self):
if self.write_off_amount and not self.write_off_account:
@ -396,12 +410,13 @@ class PurchaseInvoice(BuyingController):
# this sequence because outstanding may get -negative
self.make_gl_entries()
if self.update_stock == 1:
self.repost_future_sle_and_gle()
self.update_project()
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
def make_gl_entries(self, gl_entries=None):
if not self.grand_total:
return
def make_gl_entries(self, gl_entries=None, from_repost=False):
if not gl_entries:
gl_entries = self.get_gl_entries()
@ -409,7 +424,7 @@ class PurchaseInvoice(BuyingController):
update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
if self.docstatus == 1:
make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False)
make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False, from_repost=from_repost)
elif self.docstatus == 2:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
@ -424,9 +439,11 @@ class PurchaseInvoice(BuyingController):
self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
if self.auto_accounting_for_stock:
self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed")
self.expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
else:
self.stock_received_but_not_billed = None
self.expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
self.expenses_included_in_valuation = None
self.negative_expense_to_be_booked = 0.0
gl_entries = []
@ -437,15 +454,15 @@ class PurchaseInvoice(BuyingController):
self.get_asset_gl_entry(gl_entries)
self.make_tax_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries)
gl_entries = make_regional_gl_entries(gl_entries, self)
gl_entries = merge_similar_entries(gl_entries)
self.make_payment_gl_entries(gl_entries)
self.make_write_off_gl_entry(gl_entries)
self.make_gle_for_rounding_adjustment(gl_entries)
return gl_entries
def check_asset_cwip_enabled(self):
@ -462,31 +479,30 @@ class PurchaseInvoice(BuyingController):
# because rounded_total had value even before introcution of posting GLE based on rounded total
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
if grand_total:
# Didnot use base_grand_total to book rounding loss gle
grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
self.precision("grand_total"))
gl_entries.append(
self.get_gl_dict({
"account": self.credit_to,
"party_type": "Supplier",
"party": self.supplier,
"due_date": self.due_date,
"against": self.against_expense_account,
"credit": grand_total_in_company_currency,
"credit_in_account_currency": grand_total_in_company_currency \
if self.party_account_currency==self.company_currency else grand_total,
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype,
"project": self.project,
"cost_center": self.cost_center
}, self.party_account_currency, item=self)
)
if grand_total and not self.is_internal_transfer():
# Didnot use base_grand_total to book rounding loss gle
grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
self.precision("grand_total"))
gl_entries.append(
self.get_gl_dict({
"account": self.credit_to,
"party_type": "Supplier",
"party": self.supplier,
"due_date": self.due_date,
"against": self.against_expense_account,
"credit": grand_total_in_company_currency,
"credit_in_account_currency": grand_total_in_company_currency \
if self.party_account_currency==self.company_currency else grand_total,
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype,
"project": self.project,
"cost_center": self.cost_center
}, self.party_account_currency, item=self)
)
def make_item_gl_entries(self, gl_entries):
# item gl entries
stock_items = self.get_stock_items()
expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
if self.update_stock and self.auto_accounting_for_stock:
warehouse_account = get_warehouse_account_map(self.company)
@ -514,7 +530,6 @@ class PurchaseInvoice(BuyingController):
item, voucher_wise_stock_value, account_currency)
if item.from_warehouse:
gl_entries.append(self.get_gl_dict({
"account": warehouse_account[item.warehouse]['account'],
"against": warehouse_account[item.from_warehouse]["account"],
@ -534,16 +549,18 @@ class PurchaseInvoice(BuyingController):
"debit": -1 * flt(item.base_net_amount, item.precision("base_net_amount")),
}, warehouse_account[item.from_warehouse]["account_currency"], item=item))
gl_entries.append(
self.get_gl_dict({
"account": item.expense_account,
"against": self.supplier,
"debit": flt(item.base_net_amount, item.precision("base_net_amount")),
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"cost_center": item.cost_center,
"project": item.project
}, account_currency, item=item)
)
# Do not book expense for transfer within same company transfer
if not self.is_internal_transfer():
gl_entries.append(
self.get_gl_dict({
"account": item.expense_account,
"against": self.supplier,
"debit": flt(item.base_net_amount, item.precision("base_net_amount")),
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"cost_center": item.cost_center,
"project": item.project
}, account_currency, item=item)
)
else:
gl_entries.append(
@ -708,7 +725,8 @@ class PurchaseInvoice(BuyingController):
item.item_tax_amount / self.conversion_rate)
}, item=item))
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)
gl_entries.append(self.get_gl_dict({
@ -819,7 +837,8 @@ class PurchaseInvoice(BuyingController):
}, account_currency, item=tax)
)
# accumulate valuation tax
if self.is_opening == "No" and tax.category in ("Valuation", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount):
if self.is_opening == "No" and tax.category in ("Valuation", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount) \
and not self.is_internal_transfer():
if self.auto_accounting_for_stock and not tax.cost_center:
frappe.throw(_("Cost Center is required in row {0} in Taxes table for type {1}").format(tax.idx, _(tax.category)))
valuation_tax.setdefault(tax.name, 0)
@ -863,8 +882,19 @@ class PurchaseInvoice(BuyingController):
"against": self.supplier,
"credit": valuation_tax[tax.name],
"remarks": self.remarks or "Accounting Entry for Stock"
}, item=tax)
)
}, item=tax))
def make_internal_transfer_gl_entries(self, gl_entries):
if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges):
account_currency = get_account_currency(self.unrealized_profit_loss_account)
gl_entries.append(
self.get_gl_dict({
"account": self.unrealized_profit_loss_account,
"against": self.supplier,
"credit": flt(self.total_taxes_and_charges),
"credit_in_account_currency": flt(self.base_total_taxes_and_charges),
"cost_center": self.cost_center
}, account_currency, item=self))
def make_payment_gl_entries(self, gl_entries):
# Make Cash GL Entries
@ -969,11 +999,15 @@ class PurchaseInvoice(BuyingController):
self.delete_auto_created_batches()
self.make_gl_entries_on_cancel()
if self.update_stock == 1:
self.repost_future_sle_and_gle()
self.update_project()
frappe.db.set(self, 'status', 'Cancelled')
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
def update_project(self):
project_list = []
@ -1024,7 +1058,9 @@ class PurchaseInvoice(BuyingController):
updated_pr += update_billed_amount_based_on_po(d.po_detail, update_modified)
for pr in set(updated_pr):
frappe.get_doc("Purchase Receipt", pr).update_billing_percentage(update_modified=update_modified)
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage
pr_doc = frappe.get_doc("Purchase Receipt", pr)
update_billing_percentage(pr_doc, update_modified=update_modified)
def on_recurring(self, reference_doc, auto_repeat_doc):
self.due_date = None
@ -1080,7 +1116,9 @@ class PurchaseInvoice(BuyingController):
if self.docstatus == 2:
status = "Cancelled"
elif self.docstatus == 1:
if outstanding_amount > 0 and due_date < nowdate:
if self.is_internal_transfer():
self.status = 'Internal Transfer'
elif outstanding_amount > 0 and due_date < nowdate:
self.status = "Overdue"
elif outstanding_amount > 0 and due_date >= nowdate:
self.status = "Unpaid"

View File

@ -4,23 +4,25 @@
// render
frappe.listview_settings['Purchase Invoice'] = {
add_fields: ["supplier", "supplier_name", "base_grand_total", "outstanding_amount", "due_date", "company",
"currency", "is_return", "release_date", "on_hold"],
"currency", "is_return", "release_date", "on_hold", "represents_company", "is_internal_supplier"],
get_indicator: function(doc) {
if( (flt(doc.outstanding_amount) <= 0) && doc.docstatus == 1 && doc.status == 'Debit Note Issued') {
if ((flt(doc.outstanding_amount) <= 0) && doc.docstatus == 1 && doc.status == 'Debit Note Issued') {
return [__("Debit Note Issued"), "darkgrey", "outstanding_amount,<=,0"];
} else if(flt(doc.outstanding_amount) > 0 && doc.docstatus==1) {
} else if (flt(doc.outstanding_amount) > 0 && doc.docstatus==1) {
if(cint(doc.on_hold) && !doc.release_date) {
return [__("On Hold"), "darkgrey"];
} else if(cint(doc.on_hold) && doc.release_date && frappe.datetime.get_diff(doc.release_date, frappe.datetime.nowdate()) > 0) {
} else if (cint(doc.on_hold) && doc.release_date && frappe.datetime.get_diff(doc.release_date, frappe.datetime.nowdate()) > 0) {
return [__("Temporarily on Hold"), "darkgrey"];
} else if(frappe.datetime.get_diff(doc.due_date) < 0) {
} else if (frappe.datetime.get_diff(doc.due_date) < 0) {
return [__("Overdue"), "red", "outstanding_amount,>,0|due_date,<,Today"];
} else {
return [__("Unpaid"), "orange", "outstanding_amount,>,0|due_date,>=,Today"];
}
} else if(cint(doc.is_return)) {
} else if (cint(doc.is_return)) {
return [__("Return"), "darkgrey", "is_return,=,Yes"];
} else if(flt(doc.outstanding_amount)==0 && doc.docstatus==1) {
} else if (doc.company == doc.represents_company && doc.is_internal_supplier) {
return [__("Internal Transfer"), "darkgrey", "outstanding_amount,=,0"];
} else if (flt(doc.outstanding_amount)==0 && doc.docstatus==1) {
return [__("Paid"), "green", "outstanding_amount,=,0"];
}
}

View File

@ -9,8 +9,7 @@ import frappe.model
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from frappe.utils import cint, flt, today, nowdate, add_days, getdate
import frappe.defaults
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory, \
test_records as pr_test_records, make_purchase_receipt, get_taxes
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt, get_taxes
from erpnext.controllers.accounts_controller import get_payment_terms
from erpnext.exceptions import InvalidCurrency
from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction
@ -33,13 +32,10 @@ class TestPurchaseInvoice(unittest.TestCase):
def test_gl_entries_without_perpetual_inventory(self):
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
wrapper = frappe.copy_doc(test_records[0])
set_perpetual_inventory(0, wrapper.company)
self.assertTrue(not cint(erpnext.is_perpetual_inventory_enabled(wrapper.company)))
wrapper.insert()
wrapper.submit()
wrapper.load_from_db()
dl = wrapper
pi = frappe.copy_doc(test_records[0])
self.assertTrue(not cint(erpnext.is_perpetual_inventory_enabled(pi.company)))
pi.insert()
pi.submit()
expected_gl_entries = {
"_Test Payable - _TC": [0, 1512.0],
@ -54,12 +50,16 @@ class TestPurchaseInvoice(unittest.TestCase):
"Round Off - _TC": [0, 0.3]
}
gl_entries = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
where voucher_type = 'Purchase Invoice' and voucher_no = %s""", dl.name, as_dict=1)
where voucher_type = 'Purchase Invoice' and voucher_no = %s""", pi.name, as_dict=1)
for d in gl_entries:
self.assertEqual([d.debit, d.credit], expected_gl_entries.get(d.account))
def test_gl_entries_with_perpetual_inventory(self):
pi = make_purchase_invoice(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1", get_taxes_and_charges=True, qty=10)
pi = make_purchase_invoice(company="_Test Company with perpetual inventory",
warehouse= "Stores - TCP1", cost_center = "Main - TCP1",
expense_account ="_Test Account Cost for Goods Sold - TCP1",
get_taxes_and_charges=True, qty=10)
self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pi.company)), 1)
self.check_gle_for_pi(pi.name)
@ -198,8 +198,6 @@ class TestPurchaseInvoice(unittest.TestCase):
pr = make_purchase_receipt(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", get_taxes_and_charges=True,)
self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pr.company)), 1)
pi = make_purchase_invoice(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1", get_taxes_and_charges=True, qty=10,do_not_save= "True")
for d in pi.items:
@ -247,17 +245,11 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertRaises(frappe.CannotChangeConstantError, pi.save)
def test_gl_entries_with_aia_for_non_stock_items(self):
pi = frappe.copy_doc(test_records[1])
set_perpetual_inventory(1, pi.company)
self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pi.company)), 1)
pi.get("items")[0].item_code = "_Test Non Stock Item"
pi.get("items")[0].expense_account = "_Test Account Cost for Goods Sold - _TC"
pi.get("taxes").pop(0)
pi.get("taxes").pop(1)
pi.insert()
pi.submit()
pi.load_from_db()
def test_gl_entries_for_non_stock_items_with_perpetual_inventory(self):
pi = make_purchase_invoice(item_code = "_Test Non Stock Item",
company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
self.assertTrue(pi.status, "Unpaid")
gl_entries = frappe.db.sql("""select account, debit, credit
@ -265,17 +257,15 @@ class TestPurchaseInvoice(unittest.TestCase):
order by account asc""", pi.name, as_dict=1)
self.assertTrue(gl_entries)
expected_values = sorted([
["_Test Payable - _TC", 0, 620],
["_Test Account Cost for Goods Sold - _TC", 500.0, 0],
["_Test Account VAT - _TC", 120.0, 0],
])
expected_values = [
["_Test Account Cost for Goods Sold - TCP1", 250.0, 0],
["Creditors - TCP1", 0, 250]
]
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[i][0], gle.account)
self.assertEqual(expected_values[i][1], gle.debit)
self.assertEqual(expected_values[i][2], gle.credit)
set_perpetual_inventory(0, pi.company)
def test_purchase_invoice_calculation(self):
pi = frappe.copy_doc(test_records[0])
@ -457,12 +447,13 @@ class TestPurchaseInvoice(unittest.TestCase):
pi.cancel()
self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), existing_purchase_cost)
def test_return_purchase_invoice(self):
set_perpetual_inventory()
def test_return_purchase_invoice_with_perpetual_inventory(self):
pi = make_purchase_invoice(company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
pi = make_purchase_invoice()
return_pi = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2)
return_pi = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2,
company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
# check gl entries for return
@ -473,19 +464,15 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertTrue(gl_entries)
expected_values = {
"Creditors - _TC": [100.0, 0.0],
"Stock Received But Not Billed - _TC": [0.0, 100.0],
"Creditors - TCP1": [100.0, 0.0],
"Stock Received But Not Billed - TCP1": [0.0, 100.0],
}
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.debit)
self.assertEqual(expected_values[gle.account][1], gle.credit)
set_perpetual_inventory(0)
def test_multi_currency_gle(self):
set_perpetual_inventory(0)
pi = make_purchase_invoice(supplier="_Test Supplier USD", credit_to="_Test Payable USD - _TC",
currency="USD", conversion_rate=50)
@ -640,10 +627,9 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(len(pi.get("supplied_items")), 2)
rm_supp_cost = sum([d.amount for d in pi.get("supplied_items")])
self.assertEqual(pi.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2))
self.assertEqual(flt(pi.get("items")[0].rm_supp_cost, 2), flt(rm_supp_cost, 2))
def test_rejected_serial_no(self):
set_perpetual_inventory(0)
pi = make_purchase_invoice(item_code="_Test Serialized Item With Series", received_qty=2, qty=1,
rejected_qty=1, rate=500, update_stock=1,
rejected_warehouse = "_Test Rejected Warehouse - _TC")
@ -998,11 +984,12 @@ def make_purchase_invoice(**args):
'expense_account': args.expense_account or '_Test Account Cost for Goods Sold - _TC',
"conversion_factor": 1.0,
"serial_no": args.serial_no,
"stock_uom": "_Test UOM",
"stock_uom": args.uom or "_Test UOM",
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"project": args.project,
"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:
@ -1039,7 +1026,8 @@ def make_purchase_invoice_against_cost_center(**args):
pi.is_return = args.is_return
pi.credit_to = args.return_against or "Creditors - _TC"
pi.is_subcontracted = args.is_subcontracted or "No"
pi.supplier_warehouse = "_Test Warehouse 1 - _TC"
if args.supplier_warehouse:
pi.supplier_warehouse = "_Test Warehouse 1 - _TC"
pi.append("items", {
"item_code": args.item or args.item_code or "_Test Item",

View File

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

View File

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

View File

@ -1,92 +1,38 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2016-07-27 17:24:24.956896",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"actions": [],
"creation": "2016-07-27 17:24:24.956896",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company",
"account"
],
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "company",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Company",
"length": 0,
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Default Bank / Cash account will be automatically updated in Salary Journal Entry when this mode is selected.",
"fieldname": "default_account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Default Account",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"description": "Default Bank / Cash account will be automatically updated in Salary Journal Entry when this mode is selected.",
"fieldname": "account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Account",
"options": "Account"
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2016-09-02 07:49:06.567389",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Salary Component Account",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_seen": 0
],
"istable": 1,
"links": [],
"modified": "2020-10-18 17:57:57.110257",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Salary Component Account",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@ -1,6 +1,8 @@
{% include "erpnext/regional/india/taxes.js" %}
{% include "erpnext/regional/india/e_invoice/einvoice.js" %}
erpnext.setup_auto_gst_taxation('Sales Invoice');
erpnext.setup_einvoice_actions('Sales Invoice')
frappe.ui.form.on("Sales Invoice", {
setup: function(frm) {

View File

@ -199,7 +199,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
company: me.frm.doc.company
}
})
}, __("Get items from"));
}, __("Get Items From"));
},
quotation_btn: function() {
@ -223,7 +223,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
company: me.frm.doc.company
}
})
}, __("Get items from"));
}, __("Get Items From"));
},
delivery_note_btn: function() {
@ -251,7 +251,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
};
}
});
}, __("Get items from"));
}, __("Get Items From"));
},
tc_name: function() {
@ -580,6 +580,16 @@ frappe.ui.form.on('Sales Invoice', {
};
});
frm.set_query("unrealized_profit_loss_account", function() {
return {
filters: {
company: frm.doc.company,
is_group: 0,
root_type: "Liability",
}
};
});
frm.custom_make_buttons = {
'Delivery Note': 'Delivery',
'Sales Invoice': 'Sales Return',
@ -812,10 +822,10 @@ frappe.ui.form.on('Sales Invoice', {
if (cint(frm.doc.docstatus==0) && cur_frm.page.current_view_name!=="pos" && !frm.doc.is_return) {
frm.add_custom_button(__('Healthcare Services'), function() {
get_healthcare_services_to_invoice(frm);
},"Get items from");
},"Get Items From");
frm.add_custom_button(__('Prescriptions'), function() {
get_drugs_to_invoice(frm);
},"Get items from");
},"Get Items From");
}
}
else {
@ -1080,7 +1090,7 @@ var get_drugs_to_invoice = function(frm) {
description:'Quantity will be calculated only for items which has "Nos" as UoM. You may change as required for each invoice item.',
get_query: function(doc) {
return {
filters: {
filters: {
patient: dialog.get_value("patient"),
company: frm.doc.company,
docstatus: 1

View File

@ -19,6 +19,7 @@
"is_return",
"column_break1",
"company",
"company_tax_id",
"posting_date",
"posting_time",
"set_posting_time",
@ -156,6 +157,7 @@
"more_information",
"inter_company_invoice_reference",
"is_internal_customer",
"represents_company",
"customer_group",
"campaign",
"is_discounted",
@ -169,6 +171,7 @@
"c_form_applicable",
"c_form_no",
"column_break8",
"unrealized_profit_loss_account",
"remarks",
"sales_team_section_break",
"sales_partner",
@ -1653,7 +1656,7 @@
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled",
"options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled\nInternal Transfer",
"print_hide": 1,
"read_only": 1
},
@ -1825,7 +1828,7 @@
"fieldtype": "Table",
"hide_days": 1,
"hide_seconds": 1,
"label": "Sales Team1",
"label": "Sales Contributions and Incentives",
"oldfieldname": "sales_team",
"oldfieldtype": "Table",
"options": "Sales Team",
@ -1926,6 +1929,7 @@
},
{
"default": "0",
"depends_on": "eval:(doc.is_pos && doc.is_consolidated)",
"fieldname": "is_consolidated",
"fieldtype": "Check",
"label": "Is Consolidated",
@ -1940,13 +1944,38 @@
"hide_seconds": 1,
"label": "Is Internal Customer",
"read_only": 1
},
{
"fetch_from": "company.tax_id",
"fieldname": "company_tax_id",
"fieldtype": "Data",
"label": "Company Tax ID",
"read_only": 1
},
{
"depends_on": "eval:doc.is_internal_customer",
"description": "Unrealized Profit / Loss account for intra-company transfers",
"fieldname": "unrealized_profit_loss_account",
"fieldtype": "Link",
"label": "Unrealized Profit / Loss Account",
"options": "Account"
},
{
"depends_on": "eval:doc.is_internal_customer",
"description": "Company which internal customer represents",
"fetch_from": "customer.represents_company",
"fieldname": "represents_company",
"fieldtype": "Link",
"label": "Represents Company",
"options": "Company",
"read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 181,
"is_submittable": 1,
"links": [],
"modified": "2020-08-27 01:56:28.532140",
"modified": "2020-12-11 12:48:31.769958",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe, erpnext
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, getdate, add_days, cstr, nowdate, get_link_to_form, formatdate
from frappe import _, msgprint, throw
from erpnext.accounts.party import get_party_account, get_due_date
from frappe.model.mapper import get_mapped_doc
@ -179,6 +179,9 @@ class SalesInvoice(SellingController):
# this sequence because outstanding may get -ve
self.make_gl_entries()
if self.update_stock == 1:
self.repost_future_sle_and_gle()
if not self.is_return:
self.update_billing_status_for_zero_amount_refdoc("Delivery Note")
@ -229,9 +232,9 @@ class SalesInvoice(SellingController):
frappe.throw(_("At least one mode of payment is required for POS invoice."))
def before_cancel(self):
super(SalesInvoice, self).before_cancel()
self.update_time_sheet(None)
def on_cancel(self):
super(SalesInvoice, self).on_cancel()
@ -258,6 +261,10 @@ class SalesInvoice(SellingController):
self.update_stock_ledger()
self.make_gl_entries_on_cancel()
if self.update_stock == 1:
self.repost_future_sle_and_gle()
frappe.db.set(self, 'status', 'Cancelled')
if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction":
@ -279,7 +286,7 @@ class SalesInvoice(SellingController):
if "Healthcare" in active_domains:
manage_invoice_submit_cancel(self, "on_cancel")
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
def update_status_updater_args(self):
if cint(self.update_stock):
@ -405,6 +412,8 @@ class SalesInvoice(SellingController):
from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile
if not self.pos_profile:
pos_profile = get_pos_profile(self.company) or {}
if not pos_profile:
frappe.throw(_("No POS Profile found. Please create a New POS Profile first"))
self.pos_profile = pos_profile.get('name')
pos = {}
@ -428,7 +437,7 @@ class SalesInvoice(SellingController):
if 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',
'write_off_cost_center', 'apply_discount_on', 'cost_center'):
if (not for_validate) or (for_validate and not self.get(fieldname)):
@ -472,6 +481,11 @@ class SalesInvoice(SellingController):
return frappe.db.sql("select abbr from tabCompany where name=%s", self.company)[0][0]
def validate_debit_to_acc(self):
if not self.debit_to:
self.debit_to = get_party_account("Customer", self.customer, self.company)
if not self.debit_to:
self.raise_missing_debit_credit_account_error("Customer", self.customer)
account = frappe.get_cached_value("Account", self.debit_to,
["account_type", "report_type", "account_currency"], as_dict=True)
@ -479,14 +493,14 @@ class SalesInvoice(SellingController):
frappe.throw(_("Debit To is required"), title=_("Account Missing"))
if account.report_type != "Balance Sheet":
frappe.throw(_("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("Debit To")), title=_("Invalid Account"))
msg = _("Please ensure {} account is a Balance Sheet account. ").format(frappe.bold("Debit To"))
msg += _("You can change the parent account to a Balance Sheet account or select a different account.")
frappe.throw(msg, title=_("Invalid Account"))
if self.customer and account.account_type != "Receivable":
frappe.throw(_("Please ensure {} account is a Receivable account. \
Change the account type to Receivable or select a different account.")
.format(frappe.bold("Debit To")), title=_("Invalid Account"))
msg = _("Please ensure {} account is a Receivable account. ").format(frappe.bold("Debit To"))
msg += _("Change the account type to Receivable or select a different account.")
frappe.throw(msg, title=_("Invalid Account"))
self.party_account_currency = account.account_currency
@ -535,7 +549,12 @@ class SalesInvoice(SellingController):
self.against_income_account = ','.join(against_acc)
def add_remarks(self):
if not self.remarks: self.remarks = 'No Remarks'
if not self.remarks:
if self.po_no and self.po_date:
self.remarks = _("Against Customer Order {0} dated {1}").format(self.po_no,
formatdate(self.po_date))
else:
self.remarks = _("No Remarks")
def validate_auto_set_posting_time(self):
# Don't auto set the posting date and time if invoice is amended
@ -572,7 +591,8 @@ class SalesInvoice(SellingController):
def validate_pos(self):
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)):
frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total"))
@ -714,22 +734,20 @@ class SalesInvoice(SellingController):
if d.delivery_note and frappe.db.get_value("Delivery Note", d.delivery_note, "docstatus") != 1:
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note))
def make_gl_entries(self, gl_entries=None):
from erpnext.accounts.general_ledger import make_reverse_gl_entries
def make_gl_entries(self, gl_entries=None, from_repost=False):
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries
auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
if not gl_entries:
gl_entries = self.get_gl_entries()
if gl_entries:
from erpnext.accounts.general_ledger import make_gl_entries
# if POS and amount is written off, updating outstanding amt after posting all gl entries
update_outstanding = "No" if (cint(self.is_pos) or self.write_off_account or
cint(self.redeem_loyalty_points)) else "Yes"
if self.docstatus == 1:
make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False)
make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False, from_repost=from_repost)
elif self.docstatus == 2:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
@ -750,6 +768,7 @@ class SalesInvoice(SellingController):
self.make_customer_gl_entry(gl_entries)
self.make_tax_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries)
self.make_item_gl_entries(gl_entries)
@ -769,7 +788,7 @@ class SalesInvoice(SellingController):
# Checked both rounding_adjustment and rounded_total
# because rounded_total had value even before introcution of posting GLE based on rounded total
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
if grand_total:
if grand_total and not self.is_internal_transfer():
# Didnot use base_grand_total to book rounding loss gle
grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
self.precision("grand_total"))
@ -808,6 +827,18 @@ class SalesInvoice(SellingController):
}, account_currency, item=tax)
)
def make_internal_transfer_gl_entries(self, gl_entries):
if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges):
account_currency = get_account_currency(self.unrealized_profit_loss_account)
gl_entries.append(
self.get_gl_dict({
"account": self.unrealized_profit_loss_account,
"against": self.customer,
"debit": flt(self.total_taxes_and_charges),
"debit_in_account_currency": flt(self.base_total_taxes_and_charges),
"cost_center": self.cost_center
}, account_currency, item=self))
def make_item_gl_entries(self, gl_entries):
# income account gl entries
for item in self.get("items"):
@ -830,22 +861,24 @@ class SalesInvoice(SellingController):
asset.db_set("disposal_date", self.posting_date)
asset.set_status("Sold" if self.docstatus==1 else None)
else:
income_account = (item.income_account
if (not item.enable_deferred_revenue or self.is_return) else item.deferred_revenue_account)
# Do not book income for transfer within same company
if not self.is_internal_transfer():
income_account = (item.income_account
if (not item.enable_deferred_revenue or self.is_return) else item.deferred_revenue_account)
account_currency = get_account_currency(income_account)
gl_entries.append(
self.get_gl_dict({
"account": income_account,
"against": self.customer,
"credit": flt(item.base_net_amount, item.precision("base_net_amount")),
"credit_in_account_currency": (flt(item.base_net_amount, item.precision("base_net_amount"))
if account_currency==self.company_currency
else flt(item.net_amount, item.precision("net_amount"))),
"cost_center": item.cost_center,
"project": item.project or self.project
}, account_currency, item=item)
)
account_currency = get_account_currency(income_account)
gl_entries.append(
self.get_gl_dict({
"account": income_account,
"against": self.customer,
"credit": flt(item.base_net_amount, item.precision("base_net_amount")),
"credit_in_account_currency": (flt(item.base_net_amount, item.precision("base_net_amount"))
if account_currency==self.company_currency
else flt(item.net_amount, item.precision("net_amount"))),
"cost_center": item.cost_center,
"project": item.project or self.project
}, account_currency, item=item)
)
# expense account gl entries
if cint(self.update_stock) and \
@ -1140,8 +1173,10 @@ class SalesInvoice(SellingController):
where redeem_against=%s''', (lp_entry[0].name), as_dict=1)
if 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.
First cancel the {} No {}''').format(self.doctype, self.doctype, invoice_list))
frappe.throw(
_('''{} can't be cancelled since the Loyalty Points earned has been redeemed. First cancel the {} No {}''')
.format(self.doctype, self.doctype, invoice_list)
)
else:
frappe.db.sql('''delete from `tabLoyalty Point Entry` where invoice=%s''', (self.name))
# Set loyalty program
@ -1255,7 +1290,9 @@ class SalesInvoice(SellingController):
if self.docstatus == 2:
status = "Cancelled"
elif self.docstatus == 1:
if outstanding_amount > 0 and due_date < nowdate and self.is_discounted and discountng_status=='Disbursed':
if self.is_internal_transfer():
self.status = 'Internal Transfer'
elif outstanding_amount > 0 and due_date < nowdate and self.is_discounted and discountng_status=='Disbursed':
self.status = "Overdue and Discounted"
elif outstanding_amount > 0 and due_date < nowdate:
self.status = "Overdue"
@ -1372,7 +1409,7 @@ def get_bank_cash_account(mode_of_payment, company):
{"parent": mode_of_payment, "company": company}, "default_account")
if not account:
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 {
"account": account
}
@ -1398,6 +1435,7 @@ def make_delivery_note(source_name, target_doc=None):
def set_missing_values(source, target):
target.ignore_pricing_rule = 1
target.run_method("set_missing_values")
target.run_method("set_po_nos")
target.run_method("calculate_taxes_and_totals")
def update_item(source_doc, target_doc, source_parent):
@ -1519,9 +1557,13 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
if doctype in ["Sales Invoice", "Sales Order"]:
source_doc = frappe.get_doc(doctype, source_name)
target_doctype = "Purchase Invoice" if doctype == "Sales Invoice" else "Purchase Order"
source_document_warehouse_field = 'target_warehouse'
target_document_warehouse_field = 'from_warehouse'
else:
source_doc = frappe.get_doc(doctype, source_name)
target_doctype = "Sales Invoice" if doctype == "Purchase Invoice" else "Sales Order"
source_document_warehouse_field = 'from_warehouse'
target_document_warehouse_field = 'target_warehouse'
validate_inter_company_transaction(source_doc, doctype)
details = get_inter_company_details(source_doc, doctype)
@ -1548,6 +1590,26 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
if currency:
target_doc.currency = currency
item_field_map = {
"doctype": target_doctype + " Item",
"field_no_map": [
"income_account",
"expense_account",
"cost_center",
"warehouse"
]
}
if source_doc.get('update_stock'):
item_field_map.update({
'field_map': {
source_document_warehouse_field: target_document_warehouse_field,
'batch_no': 'batch_no',
'serial_no': 'serial_no'
}
})
doclist = get_mapped_doc(doctype, source_name, {
doctype: {
"doctype": target_doctype,
@ -1556,15 +1618,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
"taxes_and_charges"
]
},
doctype +" Item": {
"doctype": target_doctype + " Item",
"field_no_map": [
"income_account",
"expense_account",
"cost_center",
"warehouse"
]
}
doctype +" Item": item_field_map
}, target_doc, set_missing_values)
@ -1612,18 +1666,24 @@ def update_multi_mode_option(doc, pos_profile):
payment.type = payment_mode.type
doc.set('payments', [])
if not pos_profile or not pos_profile.get('payments'):
for payment_mode in get_all_mode_of_payments(doc):
append_payment(payment_mode)
return
invalid_modes = []
for pos_payment_method in pos_profile.get('payments'):
pos_payment_method = pos_payment_method.as_dict()
payment_mode = get_mode_of_payment_info(pos_payment_method.mode_of_payment, doc.company)
if payment_mode:
payment_mode[0].default = pos_payment_method.default
append_payment(payment_mode[0])
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
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):
return frappe.db.sql("""

View File

@ -13,8 +13,7 @@ def get_data():
'Auto Repeat': 'reference_document',
},
'internal_links': {
'Sales Order': ['items', 'sales_order'],
'Delivery Note': ['items', 'delivery_note']
'Sales Order': ['items', 'sales_order']
},
'transactions': [
{

View File

@ -14,8 +14,8 @@ frappe.listview_settings['Sales Invoice'] = {
"Credit Note Issued": "darkgrey",
"Unpaid and Discounted": "orange",
"Overdue and Discounted": "red",
"Overdue": "red"
"Overdue": "red",
"Internal Transfer": "darkgrey"
};
return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
},

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