Merge remote-tracking branch 'upstream/version-13-hotfix' into dev-quality-inspection-accounts

This commit is contained in:
Rohan Bansal 2021-05-26 14:32:14 +05:30
commit c5b074269a
393 changed files with 12505 additions and 7220 deletions

View File

@ -29,4 +29,5 @@ ignore =
B950,
W191,
max-line-length = 200
max-line-length = 200
exclude=.github/helper/semgrep_rules

38
.github/helper/semgrep_rules/README.md vendored Normal file
View File

@ -0,0 +1,38 @@
# Semgrep linting
## What is semgrep?
Semgrep or "semantic grep" is language agnostic static analysis tool. In simple terms semgrep is syntax-aware `grep`, so unlike regex it doesn't get confused by different ways of writing same thing or whitespaces or code split in multiple lines etc.
Example:
To check if a translate function is using f-string or not the regex would be `r"_\(\s*f[\"']"` while equivalent rule in semgrep would be `_(f"...")`. As semgrep knows grammer of language it takes care of unnecessary whitespace, type of quotation marks etc.
You can read more such examples in `.github/helper/semgrep_rules` directory.
# Why/when to use this?
We want to maintain quality of contributions, at the same time remembering all the good practices can be pain to deal with while evaluating contributions. Using semgrep if you can translate "best practice" into a rule then it can automate the task for us.
## Running locally
Install semgrep using homebrew `brew install semgrep` or pip `pip install semgrep`.
To run locally use following command:
`semgrep --config=.github/helper/semgrep_rules [file/folder names]`
## Testing
semgrep allows testing the tests. Refer to this page: https://semgrep.dev/docs/writing-rules/testing-rules/
When writing new rules you should write few positive and few negative cases as shown in the guide and current tests.
To run current tests: `semgrep --test --test-ignore-todo .github/helper/semgrep_rules`
## Reference
If you are new to Semgrep read following pages to get started on writing/modifying rules:
- https://semgrep.dev/docs/getting-started/
- https://semgrep.dev/docs/writing-rules/rule-syntax
- https://semgrep.dev/docs/writing-rules/pattern-examples/
- https://semgrep.dev/docs/writing-rules/rule-ideas/#common-use-cases

View File

@ -0,0 +1,64 @@
import frappe
from frappe import _, flt
from frappe.model.document import Document
# ruleid: frappe-modifying-but-not-comitting
def on_submit(self):
if self.value_of_goods == 0:
frappe.throw(_('Value of goods cannot be 0'))
self.status = 'Submitted'
# ok: frappe-modifying-but-not-comitting
def on_submit(self):
if self.value_of_goods == 0:
frappe.throw(_('Value of goods cannot be 0'))
self.status = 'Submitted'
self.db_set('status', 'Submitted')
# ok: frappe-modifying-but-not-comitting
def on_submit(self):
if self.value_of_goods == 0:
frappe.throw(_('Value of goods cannot be 0'))
x = "y"
self.status = x
self.db_set('status', x)
# ok: frappe-modifying-but-not-comitting
def on_submit(self):
x = "y"
self.status = x
self.save()
# ruleid: frappe-modifying-but-not-comitting-other-method
class DoctypeClass(Document):
def on_submit(self):
self.good_method()
self.tainted_method()
def tainted_method(self):
self.status = "uptate"
# ok: frappe-modifying-but-not-comitting-other-method
class DoctypeClass(Document):
def on_submit(self):
self.good_method()
self.tainted_method()
def tainted_method(self):
self.status = "update"
self.db_set("status", "update")
# ok: frappe-modifying-but-not-comitting-other-method
class DoctypeClass(Document):
def on_submit(self):
self.good_method()
self.tainted_method()
self.save()
def tainted_method(self):
self.status = "uptate"

View File

@ -0,0 +1,135 @@
# This file specifies rules for correctness according to how frappe doctype data model works.
rules:
- id: frappe-modifying-but-not-comitting
patterns:
- pattern: |
def $METHOD(self, ...):
...
self.$ATTR = ...
- pattern-not: |
def $METHOD(self, ...):
...
self.$ATTR = ...
...
self.db_set(..., self.$ATTR, ...)
- pattern-not: |
def $METHOD(self, ...):
...
self.$ATTR = $SOME_VAR
...
self.db_set(..., $SOME_VAR, ...)
- pattern-not: |
def $METHOD(self, ...):
...
self.$ATTR = $SOME_VAR
...
self.save()
- metavariable-regex:
metavariable: '$ATTR'
# this is negative look-ahead, add more attrs to ignore like (ignore|ignore_this_too|ignore_me)
regex: '^(?!ignore_linked_doctypes|status_updater)(.*)$'
- metavariable-regex:
metavariable: "$METHOD"
regex: "(on_submit|on_cancel)"
message: |
DocType modified in self.$METHOD. Please check if modification of self.$ATTR is commited to database.
languages: [python]
severity: ERROR
- id: frappe-modifying-but-not-comitting-other-method
patterns:
- pattern: |
class $DOCTYPE(...):
def $METHOD(self, ...):
...
self.$ANOTHER_METHOD()
...
def $ANOTHER_METHOD(self, ...):
...
self.$ATTR = ...
- pattern-not: |
class $DOCTYPE(...):
def $METHOD(self, ...):
...
self.$ANOTHER_METHOD()
...
def $ANOTHER_METHOD(self, ...):
...
self.$ATTR = ...
...
self.db_set(..., self.$ATTR, ...)
- pattern-not: |
class $DOCTYPE(...):
def $METHOD(self, ...):
...
self.$ANOTHER_METHOD()
...
def $ANOTHER_METHOD(self, ...):
...
self.$ATTR = $SOME_VAR
...
self.db_set(..., $SOME_VAR, ...)
- pattern-not: |
class $DOCTYPE(...):
def $METHOD(self, ...):
...
self.$ANOTHER_METHOD()
...
self.save()
def $ANOTHER_METHOD(self, ...):
...
self.$ATTR = ...
- metavariable-regex:
metavariable: "$METHOD"
regex: "(on_submit|on_cancel)"
message: |
self.$ANOTHER_METHOD is called from self.$METHOD, check if changes to self.$ATTR are commited to database.
languages: [python]
severity: ERROR
- id: frappe-print-function-in-doctypes
pattern: print(...)
message: |
Did you mean to leave this print statement in? Consider using msgprint or logger instead of print statement.
languages: [python]
severity: WARNING
paths:
exclude:
- test_*.py
include:
- "*/**/doctype/*"
- id: frappe-modifying-child-tables-while-iterating
pattern-either:
- pattern: |
for $ROW in self.$TABLE:
...
self.remove(...)
- pattern: |
for $ROW in self.$TABLE:
...
self.append(...)
message: |
Child table being modified while iterating on it.
languages: [python]
severity: ERROR
paths:
include:
- "*/**/doctype/*"
- id: frappe-same-key-assigned-twice
pattern-either:
- pattern: |
{..., $X: $A, ..., $X: $B, ...}
- pattern: |
dict(..., ($X, $A), ..., ($X, $B), ...)
- pattern: |
_dict(..., ($X, $A), ..., ($X, $B), ...)
message: |
key `$X` is uselessly assigned twice. This could be a potential bug.
languages: [python]
severity: ERROR

View File

@ -0,0 +1,6 @@
def function_name(input):
# ruleid: frappe-codeinjection-eval
eval(input)
# ok: frappe-codeinjection-eval
eval("1 + 1")

View File

@ -0,0 +1,25 @@
rules:
- id: frappe-codeinjection-eval
patterns:
- pattern-not: eval("...")
- pattern: eval(...)
message: |
Detected the use of eval(). eval() can be dangerous if used to evaluate
dynamic content. Avoid it or use safe_eval().
languages: [python]
severity: ERROR
- id: frappe-sqli-format-strings
patterns:
- pattern-inside: |
@frappe.whitelist()
def $FUNC(...):
...
- pattern-either:
- pattern: frappe.db.sql("..." % ...)
- pattern: frappe.db.sql(f"...", ...)
- pattern: frappe.db.sql("...".format(...), ...)
message: |
Detected use of raw string formatting for SQL queries. This can lead to sql injection vulnerabilities. Refer security guidelines - https://github.com/frappe/erpnext/wiki/Code-Security-Guidelines
languages: [python]
severity: WARNING

View File

@ -0,0 +1,44 @@
// ruleid: frappe-translation-empty-string
__("")
// ruleid: frappe-translation-empty-string
__('')
// ok: frappe-translation-js-formatting
__('Welcome {0}, get started with ERPNext in just a few clicks.', [full_name]);
// ruleid: frappe-translation-js-formatting
__(`Welcome ${full_name}, get started with ERPNext in just a few clicks.`);
// ok: frappe-translation-js-formatting
__('This is fine');
// ok: frappe-translation-trailing-spaces
__('This is fine');
// ruleid: frappe-translation-trailing-spaces
__(' this is not ok ');
// ruleid: frappe-translation-trailing-spaces
__('this is not ok ');
// ruleid: frappe-translation-trailing-spaces
__(' this is not ok');
// ok: frappe-translation-js-splitting
__('You have {0} subscribers in your mailing list.', [subscribers.length])
// todoruleid: frappe-translation-js-splitting
__('You have') + subscribers.length + __('subscribers in your mailing list.')
// ruleid: frappe-translation-js-splitting
__('You have' + 'subscribers in your mailing list.')
// ruleid: frappe-translation-js-splitting
__('You have {0} subscribers' +
'in your mailing list', [subscribers.length])
// ok: frappe-translation-js-splitting
__("Ctrl+Enter to add comment")
// ruleid: frappe-translation-js-splitting
__('You have {0} subscribers \
in your mailing list', [subscribers.length])

View File

@ -0,0 +1,61 @@
# Examples taken from https://frappeframework.com/docs/user/en/translations
# This file is used for testing the tests.
from frappe import _
full_name = "Jon Doe"
# ok: frappe-translation-python-formatting
_('Welcome {0}, get started with ERPNext in just a few clicks.').format(full_name)
# ruleid: frappe-translation-python-formatting
_('Welcome %s, get started with ERPNext in just a few clicks.' % full_name)
# ruleid: frappe-translation-python-formatting
_('Welcome %(name)s, get started with ERPNext in just a few clicks.' % {'name': full_name})
# ruleid: frappe-translation-python-formatting
_('Welcome {0}, get started with ERPNext in just a few clicks.'.format(full_name))
subscribers = ["Jon", "Doe"]
# ok: frappe-translation-python-formatting
_('You have {0} subscribers in your mailing list.').format(len(subscribers))
# ruleid: frappe-translation-python-splitting
_('You have') + len(subscribers) + _('subscribers in your mailing list.')
# ruleid: frappe-translation-python-splitting
_('You have {0} subscribers \
in your mailing list').format(len(subscribers))
# ok: frappe-translation-python-splitting
_('You have {0} subscribers') \
+ 'in your mailing list'
# ruleid: frappe-translation-trailing-spaces
msg = _(" You have {0} pending invoice ")
# ruleid: frappe-translation-trailing-spaces
msg = _("You have {0} pending invoice ")
# ruleid: frappe-translation-trailing-spaces
msg = _(" You have {0} pending invoice")
# ok: frappe-translation-trailing-spaces
msg = ' ' + _("You have {0} pending invoices") + ' '
# ruleid: frappe-translation-python-formatting
_(f"can not format like this - {subscribers}")
# ruleid: frappe-translation-python-splitting
_(f"what" + f"this is also not cool")
# ruleid: frappe-translation-empty-string
_("")
# ruleid: frappe-translation-empty-string
_('')
class Test:
# ok: frappe-translation-python-splitting
def __init__(
args
):
pass

View File

@ -0,0 +1,64 @@
rules:
- id: frappe-translation-empty-string
pattern-either:
- pattern: _("")
- pattern: __("")
message: |
Empty string is useless for translation.
Please refer: https://frappeframework.com/docs/user/en/translations
languages: [python, javascript, json]
severity: ERROR
- id: frappe-translation-trailing-spaces
pattern-either:
- pattern: _("=~/(^[ \t]+|[ \t]+$)/")
- pattern: __("=~/(^[ \t]+|[ \t]+$)/")
message: |
Trailing or leading whitespace not allowed in translate strings.
Please refer: https://frappeframework.com/docs/user/en/translations
languages: [python, javascript, json]
severity: ERROR
- id: frappe-translation-python-formatting
pattern-either:
- pattern: _("..." % ...)
- pattern: _("...".format(...))
- pattern: _(f"...")
message: |
Only positional formatters are allowed and formatting should not be done before translating.
Please refer: https://frappeframework.com/docs/user/en/translations
languages: [python]
severity: ERROR
- id: frappe-translation-js-formatting
patterns:
- pattern: __(`...`)
- pattern-not: __("...")
message: |
Template strings are not allowed for text formatting.
Please refer: https://frappeframework.com/docs/user/en/translations
languages: [javascript, json]
severity: ERROR
- id: frappe-translation-python-splitting
pattern-either:
- pattern: _(...) + _(...)
- pattern: _("..." + "...")
- pattern-regex: '[\s\.]_\([^\)]*\\\s*' # lines broken by `\`
- pattern-regex: '[\s\.]_\(\s*\n' # line breaks allowed by python for using ( )
message: |
Do not split strings inside translate function. Do not concatenate using translate functions.
Please refer: https://frappeframework.com/docs/user/en/translations
languages: [python]
severity: ERROR
- id: frappe-translation-js-splitting
pattern-either:
- pattern-regex: '__\([^\)]*[\\]\s+'
- pattern: __('...' + '...', ...)
- pattern: __('...') + __('...')
message: |
Do not split strings inside translate function. Do not concatenate using translate functions.
Please refer: https://frappeframework.com/docs/user/en/translations
languages: [javascript, json]
severity: ERROR

9
.github/helper/semgrep_rules/ux.js vendored Normal file
View File

@ -0,0 +1,9 @@
// ok: frappe-missing-translate-function-js
frappe.msgprint('{{ _("Both login and password required") }}');
// ruleid: frappe-missing-translate-function-js
frappe.msgprint('What');
// ok: frappe-missing-translate-function-js
frappe.throw(' {{ _("Both login and password required") }}. ');

31
.github/helper/semgrep_rules/ux.py vendored Normal file
View File

@ -0,0 +1,31 @@
import frappe
from frappe import msgprint, throw, _
# ruleid: frappe-missing-translate-function-python
throw("Error Occured")
# ruleid: frappe-missing-translate-function-python
frappe.throw("Error Occured")
# ruleid: frappe-missing-translate-function-python
frappe.msgprint("Useful message")
# ruleid: frappe-missing-translate-function-python
msgprint("Useful message")
# ok: frappe-missing-translate-function-python
translatedmessage = _("Hello")
# ok: frappe-missing-translate-function-python
throw(translatedmessage)
# ok: frappe-missing-translate-function-python
msgprint(translatedmessage)
# ok: frappe-missing-translate-function-python
msgprint(_("Helpful message"))
# ok: frappe-missing-translate-function-python
frappe.throw(_("Error occured"))

30
.github/helper/semgrep_rules/ux.yml vendored Normal file
View File

@ -0,0 +1,30 @@
rules:
- id: frappe-missing-translate-function-python
pattern-either:
- patterns:
- pattern: frappe.msgprint("...", ...)
- pattern-not: frappe.msgprint(_("..."), ...)
- patterns:
- pattern: frappe.throw("...", ...)
- pattern-not: frappe.throw(_("..."), ...)
message: |
All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations
languages: [python]
severity: ERROR
- id: frappe-missing-translate-function-js
pattern-either:
- patterns:
- pattern: frappe.msgprint("...", ...)
- pattern-not: frappe.msgprint(__("..."), ...)
# ignore microtemplating e.g. msgprint("{{ _("server side translation") }}")
- pattern-not: frappe.msgprint("=~/\{\{.*\_.*\}\}/i", ...)
- patterns:
- pattern: frappe.throw("...", ...)
- pattern-not: frappe.throw(__("..."), ...)
# ignore microtemplating
- pattern-not: frappe.throw("=~/\{\{.*\_.*\}\}/i", ...)
message: |
All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations
languages: [javascript]
severity: ERROR

View File

@ -80,15 +80,29 @@ jobs:
env:
TYPE: ${{ matrix.TYPE }}
- name: Coverage
if: matrix.TYPE == 'server'
- name: Coverage - Pull Request
if: matrix.TYPE == 'server' && github.event_name == 'pull_request'
run: |
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
cd ${GITHUB_WORKSPACE}
pip install coveralls==2.2.0
pip install coverage==4.5.4
coveralls
coveralls --service=github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
COVERALLS_SERVICE_NAME: github
- name: Coverage - Push
if: matrix.TYPE == 'server' && github.event_name == 'push'
run: |
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
cd ${GITHUB_WORKSPACE}
pip install coveralls==2.2.0
pip install coverage==4.5.4
coveralls --service=github-actions
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
COVERALLS_SERVICE_NAME: github-actions

34
.github/workflows/semgrep.yml vendored Normal file
View File

@ -0,0 +1,34 @@
name: Semgrep
on:
pull_request:
branches:
- develop
- version-13-hotfix
- version-13-pre-release
jobs:
semgrep:
name: Frappe Linter
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup python3
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Setup semgrep
run: |
python -m pip install -q semgrep
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
- name: Semgrep errors
run: |
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
[[ -d .github/helper/semgrep_rules ]] && semgrep --severity ERROR --config=.github/helper/semgrep_rules --quiet --error $files
semgrep --config="r/python.lang.correctness" --quiet --error $files
- name: Semgrep warnings
run: |
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
[[ -d .github/helper/semgrep_rules ]] && semgrep --severity WARNING --severity INFO --config=.github/helper/semgrep_rules --quiet $files

View File

@ -1 +0,0 @@
disable=access-member-before-definition

View File

@ -39,6 +39,10 @@ ERPNext is built on the [Frappe Framework](https://github.com/frappe/frappe), a
---
### Containerized Installation
Use docker to deploy ERPNext in production or for development of [Frappe](https://github.com/frappe/frappe) apps. See https://github.com/frappe/frappe_docker for more details.
### Full Install
The Easy Way: our install script for bench will install all dependencies (e.g. MariaDB). See https://github.com/frappe/bench for more details.

View File

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

View File

@ -41,7 +41,7 @@ def build_conditions(process_type, account, company):
if account:
conditions += "AND %s='%s'"%(deferred_account, account)
elif company:
conditions += "AND p.company='%s'"%(company)
conditions += f"AND p.company = {frappe.db.escape(company)}"
return conditions
@ -360,12 +360,10 @@ def make_gl_entries(doc, credit_account, debit_account, against,
frappe.flags.deferred_accounting_error = True
def send_mail(deferred_process):
title = _("Error while processing deferred accounting for {0}".format(deferred_process))
content = _("""
Deferred accounting failed for some invoices:
Please check Process Deferred Accounting {0}
and submit manually after resolving errors
""").format(get_link_to_form('Process Deferred Accounting', deferred_process))
title = _("Error while processing deferred accounting for {0}").format(deferred_process)
link = get_link_to_form('Process Deferred Accounting', deferred_process)
content = _("Deferred accounting failed for some invoices:") + "\n"
content += _("Please check Process Deferred Accounting {0} and submit manually after resolving errors.").format(link)
sendmail_to_system_managers(title, content)
def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,

View File

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

View File

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

View File

@ -27,7 +27,7 @@ class AccountingDimension(Document):
exists = frappe.db.get_value("Accounting Dimension", {'document_type': self.document_type}, ['name'])
if exists and self.is_new():
frappe.throw("Document Type already used as a dimension")
frappe.throw(_("Document Type already used as a dimension"))
if not self.is_new():
self.validate_document_type_change()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,7 +22,7 @@ def validate_company(company):
'allow_account_creation_against_child_company'])
if parent_company and (not allow_account_creation_against_child_company):
msg = _("{} is a child company. ").format(frappe.bold(company))
msg = _("{} is a child company.").format(frappe.bold(company)) + " "
msg += _("Please import accounts against parent company or enable {} in company master.").format(
frappe.bold('Allow Account Creation Against Child Company'))
frappe.throw(msg, title=_('Wrong Company'))
@ -56,7 +56,7 @@ def get_file(file_name):
extension = extension.lstrip(".")
if extension not in ('csv', 'xlsx', 'xls'):
frappe.throw("Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload")
frappe.throw(_("Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload"))
return file_doc, extension

View File

@ -42,9 +42,9 @@ class TestDunning(unittest.TestCase):
['Sales - _TC', 0.0, 20.44]
])
for gle in gl_entries:
self.assertEquals(expected_values[gle.account][0], gle.account)
self.assertEquals(expected_values[gle.account][1], gle.debit)
self.assertEquals(expected_values[gle.account][2], gle.credit)
self.assertEqual(expected_values[gle.account][0], gle.account)
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
def test_payment_entry(self):
dunning = create_dunning()

View File

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

View File

@ -27,6 +27,23 @@ class ExchangeRateRevaluation(Document):
if not (self.company and self.posting_date):
frappe.throw(_("Please select Company and Posting Date to getting entries"))
@frappe.whitelist()
def check_journal_entry_condition(self):
total_debit = frappe.db.get_value("Journal Entry Account", {
'reference_type': 'Exchange Rate Revaluation',
'reference_name': self.name,
'docstatus': 1
}, "sum(debit) as sum")
total_amt = 0
for d in self.accounts:
total_amt = total_amt + d.new_balance_in_base_currency
if total_amt != total_debit:
return True
return False
@frappe.whitelist()
def get_accounts_data(self, account=None):
accounts = []

View File

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

View File

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

View File

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

View File

@ -39,7 +39,11 @@ class JournalEntry(AccountsController):
self.validate_multi_currency()
self.set_amounts_in_company_currency()
self.validate_debit_credit_amount()
self.validate_total_debit_and_credit()
# Do not validate while importing via data import
if not frappe.flags.in_import:
self.validate_total_debit_and_credit()
self.validate_against_jv()
self.validate_reference_doc()
self.set_against_account()
@ -592,6 +596,7 @@ class JournalEntry(AccountsController):
self.validate_total_debit_and_credit()
@frappe.whitelist()
def get_outstanding_invoices(self):
self.set('accounts', [])
total = 0

View File

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

View File

@ -561,7 +561,7 @@ frappe.ui.form.on('Payment Entry', {
flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate));
if(frm.doc.payment_type == "Pay")
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount);
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, 1);
else
frm.events.set_unallocated_amount(frm);
@ -582,7 +582,7 @@ frappe.ui.form.on('Payment Entry', {
}
if(frm.doc.payment_type == "Receive")
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount);
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, 1);
else
frm.events.set_unallocated_amount(frm);
},
@ -606,9 +606,9 @@ frappe.ui.form.on('Payment Entry', {
{fieldtype:"Float", label: __("Less Than Amount"), fieldname:"outstanding_amt_less_than"},
{fieldtype:"Section Break"},
{fieldtype:"Link", label:__("Cost Center"), fieldname:"cost_center", options:"Cost Center",
"get_query": function() {
return {
"filters": {"company": frm.doc.company}
"get_query": function() {
return {
"filters": {"company": frm.doc.company}
}
}
},
@ -743,7 +743,7 @@ frappe.ui.form.on('Payment Entry', {
});
},
allocate_party_amount_against_ref_docs: function(frm, paid_amount) {
allocate_party_amount_against_ref_docs: function(frm, paid_amount, paid_amount_change) {
var total_positive_outstanding_including_order = 0;
var total_negative_outstanding = 0;
var total_deductions = frappe.utils.sum($.map(frm.doc.deductions || [],
@ -800,22 +800,15 @@ frappe.ui.form.on('Payment Entry', {
//If allocate payment amount checkbox is unchecked, set zero to allocate amount
row.allocated_amount = 0;
} else if (frappe.flags.allocate_payment_amount != 0 && !row.allocated_amount) {
if (row.outstanding_amount > 0 && allocated_positive_outstanding > 0) {
if (row.outstanding_amount >= allocated_positive_outstanding) {
row.allocated_amount = allocated_positive_outstanding;
} else {
row.allocated_amount = row.outstanding_amount;
}
} else if (frappe.flags.allocate_payment_amount != 0 && (!row.allocated_amount || paid_amount_change)) {
if (row.outstanding_amount > 0 && allocated_positive_outstanding >= 0) {
row.allocated_amount = (row.outstanding_amount >= allocated_positive_outstanding) ?
allocated_positive_outstanding : row.outstanding_amount;
allocated_positive_outstanding -= flt(row.allocated_amount);
} else if (row.outstanding_amount < 0 && allocated_negative_outstanding) {
if (Math.abs(row.outstanding_amount) >= allocated_negative_outstanding) {
row.allocated_amount = -1*allocated_negative_outstanding;
} else {
row.allocated_amount = row.outstanding_amount;
};
} else if (row.outstanding_amount < 0 && allocated_negative_outstanding) {
row.allocated_amount = (Math.abs(row.outstanding_amount) >= allocated_negative_outstanding) ?
-1*allocated_negative_outstanding : row.outstanding_amount;
allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount));
}
}

View File

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

View File

@ -234,7 +234,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
});
if (invoices) {
this.frm.fields_dict.payment.grid.update_docfield_property(
this.frm.fields_dict.payments.grid.update_docfield_property(
'invoice_number', 'options', "\n" + invoices.join("\n")
);

View File

@ -114,7 +114,7 @@ class PaymentReconciliation(Document):
'party_type': self.party_type,
'voucher_type': voucher_type,
'account': self.receivable_payable_account
}, as_dict=1, debug=1)
}, as_dict=1)
def add_payment_entries(self, entries):
self.set('payments', [])

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -96,30 +96,45 @@ class POSInvoice(SalesInvoice):
if paid_amt and pay.amount != paid_amt:
return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment))
def validate_pos_reserved_serial_nos(self, item):
serial_nos = get_serial_nos(item.serial_no)
filters = {"item_code": item.item_code, "warehouse": item.warehouse}
if item.batch_no:
filters["batch_no"] = item.batch_no
reserved_serial_nos = get_pos_reserved_serial_nos(filters)
invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos]
bold_invalid_serial_nos = frappe.bold(', '.join(invalid_serial_nos))
if len(invalid_serial_nos) == 1:
frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.")
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
elif invalid_serial_nos:
frappe.throw(_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.")
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
def validate_delivered_serial_nos(self, item):
serial_nos = get_serial_nos(item.serial_no)
delivered_serial_nos = frappe.db.get_list('Serial No', {
'item_code': item.item_code,
'name': ['in', serial_nos],
'sales_invoice': ['is', 'set']
}, pluck='name')
if delivered_serial_nos:
bold_delivered_serial_nos = frappe.bold(', '.join(delivered_serial_nos))
frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another Sales Invoice. Please select valid serial no.")
.format(item.idx, bold_delivered_serial_nos), title=_("Item Unavailable"))
def validate_stock_availablility(self):
if self.is_return:
return
allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock')
error_msg = []
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
for d in self.get('items'):
msg = ""
if d.serial_no:
filters = { "item_code": d.item_code, "warehouse": d.warehouse }
if d.batch_no:
filters["batch_no"] = d.batch_no
reserved_serial_nos = get_pos_reserved_serial_nos(filters)
serial_nos = get_serial_nos(d.serial_no)
invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos]
bold_invalid_serial_nos = frappe.bold(', '.join(invalid_serial_nos))
if len(invalid_serial_nos) == 1:
msg = (_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.")
.format(d.idx, bold_invalid_serial_nos))
elif invalid_serial_nos:
msg = (_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.")
.format(d.idx, bold_invalid_serial_nos))
self.validate_pos_reserved_serial_nos(d)
self.validate_delivered_serial_nos(d)
else:
if allow_negative_stock:
return
@ -127,15 +142,11 @@ class POSInvoice(SalesInvoice):
available_stock = get_stock_availability(d.item_code, d.warehouse)
item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)
if flt(available_stock) <= 0:
msg = (_('Row #{}: Item Code: {} is not available under warehouse {}.').format(d.idx, item_code, warehouse))
frappe.throw(_('Row #{}: Item Code: {} is not available under warehouse {}.')
.format(d.idx, item_code, warehouse), title=_("Item Unavailable"))
elif flt(available_stock) < flt(d.qty):
msg = (_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.')
.format(d.idx, item_code, warehouse, qty))
if msg:
error_msg.append(msg)
if error_msg:
frappe.throw(error_msg, title=_("Item Unavailable"), as_list=True)
frappe.throw(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.')
.format(d.idx, item_code, warehouse, available_stock), title=_("Item Unavailable"))
def validate_serialised_or_batched_item(self):
error_msg = []
@ -202,9 +213,8 @@ class POSInvoice(SalesInvoice):
for d in self.get("items"):
is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item")
if not is_stock_item:
frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice. ").format(
d.idx, frappe.bold(d.item_code)
), title=_("Invalid Item"))
frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice. ")
.format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
def validate_mode_of_payment(self):
if len(self.payments) == 0:
@ -445,29 +455,27 @@ class POSInvoice(SalesInvoice):
@frappe.whitelist()
def get_stock_availability(item_code, warehouse):
latest_sle = frappe.db.sql("""select qty_after_transaction
from `tabStock Ledger Entry`
bin_qty = frappe.db.sql("""select actual_qty from `tabBin`
where item_code = %s and warehouse = %s
order by posting_date desc, posting_time desc
limit 1""", (item_code, warehouse), as_dict=1)
pos_sales_qty = frappe.db.sql("""select sum(p_item.qty) as qty
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
bin_qty = bin_qty[0].actual_qty or 0 if bin_qty else 0
return bin_qty - pos_sales_qty
def get_pos_reserved_qty(item_code, warehouse):
reserved_qty = frappe.db.sql("""select sum(p_item.qty) as qty
from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item
where p.name = p_item.parent
and p.consolidated_invoice is NULL
and p.docstatus = 1
and ifnull(p.consolidated_invoice, '') = ''
and p_item.docstatus = 1
and p_item.item_code = %s
and p_item.warehouse = %s
""", (item_code, warehouse), as_dict=1)
sle_qty = latest_sle[0].qty_after_transaction or 0 if latest_sle else 0
pos_sales_qty = pos_sales_qty[0].qty or 0 if pos_sales_qty else 0
if sle_qty and pos_sales_qty:
return sle_qty - pos_sales_qty
else:
return sle_qty
return reserved_qty[0].qty or 0 if reserved_qty else 0
@frappe.whitelist()
def make_sales_return(source_name, target_doc=None):

View File

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

View File

@ -13,8 +13,7 @@ from frappe.model.mapper import map_doc, map_child_doc
from frappe.utils.scheduler import is_scheduler_inactive
from frappe.core.page.background_jobs.background_jobs import get_info
import json
from six import iteritems
import six
class POSInvoiceMergeLog(Document):
def validate(self):
@ -43,8 +42,9 @@ class POSInvoiceMergeLog(Document):
if return_against_status != "Consolidated":
# if return entry is not getting merged in the current pos closing and if it is not consolidated
bold_unconsolidated = frappe.bold("not Consolidated")
msg = (_("Row #{}: Original Invoice {} of return invoice {} is {}. ")
msg = (_("Row #{}: Original Invoice {} of return invoice {} is {}.")
.format(d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated))
msg += " "
msg += _("Original invoice should be consolidated before or along with the return invoice.")
msg += "<br><br>"
msg += _("You can add original invoice {} manually to proceed.").format(bold_return_against)
@ -57,12 +57,12 @@ class POSInvoiceMergeLog(Document):
sales = [d for d in pos_invoice_docs if d.get('is_return') == 0]
sales_invoice, credit_note = "", ""
if sales:
sales_invoice = self.process_merging_into_sales_invoice(sales)
if returns:
credit_note = self.process_merging_into_credit_note(returns)
if sales:
sales_invoice = self.process_merging_into_sales_invoice(sales)
self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log
self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note)
@ -235,11 +235,11 @@ def get_invoice_customer_map(pos_invoices):
return pos_invoice_customer_map
def consolidate_pos_invoices(pos_invoices=[], closing_entry={}):
invoices = pos_invoices or closing_entry.get('pos_transactions') or get_all_unconsolidated_invoices()
def consolidate_pos_invoices(pos_invoices=None, closing_entry=None):
invoices = pos_invoices or (closing_entry and closing_entry.get('pos_transactions')) or get_all_unconsolidated_invoices()
invoice_by_customer = get_invoice_customer_map(invoices)
if len(invoices) >= 5 and closing_entry:
if len(invoices) >= 10 and closing_entry:
closing_entry.set_status(update=True, status='Queued')
enqueue_job(create_merge_logs, invoice_by_customer=invoice_by_customer, closing_entry=closing_entry)
else:
@ -252,51 +252,83 @@ def unconsolidate_pos_invoices(closing_entry):
pluck='name'
)
if len(merge_logs) >= 5:
if len(merge_logs) >= 10:
closing_entry.set_status(update=True, status='Queued')
enqueue_job(cancel_merge_logs, merge_logs=merge_logs, closing_entry=closing_entry)
else:
cancel_merge_logs(merge_logs, closing_entry)
def create_merge_logs(invoice_by_customer, closing_entry={}):
for customer, invoices in iteritems(invoice_by_customer):
merge_log = frappe.new_doc('POS Invoice Merge Log')
merge_log.posting_date = getdate(closing_entry.get('posting_date'))
merge_log.customer = customer
merge_log.pos_closing_entry = closing_entry.get('name', None)
def create_merge_logs(invoice_by_customer, closing_entry=None):
try:
for customer, invoices in six.iteritems(invoice_by_customer):
merge_log = frappe.new_doc('POS Invoice Merge Log')
merge_log.posting_date = getdate(closing_entry.get('posting_date')) if closing_entry else nowdate()
merge_log.customer = customer
merge_log.pos_closing_entry = closing_entry.get('name') if closing_entry else None
merge_log.set('pos_invoices', invoices)
merge_log.save(ignore_permissions=True)
merge_log.submit()
merge_log.set('pos_invoices', invoices)
merge_log.save(ignore_permissions=True)
merge_log.submit()
if closing_entry:
closing_entry.set_status(update=True, status='Submitted')
closing_entry.update_opening_entry()
if closing_entry:
closing_entry.set_status(update=True, status='Submitted')
closing_entry.db_set('error_message', '')
closing_entry.update_opening_entry()
def cancel_merge_logs(merge_logs, closing_entry={}):
for log in merge_logs:
merge_log = frappe.get_doc('POS Invoice Merge Log', log)
merge_log.flags.ignore_permissions = True
merge_log.cancel()
except Exception as e:
frappe.db.rollback()
message_log = frappe.message_log.pop() if frappe.message_log else str(e)
error_message = safe_load_json(message_log)
if closing_entry:
closing_entry.set_status(update=True, status='Cancelled')
closing_entry.update_opening_entry(for_cancel=True)
if closing_entry:
closing_entry.set_status(update=True, status='Failed')
closing_entry.db_set('error_message', error_message)
raise
def enqueue_job(job, merge_logs=None, invoice_by_customer=None, closing_entry=None):
finally:
frappe.db.commit()
frappe.publish_realtime('closing_process_complete', {'user': frappe.session.user})
def cancel_merge_logs(merge_logs, closing_entry=None):
try:
for log in merge_logs:
merge_log = frappe.get_doc('POS Invoice Merge Log', log)
merge_log.flags.ignore_permissions = True
merge_log.cancel()
if closing_entry:
closing_entry.set_status(update=True, status='Cancelled')
closing_entry.db_set('error_message', '')
closing_entry.update_opening_entry(for_cancel=True)
except Exception as e:
frappe.db.rollback()
message_log = frappe.message_log.pop() if frappe.message_log else str(e)
error_message = safe_load_json(message_log)
if closing_entry:
closing_entry.set_status(update=True, status='Submitted')
closing_entry.db_set('error_message', error_message)
raise
finally:
frappe.db.commit()
frappe.publish_realtime('closing_process_complete', {'user': frappe.session.user})
def enqueue_job(job, **kwargs):
check_scheduler_status()
closing_entry = kwargs.get('closing_entry') or {}
job_name = closing_entry.get("name")
if not job_already_enqueued(job_name):
enqueue(
job,
**kwargs,
queue="long",
timeout=10000,
event="processing_merge_logs",
job_name=job_name,
closing_entry=closing_entry,
invoice_by_customer=invoice_by_customer,
merge_logs=merge_logs,
now=frappe.conf.developer_mode or frappe.flags.in_test
)
@ -314,4 +346,12 @@ def check_scheduler_status():
def job_already_enqueued(job_name):
enqueued_jobs = [d.get("job_name") for d in get_info()]
if job_name in enqueued_jobs:
return True
return True
def safe_load_json(message):
try:
json_message = json.loads(message).get('message')
except Exception:
json_message = message
return json_message

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -173,7 +173,7 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
if parenttype in ["Customer Group", "Item Group", "Territory"]:
parent_field = "parent_{0}".format(frappe.scrub(parenttype))
root_name = frappe.db.get_list(parenttype,
{"is_group": 1, parent_field: ("is", "not set")}, "name", as_list=1)
{"is_group": 1, parent_field: ("is", "not set")}, "name", as_list=1, ignore_permissions=True)
if root_name and root_name[0][0]:
parent_groups.append(root_name[0][0])

View File

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

View File

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

View File

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

View File

@ -4,10 +4,12 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from erpnext.accounts.report.general_ledger.general_ledger import execute as get_soa
from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_summary import execute as get_ageing
from frappe.core.doctype.communication.email import make
from erpnext import get_company_currency
from erpnext.accounts.party import get_party_account_currency
from frappe.utils.print_format import report_to_pdf
from frappe.utils.pdf import get_pdf
@ -29,7 +31,7 @@ class ProcessStatementOfAccounts(Document):
validate_template(self.body)
if not self.customers:
frappe.throw(frappe._('Customers not selected.'))
frappe.throw(_('Customers not selected.'))
if self.enable_auto_email:
self.to_date = self.start_date
@ -38,7 +40,7 @@ class ProcessStatementOfAccounts(Document):
def get_report_pdf(doc, consolidated=True):
statement_dict = {}
aging = ''
ageing = ''
base_template_path = "frappe/www/printview.html"
template_path = "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
@ -54,26 +56,33 @@ def get_report_pdf(doc, consolidated=True):
'range4': 120,
'customer': entry.customer
})
col1, aging = get_ageing(ageing_filters)
aging[0]['ageing_based_on'] = doc.ageing_based_on
col1, ageing = get_ageing(ageing_filters)
if ageing:
ageing[0]['ageing_based_on'] = doc.ageing_based_on
tax_id = frappe.get_doc('Customer', entry.customer).tax_id
presentation_currency = get_party_account_currency('Customer', entry.customer, doc.company) \
or doc.currency or get_company_currency(doc.company)
if doc.letter_head:
from frappe.www.printview import get_letter_head
letter_head = get_letter_head(doc, 0)
filters= frappe._dict({
'from_date': doc.from_date,
'to_date': doc.to_date,
'company': doc.company,
'finance_book': doc.finance_book if doc.finance_book else None,
"account": doc.account if doc.account else None,
'account': doc.account if doc.account else None,
'party_type': 'Customer',
'party': [entry.customer],
'presentation_currency': presentation_currency,
'group_by': doc.group_by,
'currency': doc.currency,
'cost_center': [cc.cost_center_name for cc in doc.cost_center],
'project': [p.project_name for p in doc.project],
'show_opening_entries': 0,
'include_default_book_entries': 0,
'show_cancelled_entries': 1,
'tax_id': tax_id if tax_id else None
})
col, res = get_soa(filters)
@ -83,11 +92,17 @@ def get_report_pdf(doc, consolidated=True):
if len(res) == 3:
continue
html = frappe.render_template(template_path, \
{"filters": filters, "data": res, "aging": aging[0] if doc.include_ageing else None})
{"filters": filters, "data": res, "ageing": ageing[0] if doc.include_ageing else None,
"letter_head": letter_head if doc.letter_head else None,
"terms_and_conditions": frappe.db.get_value('Terms and Conditions', doc.terms_and_conditions, 'terms')
if doc.terms_and_conditions else None})
html = frappe.render_template(base_template_path, {"body": html, \
"css": get_print_style(), "title": "Statement For " + entry.customer})
statement_dict[entry.customer] = html
if not bool(statement_dict):
return False
elif consolidated:
@ -167,7 +182,7 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
if customer_collection == 'Sales Person':
customers = get_customers_based_on_sales_person(collection_name)
if not bool(customers):
frappe.throw('No Customers found with selected options.')
frappe.throw(_('No Customers found with selected options.'))
else:
if customer_collection == 'Sales Partner':
customers = frappe.get_list('Customer', fields=['name', 'email_id'], \
@ -199,14 +214,14 @@ def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=Tr
if len(billing_email) == 0 or (billing_email[0][0] is None):
if billing_and_primary:
frappe.throw('No billing email found for customer: '+ customer_name)
frappe.throw(_("No billing email found for customer: {0}").format(customer_name))
else:
return ''
if billing_and_primary:
primary_email = frappe.get_value('Customer', customer_name, 'email_id')
if primary_email is None and int(primary_mandatory):
frappe.throw('No primary email found for customer: '+ customer_name)
frappe.throw(_("No primary email found for customer: {0}").format(customer_name))
return [primary_email or '', billing_email[0][0]]
else:
return billing_email[0][0] or ''

View File

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

View File

@ -514,6 +514,28 @@ frappe.ui.form.on("Purchase Invoice", {
}
},
refresh: function(frm) {
frm.events.add_custom_buttons(frm);
},
add_custom_buttons: function(frm) {
if (frm.doc.per_received < 100) {
frm.add_custom_button(__('Purchase Receipt'), () => {
frm.events.make_purchase_receipt(frm);
}, __('Create'));
}
if (frm.doc.docstatus == 1 && frm.doc.per_received > 0) {
frm.add_custom_button(__('Purchase Receipt'), () => {
frappe.route_options = {
'purchase_invoice': frm.doc.name
}
frappe.set_route("List", "Purchase Receipt", "List")
}, __('View'));
}
},
onload: function(frm) {
if(frm.doc.__onload && frm.is_new()) {
if(frm.doc.supplier) {
@ -539,5 +561,13 @@ frappe.ui.form.on("Purchase Invoice", {
update_stock: function(frm) {
hide_fields(frm.doc);
frm.fields_dict.items.grid.toggle_reqd("item_code", frm.doc.update_stock? true: false);
},
make_purchase_receipt: function(frm) {
frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_purchase_receipt",
frm: frm,
freeze_message: __("Creating Purchase Receipt ...")
})
}
})

View File

@ -163,7 +163,8 @@
"to_date",
"column_break_114",
"auto_repeat",
"update_auto_repeat_reference"
"update_auto_repeat_reference",
"per_received"
],
"fields": [
{
@ -1364,13 +1365,22 @@
"print_hide": 1,
"print_width": "50px",
"width": "50px"
},
{
"fieldname": "per_received",
"fieldtype": "Percent",
"hidden": 1,
"label": "Per Received",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2021-03-30 22:45:58.334107",
"modified": "2021-04-30 22:45:58.334107",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@ -1207,3 +1207,41 @@ def make_inter_company_sales_invoice(source_name, target_doc=None):
def on_doctype_update():
frappe.db.add_index("Purchase Invoice", ["supplier", "is_return", "return_against"])
@frappe.whitelist()
def make_purchase_receipt(source_name, target_doc=None):
def update_item(obj, target, source_parent):
target.qty = flt(obj.qty) - flt(obj.received_qty)
target.received_qty = flt(obj.qty) - flt(obj.received_qty)
target.stock_qty = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.conversion_factor)
target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate)
target.base_amount = (flt(obj.qty) - flt(obj.received_qty)) * \
flt(obj.rate) * flt(source_parent.conversion_rate)
doc = get_mapped_doc("Purchase Invoice", source_name, {
"Purchase Invoice": {
"doctype": "Purchase Receipt",
"validation": {
"docstatus": ["=", 1],
}
},
"Purchase Invoice Item": {
"doctype": "Purchase Receipt Item",
"field_map": {
"name": "purchase_invoice_item",
"parent": "purchase_invoice",
"bom": "bom",
"purchase_order": "purchase_order",
"po_detail": "purchase_order_item",
"material_request": "material_request",
"material_request_item": "material_request_item"
},
"postprocess": update_item,
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty)
},
"Purchase Taxes and Charges": {
"doctype": "Purchase Taxes and Charges"
}
}, target_doc)
return doc

View File

@ -397,7 +397,7 @@ class TestPurchaseInvoice(unittest.TestCase):
pi.update({
"payment_schedule": get_payment_terms("_Test Payment Term Template",
pi.posting_date, pi.grand_total)
pi.posting_date, pi.grand_total, pi.base_grand_total)
})
pi.save()

View File

@ -607,6 +607,7 @@
"oldfieldname": "purchase_order",
"oldfieldtype": "Link",
"options": "Purchase Order",
"print_hide": 1,
"read_only": 1,
"search_index": 1
},
@ -853,7 +854,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-02-23 00:59:52.614805",
"modified": "2021-03-30 09:02:39.256602",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@ -17,7 +17,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
var me = this;
this._super();
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice'];
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet'];
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
// show debit_to in print format
this.frm.set_df_property("debit_to", "print_hide", 0);
@ -356,11 +356,11 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
},
items_on_form_rendered: function() {
erpnext.setup_serial_no();
erpnext.setup_serial_or_batch_no();
},
packed_items_on_form_rendered: function(doc, grid_row) {
erpnext.setup_serial_no();
erpnext.setup_serial_or_batch_no();
},
make_sales_return: function() {
@ -582,6 +582,16 @@ frappe.ui.form.on('Sales Invoice', {
};
});
frm.set_query("adjustment_against", function() {
return {
filters: {
company: frm.doc.company,
customer: frm.doc.customer,
docstatus: 1
}
};
});
frm.custom_make_buttons = {
'Delivery Note': 'Delivery',
'Sales Invoice': 'Return / Credit Note',
@ -685,14 +695,16 @@ frappe.ui.form.on('Sales Invoice', {
},
project: function(frm){
frm.call({
method: "add_timesheet_data",
doc: frm.doc,
callback: function(r, rt) {
refresh_field(['timesheets'])
}
})
frm.refresh();
if (!frm.doc.is_return) {
frm.call({
method: "add_timesheet_data",
doc: frm.doc,
callback: function(r, rt) {
refresh_field(['timesheets'])
}
})
frm.refresh();
}
},
onload: function(frm) {
@ -807,14 +819,27 @@ frappe.ui.form.on('Sales Invoice', {
}
},
add_timesheet_row: function(frm, row, exchange_rate) {
frm.add_child('timesheets', {
'activity_type': row.activity_type,
'description': row.description,
'time_sheet': row.parent,
'billing_hours': row.billing_hours,
'billing_amount': flt(row.billing_amount) * flt(exchange_rate),
'timesheet_detail': row.name
});
frm.refresh_field('timesheets');
calculate_total_billing_amount(frm);
},
refresh: function(frm) {
if (frm.doc.project) {
if (frm.doc.docstatus===0 && !frm.doc.is_return) {
frm.add_custom_button(__('Fetch Timesheet'), function() {
let d = new frappe.ui.Dialog({
title: __('Fetch Timesheet'),
fields: [
{
"label" : "From",
"label" : __("From"),
"fieldname": "from_time",
"fieldtype": "Date",
"reqd": 1,
@ -824,11 +849,18 @@ frappe.ui.form.on('Sales Invoice', {
fieldname: 'col_break_1',
},
{
"label" : "To",
"label" : __("To"),
"fieldname": "to_time",
"fieldtype": "Date",
"reqd": 1,
}
},
{
"label" : __("Project"),
"fieldname": "project",
"fieldtype": "Link",
"options": "Project",
"default": frm.doc.project
},
],
primary_action: function() {
let data = d.get_values();
@ -837,27 +869,35 @@ frappe.ui.form.on('Sales Invoice', {
args: {
from_time: data.from_time,
to_time: data.to_time,
project: frm.doc.project
project: data.project
},
callback: function(r) {
if(!r.exc) {
if(r.message.length > 0) {
frm.clear_table('timesheets')
r.message.forEach((d) => {
frm.add_child('timesheets',{
'time_sheet': d.parent,
'billing_hours': d.billing_hours,
'billing_amount': d.billing_amt,
'timesheet_detail': d.name
if (!r.exc && r.message.length > 0) {
frm.clear_table('timesheets')
r.message.forEach((d) => {
let exchange_rate = 1.0;
if (frm.doc.currency != d.currency) {
frappe.call({
method: 'erpnext.setup.utils.get_exchange_rate',
args: {
from_currency: d.currency,
to_currency: frm.doc.currency
},
callback: function(r) {
if (r.message) {
exchange_rate = r.message;
frm.events.add_timesheet_row(frm, d, exchange_rate);
}
}
});
});
frm.refresh_field('timesheets')
}
else {
frappe.msgprint(__('No Timesheet Found.'))
}
d.hide();
} else {
frm.events.add_timesheet_row(frm, d, exchange_rate);
}
});
} else {
frappe.msgprint(__('No Timesheets found with the selected filters.'))
}
d.hide();
}
});
},
@ -867,6 +907,10 @@ frappe.ui.form.on('Sales Invoice', {
})
}
if (frm.doc.is_debit_note) {
frm.set_df_property('return_against', 'label', 'Adjustment Against');
}
if (frappe.boot.active_domains.includes("Healthcare")) {
frm.set_df_property("patient", "hidden", 0);
frm.set_df_property("patient_name", "hidden", 0);

View File

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

View File

@ -46,7 +46,6 @@ class SalesInvoice(SellingController):
'target_parent_dt': 'Sales Order',
'target_parent_field': 'per_billed',
'source_field': 'amount',
'join_field': 'so_detail',
'percent_join_field': 'sales_order',
'status_field': 'billing_status',
'keyword': 'Billed',
@ -126,6 +125,8 @@ class SalesInvoice(SellingController):
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount", "items")
if not self.is_return:
self.validate_serial_numbers()
else:
self.timesheets = []
self.update_packing_list()
self.set_billing_hours_and_amount()
self.update_timesheet_billing_for_project()
@ -276,7 +277,7 @@ class SalesInvoice(SellingController):
pluck="pos_closing_entry"
)
if pos_closing_entry:
msg = _("To cancel a {} you need to cancel the POS Closing Entry {}. ").format(
msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format(
frappe.bold("Consolidated Sales Invoice"),
get_link_to_form("POS Closing Entry", pos_closing_entry[0])
)
@ -338,7 +339,7 @@ class SalesInvoice(SellingController):
if "Healthcare" in active_domains:
manage_invoice_submit_cancel(self, "on_cancel")
self.unlink_sales_invoice_from_timesheets()
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
def update_status_updater_args(self):
@ -394,6 +395,18 @@ class SalesInvoice(SellingController):
if validate_against_credit_limit:
check_credit_limit(self.customer, self.company, bypass_credit_limit_check_at_sales_order)
def unlink_sales_invoice_from_timesheets(self):
for row in self.timesheets:
timesheet = frappe.get_doc('Timesheet', row.time_sheet)
for time_log in timesheet.time_logs:
if time_log.sales_invoice == self.name:
time_log.sales_invoice = None
timesheet.calculate_total_amounts()
timesheet.calculate_percentage_billed()
timesheet.flags.ignore_validate_update_after_submit = True
timesheet.set_status()
timesheet.db_update_all()
@frappe.whitelist()
def set_missing_values(self, for_validate=False):
pos = self.set_pos_fields(for_validate)
@ -428,7 +441,7 @@ class SalesInvoice(SellingController):
timesheet.calculate_percentage_billed()
timesheet.flags.ignore_validate_update_after_submit = True
timesheet.set_status()
timesheet.save()
timesheet.db_update_all()
def update_time_sheet_detail(self, timesheet, args, sales_invoice):
for data in timesheet.time_logs:
@ -549,12 +562,12 @@ class SalesInvoice(SellingController):
frappe.throw(_("Debit To is required"), title=_("Account Missing"))
if account.report_type != "Balance Sheet":
msg = _("Please ensure {} account is a Balance Sheet account. ").format(frappe.bold("Debit To"))
msg = _("Please ensure {} account is a Balance Sheet account.").format(frappe.bold("Debit To")) + " "
msg += _("You can change the parent account to a Balance Sheet account or select a different account.")
frappe.throw(msg, title=_("Invalid Account"))
if self.customer and account.account_type != "Receivable":
msg = _("Please ensure {} account is a Receivable account. ").format(frappe.bold("Debit To"))
msg = _("Please ensure {} account is a Receivable account.").format(frappe.bold("Debit To")) + " "
msg += _("Change the account type to Receivable or select a different account.")
frappe.throw(msg, title=_("Invalid Account"))
@ -742,8 +755,10 @@ class SalesInvoice(SellingController):
self.append('timesheets', {
'time_sheet': data.parent,
'billing_hours': data.billing_hours,
'billing_amount': data.billing_amt,
'timesheet_detail': data.name
'billing_amount': data.billing_amount,
'timesheet_detail': data.name,
'activity_type': data.activity_type,
'description': data.description
})
self.calculate_billing_amount_for_timesheet()
@ -1112,7 +1127,7 @@ class SalesInvoice(SellingController):
if not item.serial_no:
continue
for serial_no in item.serial_no.split("\n"):
for serial_no in get_serial_nos(item.serial_no):
if serial_no and frappe.db.get_value('Serial No', serial_no, 'item_code') == item.item_code:
frappe.db.set_value('Serial No', serial_no, 'sales_invoice', invoice)
@ -1122,7 +1137,6 @@ class SalesInvoice(SellingController):
"""
self.set_serial_no_against_delivery_note()
self.validate_serial_against_delivery_note()
self.validate_serial_against_sales_invoice()
def set_serial_no_against_delivery_note(self):
for item in self.items:
@ -1153,26 +1167,6 @@ class SalesInvoice(SellingController):
frappe.throw(_("Row {0}: {1} Serial numbers required for Item {2}. You have provided {3}.").format(
item.idx, item.qty, item.item_code, len(si_serial_nos)))
def validate_serial_against_sales_invoice(self):
""" check if serial number is already used in other sales invoice """
for item in self.items:
if not item.serial_no:
continue
for serial_no in item.serial_no.split("\n"):
serial_no_details = frappe.db.get_value("Serial No", serial_no,
["sales_invoice", "item_code"], as_dict=1)
if not serial_no_details:
continue
if serial_no_details.sales_invoice and serial_no_details.item_code == item.item_code \
and self.name != serial_no_details.sales_invoice:
sales_invoice_company = frappe.db.get_value("Sales Invoice", serial_no_details.sales_invoice, "company")
if sales_invoice_company == self.company:
frappe.throw(_("Serial Number: {0} is already referenced in Sales Invoice: {1}")
.format(serial_no, serial_no_details.sales_invoice))
def update_project(self):
if self.project:
project = frappe.get_doc("Project", self.project)
@ -1756,15 +1750,10 @@ def update_pr_items(doc, sales_item_map, purchase_item_map, parent_child_map, wa
item.purchase_order = parent_child_map.get(sales_item_map.get(item.delivery_note_item))
def get_delivery_note_details(internal_reference):
so_item_map = {}
si_item_details = frappe.get_all('Delivery Note Item', fields=['name', 'so_detail'],
filters={'parent': internal_reference})
for d in si_item_details:
so_item_map.setdefault(d.name, d.so_detail)
return so_item_map
return {d.name: d.so_detail for d in si_item_details if d.so_detail}
def get_sales_invoice_details(internal_reference):
dn_item_map = {}

View File

@ -933,12 +933,6 @@ class TestSalesInvoice(unittest.TestCase):
self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "warehouse"))
self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0],
"delivery_document_no"), si.name)
self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "sales_invoice"),
si.name)
# check if the serial number is already linked with any other Sales Invoice
_si = frappe.copy_doc(si.as_dict())
self.assertRaises(frappe.ValidationError, _si.insert)
return si
@ -1879,7 +1873,17 @@ class TestSalesInvoice(unittest.TestCase):
def test_einvoice_submission_without_irn(self):
# init
frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 1)
einvoice_settings = frappe.get_doc('E Invoice Settings')
einvoice_settings.enable = 1
einvoice_settings.applicable_from = nowdate()
einvoice_settings.append('credentials', {
'company': '_Test Company',
'gstin': '27AAECE4835E1ZR',
'username': 'test',
'password': 'test'
})
einvoice_settings.save()
country = frappe.flags.country
frappe.flags.country = 'India'
@ -1890,7 +1894,8 @@ class TestSalesInvoice(unittest.TestCase):
si.submit()
# reset
frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 0)
einvoice_settings = frappe.get_doc('E Invoice Settings')
einvoice_settings.enable = 0
frappe.flags.country = country
def test_einvoice_json(self):

View File

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

View File

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

View File

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

View File

@ -21,7 +21,10 @@ def get_party_details(inv):
else:
party_type = 'Supplier'
party = inv.supplier
if not party:
frappe.throw(_("Please select {0} first").format(party_type))
return party_type, party
def get_party_tax_withholding_details(inv, tax_withholding_category=None):
@ -251,7 +254,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu
threshold = tax_details.get('threshold', 0)
cumulative_threshold = tax_details.get('cumulative_threshold', 0)
if ((threshold and supp_credit_amt >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)):
if ((threshold and inv.net_total >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)):
if ldc and is_valid_certificate(
ldc.valid_from, ldc.valid_upto,
inv.posting_date, tax_deducted,
@ -324,7 +327,7 @@ def get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, post
net_total, ldc.certificate_limit
):
tds_amount = get_ltds_amount(net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details)
return tds_amount
def get_debit_note_amount(suppliers, fiscal_year_details, company=None):

View File

@ -87,50 +87,6 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in invoices:
d.cancel()
def test_single_threshold_tds_with_previous_vouchers(self):
invoices = []
frappe.db.set_value("Supplier", "Test TDS Supplier2", "tax_withholding_category", "Single Threshold TDS")
pi = create_purchase_invoice(supplier="Test TDS Supplier2")
pi.submit()
invoices.append(pi)
pi = create_purchase_invoice(supplier="Test TDS Supplier2")
pi.submit()
invoices.append(pi)
self.assertEqual(pi.taxes_and_charges_deducted, 2000)
self.assertEqual(pi.grand_total, 8000)
# delete invoices to avoid clashing
for d in invoices:
d.cancel()
def test_single_threshold_tds_with_previous_vouchers_and_no_tds(self):
invoices = []
doc = create_supplier(supplier_name = "Test TDS Supplier ABC",
tax_withholding_category="Single Threshold TDS")
supplier = doc.name
pi = create_purchase_invoice(supplier=supplier)
pi.submit()
invoices.append(pi)
# TDS not applied
pi = create_purchase_invoice(supplier=supplier, do_not_apply_tds=True)
pi.submit()
invoices.append(pi)
pi = create_purchase_invoice(supplier=supplier)
pi.submit()
invoices.append(pi)
self.assertEqual(pi.taxes_and_charges_deducted, 2000)
self.assertEqual(pi.grand_total, 8000)
# delete invoices to avoid clashing
for d in invoices:
d.cancel()
def test_cumulative_threshold_tcs(self):
frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS")
invoices = []

View File

@ -18,7 +18,8 @@ def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, upd
gl_map = process_gl_map(gl_map, merge_entries)
if gl_map and len(gl_map) > 1:
save_entries(gl_map, adv_adj, update_outstanding, from_repost)
else:
# Post GL Map proccess there may no be any GL Entries
elif gl_map:
frappe.throw(_("Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction."))
else:
make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding)
@ -170,7 +171,7 @@ def round_off_debit_credit(gl_map):
else:
allowance = .5
if abs(debit_credit_diff) >= allowance:
if abs(debit_credit_diff) > allowance:
frappe.throw(_("Debit and Credit not equal for {0} #{1}. Difference is {2}.")
.format(gl_map[0].voucher_type, gl_map[0].voucher_no, debit_credit_diff))

View File

@ -1,5 +1,6 @@
{
"attach_print": 0,
"channel": "Email",
"condition": "doc.auto_created",
"creation": "2018-04-25 14:19:05.440361",
"days_in_advance": 0,

View File

@ -364,7 +364,7 @@ class ReceivablePayableReport(object):
payment_terms_details = frappe.db.sql("""
select
si.name, si.party_account_currency, si.currency, si.conversion_rate,
ps.due_date, ps.payment_amount, ps.description, ps.paid_amount, ps.discounted_amount
ps.due_date, ps.payment_term, ps.payment_amount, ps.description, ps.paid_amount, ps.discounted_amount
from `tab{0}` si, `tabPayment Schedule` ps
where
si.name = ps.parent and
@ -394,7 +394,7 @@ class ReceivablePayableReport(object):
"due_date": d.due_date,
"invoiced": invoiced,
"invoice_grand_total": row.invoiced,
"payment_term": d.description,
"payment_term": d.description or d.payment_term,
"paid": d.paid_amount + d.discounted_amount,
"credit_note": 0.0,
"outstanding": invoiced - d.paid_amount - d.discounted_amount

View File

@ -5,7 +5,8 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import flt, cint
from erpnext.accounts.report.financial_statements import (get_period_list, get_columns, get_data)
from erpnext.accounts.report.financial_statements import (get_period_list, get_columns, get_data,
get_filtered_list_for_consolidated_report)
def execute(filters=None):
period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year,
@ -132,6 +133,10 @@ def get_report_summary(period_list, asset, liability, equity, provisional_profit
if filters.get('accumulated_values'):
period_list = [period_list[-1]]
# from consolidated financial statement
if filters.get('accumulated_in_group_company'):
period_list = get_filtered_list_for_consolidated_report(filters, period_list)
for period in period_list:
key = period if consolidated else period.key
if asset:

View File

@ -0,0 +1,29 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports['Billed Items To Be Received'] = {
'filters': [
{
'label': __('Company'),
'fieldname': 'company',
'fieldtype': 'Link',
'options': 'Company',
'reqd': 1,
'default': frappe.defaults.get_default('Company')
},
{
'label': __('As on Date'),
'fieldname': 'posting_date',
'fieldtype': 'Date',
'reqd': 1,
'default': get_today()
},
{
'label': __('Purchase Invoice'),
'fieldname': 'purchase_invoice',
'fieldtype': 'Link',
'options': 'Purchase Invoice'
}
]
};

View File

@ -0,0 +1,39 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-03-30 09:35:38.683028",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-03-31 08:48:30.944429",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Billed Items To Be Received",
"owner": "Administrator",
"prepared_report": 0,
"query": "",
"ref_doctype": "Purchase Invoice",
"report_name": "Billed Items To Be Received",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
},
{
"role": "Purchase User"
},
{
"role": "Accounts Manager"
},
{
"role": "Auditor"
},
{
"role": "Stock User"
}
]
}

View File

@ -0,0 +1,107 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
def execute(filters=None):
data = get_data(filters) or []
columns = get_columns()
return columns, data
def get_data(report_filters):
filters = get_report_filters(report_filters)
fields = get_report_fields()
return frappe.get_all('Purchase Invoice',
fields= fields, filters=filters)
def get_report_filters(report_filters):
filters = [['Purchase Invoice','company','=',report_filters.get('company')],
['Purchase Invoice','posting_date','<=',report_filters.get('posting_date')], ['Purchase Invoice','docstatus','=',1],
['Purchase Invoice','per_received','<',100], ['Purchase Invoice','update_stock','=',0]]
if report_filters.get('purchase_invoice'):
filters.append(['Purchase Invoice','per_received','in',[report_filters.get('purchase_invoice')]])
return filters
def get_report_fields():
fields = []
for p_field in ['name', 'supplier', 'company', 'posting_date', 'currency']:
fields.append('`tabPurchase Invoice`.`{}`'.format(p_field))
for c_field in ['item_code', 'item_name', 'uom', 'qty', 'received_qty', 'rate', 'amount']:
fields.append('`tabPurchase Invoice Item`.`{}`'.format(c_field))
return fields
def get_columns():
return [
{
'label': _('Purchase Invoice'),
'fieldname': 'name',
'fieldtype': 'Link',
'options': 'Purchase Invoice',
'width': 170
},
{
'label': _('Supplier'),
'fieldname': 'supplier',
'fieldtype': 'Link',
'options': 'Supplier',
'width': 120
},
{
'label': _('Posting Date'),
'fieldname': 'posting_date',
'fieldtype': 'Date',
'width': 100
},
{
'label': _('Item Code'),
'fieldname': 'item_code',
'fieldtype': 'Link',
'options': 'Item',
'width': 100
},
{
'label': _('Item Name'),
'fieldname': 'item_name',
'fieldtype': 'Data',
'width': 100
},
{
'label': _('UOM'),
'fieldname': 'uom',
'fieldtype': 'Link',
'options': 'UOM',
'width': 100
},
{
'label': _('Invoiced Qty'),
'fieldname': 'qty',
'fieldtype': 'Float',
'width': 100
},
{
'label': _('Received Qty'),
'fieldname': 'received_qty',
'fieldtype': 'Float',
'width': 100
},
{
'label': _('Rate'),
'fieldname': 'rate',
'fieldtype': 'Currency',
'width': 100
},
{
'label': _('Amount'),
'fieldname': 'amount',
'fieldtype': 'Currency',
'width': 100
}
]

View File

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import cint, cstr
from erpnext.accounts.report.financial_statements import (get_period_list, get_columns, get_data)
from erpnext.accounts.report.financial_statements import (get_period_list, get_columns, get_data, get_filtered_list_for_consolidated_report)
from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import get_net_profit_loss
from erpnext.accounts.utils import get_fiscal_year
from six import iteritems
@ -67,9 +67,9 @@ def execute(filters=None):
section_data.append(account_data)
add_total_row_account(data, section_data, cash_flow_account['section_footer'],
period_list, company_currency, summary_data)
period_list, company_currency, summary_data, filters)
add_total_row_account(data, data, _("Net Change in Cash"), period_list, company_currency, summary_data)
add_total_row_account(data, data, _("Net Change in Cash"), period_list, company_currency, summary_data, filters)
columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company)
chart = get_chart_data(columns, data)
@ -162,18 +162,26 @@ def get_start_date(period, accumulated_values, company):
return start_date
def add_total_row_account(out, data, label, period_list, currency, summary_data, consolidated = False):
def add_total_row_account(out, data, label, period_list, currency, summary_data, filters, consolidated=False):
total_row = {
"account_name": "'" + _("{0}").format(label) + "'",
"account": "'" + _("{0}").format(label) + "'",
"currency": currency
}
summary_data[label] = 0
# from consolidated financial statement
if filters.get('accumulated_in_group_company'):
period_list = get_filtered_list_for_consolidated_report(filters, period_list)
for row in data:
if row.get("parent_account"):
for period in period_list:
key = period if consolidated else period['key']
total_row.setdefault(key, 0.0)
total_row[key] += row.get(key, 0.0)
summary_data[label] += row.get(key)
total_row.setdefault("total", 0.0)
total_row["total"] += row["total"]
@ -181,7 +189,6 @@ def add_total_row_account(out, data, label, period_list, currency, summary_data,
out.append(total_row)
out.append({})
summary_data[label] = total_row["total"]
def get_report_summary(summary_data, currency):
report_summary = []

View File

@ -165,7 +165,7 @@ def add_data_for_operating_activities(
if profit_data:
profit_data.update({
"indent": 1,
"parent_account": get_mapper_for(light_mappers, position=0)['section_header']
"parent_account": get_mapper_for(light_mappers, position=1)['section_header']
})
data.append(profit_data)
section_data.append(profit_data)
@ -312,10 +312,10 @@ def add_data_for_other_activities(
def compute_data(filters, company_currency, profit_data, period_list, light_mappers, full_mapper):
data = []
operating_activities_mapper = get_mapper_for(light_mappers, position=0)
operating_activities_mapper = get_mapper_for(light_mappers, position=1)
other_mappers = [
get_mapper_for(light_mappers, position=1),
get_mapper_for(light_mappers, position=2)
get_mapper_for(light_mappers, position=2),
get_mapper_for(light_mappers, position=3)
]
if operating_activities_mapper:

View File

@ -2,118 +2,128 @@
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Consolidated Financial Statement"] = {
"filters": [
{
"fieldname":"company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"default": frappe.defaults.get_user_default("Company"),
"reqd": 1
},
{
"fieldname":"filter_based_on",
"label": __("Filter Based On"),
"fieldtype": "Select",
"options": ["Fiscal Year", "Date Range"],
"default": ["Fiscal Year"],
"reqd": 1,
on_change: function() {
let filter_based_on = frappe.query_report.get_filter_value('filter_based_on');
frappe.query_report.toggle_filter_display('from_fiscal_year', filter_based_on === 'Date Range');
frappe.query_report.toggle_filter_display('to_fiscal_year', filter_based_on === 'Date Range');
frappe.query_report.toggle_filter_display('period_start_date', filter_based_on === 'Fiscal Year');
frappe.query_report.toggle_filter_display('period_end_date', filter_based_on === 'Fiscal Year');
frappe.require("assets/erpnext/js/financial_statements.js", function() {
frappe.query_reports["Consolidated Financial Statement"] = {
"filters": [
{
"fieldname":"company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"default": frappe.defaults.get_user_default("Company"),
"reqd": 1
},
{
"fieldname":"filter_based_on",
"label": __("Filter Based On"),
"fieldtype": "Select",
"options": ["Fiscal Year", "Date Range"],
"default": ["Fiscal Year"],
"reqd": 1,
on_change: function() {
let filter_based_on = frappe.query_report.get_filter_value('filter_based_on');
frappe.query_report.toggle_filter_display('from_fiscal_year', filter_based_on === 'Date Range');
frappe.query_report.toggle_filter_display('to_fiscal_year', filter_based_on === 'Date Range');
frappe.query_report.toggle_filter_display('period_start_date', filter_based_on === 'Fiscal Year');
frappe.query_report.toggle_filter_display('period_end_date', filter_based_on === 'Fiscal Year');
frappe.query_report.refresh();
frappe.query_report.refresh();
}
},
{
"fieldname":"period_start_date",
"label": __("Start Date"),
"fieldtype": "Date",
"hidden": 1,
"reqd": 1
},
{
"fieldname":"period_end_date",
"label": __("End Date"),
"fieldtype": "Date",
"hidden": 1,
"reqd": 1
},
{
"fieldname":"from_fiscal_year",
"label": __("Start Year"),
"fieldtype": "Link",
"options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"),
"reqd": 1
},
{
"fieldname":"to_fiscal_year",
"label": __("End Year"),
"fieldtype": "Link",
"options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"),
"reqd": 1
},
{
"fieldname":"finance_book",
"label": __("Finance Book"),
"fieldtype": "Link",
"options": "Finance Book"
},
{
"fieldname":"report",
"label": __("Report"),
"fieldtype": "Select",
"options": ["Profit and Loss Statement", "Balance Sheet", "Cash Flow"],
"default": "Balance Sheet",
"reqd": 1
},
{
"fieldname": "presentation_currency",
"label": __("Currency"),
"fieldtype": "Select",
"options": erpnext.get_presentation_currency_list(),
"default": frappe.defaults.get_user_default("Currency")
},
{
"fieldname":"accumulated_in_group_company",
"label": __("Accumulated Values in Group Company"),
"fieldtype": "Check",
"default": 0
},
{
"fieldname": "include_default_book_entries",
"label": __("Include Default Book Entries"),
"fieldtype": "Check",
"default": 1
}
],
"formatter": function(value, row, column, data, default_formatter) {
if (data && column.fieldname=="account") {
value = data.account_name || value;
column.link_onclick =
"erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")";
column.is_tree = true;
}
},
{
"fieldname":"period_start_date",
"label": __("Start Date"),
"fieldtype": "Date",
"hidden": 1,
"reqd": 1
},
{
"fieldname":"period_end_date",
"label": __("End Date"),
"fieldtype": "Date",
"hidden": 1,
"reqd": 1
},
{
"fieldname":"from_fiscal_year",
"label": __("Start Year"),
"fieldtype": "Link",
"options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"),
"reqd": 1
},
{
"fieldname":"to_fiscal_year",
"label": __("End Year"),
"fieldtype": "Link",
"options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"),
"reqd": 1
},
{
"fieldname":"finance_book",
"label": __("Finance Book"),
"fieldtype": "Link",
"options": "Finance Book"
},
{
"fieldname":"report",
"label": __("Report"),
"fieldtype": "Select",
"options": ["Profit and Loss Statement", "Balance Sheet", "Cash Flow"],
"default": "Balance Sheet",
"reqd": 1
},
{
"fieldname": "presentation_currency",
"label": __("Currency"),
"fieldtype": "Select",
"options": erpnext.get_presentation_currency_list(),
"default": frappe.defaults.get_user_default("Currency")
},
{
"fieldname":"accumulated_in_group_company",
"label": __("Accumulated Values in Group Company"),
"fieldtype": "Check",
"default": 0
},
{
"fieldname": "include_default_book_entries",
"label": __("Include Default Book Entries"),
"fieldtype": "Check",
"default": 1
}
],
"formatter": function(value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (!data.parent_account) {
value = $(`<span>${value}</span>`);
value = default_formatter(value, row, column, data);
var $value = $(value).css("font-weight", "bold");
if (!data.parent_account) {
value = $(`<span>${value}</span>`);
value = $value.wrap("<p></p>").parent().html();
}
return value;
},
onload: function() {
let fiscal_year = frappe.defaults.get_user_default("fiscal_year")
var $value = $(value).css("font-weight", "bold");
frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
frappe.query_report.set_filter_value({
period_start_date: fy.year_start_date,
period_end_date: fy.year_end_date
value = $value.wrap("<p></p>").parent().html();
}
return value;
},
onload: function() {
let fiscal_year = frappe.defaults.get_user_default("fiscal_year")
frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
frappe.query_report.set_filter_value({
period_start_date: fy.year_start_date,
period_end_date: fy.year_end_date
});
});
});
}
}
}
});

View File

@ -94,7 +94,7 @@ def get_profit_loss_data(fiscal_year, companies, columns, filters):
chart = get_pl_chart_data(filters, columns, income, expense, net_profit_loss)
report_summary = get_pl_summary(companies, '', income, expense, net_profit_loss, company_currency, True)
report_summary = get_pl_summary(companies, '', income, expense, net_profit_loss, company_currency, filters, True)
return data, None, chart, report_summary
@ -149,9 +149,9 @@ def get_cash_flow_data(fiscal_year, companies, filters):
section_data.append(account_data)
add_total_row_account(data, section_data, cash_flow_account['section_footer'],
companies, company_currency, summary_data, True)
companies, company_currency, summary_data, filters, True)
add_total_row_account(data, data, _("Net Change in Cash"), companies, company_currency, summary_data, True)
add_total_row_account(data, data, _("Net Change in Cash"), companies, company_currency, summary_data, filters, True)
report_summary = get_cash_flow_summary(summary_data, company_currency)
@ -329,8 +329,9 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com
has_value = False
total = 0
row = frappe._dict({
"account_name": _(d.account_name),
"account": _(d.account_name),
"account_name": ('%s - %s' %(_(d.account_number), _(d.account_name))
if d.account_number else _(d.account_name)),
"account": _(d.name),
"parent_account": _(d.parent_account),
"indent": flt(d.indent),
"year_start_date": start_date,

View File

@ -0,0 +1,81 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.require("assets/erpnext/js/financial_statements.js", function() {
frappe.query_reports["Dimension-wise Accounts Balance Report"] = {
"filters": [
{
"fieldname": "company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"default": frappe.defaults.get_user_default("Company"),
"reqd": 1
},
{
"fieldname": "fiscal_year",
"label": __("Fiscal Year"),
"fieldtype": "Link",
"options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"),
"reqd": 1,
"on_change": function(query_report) {
var fiscal_year = query_report.get_values().fiscal_year;
if (!fiscal_year) {
return;
}
frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
frappe.query_report.set_filter_value({
from_date: fy.year_start_date,
to_date: fy.year_end_date
});
});
}
},
{
"fieldname": "from_date",
"label": __("From Date"),
"fieldtype": "Date",
"default": frappe.defaults.get_user_default("year_start_date"),
},
{
"fieldname": "to_date",
"label": __("To Date"),
"fieldtype": "Date",
"default": frappe.defaults.get_user_default("year_end_date"),
},
{
"fieldname": "finance_book",
"label": __("Finance Book"),
"fieldtype": "Link",
"options": "Finance Book",
},
{
"fieldname": "dimension",
"label": __("Select Dimension"),
"fieldtype": "Select",
"options": get_accounting_dimension_options(),
"reqd": 1,
},
],
"formatter": erpnext.financial_statements.formatter,
"tree": true,
"name_field": "account",
"parent_field": "parent_account",
"initial_depth": 3
}
});
function get_accounting_dimension_options() {
let options =["", "Cost Center", "Project"];
frappe.db.get_list('Accounting Dimension',
{fields:['document_type']}).then((res) => {
res.forEach((dimension) => {
options.push(dimension.document_type);
});
});
return options
}

View File

@ -0,0 +1,22 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-04-09 16:48:59.548018",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-04-09 16:48:59.548018",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Dimension-wise Accounts Balance Report",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "GL Entry",
"report_name": "Dimension-wise Accounts Balance Report",
"report_type": "Script Report",
"roles": []
}

View File

@ -0,0 +1,213 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe, erpnext
from frappe import _
from frappe.utils import (flt, cstr)
from erpnext.accounts.report.financial_statements import filter_accounts, filter_out_zero_value_rows
from erpnext.accounts.report.trial_balance.trial_balance import validate_filters
from six import itervalues
def execute(filters=None):
validate_filters(filters)
dimension_items_list = get_dimension_items_list(filters.dimension, filters.company)
if not dimension_items_list:
return [], []
dimension_items_list = [''.join(d) for d in dimension_items_list]
columns = get_columns(dimension_items_list)
data = get_data(filters, dimension_items_list)
return columns, data
def get_data(filters, dimension_items_list):
company_currency = erpnext.get_company_currency(filters.company)
acc = frappe.db.sql("""
select
name, account_number, parent_account, lft, rgt, root_type,
report_type, account_name, include_in_gross, account_type, is_group
from
`tabAccount`
where
company=%s
order by lft""", (filters.company), as_dict=True)
if not acc:
return None
accounts, accounts_by_name, parent_children_map = filter_accounts(acc)
min_lft, max_rgt = frappe.db.sql("""select min(lft), max(rgt) from `tabAccount`
where company=%s""", (filters.company))[0]
account = frappe.db.sql_list("""select name from `tabAccount`
where lft >= %s and rgt <= %s and company = %s""", (min_lft, max_rgt, filters.company))
gl_entries_by_account = {}
set_gl_entries_by_account(dimension_items_list, filters, account, gl_entries_by_account)
format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_list)
accumulate_values_into_parents(accounts, accounts_by_name, dimension_items_list)
out = prepare_data(accounts, filters, parent_children_map, company_currency, dimension_items_list)
out = filter_out_zero_value_rows(out, parent_children_map)
return out
def set_gl_entries_by_account(dimension_items_list, filters, account, gl_entries_by_account):
for item in dimension_items_list:
condition = get_condition(filters.from_date, item, filters.dimension)
if account:
condition += " and account in ({})"\
.format(", ".join([frappe.db.escape(d) for d in account]))
gl_filters = {
"company": filters.get("company"),
"from_date": filters.get("from_date"),
"to_date": filters.get("to_date"),
"finance_book": cstr(filters.get("finance_book"))
}
gl_filters['item'] = ''.join(item)
if filters.get("include_default_book_entries"):
gl_filters["company_fb"] = frappe.db.get_value("Company",
filters.company, 'default_finance_book')
for key, value in filters.items():
if value:
gl_filters.update({
key: value
})
gl_entries = frappe.db.sql("""
select
posting_date, account, debit, credit, is_opening, fiscal_year,
debit_in_account_currency, credit_in_account_currency, account_currency
from
`tabGL Entry`
where
company=%(company)s
{condition}
and posting_date <= %(to_date)s
and is_cancelled = 0
order by account, posting_date""".format(
condition=condition),
gl_filters, as_dict=True) #nosec
for entry in gl_entries:
entry['dimension_item'] = ''.join(item)
gl_entries_by_account.setdefault(entry.account, []).append(entry)
def format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_list):
for entries in itervalues(gl_entries_by_account):
for entry in entries:
d = accounts_by_name.get(entry.account)
if not d:
frappe.msgprint(
_("Could not retrieve information for {0}.").format(entry.account), title="Error",
raise_exception=1
)
for item in dimension_items_list:
if item == entry.dimension_item:
d[frappe.scrub(item)] = d.get(frappe.scrub(item), 0.0) + flt(entry.debit) - flt(entry.credit)
def prepare_data(accounts, filters, parent_children_map, company_currency, dimension_items_list):
data = []
for d in accounts:
has_value = False
total = 0
row = {
"account": d.name,
"parent_account": d.parent_account,
"indent": d.indent,
"from_date": filters.from_date,
"to_date": filters.to_date,
"currency": company_currency,
"account_name": ('{} - {}'.format(d.account_number, d.account_name)
if d.account_number else d.account_name)
}
for item in dimension_items_list:
row[frappe.scrub(item)] = flt(d.get(frappe.scrub(item), 0.0), 3)
if abs(row[frappe.scrub(item)]) >= 0.005:
# ignore zero values
has_value = True
total += flt(d.get(frappe.scrub(item), 0.0), 3)
row["has_value"] = has_value
row["total"] = total
data.append(row)
return data
def accumulate_values_into_parents(accounts, accounts_by_name, dimension_items_list):
"""accumulate children's values in parent accounts"""
for d in reversed(accounts):
if d.parent_account:
for item in dimension_items_list:
accounts_by_name[d.parent_account][frappe.scrub(item)] = \
accounts_by_name[d.parent_account].get(frappe.scrub(item), 0.0) + d.get(frappe.scrub(item), 0.0)
def get_condition(from_date, item, dimension):
conditions = []
if from_date:
conditions.append("posting_date >= %(from_date)s")
if dimension:
if dimension not in ['Cost Center', 'Project']:
if dimension in ['Customer', 'Supplier']:
dimension = 'Party'
else:
dimension = 'Voucher No'
txt = "{0} = %(item)s".format(frappe.scrub(dimension))
conditions.append(txt)
return " and {}".format(" and ".join(conditions)) if conditions else ""
def get_dimension_items_list(dimension, company):
meta = frappe.get_meta(dimension, cached=False)
fieldnames = [d.fieldname for d in meta.get("fields")]
filters = {}
if 'company' in fieldnames:
filters['company'] = company
return frappe.get_all(dimension, filters, as_list=True)
def get_columns(dimension_items_list, accumulated_values=1, company=None):
columns = [{
"fieldname": "account",
"label": _("Account"),
"fieldtype": "Link",
"options": "Account",
"width": 300
}]
if company:
columns.append({
"fieldname": "currency",
"label": _("Currency"),
"fieldtype": "Link",
"options": "Currency",
"hidden": 1
})
for item in dimension_items_list:
columns.append({
"fieldname": frappe.scrub(item),
"label": item,
"fieldtype": "Currency",
"options": "currency",
"width": 150
})
columns.append({
"fieldname": "total",
"label": "Total",
"fieldtype": "Currency",
"options": "currency",
"width": 150
})
return columns

View File

@ -119,10 +119,10 @@ def validate_fiscal_year(fiscal_year, from_fiscal_year, to_fiscal_year):
def validate_dates(from_date, to_date):
if not from_date or not to_date:
frappe.throw("From Date and To Date are mandatory")
frappe.throw(_("From Date and To Date are mandatory"))
if to_date < from_date:
frappe.throw("To Date cannot be less than From Date")
frappe.throw(_("To Date cannot be less than From Date"))
def get_months(start_date, end_date):
diff = (12 * end_date.year + end_date.month) - (12 * start_date.year + start_date.month)
@ -522,4 +522,12 @@ def get_columns(periodicity, period_list, accumulated_values=1, company=None):
"width": 150
})
return columns
return columns
def get_filtered_list_for_consolidated_report(filters, period_list):
filtered_summary_list = []
for period in period_list:
if period == filters.get('company'):
filtered_summary_list.append(period)
return filtered_summary_list

View File

@ -166,6 +166,11 @@ frappe.query_reports["General Ledger"] = {
"fieldname": "show_cancelled_entries",
"label": __("Show Cancelled Entries"),
"fieldtype": "Check"
},
{
"fieldname": "show_net_values_in_party_account",
"label": __("Show Net Values in Party Account"),
"fieldtype": "Check"
}
]
}

View File

@ -344,6 +344,9 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
consolidated_gle = OrderedDict()
group_by = group_by_field(filters.get('group_by'))
if filters.get('show_net_values_in_party_account'):
account_type_map = get_account_type_map(filters.get('company'))
def update_value_in_dict(data, key, gle):
data[key].debit += flt(gle.debit)
data[key].credit += flt(gle.credit)
@ -351,6 +354,24 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
data[key].debit_in_account_currency += flt(gle.debit_in_account_currency)
data[key].credit_in_account_currency += flt(gle.credit_in_account_currency)
if filters.get('show_net_values_in_party_account') and \
account_type_map.get(data[key].account) in ('Receivable', 'Payable'):
net_value = flt(data[key].debit) - flt(data[key].credit)
net_value_in_account_currency = flt(data[key].debit_in_account_currency) \
- flt(data[key].credit_in_account_currency)
if net_value < 0:
dr_or_cr = 'credit'
rev_dr_or_cr = 'debit'
else:
dr_or_cr = 'debit'
rev_dr_or_cr = 'credit'
data[key][dr_or_cr] = abs(net_value)
data[key][dr_or_cr+'_in_account_currency'] = abs(net_value_in_account_currency)
data[key][rev_dr_or_cr] = 0
data[key][rev_dr_or_cr+'_in_account_currency'] = 0
if data[key].against_voucher and gle.against_voucher:
data[key].against_voucher += ', ' + gle.against_voucher
@ -388,6 +409,12 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
return totals, entries
def get_account_type_map(company):
account_type_map = frappe._dict(frappe.get_all('Account', fields=['name', 'account_type'],
filters={'company': company}, as_list=1))
return account_type_map
def get_result_as_list(data, filters):
balance, balance_in_account_currency = 0, 0
inv_details = get_supplier_invoice_details()

View File

@ -116,22 +116,19 @@ def validate_filters(filters):
frappe.throw(_("Can not filter based on Payment Method, if grouped by Payment Method"))
def get_conditions(filters):
conditions = "company = %(company)s AND posting_date >= %(from_date)s AND posting_date <= %(to_date)s".format(
company=filters.get("company"),
from_date=filters.get("from_date"),
to_date=filters.get("to_date"))
conditions = "company = %(company)s AND posting_date >= %(from_date)s AND posting_date <= %(to_date)s"
if filters.get("pos_profile"):
conditions += " AND pos_profile = %(pos_profile)s".format(pos_profile=filters.get("pos_profile"))
conditions += " AND pos_profile = %(pos_profile)s"
if filters.get("owner"):
conditions += " AND owner = %(owner)s".format(owner=filters.get("owner"))
conditions += " AND owner = %(owner)s"
if filters.get("customer"):
conditions += " AND customer = %(customer)s".format(customer=filters.get("customer"))
conditions += " AND customer = %(customer)s"
if filters.get("is_return"):
conditions += " AND is_return = %(is_return)s".format(is_return=filters.get("is_return"))
conditions += " AND is_return = %(is_return)s"
if filters.get("mode_of_payment"):
conditions += """

View File

@ -5,7 +5,8 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import flt
from erpnext.accounts.report.financial_statements import (get_period_list, get_columns, get_data)
from erpnext.accounts.report.financial_statements import (get_period_list, get_columns, get_data,
get_filtered_list_for_consolidated_report)
def execute(filters=None):
period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year,
@ -33,13 +34,17 @@ def execute(filters=None):
chart = get_chart_data(filters, columns, income, expense, net_profit_loss)
currency = filters.presentation_currency or frappe.get_cached_value('Company', filters.company, "default_currency")
report_summary = get_report_summary(period_list, filters.periodicity, income, expense, net_profit_loss, currency)
report_summary = get_report_summary(period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters)
return columns, data, None, chart, report_summary
def get_report_summary(period_list, periodicity, income, expense, net_profit_loss, currency, consolidated=False):
def get_report_summary(period_list, periodicity, income, expense, net_profit_loss, currency, filters, consolidated=False):
net_income, net_expense, net_profit = 0.0, 0.0, 0.0
# from consolidated financial statement
if filters.get('accumulated_in_group_company'):
period_list = get_filtered_list_for_consolidated_report(filters, period_list)
for period in period_list:
key = period if consolidated else period.key
if income:

View File

@ -81,8 +81,7 @@ def convert_to_presentation_currency(gl_entries, currency_info, company):
presentation_currency = currency_info['presentation_currency']
company_currency = currency_info['company_currency']
pl_accounts = [d.name for d in frappe.get_list('Account',
filters={'report_type': 'Profit and Loss', 'company': company})]
account_currencies = list(set(entry['account_currency'] for entry in gl_entries))
for entry in gl_entries:
account = entry['account']
@ -92,10 +91,15 @@ def convert_to_presentation_currency(gl_entries, currency_info, company):
credit_in_account_currency = flt(entry['credit_in_account_currency'])
account_currency = entry['account_currency']
if account_currency != presentation_currency:
value = debit or credit
if len(account_currencies) == 1 and account_currency == presentation_currency:
if entry.get('debit'):
entry['debit'] = debit_in_account_currency
date = entry['posting_date'] if account in pl_accounts else currency_info['report_date']
if entry.get('credit'):
entry['credit'] = credit_in_account_currency
else:
value = debit or credit
date = currency_info['report_date']
converted_value = convert(value, presentation_currency, company_currency, date)
if entry.get('debit'):
@ -104,13 +108,6 @@ def convert_to_presentation_currency(gl_entries, currency_info, company):
if entry.get('credit'):
entry['credit'] = converted_value
elif account_currency == presentation_currency:
if entry.get('debit'):
entry['debit'] = debit_in_account_currency
if entry.get('credit'):
entry['credit'] = credit_in_account_currency
converted_gl_list.append(entry)
return converted_gl_list

View File

@ -15,6 +15,7 @@
"hide_custom": 0,
"icon": "accounting",
"idx": 0,
"is_default": 0,
"is_standard": 1,
"label": "Accounting",
"links": [
@ -625,9 +626,9 @@
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Bank Reconciliation",
"link_to": "bank-reconciliation",
"link_type": "Page",
"label": "Bank Reconciliation Tool",
"link_to": "Bank Reconciliation Tool",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
@ -641,26 +642,6 @@
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Bank Statement Transaction Entry",
"link_to": "Bank Statement Transaction Entry",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Bank Statement Settings",
"link_to": "Bank Statement Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
@ -1071,7 +1052,7 @@
"type": "Link"
}
],
"modified": "2021-03-04 00:38:35.349024",
"modified": "2021-05-12 11:48:01.905144",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting",

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